LeetCode高频面试题:gh_mirrors/leet/leetcode项目重点题解
在技术面试中,算法题往往是考察候选人逻辑思维和编程能力的关键环节。本文将围绕gh_mirrors/leet/leetcode项目中的重点题解,从动态规划、广度优先搜索、深度优先搜索等多个维度,为你剖析高频面试题的解题思路与技巧,帮助你在面试中脱颖而出。
动态规划:从局部最优到全局最优
动态规划(Dynamic Programming, DP)是解决多阶段决策问题的有效方法,其核心思想是将复杂问题分解为重叠子问题,并通过存储子问题的解来避免重复计算。在LeetCode中,动态规划类题目出现频率极高,掌握这类题目的解题套路至关重要。
最大子数组和(Maximum Subarray)
问题描述:给定一个整数数组,找出其中和最大的连续子数组。
解题思路:设状态f[j]表示以S[j]结尾的最大连续子序列和。当遍历到数组中的某个元素时,它有两种选择:加入之前的子数组或自己另起一个子数组。若之前子数组的和大于0,则加入;否则,另起炉灶。状态转移方程为: f[j] = max(f[j-1] + S[j], S[j])
代码示例:
// LeetCode, Maximum Subarray
// 时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT_MIN, f = 0;
for (int i = 0; i < nums.size(); ++i) {
f = max(f + nums[i], nums[i]);
result = max(result, f);
}
return result;
}
};
题目来源:C++/chapDynamicProgramming.tex
最小路径和(Minimum Path Sum)
问题描述:给定一个包含非负整数的m x n网格,从左上角到右下角的路径中,找出路径上数字总和最小的路径。每次只能向下或向右移动一步。
解题思路:与Unique Paths类似,设状态f[i][j]表示从起点(0,0)到达(i,j)的最小路径和。状态转移方程为: f[i][j] = min(f[i-1][j], f[i][j-1]) + grid[i][j]
代码示例:
// LeetCode, Minimum Path Sum
// 二维动规
class Solution {
public:
int minPathSum(vector<vector<int> > &grid) {
if (grid.empty()) return 0;
const int m = grid.size();
const int n = grid[0].size();
int f[m][n];
f[0][0] = grid[0][0];
for (int i = 1; i < m; i++) {
f[i][0] = f[i - 1][0] + grid[i][0];
}
for (int i = 1; i < n; i++) {
f[0][i] = f[0][i - 1] + grid[0][i];
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
f[i][j] = min(f[i - 1][j], f[i][j - 1]) + grid[i][j];
}
}
return f[m - 1][n - 1];
}
};
题目来源:[C++/chapDynamicProgramming.tex#L745-L825]
广度优先搜索:逐层扩展的艺术
广度优先搜索(Breadth-First Search, BFS)是一种图形搜索算法,它从起始节点开始,逐层遍历其所有邻居节点,直到找到目标节点或遍历完所有节点。BFS通常用于解决最短路径、连通性等问题。
单词接龙(Word Ladder)
问题描述:给定两个单词(beginWord和endWord)和一个字典,找到从beginWord到endWord的最短转换序列的长度。转换需遵循每次只能改变一个字母,且转换过程中的中间单词必须在字典中。
解题思路:将每个单词视为图中的一个节点,若两个单词仅相差一个字母,则它们之间有一条边。问题转化为求从起始节点到目标节点的最短路径长度,适合用BFS求解。
代码示例(双队列优化):
// LeetCode, Word Ladder
// 双队列,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
int ladderLength(const string& start, const string &end,
const unordered_set<string> &dict) {
queue<string> current, next; // 当前层,下一层
unordered_set<string> visited; // 判重
int level = -1; // 层次
auto state_is_valid = & {
return dict.find(s) != dict.end() || s == end;
};
auto state_is_target = & {return s == end;};
auto state_extend = & {
unordered_set<string> result;
for (size_t i = 0; i < s.size(); ++i) {
string new_word(s);
for (char c = 'a'; c <= 'z'; c++) {
if (c == new_word[i]) continue;
swap(c, new_word[i]);
if (state_is_valid(new_word) &&
visited.find(new_word) == visited.end()) {
result.insert(new_word);
}
swap(c, new_word[i]); // 恢复该单词
}
}
return result;
};
current.insert(start);
visited.insert(start);
while (!current.empty()) {
++ level;
while (!current.empty()) {
const auto state = current.front();
current.pop();
if (state_is_target(state)) {
return level + 1;
}
const auto new_states = state_extend(state);
for (const auto& new_state : new_states) {
next.insert(new_state);
visited.insert(new_state);
}
}
swap(next, current);
}
return 0;
}
};
题目来源:[C++/chapBFS.tex#L10-L195]
被围绕的区域(Surrounded Regions)
问题描述:给定一个二维的矩阵,包含 'X' 和 'O'。将所有被 'X' 围绕的 'O' 转换为 'X'。被围绕的区域不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。
解题思路:从边界上的 'O' 开始进行BFS,将所有与边界相连的 'O' 标记为特殊符号(如 '+')。遍历结束后,矩阵中剩余的 'O' 就是被围绕的区域,将其转换为 'X',并将 '+' 恢复为 'O'。
代码示例:
// LeetCode, Surrounded Regions
// BFS,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
void solve(vector<vector<char>> &board) {
if (board.empty()) return;
const int m = board.size();
const int n = board[0].size();
for (int i = 0; i < n; i++) {
bfs(board, 0, i);
bfs(board, m - 1, i);
}
for (int j = 1; j < m - 1; j++) {
bfs(board, j, 0);
bfs(board, j, n - 1);
}
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (board[i][j] == 'O')
board[i][j] = 'X';
else if (board[i][j] == '+')
board[i][j] = 'O';
}
private:
void bfs(vector<vector<char>> &board, int i, int j) {
typedef pair<int, int> state_t;
queue<state_t> q;
const int m = board.size();
const int n = board[0].size();
auto state_is_valid = & {
const int x = s.first;
const int y = s.second;
if (x < 0 || x >= m || y < 0 || y >= n || board[x][y] != 'O')
return false;
return true;
};
auto state_extend = & {
vector<state_t> result;
const int x = s.first;
const int y = s.second;
// 上下左右
const state_t new_states[4] = {{x-1,y}, {x+1,y},
{x,y-1}, {x,y+1}};
for (int k = 0; k < 4; ++k) {
if (state_is_valid(new_states[k])) {
// 既有标记功能又有去重功能
board[new_states[k].first][new_states[k].second] = '+';
result.push_back(new_states[k]);
}
}
return result;
};
state_t start = { i, j };
if (state_is_valid(start)) {
board[i][j] = '+';
q.push(start);
}
while (!q.empty()) {
auto cur = q.front();
q.pop();
auto new_states = state_extend(cur);
for (auto s : new_states) q.push(s);
}
}
};
题目来源:[C++/chapBFS.tex#L639-L741]
深度优先搜索:探索所有可能的路径
深度优先搜索(Depth-First Search, DFS)是另一种重要的图遍历算法。它沿着树的深度优先遍历树的节点,尽可能深地搜索树的分支。当节点v的所有边都已被探寻过,搜索将回溯到发现节点v的那条边的起始节点。DFS常用于解决排列组合、子集生成等问题。
N皇后问题(N-Queens)
问题描述:在一个N×N的棋盘上放置N个皇后,使得它们不能互相攻击(即任意两个皇后不能处于同一行、同一列或同一斜线上)。找出所有可能的放置方案。
解题思路:使用回溯法(DFS的一种)。用一个数组C记录每一行皇后所在的列编号,C[i]表示第i行皇后在第C[i]列。在放置第row行皇后时,尝试每一列col,检查是否与之前放置的皇后冲突,若不冲突则继续递归放置下一行。
代码示例:
// LeetCode, N-Queens
// 深搜+剪枝,时间复杂度O(n!),空间复杂度O(n)
class Solution {
public:
vector<vector<string> > solveNQueens(int n) {
vector<vector<string> > result;
vector<int> C(n, -1); // C[i]表示第i行皇后所在的列编号
dfs(C, result, 0);
return result;
}
private:
void dfs(vector<int> &C, vector<vector<string> > &result, int row) {
const int N = C.size();
if (row == N) { // 终止条件,也是收敛条件,意味着找到了一个可行解
vector<string> solution;
for (int i = 0; i < N; ++i) {
string s(N, '.');
for (int j = 0; j < N; ++j) {
if (j == C[i]) s[j] = 'Q';
}
solution.push_back(s);
}
result.push_back(solution);
return;
}
for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试
const bool ok = isValid(C, row, j);
if (!ok) continue; // 剪枝,如果非法,继续尝试下一列
// 执行扩展动作
C[row] = j;
dfs(C, result, row + 1);
// 撤销动作
// C[row] = -1;
}
}
/**
* 能否在 (row, col) 位置放一个皇后.
*
* @param C 棋局
* @param row 当前正在处理的行,前面的行都已经放了皇后了
* @param col 当前列
* @return 能否放一个皇后
*/
bool isValid(const vector<int> &C, int row, int col) {
for (int i = 0; i < row; ++i) {
// 在同一列
if (C[i] == col) return false;
// 在同一对角线上
if (abs(i - row) == abs(C[i] - col)) return false;
}
return true;
}
};
题目来源:[C++/chapDFS.tex#L452-L508] 
组合总和(Combination Sum)
问题描述:给定一个无重复元素的数组candidates和一个目标数target,找出candidates中所有可以使数字和为target的组合。candidates中的数字可以无限制重复使用。
解题思路:排序数组后进行DFS。对于每个元素,我们可以选择将其加入当前组合或不加入。如果加入后组合的和仍小于等于target,则继续递归;否则,剪枝回溯。
代码示例:
// LeetCode, Combination Sum
// 时间复杂度O(n!),空间复杂度O(n)
class Solution {
public:
vector<vector<int> > combinationSum(vector<int> &nums, int target) {
sort(nums.begin(), nums.end());
vector<vector<int> > result; // 最终结果
vector<int> path; // 中间结果
dfs(nums, path, result, target, 0);
return result;
}
private:
void dfs(vector<int>& nums, vector<int>& path, vector<vector<int> > &result,
int gap, int start) {
if (gap == 0) { // 找到一个合法解
result.push_back(path);
return;
}
for (size_t i = start; i < nums.size(); i++) { // 扩展状态
if (gap < nums[i]) return; // 剪枝
path.push_back(nums[i]); // 执行扩展动作
dfs(nums, path, result, gap - nums[i], i);
path.pop_back(); // 撤销动作
}
}
};
题目来源:[C++/chapDFS.tex#L773-L830]
贪心算法:每一步都是最优选择
贪心算法(Greedy Algorithm)是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。贪心算法在很多优化问题中能得到最优解或近似最优解。
跳跃游戏(Jump Game)
问题描述:给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
解题思路:贪心法。维护一个变量reach表示当前能够到达的最远位置。遍历数组,对于每个位置i,如果i <= reach,则更新reach为max(reach, i + nums[i])。如果reach >= 数组最后一个位置的索引,则返回true;否则,返回false。
代码示例:
// LeetCode, Jump Game
// 思路1,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
bool canJump(const vector<int>& nums) {
int reach = 1; // 最右能跳到哪里
for (int i = 0; i < reach && reach < nums.size(); ++i)
reach = max(reach, i + 1 + nums[i]);
return reach >= nums.size();
}
};
题目来源:[C++/chapGreedy.tex#L4-L94]
买卖股票的最佳时机(Best Time to Buy and Sell Stock)
问题描述:给定一个数组,它的第i个元素是一支给定股票第i天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
解题思路:贪心法。遍历数组,记录当前遇到的最低价格cur_min。对于每一天的价格,计算当天卖出能获得的利润(prices[i] - cur_min),并更新最大利润。
代码示例:
// LeetCode, Best Time to Buy and Sell Stock
// 时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
int maxProfit(vector<int> &prices) {
if (prices.size() < 2) return 0;
int profit = 0; // 差价,也就是利润
int cur_min = prices[0]; // 当前最小
for (int i = 1; i < prices.size(); i++) {
profit = max(profit, prices[i] - cur_min);
cur_min = min(cur_min, prices[i]);
}
return profit;
}
};
题目来源:[C++/chapGreedy.tex#L179-L212]
树的遍历:掌握二叉树的操作
树是面试中另一个高频考点,尤其是二叉树。掌握二叉树的各种遍历方式(前序、中序、后序、层次遍历)及其应用,对于解决树相关问题至关重要。
二叉树的层序遍历(Binary Tree Level Order Traversal)
问题描述:给你一个二叉树,请你返回其按层序遍历得到的节点值。(即逐层地,从左到右访问所有节点)。
解题思路:使用队列进行BFS。首先将根节点入队,然后每次从队列中取出一个节点,访问其值,并将其左右孩子入队(如果存在)。为了区分不同的层,可以使用两个队列或者一个队列加一个标记变量。
代码示例(迭代版):
// LeetCode, Binary Tree Level Order Traversal
// 迭代版,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
vector<vector<int> > levelOrder(TreeNode *root) {
vector<vector<int> > result;
queue<TreeNode*> current, next;
if(root == nullptr) {
return result;
} else {
current.push(root);
}
while (!current.empty()) {
vector<int> level; // elments in one level
while (!current.empty()) {
TreeNode* node = current.front();
current.pop();
level.push_back(node->val);
if (node->left != nullptr) next.push(node->left);
if (node->right != nullptr) next.push(node->right);
}
result.push_back(level);
swap(next, current);
}
return result;
}
};
题目来源:[C++/chapTree.tex#L370-L457] 
验证二叉搜索树(Validate Binary Search Tree)
问题描述:给定一个二叉树,判断其是否是一个有效的二叉搜索树(BST)。假设一个BST具有如下特征:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是BST。
解题思路:利用BST的中序遍历特性——中序遍历结果是一个严格递增的序列。可以在中序遍历过程中,记录前一个节点的值,若当前节点值小于等于前一个节点值,则不是BST。
代码示例(中序遍历):
// 基于中序遍历的方法
class Solution {
public:
bool isValidBST(TreeNode* root) {
vector<int> vals;
inorder(root, vals);
for (int i = 1; i < vals.size(); ++i) {
if (vals[i] <= vals[i-1]) return false;
}
return true;
}
private:
void inorder(TreeNode* root, vector<int>& vals) {
if (!root) return;
inorder(root->left, vals);
vals.push_back(root->val);
inorder(root->right, vals);
}
};
题目来源:[C++/chapTree.tex] (类似问题可参考树的遍历与验证相关章节)
总结与展望
本文通过对gh_mirrors/leet/leetcode项目中动态规划、BFS、DFS、贪心算法及树的遍历等重点题解的剖析,展示了各类算法在解决实际问题时的应用。掌握这些高频面试题不仅能帮助你应对面试,更能提升你的算法思维和编程能力。
建议你在学习过程中,不仅要记住解题代码,更要理解其背后的算法思想和解题套路。通过大量练习,将这些知识内化为自己的能力,以便在面对新问题时能够灵活运用。
项目的完整题解可参考:C++/leetcode-cpp.pdf。祝你在算法学习的道路上不断进步,面试顺利!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



