LeetCode Hot 100 知识点总结与算法指南
作者 :玖釉-
适用人群 :准备面试的程序员、算法竞赛入门者、计算机科学学生
代码语言 :C++
题目来源:LeetCode Hot 100 题单https://leetcode.cn/problem-list/LTRv2Gcc/
目录
-
- 5.1 前缀树 (Trie)
- 5.2 [LRU 缓存](#LRU 缓存)
- 5.3 位运算技巧
1. 基础数据结构篇
1.1 数组与字符串
数组是最基础的数据结构,掌握数组的操作是学习算法的第一步。
1.1.1 两数之和 (LeetCode 1)
题目描述 :给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值的两个整数。
解题思路 :使用哈希表存储已遍历的数字,对于每个数字计算其补数(target - nums[i]),如果补数在哈希表中则找到答案。
cpp
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> numMap;
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
if (numMap.find(complement) != numMap.end()) {
return {numMap[complement], i};
}
numMap[nums[i]] = i;
}
return {};
}
知识点:
- 哈希表的基本使用
- 空间换时间的思想
- 时间复杂度:O(n),空间复杂度:O(n)
1.1.2 字符串转换整数 (LeetCode 8)
题目描述 :实现一个 myAtoi 函数,将字符串转换为 32 位有符号整数。
解题思路:按照题目要求依次处理:跳过前导空格、处理符号位、逐字符转换数字、处理溢出。
cpp
int myAtoi(string s) {
int i = 0, sign = 1;
long result = 0;
while (i < n && s[i] == ' ') i++;
if (i < n && (s[i] == '+' || s[i] == '-')) {
sign = (s[i] == '-') ? -1 : 1;
i++;
}
while (i < n && isdigit(s[i])) {
int digit = s[i] - '0';
if (result > (LONG_MAX - digit) / 10) {
return (sign == 1) ? INT_MAX : INT_MIN;
}
result = result * 10 + digit;
i++;
}
return static_cast<int>(result * sign);
}
知识点:
- 字符处理与 ASCII 码转换
- 整数溢出的检测与处理
- 边界条件的全面考虑
1.1.3 下一个排列 (LeetCode 31)
题目描述:实现获取下一个排列的函数,将数字重新排列成字典序中下一个更大的排列。
解题思路:
- 从右向左找到第一个非递增元素
nums[i] - 从右向左找到第一个大于
nums[i]的元素nums[j] - 交换
nums[i]和nums[j] - 反转
i+1到末尾的元素
cpp
void nextPermutation(vector<int>& nums) {
int n = nums.size();
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) i--;
if (i >= 0) {
int j = n - 1;
while (nums[j] <= nums[i]) j--;
swap(nums[i], nums[j]);
}
reverse(nums.begin() + i + 1, nums.end());
}
知识点:
- 排列的字典序概念
- 双指针在数组中的应用
- 原地操作数组的技巧
1.1.4 旋转图像 (LeetCode 48)
题目描述:给定一个 n×n 的矩阵,将图像顺时针旋转 90 度。
解题思路:先沿主对角线转置矩阵,然后水平翻转每一行。
cpp
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 转置
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
// 水平翻转
for (int i = 0; i < n; i++) {
for (int j = 0; j < n / 2; j++) {
swap(matrix[i][j], matrix[i][n - 1 - j]);
}
}
}
知识点:
- 矩阵操作的基本技巧
- 分解复杂操作为简单步骤
- 原地算法节省空间
1.1.5 除自身以外数组的乘积 (LeetCode 238)
题目描述 :给定一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
解题思路:使用前缀积和后缀积的思想,分别计算每个位置左边所有元素的乘积和右边所有元素的乘积。
cpp
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> answer(n, 1);
int prefix = 1;
for (int i = 0; i < n; i++) {
answer[i] = prefix;
prefix *= nums[i];
}
int suffix = 1;
for (int i = n - 1; i >= 0; i--) {
answer[i] *= suffix;
suffix *= nums[i];
}
return answer;
}
知识点:
- 前缀积/后缀积技巧
- 两次遍历的算法模式
- 不使用除法解决问题
1.1.6 找到所有数组中消失的数字 (LeetCode 448)
题目描述:给你一个含 n 个整数的数组 nums,找出所有在 1, n 范围内没有出现在数组中的数字。
解题思路:利用数组本身作为哈希表,将出现过的数字对应位置标记为负数,最后正数位置即为消失的数字。
cpp
vector<int> findDisappearedNumbers(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n; i++) {
int index = abs(nums[i]) - 1;
if (nums[index] > 0) {
nums[index] = -nums[index];
}
}
vector<int> result;
for (int i = 0; i < n; i++) {
if (nums[i] > 0) {
result.push_back(i + 1);
}
}
return result;
}
知识点:
- 原地哈希技巧
- 利用数组索引作为键值
- O(1) 空间复杂度的优化思路
1.2 哈希表
哈希表是算法面试中最重要的数据结构之一,能够实现 O(1) 的查找、插入和删除操作。
1.2.1 字母异位词分组 (LeetCode 49)
题目描述:给定一个字符串数组,将字母异位词组合在一起。
解题思路:将每个字符串排序后作为哈希表的键,相同键的字符串即为异位词。
cpp
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> map;
for (string s : strs) {
string key = s;
sort(key.begin(), key.end());
map[key].push_back(s);
}
vector<vector<string>> result;
for (auto& pair : map) {
result.push_back(pair.second);
}
return result;
}
知识点:
- 哈希表的分组应用
- 字符串排序作为标准化键值
- 时间复杂度:O(n * k log k),k 为字符串最大长度
1.2.2 最长连续序列 (LeetCode 128)
题目描述:给定一个未排序的整数数组 nums,找出数字连续的最长序列的长度。
解题思路 :使用哈希集合存储所有数字,对于每个数字,如果它是某个序列的起点(即 num-1 不在集合中),则向后查找连续数字。
cpp
int longestConsecutive(vector<int>& nums) {
unordered_set<int> numSet(nums.begin(), nums.end());
int longest = 0;
for (int num : numSet) {
if (numSet.count(num - 1)) continue; // 不是起点
int currentNum = num;
int currentLength = 1;
while (numSet.count(currentNum + 1)) {
currentNum++;
currentLength++;
}
longest = max(longest, currentLength);
}
return longest;
}
知识点:
- 哈希集合的使用
- 避免重复计算的优化技巧
- 时间复杂度:O(n),每个元素最多访问两次
1.2.3 和为 K 的子数组 (LeetCode 560)
题目描述:给定一个整数数组 nums 和一个整数 k,统计该数组中和为 k 的连续子数组的个数。
解题思路 :使用前缀和 + 哈希表。记录每个前缀和出现的次数,对于当前位置的前缀和 prefixSum,查找 prefixSum - k 出现的次数。
cpp
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> prefixCount;
prefixCount[0] = 1;
int prefixSum = 0, result = 0;
for (int num : nums) {
prefixSum += num;
int need = prefixSum - k;
if (prefixCount.find(need) != prefixCount.end()) {
result += prefixCount[need];
}
prefixCount[prefixSum]++;
}
return result;
}
知识点:
- 前缀和的概念与应用
- 哈希表加速查找
- 子数组问题的通用解法
1.3 栈与队列
栈和队列是最基本的线性数据结构,分别遵循 LIFO(后进先出)和 FIFO(先进先出)原则。
1.3.1 有效的括号 (LeetCode 20)
题目描述 :给定一个只包括 (,),{,},[,] 的字符串,判断字符串是否有效。
解题思路:使用栈存储左括号,遇到右括号时检查栈顶是否匹配。
cpp
bool isValid(string s) {
stack<char> st;
unordered_map<char, char> mapping = {
{')', '('}, {'}', '{'}, {']', '['}
};
for (char c : s) {
if (mapping.find(c) != mapping.end()) {
if (st.empty() || st.top() != mapping[c]) {
return false;
}
st.pop();
} else {
st.push(c);
}
}
return st.empty();
}
知识点:
- 栈的基本操作
- 括号匹配问题的通用解法
- 哈希表辅助匹配
1.3.2 最长有效括号 (LeetCode 32)
题目描述 :给你一个只包含 ( 和 ) 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
解题思路:使用栈存储索引,初始化栈底为 -1。遇到左括号入栈,遇到右括号出栈并计算长度。
cpp
int longestValidParentheses(string s) {
stack<int> st;
st.push(-1);
int max_len = 0;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') {
st.push(i);
} else {
st.pop();
if (st.empty()) {
st.push(i);
} else {
max_len = max(max_len, i - st.top());
}
}
}
return max_len;
}
知识点:
- 栈存储索引的技巧
- 初始化哨兵节点的技巧
- 动态计算区间长度
1.3.3 最小栈 (LeetCode 155)
题目描述:设计一个支持 push、pop、top 操作,并能在常数时间内检索到最小元素的栈。
解题思路:使用辅助栈存储当前栈中的最小值。
cpp
class MinStack {
private:
stack<int> mainStack;
stack<int> minStack;
public:
void push(int val) {
mainStack.push(val);
if (minStack.empty()) {
minStack.push(val);
} else {
minStack.push(min(val, minStack.top()));
}
}
void pop() {
mainStack.pop();
minStack.pop();
}
int top() { return mainStack.top(); }
int getMin() { return minStack.top(); }
};
知识点:
- 辅助栈的设计
- 空间换时间的思想
- 数据结构设计题的解法
1.3.4 字符串解码 (LeetCode 394)
题目描述:给定一个经过编码的字符串,返回它解码后的字符串。
解题思路 :使用两个栈分别存储数字和字符串,遇到 [ 时入栈,遇到 ] 时出栈并拼接。
cpp
string decodeString(string s) {
stack<int> countStack;
stack<string> stringStack;
string currentString = "";
int count = 0;
for (char c : s) {
if (isdigit(c)) {
count = count * 10 + (c - '0');
} else if (c == '[') {
countStack.push(count);
stringStack.push(currentString);
currentString = "";
count = 0;
} else if (c == ']') {
string prevString = stringStack.top();
stringStack.pop();
int repeatCount = countStack.top();
countStack.pop();
for (int i = 0; i < repeatCount; i++) {
prevString += currentString;
}
currentString = prevString;
} else {
currentString += c;
}
}
return currentString;
}
知识点:
- 双栈处理嵌套结构
- 字符串处理技巧
- 递归结构的非递归实现
1.3.5 每日温度 (LeetCode 739)
题目描述:给定每天的温度数组,返回一个数组,表示每天需要等几天才能等到更高的温度。
解题思路:使用单调递减栈,存储温度的索引。当遇到更高温度时,弹出栈顶并计算天数差。
cpp
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> answer(n, 0);
stack<int> st;
for (int i = 0; i < n; i++) {
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
int prevIndex = st.top();
st.pop();
answer[prevIndex] = i - prevIndex;
}
st.push(i);
}
return answer;
}
知识点:
- 单调栈的概念与应用
- 「下一个更大元素」问题的通用解法
- 时间复杂度:O(n),每个元素最多入栈出栈一次
1.4 链表
链表是一种动态数据结构,擅长插入和删除操作,但不支持随机访问。
1.4.1 反转链表 (LeetCode 206)
题目描述:反转一个单链表。
解题思路 :迭代法:使用三个指针 prev、curr、next,逐个反转节点的指向。
cpp
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
知识点:
- 链表指针操作的基本功
- 迭代与递归两种实现方式
- 时间复杂度:O(n),空间复杂度:O(1)
1.4.2 合并两个有序链表 (LeetCode 21)
题目描述:将两个升序链表合并为一个新的升序链表。
解题思路:使用虚拟头节点(dummy node),比较两个链表的节点值,依次连接较小的节点。
cpp
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummy = new ListNode(-1);
ListNode* curr = dummy;
while (list1 != nullptr && list2 != nullptr) {
if (list1->val < list2->val) {
curr->next = list1;
list1 = list1->next;
} else {
curr->next = list2;
list2 = list2->next;
}
curr = curr->next;
}
curr->next = (list1 != nullptr) ? list1 : list2;
ListNode* result = dummy->next;
delete dummy;
return result;
}
知识点:
- 虚拟头节点技巧
- 双指针合并操作
- 链表操作的内存管理
1.4.3 合并 K 个升序链表 (LeetCode 23)
题目描述:给你一个链表数组,每个链表都已按升序排列,请将所有链表合并到一个升序链表中。
解题思路:分治法,每次合并两个链表,逐步减少链表数量。
cpp
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty()) return nullptr;
int k = lists.size();
while (k > 1) {
for (int i = 0; i < k / 2; i++) {
lists[i] = mergeTwoLists(lists[i], lists[k - 1 - i]);
}
k = (k + 1) / 2;
}
return lists[0];
}
知识点:
- 分治算法的思想
- 归并操作的复用
- 时间复杂度:O(N log k)
1.4.4 环形链表 (LeetCode 141 & 142)
题目描述:判断链表是否有环;如果有环,找到环的入口节点。
解题思路:使用快慢指针(Floyd 判圈算法)。快指针每次走两步,慢指针每次走一步,如果有环则必然相遇。要找入口节点,让一个指针从头出发,另一个从相遇点出发,再次相遇即为入口。
cpp
ListNode* detectCycle(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
bool hasCycle = false;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
hasCycle = true;
break;
}
}
if (!hasCycle) return nullptr;
slow = head;
while (slow != fast) {
slow = slow->next;
fast = fast->next;
}
return slow;
}
知识点:
- Floyd 判圈算法(龟兔赛跑算法)
- 快慢指针的经典应用
- 数学证明:设头到入口距离为 a,入口到相遇点距离为 b,环长为 c,则 a = c - b
1.4.5 删除链表的倒数第 N 个结点 (LeetCode 19)
题目描述:给你一个链表,删除链表的倒数第 n 个结点。
解题思路:使用快慢指针,快指针先走 n 步,然后快慢指针一起走,当快指针到达末尾时,慢指针指向待删除节点的前一个节点。
cpp
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(-1);
dummy->next = head;
ListNode* fast = dummy;
ListNode* slow = dummy;
for (int i = 0; i <= n; i++) {
fast = fast->next;
}
while (fast != nullptr) {
fast = fast->next;
slow = slow->next;
}
ListNode* temp = slow->next;
slow->next = slow->next->next;
delete temp;
ListNode* result = dummy->next;
delete dummy;
return result;
}
知识点:
- 快慢指针的经典应用
- 虚拟头节点简化边界处理
- 一次遍历解决问题
1.4.6 相交链表 (LeetCode 160)
题目描述:给定两个单链表的头节点,找出并返回两个单链表相交的起始节点。
解题思路:双指针法,两个指针分别从两个链表头出发,到达末尾后切换到另一个链表的头部,最终会在相交点相遇。
cpp
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
ListNode* pa = headA;
ListNode* pb = headB;
while (pa != pb) {
pa = (pa == nullptr) ? headB : pa->next;
pb = (pb == nullptr) ? headA : pb->next;
}
return pa;
}
知识点:
- 双指针的巧妙应用
- 路径等长的思想
- 时间复杂度:O(m+n),空间复杂度:O(1)
1.4.7 排序链表 (LeetCode 148)
题目描述:给定链表的头结点,将其排序。
解题思路:归并排序,使用快慢指针找到链表中点,递归排序左右两半,然后合并。
cpp
ListNode* sortList(ListNode* head) {
if (head == nullptr || head->next == nullptr) return head;
ListNode* mid = getMid(head);
ListNode* rightHead = mid->next;
mid->next = nullptr;
ListNode* left = sortList(head);
ListNode* right = sortList(rightHead);
return merge(left, right);
}
ListNode* getMid(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head->next;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
知识点:
- 链表上的归并排序
- 快慢指针找中点
- 时间复杂度:O(n log n)
1.4.8 回文链表 (LeetCode 234)
题目描述:判断一个链表是否为回文链表。
解题思路:快慢指针找中点,反转后半部分链表,然后比较前半部分和反转后的后半部分。
cpp
bool isPalindrome(ListNode* head) {
if (head == nullptr || head->next == nullptr) return true;
ListNode* slow = head;
ListNode* fast = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* secondHalf = reverseList(slow->next);
ListNode* p1 = head;
ListNode* p2 = secondHalf;
bool result = true;
while (p2 != nullptr) {
if (p1->val != p2->val) {
result = false;
break;
}
p1 = p1->next;
p2 = p2->next;
}
return result;
}
知识点:
- 快慢指针找中点
- 链表反转
- 组合多种技巧解决问题
1.4.9 LRU 缓存 (LeetCode 146)
题目描述:设计并实现一个 LRU(最近最少使用)缓存机制。
解题思路:使用哈希表 + 双向链表。哈希表实现 O(1) 查找,双向链表实现 O(1) 的插入删除和顺序维护。
cpp
class LRUCache {
private:
int capacity;
unordered_map<int, Node*> cache;
Node* head;
Node* tail;
void removeNode(Node* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void addToHead(Node* node) {
node->next = head->next;
node->prev = head;
head->next->prev = node;
head->next = node;
}
public:
int get(int key) {
if (cache.find(key) == cache.end()) return -1;
Node* node = cache[key];
removeNode(node);
addToHead(node);
return node->value;
}
void put(int key, int value) {
if (cache.find(key) != cache.end()) {
Node* node = cache[key];
node->value = value;
removeNode(node);
addToHead(node);
return;
}
if (cache.size() == capacity) {
Node* last = tail->prev;
removeNode(last);
cache.erase(last->key);
delete last;
}
Node* newNode = new Node(key, value);
cache[key] = newNode;
addToHead(newNode);
}
};
知识点:
- 哈希表 + 双向链表的组合
- 设计题的通用解法
- 虚拟头尾节点简化操作
2. 核心算法思想篇
2.1 双指针技巧
双指针是数组和链表问题中最常用的技巧之一,可以将 O(n²) 的暴力解法优化到 O(n)。
2.1.1 盛最多水的容器 (LeetCode 11)
题目描述:给定 n 个非负整数,每个数代表坐标中的一个点,找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
解题思路:左右双指针,每次移动较短的那一边,因为移动较长的边不可能使面积增大。
cpp
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int maxWater = 0;
while (left < right) {
int h = min(height[left], height[right]);
int width = right - left;
maxWater = max(maxWater, h * width);
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxWater;
}
知识点:
- 贪心思想:移动较短的边才可能增大面积
- 双指针的收敛性保证
- 时间复杂度:O(n)
2.1.2 三数之和 (LeetCode 15)
题目描述:给你一个整数数组 nums,判断是否存在三元组 nums\[i, numsj, numsk] 满足 i != j != k 且 numsi + numsj + numsk == 0。
解题思路:排序 + 双指针。固定第一个数,对剩余部分使用双指针寻找两数之和。注意去重处理。
cpp
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() - 2; i++) {
if (nums[i] > 0) break;
if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重
int left = i + 1, right = nums.size() - 1;
int target = -nums[i];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
result.push_back({nums[i], nums[left], nums[right]});
while (left < right && nums[left] == nums[left + 1]) left++; // 去重
while (left < right && nums[right] == nums[right - 1]) right--; // 去重
left++;
right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
return result;
}
知识点:
- 排序 + 双指针的经典组合
- 去重处理的技巧
- 时间复杂度:O(n²)
2.1.3 接雨水 (LeetCode 42)
题目描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度,计算按此排列的柱子,下雨之后能够接多少雨水。
解题思路 :双指针法,维护左右最大高度。对于每个位置,能接的雨水量等于 min(leftMax, rightMax) - height[i]。
cpp
int trap(vector<int>& height) {
int left = 0, right = height.size() - 1;
int left_max = 0, right_max = 0;
int water = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= left_max) {
left_max = height[left];
} else {
water += left_max - height[left];
}
left++;
} else {
if (height[right] >= right_max) {
right_max = height[right];
} else {
water += right_max - height[right];
}
right--;
}
}
return water;
}
知识点:
- 双指针处理数组问题
- 动态维护最大值
- 时间复杂度:O(n),空间复杂度:O(1)
2.1.4 移动零 (LeetCode 283)
题目描述:给定一个数组 nums,将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
解题思路:快慢指针,快指针遍历数组,慢指针指向下一个非零元素应该放置的位置。
cpp
void moveZeroes(vector<int>& nums) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++) {
if (nums[fast] != 0) {
nums[slow++] = nums[fast];
}
}
for (int i = slow; i < nums.size(); i++) {
nums[i] = 0;
}
}
知识点:
- 快慢指针的基本应用
- 原地操作数组
- 保持相对顺序的技巧
2.1.5 寻找重复数 (LeetCode 287)
题目描述:给定一个包含 n + 1 个整数的数组 nums,其数字都在 1, n 范围内,找出重复的数字。
解题思路:将数组视为链表(值指向下一个索引),使用 Floyd 判圈算法找环的入口。
cpp
int findDuplicate(vector<int>& nums) {
int slow = 0, fast = 0;
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
slow = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
知识点:
- 数组到链表的转化思想
- Floyd 判圈算法的巧妙应用
- 时间复杂度:O(n),空间复杂度:O(1)
2.2 滑动窗口
滑动窗口是解决子串/子数组问题的利器,核心思想是维护一个动态的窗口,通过调整窗口的左右边界来满足条件。
2.2.1 找到字符串中所有字母异位词 (LeetCode 438)
题目描述:给定两个字符串 s 和 p,找到 s 中所有 p 的异位词的子串。
解题思路:固定大小的滑动窗口,维护字符频率数组,比较窗口内字符频率是否与目标相同。
cpp
vector<int> findAnagrams(string s, string p) {
vector<int> result;
if (s.size() < p.size()) return result;
vector<int> pCount(26, 0), sCount(26, 0);
for (int i = 0; i < p.size(); i++) {
pCount[p[i] - 'a']++;
sCount[s[i] - 'a']++;
}
if (pCount == sCount) result.push_back(0);
for (int i = p.size(); i < s.size(); i++) {
sCount[s[i] - 'a']++;
sCount[s[i - p.size()] - 'a']--;
if (pCount == sCount) {
result.push_back(i - p.size() + 1);
}
}
return result;
}
知识点:
- 固定大小滑动窗口
- 字符频率数组的使用
- 时间复杂度:O(n)
2.2.2 最小覆盖子串 (LeetCode 76)
题目描述:给你一个字符串 s、一个字符串 t,返回 s 中涵盖 t 所有字符的最小子串。
解题思路 :可变大小滑动窗口,使用两个哈希表分别记录需要的字符和当前窗口的字符,用 valid 变量跟踪已满足条件的字符种类数。
cpp
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, valid = 0, start = 0, len = INT_MAX;
for (int right = 0; right < s.size(); right++) {
char c = s[right];
if (need.count(c)) {
window[c]++;
if (window[c] == need[c]) valid++;
}
while (valid == need.size()) {
if (right - left + 1 < len) {
start = left;
len = right - left + 1;
}
char d = s[left];
left++;
if (need.count(d)) {
if (window[d] == need[d]) valid--;
window[d]--;
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
知识点:
- 可变大小滑动窗口的模板
- 哈希表跟踪窗口状态
- 时间复杂度:O(n)
2.2.3 滑动窗口最大值 (LeetCode 239)
题目描述:给你一个整数数组 nums 和一个大小为 k 的滑动窗口,返回滑动窗口中的最大值。
解题思路:使用单调递减双端队列(deque),队首始终是当前窗口的最大值。
cpp
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> result;
deque<int> dq;
for (int i = 0; i < nums.size(); i++) {
while (!dq.empty() && dq.front() < i - k + 1) {
dq.pop_front();
}
while (!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
dq.push_back(i);
if (i >= k - 1) {
result.push_back(nums[dq.front()]);
}
}
return result;
}
知识点:
- 单调队列的概念与实现
- 双端队列的应用
- 时间复杂度:O(n)
2.3 二分查找
二分查找是高效的搜索算法,适用于有序数组或满足单调性的搜索空间。
2.3.1 搜索旋转排序数组 (LeetCode 33)
题目描述:在旋转排序数组中搜索目标值。
解题思路:二分查找,每次判断哪一半是有序的,然后决定搜索方向。
cpp
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
if (nums[left] <= nums[mid]) { // 左半部分有序
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else { // 右半部分有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
知识点:
- 二分查找的变体
- 判断有序区间的方法
- 时间复杂度:O(log n)
2.3.2 在排序数组中查找元素的第一个和最后一个位置 (LeetCode 34)
题目描述:给定一个按照升序排列的整数数组和一个目标值,找出给定目标值在数组中的开始位置和结束位置。
解题思路:两次二分查找,分别找左边界和右边界。
cpp
vector<int> searchRange(vector<int>& nums, int target) {
return {findLeft(nums, target), findRight(nums, target)};
}
int findLeft(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1, result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
result = mid;
right = mid - 1; // 继续向左找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
int findRight(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1, result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
result = mid;
left = mid + 1; // 继续向右找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
知识点:
- 二分查找找边界
- 左右边界的不同处理
- 时间复杂度:O(log n)
2.3.3 搜索二维矩阵 II (LeetCode 240)
题目描述:编写一个高效的算法来搜索 m x n 矩阵中的一个目标值。
解题思路:从右上角开始搜索,如果当前值大于目标值则向左移动,小于则向下移动。
cpp
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int row = 0, col = matrix[0].size() - 1;
while (row < matrix.size() && col >= 0) {
if (matrix[row][col] == target) {
return true;
} else if (matrix[row][col] > target) {
col--;
} else {
row++;
}
}
return false;
}
知识点:
- 二维矩阵的搜索技巧
- 利用矩阵的有序性质
- 时间复杂度:O(m + n)
2.4 排序算法
2.4.1 颜色分类 (LeetCode 75)
题目描述:给定一个包含红色、白色和蓝色的数组,原地对它们进行排序,使得相同颜色的元素相邻。
解题思路 :三指针(荷兰国旗问题),left 指向 0 的右边界,right 指向 2 的左边界,curr 遍历数组。
cpp
void sortColors(vector<int>& nums) {
int left = 0, right = nums.size() - 1, curr = 0;
while (curr <= right) {
if (nums[curr] == 0) {
swap(nums[curr], nums[left]);
left++;
curr++;
} else if (nums[curr] == 2) {
swap(nums[curr], nums[right]);
right--;
} else {
curr++;
}
}
}
知识点:
- 荷兰国旗问题
- 三指针分区技巧
- 时间复杂度:O(n),一次遍历
2.4.2 数组中的第K个最大元素 (LeetCode 215)
题目描述:给定整数数组 nums 和整数 k,返回数组中第 k 个最大的元素。
解题思路:可以使用排序、堆或快速选择算法。这里展示排序方法。
cpp
int findKthLargest(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
return nums[nums.size() - k];
}
知识点:
- 排序的基本应用
- 堆(优先队列)的应用
- 快速选择算法(平均 O(n))
2.4.3 前 K 个高频元素 (LeetCode 347)
题目描述:给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
解题思路:桶排序思想,使用频率作为桶的索引,从高频到低频收集结果。
cpp
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> freqMap;
int maxFreq = 0;
for (int num : nums) {
freqMap[num]++;
maxFreq = max(maxFreq, freqMap[num]);
}
vector<vector<int>> buckets(maxFreq + 1);
for (auto& pair : freqMap) {
buckets[pair.second].push_back(pair.first);
}
vector<int> result;
for (int i = maxFreq; i >= 0 && result.size() <; i--) {
for (int num : buckets[i]) {
result.push_back(num);
if (result.size() == k) break;
}
}
return result;
}
知识点:
- 桶排序的应用
- 频率统计技巧
- 时间复杂度:O(n)
3. 搜索与遍历篇
3.1 二叉树基础
二叉树是面试中最常见的数据结构之一,掌握树的遍历和递归思维至关重要。
3.1.1 二叉树的中序遍历 (LeetCode 94)
题目描述:给定一个二叉树的根节点 root,返回它的中序遍历。
cpp
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
inorder(root, result);
return result;
}
void inorder(TreeNode* node, vector<int>& result) {
if (node == nullptr) return;
inorder(node->left, result);
result.push_back(node->val);
inorder(node->right, result);
}
知识点:
- 二叉树的三种遍历方式(前序、中序、后序)
- 递归与迭代两种实现
- 栈在非递归遍历中的应用
3.1.2 二叉树的最大深度 (LeetCode 104)
题目描述:给定一个二叉树,找出其最大深度。
cpp
int maxDepth(TreeNode* root) {
if (root == nullptr) return 0;
int leftDepth = maxDepth(root->left);
int rightDepth = maxDepth(root->right);
return max(leftDepth, rightDepth) + 1;
}
知识点:
- 递归的基本思维
- 树的深度定义
- 时间复杂度:O(n)
3.1.3 翻转二叉树 (LeetCode 226)
题目描述:翻转一棵二叉树。
cpp
TreeNode* invertTree(TreeNode* root) {
if (root == nullptr) return nullptr;
TreeNode* left = invertTree(root->left);
TreeNode* right = invertTree(root->right);
root->left = right;
root->right = left;
return root;
}
知识点:
- 递归处理树的问题
- 后序遍历的思想
- 原地修改树结构
3.1.4 对称二叉树 (LeetCode 101)
题目描述:给定一个二叉树,检查它是否是镜像对称的。
cpp
bool isSymmetric(TreeNode* root) {
if (root == nullptr) return true;
return isMirror(root->left, root->right);
}
bool isMirror(TreeNode* left, TreeNode* right) {
if (left == nullptr && right == nullptr) return true;
if (left == nullptr || right == nullptr) return false;
return (left->val == right->val) &&
isMirror(left->left, right->right) &&
isMirror(left->right, right->left);
}
知识点:
- 递归比较两棵树
- 镜像对称的判断条件
- 时间复杂度:O(n)
3.1.5 二叉树的层序遍历 (LeetCode 102)
题目描述:给你一个二叉树的根节点,返回其节点值的层序遍历。
cpp
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if (root == nullptr) return result;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int levelSize = q.size();
vector<int> currentLevel;
for (int i = 0; i < levelSize; i++) {
TreeNode* node = q.front();
q.pop();
currentLevel.push_back(node->val);
if (node->left != nullptr) q.push(node->left);
if (node->right != nullptr) q.push(node->right);
}
result.push_back(currentLevel);
}
return result;
}
知识点:
- BFS(广度优先搜索)
- 队列在层序遍历中的应用
- 按层处理的技巧
3.1.6 从前序与中序遍历序列构造二叉树 (LeetCode 105)
题目描述:给定一棵树的前序遍历和中序遍历,构造二叉树。
cpp
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
unordered_map<int, int> inorderMap;
for (int i = 0; i < inorder.size(); i++) {
inorderMap[inorder[i]] = i;
}
return build(preorder, 0, preorder.size() - 1,
inorder, 0, inorder.size() - 1, inorderMap);
}
TreeNode* build(vector<int>& preorder, int preStart, int preEnd,
vector<int>& inorder, int inStart, int inEnd,
unordered_map<int, int>& inorderMap) {
if (preStart > preEnd) return nullptr;
int rootVal = preorder[preStart];
TreeNode* root = new TreeNode(rootVal);
int rootIndex = inorderMap[rootVal];
int leftSize = rootIndex - inStart;
root->left = build(preorder, preStart + 1, preStart + leftSize,
inorder, inStart, rootIndex - 1, inorderMap);
root->right = build(preorder, preStart + leftSize + 1, preEnd,
inorder, rootIndex + 1, inEnd, inorderMap);
return root;
}
知识点:
- 前序遍历和中序遍历的性质
- 递归构建树的方法
- 哈希表加速查找根节点位置
3.1.7 二叉树展开为链表 (LeetCode 114)
题目描述:给定一个二叉树,原地将它展开为一个单链表。
cpp
void flatten(TreeNode* root) {
TreeNode* curr = root;
while (curr != nullptr) {
if (curr->left != nullptr) {
TreeNode* pred = curr->left;
while (pred->right != nullptr) {
pred = pred->right;
}
pred->right = curr->right;
curr->right = curr->left;
curr->left = nullptr;
}
curr = curr->right;
}
}
知识点:
- 前驱节点的概念
- 原地修改树结构
- Morris 遍历的思想
3.1.8 二叉树的直径 (LeetCode 543)
题目描述:给定一棵二叉树,返回它的直径(任意两个节点之间最长路径的长度)。
cpp
int diameterOfBinaryTree(TreeNode* root) {
int diameter = 0;
dfs(root, diameter);
return diameter;
}
int dfs(TreeNode* node, int& diameter) {
if (node == nullptr) return 0;
int left_depth = dfs(node->left, diameter);
int right_depth = dfs(node->right, diameter);
diameter = max(diameter, left_depth + right_depth);
return max(left_depth, right_depth) + 1;
}
知识点:
- 递归过程中维护全局最优解
- 深度与直径的关系
- 时间复杂度:O(n)
3.1.9 合并二叉树 (LeetCode 617)
题目描述:给定两个二叉树,合并成一个新的二叉树。
cpp
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
if (root1 == nullptr) return root2;
if (root2 == nullptr) return root1;
TreeNode* merged = new TreeNode(root1->val + root2->val);
merged->left = mergeTrees(root1->left, root2->left);
merged->right = mergeTrees(root1->right, root2->right);
return merged;
}
知识点:
- 递归处理两棵树
- 空节点的处理
- 时间复杂度:O(min(n1, n2))
3.2 二叉搜索树
二叉搜索树(BST)具有特殊的性质:左子树所有节点值小于根节点,右子树所有节点值大于根节点。
3.2.1 验证二叉搜索树 (LeetCode 98)
题目描述:给定一个二叉树的根节点,判断其是否是一个有效的二叉搜索树。
cpp
bool isValidBST(TreeNode* root) {
return validate(root, LONG_MIN, LONG_MAX);
}
bool validate(TreeNode* node, long minVal, long maxVal) {
if (node == nullptr) return true;
if (node->val <= minVal || node->val >= maxVal) return false;
return validate(node->left, minVal, node->val) &&
validate(node->right, node->val, maxVal);
}
知识点:
- BST 的性质
- 递归传递范围约束
- 使用 long 类型避免边界溢出
3.2.2 不同的二叉搜索树 (LeetCode 96)
题目描述:给你一个整数 n,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的二叉搜索树有多少种。
cpp
int numTrees(int n) {
vector<int> dp(n + 1, 0);
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
知识点:
- 卡特兰数
- 动态规划在树问题中的应用
- 分治思想
3.2.3 把二叉搜索树转换为累加树 (LeetCode 538)
题目描述:给出二叉搜索树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree)。
cpp
TreeNode* convertBST(TreeNode* root) {
int sum = 0;
dfs(root, sum);
return root;
}
void dfs(TreeNode* node, int& sum) {
if (node == nullptr) return;
dfs(node->right, sum);
int temp = node->val;
node->val += sum;
sum += temp;
dfs(node->left, sum);
}
知识点:
- BST 的反向中序遍历(右-根-左)
- 累加和的维护
- 时间复杂度:O(n)
3.3 图论基础
3.3.1 岛屿数量 (LeetCode 200)
题目描述:给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。
解题思路:遍历网格,遇到 '1' 时岛屿数量加 1,并使用 DFS 将相连的陆地标记为已访问。
cpp
int numIslands(vector<vector<char>>& grid) {
if (grid.empty()) return 0;
int m = grid.size(), n = grid[0].size();
int count = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
count++;
dfs(grid, i, j);
}
}
}
return count;
}
void dfs(vector<vector<char>>& grid, int i, int j) {
if (i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size()
|| grid[i][j] == '0') {
return;
}
grid[i][j] = '0';
dfs(grid, i - 1, j);
dfs(grid, i + 1, j);
dfs(grid, i, j - 1);
dfs(grid, i, j + 1);
}
知识点:
- DFS 在二维网格中的应用
- 连通分量的概念
- 原地标记避免重复访问
3.3.2 课程表 (LeetCode 207)
题目描述:判断你是否能够完成所有课程(检测有向图是否有环)。
解题思路:拓扑排序,使用 BFS 处理入度为 0 的节点。
cpp
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> adj(numCourses);
vector<int> inDegree(numCourses, 0);
for (auto& pre : prerequisites) {
adj[pre[1]].push_back(pre[0]);
inDegree[pre[0]]++;
}
queue<int> q;
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) q.push(i);
}
int count = 0;
while (!q.empty()) {
int node = q.front();
q.pop();
count++;
for (int neighbor : adj[node]) {
if (--inDegree[neighbor] == 0) {
q.push(neighbor);
}
}
}
return count == numCourses;
}
知识点:
- 拓扑排序的概念
- BFS 实现拓扑排序
- 检测有向环的方法
3.3.3 除法求值 (LeetCode 399)
题目描述:给你一些除法等式,回答一些查询的除法结果。
解题思路:使用并查集(带权),维护节点到根节点的比值关系。
cpp
class UnionFind {
private:
unordered_map<string, string> parent;
unordered_map<string, double> value;
public:
string find(string x) {
if (parent.find(x) == parent.end()) {
parent[x] = x;
value[x] = 1.0;
return x;
}
if (parent[x] == x) return x;
string root = find(parent[x]);
value[x] = value[x] * value[parent[x]];
parent[x] = root;
return root;
}
void unionSet(string x, string y, double val) {
string rootX = find(x), rootY = find(y);
if (rootX == rootY) return;
parent[rootX] = rootY;
value[rootX] = val * value[y] / value[x];
}
double getResult(string x, string y) {
if (find(x) != find(y)) return -1.0;
return value[x] / value[y];
}
};
知识点:
- 并查集的基本操作
- 带权并查集
- 路径压缩与按秩合并
4. 高级算法篇
4.1 回溯算法
回溯算法是一种通过探索所有可能的候选解来找出所有解的算法,如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来丢弃该解。
4.1.1 电话号码的字母组合 (LeetCode 17)
题目描述:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
cpp
vector<string> letterCombinations(string digits) {
vector<string> result;
if (digits.empty()) return result;
unordered_map<char, string> mapping = {
{'2', "abc"}, {'3', "def"}, {'4', "ghi"},
{'5', "jkl"}, {'6', "mno"}, {'7', "pqrs"},
{'8', "tuv"}, {'9', "wxyz"}
};
string current;
backtrack(digits, 0, mapping, current, result);
return result;
}
void backtrack(string& digits, int index, unordered_map<char, string>& mapping,
string& current, vector<string>& result) {
if (index == digits.size()) {
result.push_back(current);
return;
}
string letters = mapping[digits[index]];
for (char c : letters) {
current.push_back(c);
backtrack(digits, index + 1, mapping, current, result);
current.pop_back();
}
}
知识点:
- 回溯算法的基本框架
- 选择-探索-撤销的模式
- 时间复杂度:O(4^n)
4.1.2 括号生成 (LeetCode 22)
题目描述:数字 n 代表生成括号的对数,生成所有可能的并且有效的括号组合。
cpp
vector<string> generateParenthesis(int n) {
vector<string> result;
string current;
backtrack(result, current, 0, 0, n);
return result;
}
void backtrack(vector<string>& result, string& current, int open, int close, int n) {
if (current.size() == 2 * n) {
result.push_back(current);
return;
}
if (open < n) {
current.push_back('(');
backtrack(result, current, open + 1, close, n);
current.pop_back();
}
if (close < open) {
current.push_back(')');
backtrack(result, current, open, close + 1, n);
current.pop_back();
}
}
知识点:
- 剪枝策略:只有当
close < open时才能添加右括号 - 合法括号序列的性质
- 时间复杂度:卡特兰数 O(4^n / sqrt(n))
4.1.3 组合总和 (LeetCode 39)
题目描述:给定一个无重复元素的正整数数组 candidates 和一个目标数 target,找出所有可以使数字和为目标数的组合(同一个数字可以无限重复使用)。
cpp
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> result;
vector<int> path;
backtrack(result, path, candidates, target, 0);
return result;
}
void backtrack(vector<vector<int>>& result, vector<int>& path,
const vector<int>& candidates, int target, int start) {
if (target == 0) {
result.push_back(path);
return;
}
for (int i = start; i < candidates.size(); i++) {
if (candidates[i] > target) continue;
path.push_back(candidates[i]);
backtrack(result, path, candidates, target - candidates[i], i);
path.pop_back();
}
}
知识点:
- 可重复选择的组合问题
start参数避免重复组合- 剪枝优化
4.1.4 全排列 (LeetCode 46)
题目描述:给定一个不含重复数字的数组,返回其所有可能的全排列。
cpp
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result;
backtrack(result, nums, 0);
return result;
}
void backtrack(vector<vector<int>>& result, vector<int>& nums, int start) {
if (start == nums.size()) {
result.push_back(nums);
return;
}
for (int i = start; i < nums.size(); i++) {
swap(nums[start], nums[i]);
backtrack(result, nums, start + 1);
swap(nums[start], nums[i]);
}
}
知识点:
- 交换法生成排列
- 原地修改数组
- 时间复杂度:O(n! * n)
4.1.5 子集 (LeetCode 78)
题目描述:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集。
cpp
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
vector<int> current;
backtrack(nums, 0, current, result);
return result;
}
void backtrack(vector<int>& nums, int start, vector<int>& current,
vector<vector<int>>& result) {
result.push_back(current);
for (int i = start; i < nums.size(); i++) {
current.push_back(nums[i]);
backtrack(nums, i + 1, current, result);
current.pop_back();
}
}
知识点:
- 子集问题与组合问题的区别
- 每个节点都是合法解
- 时间复杂度:O(n * 2^n)
4.1.6 单词搜索 (LeetCode 79)
题目描述:给定一个 m x n 二维字符网格 board 和一个字符串 word,判断 word 是否存在于网格中。
cpp
bool exist(vector<vector<char>>& board, string word) {
int m = board.size(), n = board[0].size();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == word[0]) {
if (dfs(board, word, i, j, 0)) return true;
}
}
}
return false;
}
bool dfs(vector<vector<char>>& board, string& word, int i, int j, int index) {
if (index == word.size()) return true;
int m = board.size(), n = board[0].size();
if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != word[index]) {
return false;
}
char temp = board[i][j];
board[i][j] = '#';
bool found = dfs(board, word, i + 1, j, index + 1) ||
dfs(board, word, i - 1, j, index + 1) ||
dfs(board, word, i, j + 1, index + 1) ||
dfs(board, word, i, j - 1, index + 1);
board[i][j] = temp;
return found;
}
知识点:
- DFS + 回溯在二维网格中的应用
- 标记已访问位置避免重复使用
- 四方向搜索
4.1.7 删除无效的括号 (LeetCode 301)
题目描述:删除最小数量的无效括号,使得输入的字符串有效。
解题思路:BFS 逐层删除括号,找到第一个有效的字符串集合。
cpp
vector<string> removeInvalidParentheses(string s) {
vector<string> result;
queue<string> q;
unordered_set<string> visited;
q.push(s);
visited.insert(s);
bool found = false;
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
string current = q.front();
q.pop();
if (isValid(current)) {
result.push_back(current);
found = true;
}
if (found) continue;
for (int j = 0; j < current.size(); j++) {
if (current[j] != '(' && current[j] != ')') continue;
string next = current.substr(0, j) + current.substr(j + 1);
if (visited.find(next) == visited.end()) {
visited.insert(next);
q.push(next);
}
}
}
if (found) break;
}
return result;
}
知识点:
- BFS 求最少删除
- 去重使用哈希集合
- 逐层搜索保证最少删除
4.2 贪心算法
贪心算法在每一步选择中都采取在当前状态下最好或最优的选择,从而希望导致结果是全局最好或最优的。
4.2.1 跳跃游戏 (LeetCode 55)
题目描述:给定一个非负整数数组 nums,你最初位于数组的第一个下标。判断是否能够到达最后一个下标。
cpp
bool canJump(vector<int>& nums) {
int maxReach = 0;
for (int i = 0; i < nums.size(); i++) {
if (i > maxReach) return false;
maxReach = max(maxReach, i + nums[i]);
}
return true;
}
知识点:
- 贪心维护最远可达位置
- 时间复杂度:O(n)
4.2.2 合并区间 (LeetCode 56)
题目描述:给出一组区间,请合并所有重叠的区间。
cpp
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end());
vector<vector<int>> result;
result.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] <= result.back()[1]) {
result.back()[1] = max(result.back()[1], intervals[i][1]);
} else {
result.push_back(intervals[i]);
}
}
return result;
}
知识点:
- 排序后贪心合并
- 区间问题的通用解法
- 时间复杂度:O(n log n)
4.2.3 根据身高重建队列 (LeetCode 406)
题目描述:假设有打乱顺序的一群人站成一个队列,按身高降序、k 值升序排序后,逐个插入到正确位置。
cpp
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(), people.end(), [](const vector<int>& a, const vector<int>& b) {
if (a[0] != b[0]) return a[0] > b[0];
return a[1] < b[1];
});
vector<vector<int>> result;
for (const auto& p : people) {
result.insert(result.begin() + p[1], p);
}
return result;
}
知识点:
- 贪心策略:先处理高个子
vector::insert的使用- 时间复杂度:O(n²)
4.2.4 任务调度器 (LeetCode 621)
题目描述:给定一个用字符数组表示的 CPU 需要执行的任务列表和一个冷却时间 n,计算完成所有任务所需要的最短时间。
cpp
int leastInterval(vector<char>& tasks, int n) {
vector<int> count(26, 0);
for (char task : tasks) {
count[task - 'A']++;
}
sort(count.begin(), count.end(), greater<int>());
int maxFreq = count[0];
int maxFreqCount = 1;
for (int i = 1; i < 26; i++) {
if (count[i] == maxFreq) maxFreqCount++;
else break;
}
int result = (maxFreq - 1) * (n + 1) + maxFreqCount;
return max(result, (int)tasks.size());
}
知识点:
- 贪心:先安排频率最高的任务
- 公式的推导
- 边界情况:任务数超过计算值
4.2.5 最短无序连续子数组 (LeetCode 581)
题目描述:找到一个连续子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
cpp
int findUnsortedSubarray(vector<int>& nums) {
int n = nums.size();
int maxVal = nums[0], minVal = nums[n - 1];
int end = -1, start = 0;
for (int i = 1; i < n; i++) {
if (nums[i] < maxVal) end = i;
else maxVal = nums[i];
}
for (int i = n - 2; i >= 0; i--) {
if (nums[i] > minVal) start = i;
else minVal = nums[i];
}
return end - start + 1;
}
知识点:
- 双向扫描找边界
- 维护前缀最大值和后缀最小值
- 时间复杂度:O(n)
4.3 动态规划
动态规划是算法面试中最重要也是最难的部分,核心思想是将复杂问题分解为重叠子问题。
4.3.1 最大子数组和 (LeetCode 53)
题目描述:给定一个整数数组 nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
cpp
int maxSubArray(vector<int>& nums) {
int maxSum = nums[0];
int currSum = nums[0];
for (int i = 1; i < nums.size(); i++) {
currSum = max(nums[i], currSum + nums[i]);
maxSum = max(maxSum, currSum);
}
return maxSum;
}
知识点:
- Kadane 算法
- 状态转移:
dp[i] = max(nums[i], dp[i-1] + nums[i]) - 时间复杂度:O(n)
4.3.2 爬楼梯 (LeetCode 70)
题目描述:假设你正在爬楼梯,需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶,有多少种不同的方法可以爬到楼顶?
cpp
int climbStairs(int n) {
if (n <= 2) return n;
int prev2 = 1, prev1 = 2;
for (int i = 3; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
知识点:
- 斐波那契数列
- 空间优化:只用两个变量
- 时间复杂度:O(n),空间复杂度:O(1)
4.3.3 不同路径 (LeetCode 62)
题目描述:一个机器人从左上角出发,每次只能向下或向右移动一步,到达右下角有多少条不同路径。
cpp
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
知识点:
- 二维动态规划
- 边界初始化
- 状态转移方程
4.3.4 最小路径和 (LeetCode 64)
题目描述:给定一个包含非负整数的 m x n 网格,找一条从左上角到右下角的路径,使得路径上的数字总和为最小。
cpp
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
for (int i = 1; i < m; i++) grid[i][0] += grid[i - 1][0];
for (int j = 1; j < n; j++) grid[0][j] += grid[0][j - 1];
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
grid[i][j] += min(grid[i - 1][j], grid[i][j - 1]);
}
}
return grid[m - 1][n - 1];
}
知识点:
- 原地修改 grid 节省空间
- 状态转移方程
- 时间复杂度:O(m * n)
4.3.5 编辑距离 (LeetCode 72)
题目描述:给定两个单词 word1 和 word2,计算将 word1 转换成 word2 所使用的最少操作数。
cpp
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}
}
}
return dp[m][n];
}
知识点:
- 经典编辑距离问题
- 三种操作:插入、删除、替换
- 状态转移方程的推导
4.3.6 最长回文子串 (LeetCode 5)
题目描述:给你一个字符串 s,找到 s 中最长的回文子串。
解题思路:中心扩展法,遍历每个可能的回文中心,向两边扩展。
cpp
// 在数组题部分已实现最长回文子串的解法(LeetCode 647 使用中心扩展)
// 最长回文子串同样使用中心扩展:
string longestPalindrome(string s) {
int n = s.size(), start = 0, maxLen = 1;
for (int i = 0; i < 2 * n - 1; i++) {
int l = i / 2, r = i / 2 + i % 2;
while (l >= 0 && r < n && s[l] == s[r]) {
if (r - l + 1 > maxLen) {
start = l;
maxLen = r - l + 1;
}
--l;
++r;
}
}
return s.substr(start, maxLen);
}
知识点:
- 中心扩展法
- 奇数和偶数长度回文的处理
- 时间复杂度:O(n²)
4.3.7 正则表达式匹配 (LeetCode 10)
题目描述 :实现一个正则表达式函数,支持 . 和 *。
cpp
bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
dp[0][0] = true;
for (int j = 1; j <= n; j++) {
if (p[j - 1] == '*') dp[0][j] = dp[0][j - 2];
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p[j - 1] == s[i - 1] || p[j - 1] == '.') {
dp[i][j] = dp[i - 1][j - 1];
} else if (p[j - 1] == '*') {
dp[i][j] = dp[i][j - 2];
if (p[j - 2] == s[i - 1] || p[j - 2] == '.') {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
}
return dp[m][n];
}
知识点:
- 二维动态规划
*的两种使用方式:匹配 0 次或多次- 初始化的特殊情况
4.3.8 买卖股票的最佳时机 (LeetCode 121)
题目描述:给定一个数组 prices,它的第 i 个元素 pricesi 表示一支给定股票第 i 天的价格,设计一个算法来计算你所能获取的最大利润。
cpp
int maxProfit(vector<int>& prices) {
int minPrice = INT_MAX;
int maxProfit = 0;
for (int price : prices) {
minPrice = min(minPrice, price);
maxProfit = max(maxProfit, price - minPrice);
}
return maxProfit;
}
知识点:
- 维护历史最低价
- 贪心思想
- 时间复杂度:O(n)
4.3.9 最佳买卖股票时机含冷冻期 (LeetCode 309)
题目描述:设计一个算法计算最大利润,卖出股票后无法在第二天买入(冷冻期一天)。
cpp
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(3, 0));
dp[0][0] = -prices[0];
for (int i = 1; i < n; i++) {
dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i]);
dp[i][1] = dp[i-1][0] + prices[i];
dp[i][2] = max(dp[i-1][1], dp[i-1][2]);
}
return max(dp[n-1][1], dp[n-1][2]);
}
知识点:
- 多状态动态规划
- 状态机模型
- 三种状态的定义与转移
4.3.10 单词拆分 (LeetCode 139)
题目描述:给定一个字符串 s 和一个字符串字典 wordDict,判断 s 是否可以被拆分为一个或多个在字典中出现的单词。
cpp
bool wordBreak(string s, vector<string>& wordDict) {
int n = s.size();
vector<bool> dp(n + 1, false);
dp[0] = true;
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && wordSet.count(s.substr(j, i - j))) {
dp[i] = true;
break;
}
}
}
return dp[n];
}
知识点:
- 一维动态规划
- 哈希集合加速查找
- 双重循环遍历所有分割点
4.3.11 乘积最大子数组 (LeetCode 152)
题目描述:给你一个整数数组 nums,找出数组中乘积最大的非空连续子数组。
cpp
int maxProduct(vector<int>& nums) {
int maxProd = nums[0], currMax = nums[0], currMin = nums[0];
for (int i = 1; i < nums.size(); i++) {
int tempMax = currMax;
currMax = max({nums[i], currMax * nums[i], currMin * nums[i]});
currMin = min({nums[i], tempMax * nums[i], currMin * nums[i]});
maxProd = max(maxProd, currMax);
}
return maxProd;
}
知识点:
- 同时维护最大值和最小值(负数情况)
- 三个候选值的比较
- 时间复杂度:O(n)
4.3.12 打家劫舍 (LeetCode 198)
题目描述:你是一个专业的小偷,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。计算一夜之内能够偷到的最高金额。
cpp
int rob(vector<int>& nums) {
if (nums.empty()) return 0;
int n = nums.size();
if (n == 1) return nums[0];
int prev2 = nums[0];
int prev1 = max(nums[0], nums[1]);
for (int i = 2; i < n; i++) {
int curr = max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
知识点:
- 经典动态规划问题
- 状态压缩:O(1) 空间
- 状态转移方程:
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
4.3.13 打家劫舍 III (LeetCode 337)
题目描述:小偷发现了一个新的可行窃的地区,这个地区只有一个入口,称之为"根"。除了"根"之外,每栋房子有且只有一个"父"房子与之相连。
cpp
int rob(TreeNode* root) {
vector<int> result = dfs(root);
return max(result[0], result[1]);
}
vector<int> dfs(TreeNode* node) {
if (node == nullptr) return {0, 0};
vector<int> left = dfs(node->left);
vector<int> right = dfs(node->right);
int robCurrent = node->val + left[1] + right[1];
int notRobCurrent = max(left[0], left[1]) + max(right[0], right[1]);
return {robCurrent, notRobCurrent};
}
知识点:
- 树形动态规划
- 返回值为二元组(偷/不偷当前节点)
- 后序遍历 + 状态转移
4.3.14 完全平方数 (LeetCode 279)
题目描述:给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。
cpp
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
dp[i] = min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
知识点:
- 完全背包问题的变体
- 枚举所有可能的完全平方数
- 时间复杂度:O(n * sqrt(n))
4.3.15 零钱兑换 (LeetCode 322)
题目描述:给定不同面额的硬币 coins 和一个总金额 amount,计算可以凑成总金额所需的最少的硬币个数。
cpp
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i && dp[i - coin] != INT_MAX) {
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
知识点:
- 完全背包问题
- 状态转移方程
INT_MAX溢出的处理
4.3.16 最长递增子序列 (LeetCode 300)
题目描述:给你一个整数数组 nums,找到其中最长严格递增子序列的长度。
cpp
int lengthOfLIS(vector<int>& nums) {
vector<int> tails;
for (int num : nums) {
auto it = lower_bound(tails.begin(), tails.end(), num);
if (it == tails.end()) {
tails.push_back(num);
} else {
*it = num;
}
}
return tails.size();
}
知识点:
- 贪心 + 二分查找
tails数组维护每个长度的最小末尾- 时间复杂度:O(n log n)
4.3.17 最大正方形 (LeetCode 221)
题目描述:在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。
cpp
int maximalSquare(vector<vector<char>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
int maxSide = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
}
maxSide = max(maxSide, dp[i][j]);
}
}
}
return maxSide * maxSide;
}
知识点:
- 二维动态规划
- 状态转移方程:
dp[i][j] = min(左, 上, 左上) + 1 - 时间复杂度:O(m * n)
4.3.18 戳气球 (LeetCode 312)
题目描述 :有 n 个气球,每次戳破一个,得到的金币数等于 nums[left] * nums[i] * nums[right],求最多金币数。
cpp
int maxCoins(vector<int>& nums) {
int n = nums.size();
vector<int> val(n + 2, 1);
for (int i = 0; i < n; i++) val[i + 1] = nums[i];
vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
for (int len = 1; len <= n; len++) {
for (int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
for (int k = i; k <= j; k++) {
dp[i][j] = max(dp[i][j],
dp[i][k - 1] + dp[k + 1][j] + val[i - 1] * val[k] * val[j + 1]);
}
}
}
return dp[1][n];
}
知识点:
- 区间动态规划
- 逆向思维:最后戳破哪个气球
- 两侧添加虚拟边界值 1
4.3.19 比特位计数 (LeetCode 338)
题目描述:给定一个整数 n,对于 0 <= i <= n 中的每个 i,计算其二进制表示中 1 的个数。
cpp
vector<int> countBits(int n) {
vector<int> dp(n + 1, 0);
for (int i = 1; i <= n; i++) {
dp[i] = dp[i >> 1] + (i & 1);
}
return dp;
}
知识点:
- 位运算技巧
i >> 1相当于i / 2i & 1判断奇偶- 时间复杂度:O(n)
4.3.20 分割等和子集 (LeetCode 416)
题目描述:给定一个只包含正整数的非空数组,判断是否可以将这个数组分割为两个元素和相等的子集。
cpp
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 != 0) return false;
int target = sum / 2;
vector<bool> dp(target + 1, false);
dp[0] = true;
for (int num : nums) {
for (int j = target; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target];
}
知识点:
- 0-1 背包问题
- 空间优化:一维数组
- 倒序遍历避免重复选择
4.3.21 目标和 (LeetCode 494)
题目描述:给你一个整数数组 nums 和一个整数 target,向数组中的每个整数前添加 '+' 或 '-',计算可以使最终结果为 target 的方法数。
cpp
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum < abs(target) || (sum + target) % 2 != 0) return 0;
int targetSum = (sum + target) / 2;
vector<int> dp(targetSum + 1, 0);
dp[0] = 1;
for (int num : nums) {
for (int j = targetSum; j >= num; j--) {
dp[j] += dp[j - num];
}
}
return dp[targetSum];
}
知识点:
- 问题转化:转化为背包问题
- 0-1 背包求方案数
- 数学推导:
package_a = (target + sum) / 2
4.3.22 回文子串 (LeetCode 647)
题目描述:给定一个字符串,返回这个字符串中回文子串的数目。
cpp
int countSubstrings(string s) {
int n = s.size(), ans = 0;
for (int i = 0; i < 2 * n - 1; ++i) {
int l = i / 2, r = i / 2 + i % 2;
while (l >= 0 && r < n && s[l] == s[r]) {
--l;
++r;
++ans;
}
}
return ans;
}
知识点:
- 中心扩展法
- 奇数和偶数长度的回文统一处理
- 时间复杂度:O(n²)
4.4 单调栈与单调队列
4.4.1 柱状图中最大的矩形 (LeetCode 84)
题目描述:给定 n 个非负整数表示柱状图中各个柱子的高度,求能勾勒出的最大矩形面积。
cpp
int largestRectangleArea(vector<int>& heights) {
stack<int> st;
int maxArea = 0;
for (int i = 0; i <= heights.size(); i++) {
int h = (i == heights.size()) ? 0 : heights[i];
while (!st.empty() && h < heights[st.top()]) {
int height = heights[st.top()];
st.pop();
int width = st.empty() ? i : i - st.top() - 1;
maxArea = max(maxArea, height * width);
}
st.push(i);
}
return maxArea;
}
知识点:
- 单调递增栈的应用
- 找每个柱子左右两边第一个比它矮的柱子
- 时间复杂度:O(n)
4.4.2 最大矩形 (LeetCode 85)
题目描述:给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形并返回其面积。
cpp
int maximalRectangle(vector<vector<char>>& matrix) {
if (matrix.empty()) return 0;
int rows = matrix.size(), cols = matrix[0].size();
vector<int> heights(cols, 0);
int maxArea = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
heights[j] = (matrix[i][j] == '1') ? heights[j] + 1 : 0;
}
maxArea = max(maxArea, largestRectangleArea(heights));
}
return maxArea;
}
知识点:
- 将二维问题转化为一维问题
- 复用柱状图最大矩形算法
- 时间复杂度:O(m * n)
4.5 并查集
4.5.1 执行交换后的最小汉明距离 (LeetCode 1722)
题目描述:给你两个整数数组 source 和 target,以及一个允许交换的下标对数组,求最小汉明距离。
cpp
int minimumHammingDistance(vector<int>& source, vector<int>& target,
vector<vector<int>>& allowedSwaps) {
int n = source.size();
vector<int> parent(n), rank(n, 0);
iota(parent.begin(), parent.end(), 0);
for (auto& swap : allowedSwaps) unite(parent, rank, swap[0], swap[1]);
unordered_map<int, unordered_map<int, int>> sourceGroups, targetGroups;
for (int i = 0; i < n; i++) {
int root = find(parent, i);
sourceGroups[root][source[i]]++;
targetGroups[root][target[i]]++;
}
int matches = 0;
for (auto& [root, sourceCount] : sourceGroups) {
for (auto& [val, count] : sourceCount) {
matches += min(count, targetGroups[root][val]);
}
}
return n - matches;
}
知识点:
- 并查集的应用
- 分组统计匹配
- 时间复杂度:O(n * alpha(n))
5. 高级数据结构篇
5.1 前缀树 (Trie)
5.1.1 实现 Trie (LeetCode 208)
题目描述 :实现一个 Trie(前缀树),包含 insert、search 和 startsWith 三个操作。
cpp
struct TrieNode {
bool isEnd;
TrieNode* children[26];
TrieNode() : isEnd(false) {
for (int i = 0; i < 26; i++) children[i] = nullptr;
}
};
class Trie {
private:
TrieNode* root;
public:
Trie() { root = new TrieNode(); }
void insert(string word) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (node->children[idx] == nullptr) {
node->children[idx] = new TrieNode();
}
node = node->children[idx];
}
node->isEnd = true;
}
bool search(string word) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (node->children[idx] == nullptr) return false;
node = node->children[idx];
}
return node->isEnd;
}
bool startsWith(string prefix) {
TrieNode* node = root;
for (char c : prefix) {
int idx = c - 'a';
if (node->children[idx] == nullptr) return false;
node = node->children[idx];
}
return true;
}
};
知识点:
- 前缀树的结构设计
- 字符串前缀匹配
- 空间换时间的思想
5.2 LRU 缓存
(已在链表部分 1.4.9 详细介绍)
5.3 位运算技巧
5.3.1 只出现一次的数字 (LeetCode 136)
题目描述:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
cpp
int singleNumber(vector<int>& nums) {
int result = 0;
for (int num : nums) {
result ^= num;
}
return result;
}
知识点:
- 异或运算的性质:
a ^ a = 0,a ^ 0 = a - 异或的交换律和结合律
- 时间复杂度:O(n),空间复杂度:O(1)
6. 二叉树进阶
6.1 二叉树中的最大路径和 (LeetCode 124)
题目描述:给你一个二叉树的根节点 root,返回其最大路径和(路径可以不经过根节点)。
cpp
int maxPathSum(TreeNode* root) {
int maxSum = INT_MIN;
dfs(root, maxSum);
return maxSum;
}
int dfs(TreeNode* node, int& maxSum) {
if (node == nullptr) return 0;
int leftGain = max(dfs(node->left, maxSum), 0);
int rightGain = max(dfs(node->right, maxSum), 0);
int currentPathSum = node->val + leftGain + rightGain;
maxSum = max(maxSum, currentPathSum);
return node->val + max(leftGain, rightGain);
}
知识点:
- 递归过程中维护全局最大值
- 左右子树贡献值取 0(负数不选)
- 区分"经过当前节点的路径"和"从当前节点出发的路径"
6.2 二叉树的最近公共祖先 (LeetCode 236)
题目描述:给定一个二叉树,找到该树中两个指定节点的最近公共祖先。
cpp
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr) return nullptr;
if (root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left != nullptr && right != nullptr) return root;
return (left != nullptr) ? left : right;
}
知识点:
- 后序遍历的思想
- 四种情况的判断
- 时间复杂度:O(n)
6.3 路径总和 III (LeetCode 437)
题目描述:给定一个二叉树的根节点和一个整数 targetSum,求该二叉树里节点值之和等于 targetSum 的路径的数目。
cpp
int pathSum(TreeNode* root, int targetSum) {
unordered_map<long long, int> prefixSumCount;
prefixSumCount[0] = 1;
return dfs(root, 0, targetSum, prefixSumCount);
}
int dfs(TreeNode* node, long long currentSum, int targetSum,
unordered_map<long long, int>& prefixSumCount) {
if (node == nullptr) return 0;
currentSum += node->val;
int count = prefixSumCount[currentSum - targetSum];
prefixSumCount[currentSum]++;
count += dfs(node->left, currentSum, targetSum, prefixSumCount);
count += dfs(node->right, currentSum, targetSum, prefixSumCount);
prefixSumCount[currentSum]--;
return count;
}
知识点:
- 前缀和在树上的应用
- 回溯维护前缀和计数
- 时间复杂度:O(n)
6.4 二叉树的序列化与反序列化 (LeetCode 297)
题目描述:设计一个算法来序列化和反序列化二叉树。
cpp
class Codec {
public:
string serialize(TreeNode* root) {
if (!root) return "[]";
stringstream ss;
ss << "[";
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* node = q.front();
q.pop();
if (node) {
ss << node->val;
q.push(node->left);
q.push(node->right);
} else {
ss << "null";
}
if (!q.empty()) ss << ",";
}
ss << "]";
return ss.str();
}
TreeNode* deserialize(string data) {
if (data == "[]") return nullptr;
string content = data.substr(1, data.size() - 2);
stringstream ss(content);
string token;
getline(ss, token, ',');
TreeNode* root = new TreeNode(stoi(token));
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* node = q.front();
q.pop();
if (!getline(ss, token, ',')) break;
if (token != "null") {
node->left = new TreeNode(stoi(token));
q.push(node->left);
}
if (!getline(ss, token, ',')) break;
if (token != "null") {
node->right = new TreeNode(stoi(token));
q.push(node->right);
}
}
return root;
}
};
知识点:
- 层序遍历实现序列化
- 使用 stringstream 处理字符串
- BFS 重建二叉树
7. 算法模板总结
7.1 回溯算法模板
cpp
void backtrack(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合) {
处理节点;
backtrack(路径,选择列表);
回溯,撤销处理结果;
}
}
7.2 滑动窗口模板
cpp
int slidingWindow(string s) {
unordered_map<char, int> window, need;
int left = 0, valid = 0;
for (int right = 0; right < s.size(); right++) {
char c = s[right];
// 窗口扩大逻辑
while (窗口需要收缩) {
char d = s[left];
left++;
// 窗口收缩逻辑
}
}
}
7.3 二分查找模板
cpp
int binarySearch(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
7.4 BFS 模板
cpp
int bfs(Node* start) {
queue<Node*> q;
unordered_set<Node*> visited;
q.push(start);
visited.insert(start);
int step = 0;
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
Node* curr = q.front();
q.pop();
if (curr == target) return step;
for (Node* neighbor : curr->neighbors) {
if (visited.find(neighbor) == visited.end()) {
q.push(neighbor);
visited.insert(neighbor);
}
}
}
step++;
}
return -1;
}
7.5 DFS 模板
cpp
void dfs(Node* node, unordered_set<Node*>& visited) {
if (node == nullptr) return;
visited.insert(node);
// 处理当前节点
for (Node* neighbor : node->neighbors) {
if (visited.find(neighbor) == visited.end()) {
dfs(neighbor, visited);
}
}
}
7.6 动态规划模板
cpp
// 1. 定义状态:dp[i] 或 dp[i][j] 表示什么
// 2. 写出状态转移方程
// 3. 确定初始条件
// 4. 确定遍历顺序
// 5. 确定返回值
int dpSolution(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 0);
// 初始条件
dp[0] = nums[0];
// 状态转移
for (int i = 1; i < n; i++) {
dp[i] = max(dp[i-1] + nums[i], nums[i]);
}
// 返回结果
return *max_element(dp.begin(), dp.end());
}
8. 时间复杂度与空间复杂度对照表
| 算法/数据结构 | 时间复杂度 | 空间复杂度 | 对应题目 |
|---|---|---|---|
| 哈希表查找 | O(1) 平均 | O(n) | 1, 49, 128, 560 |
| 双指针 | O(n) | O(1) | 11, 15, 19, 42, 283 |
| 滑动窗口 | O(n) | O(k) | 76, 438, 239 |
| 二分查找 | O(log n) | O(1) | 33, 34, 240 |
| BFS/DFS | O(V+E) | O(V) | 200, 207, 102 |
| 回溯 | O(2^n) 或 O(n!) | O(n) | 17, 22, 39, 46, 78, 79 |
| 动态规划 | O(n²) 或 O(mn) | O(n) 或 O(mn) | 53, 62, 72, 139, 300 |
| 排序 | O(n log n) | O(1) 或 O(n) | 75, 148, 215, 347 |
| 并查集 | O(n * α(n)) | O(n) | 399, 1722 |
| 单调栈 | O(n) | O(n) | 84, 85, 739 |
| 前缀和 | O(n) | O(n) | 560, 437 |
| 位运算 | O(n) | O(1) | 136, 338 |
9. 面试技巧与建议
9.1 解题步骤
- 理解题意:确保完全理解题目要求,确认输入输出格式
- 举例验证:用示例手动推演,验证理解是否正确
- 暴力解法:先想出最直接的解法,再优化
- 分析复杂度:评估时间和空间复杂度
- 优化思路:思考是否可以使用更优的数据结构或算法
- 编码实现:清晰地实现算法
- 测试验证:用示例和边界情况测试
9.2 常见优化技巧
| 技巧 | 适用场景 | 示例 |
|---|---|---|
| 空间换时间 | 频繁查找 | 哈希表 (1, 49) |
| 双指针 | 数组/链表遍历 | 11, 15, 19 |
| 滑动窗口 | 子串/子数组 | 76, 438 |
| 前缀和 | 区间和查询 | 560, 437 |
| 单调栈 | 下一个更大/更小元素 | 739, 84 |
| 位运算 | 状态压缩 | 136, 338 |
| 二分查找 | 有序数组搜索 | 33, 34 |
| 虚拟头节点 | 链表头节点可能变化 | 19, 21 |
9.3 常见错误提醒
- 整数溢出 :使用
long类型存储中间结果 (LeetCode 8) - 空指针访问 :链表操作前检查
nullptr(LeetCode 21) - 数组越界:循环条件和索引范围检查 (LeetCode 15)
- 重复计算:使用记忆化或 DP 避免重复计算 (LeetCode 70)
- 边界条件:空数组、单元素、全相同等特殊情况 (LeetCode 15)
10. 学习路线建议
入门阶段 (1-2 周)
- 掌握基本数据结构:数组、链表、栈、队列、哈希表
- 学习基本算法:排序、二分查找、双指针
- 对应题目:1, 15, 20, 21, 53, 70, 121, 136, 141, 206, 283
进阶阶段 (2-3 周)
- 深入学习:滑动窗口、BFS/DFS、树的遍历
- 掌握回溯算法和贪心算法
- 对应题目:22, 33, 39, 46, 49, 56, 78, 79, 94, 101, 102, 104, 200
高级阶段 (3-4 周)
- 精通动态规划:背包问题、区间DP、树形DP
- 掌握高级数据结构:并查集、Trie、单调栈/队列
- 对应题目:42, 72, 84, 139, 146, 207, 208, 300, 312, 322, 337, 399
冲刺阶段 (1-2 周)
- 综合练习,限时做题
- 复习薄弱知识点
- 模拟面试环境
结语
LeetCode Hot 100 是面试中最常见的题目集合,涵盖了数据结构与算法的核心知识点。通过系统学习这些题目,不仅可以掌握算法思想,还能培养解决问题的思维方式。
记住以下关键原则:
- 理解比记忆更重要:理解算法背后的原理,而不是死记硬背代码
- 多画图多推演:对于复杂问题,画图有助于理解
- 从暴力到优化:先理解暴力解法,再思考优化方案
- 举一反三:掌握一类问题的解法,而不是单个题目
- 持续练习:算法能力需要长期积累
祝你在算法学习的道路上不断进步!