LeetCode Hot 100 知识点总结与算法指南

LeetCode Hot 100 知识点总结与算法指南

作者 :玖釉-

适用人群 :准备面试的程序员、算法竞赛入门者、计算机科学学生

代码语言 :C++

题目来源:LeetCode Hot 100 题单https://leetcode.cn/problem-list/LTRv2Gcc/


目录

  1. 基础数据结构篇

  2. 核心算法思想篇

  3. 搜索与遍历篇

  4. 高级算法篇

  5. 高级数据结构篇


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)

题目描述:实现获取下一个排列的函数,将数字重新排列成字典序中下一个更大的排列。

解题思路

  1. 从右向左找到第一个非递增元素 nums[i]
  2. 从右向左找到第一个大于 nums[i] 的元素 nums[j]
  3. 交换 nums[i]nums[j]
  4. 反转 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)

题目描述:反转一个单链表。

解题思路 :迭代法:使用三个指针 prevcurrnext,逐个反转节点的指向。

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 / 2
  • i & 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(前缀树),包含 insertsearchstartsWith 三个操作。

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 = 0a ^ 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 解题步骤

  1. 理解题意:确保完全理解题目要求,确认输入输出格式
  2. 举例验证:用示例手动推演,验证理解是否正确
  3. 暴力解法:先想出最直接的解法,再优化
  4. 分析复杂度:评估时间和空间复杂度
  5. 优化思路:思考是否可以使用更优的数据结构或算法
  6. 编码实现:清晰地实现算法
  7. 测试验证:用示例和边界情况测试

9.2 常见优化技巧

技巧 适用场景 示例
空间换时间 频繁查找 哈希表 (1, 49)
双指针 数组/链表遍历 11, 15, 19
滑动窗口 子串/子数组 76, 438
前缀和 区间和查询 560, 437
单调栈 下一个更大/更小元素 739, 84
位运算 状态压缩 136, 338
二分查找 有序数组搜索 33, 34
虚拟头节点 链表头节点可能变化 19, 21

9.3 常见错误提醒

  1. 整数溢出 :使用 long 类型存储中间结果 (LeetCode 8)
  2. 空指针访问 :链表操作前检查 nullptr (LeetCode 21)
  3. 数组越界:循环条件和索引范围检查 (LeetCode 15)
  4. 重复计算:使用记忆化或 DP 避免重复计算 (LeetCode 70)
  5. 边界条件:空数组、单元素、全相同等特殊情况 (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 是面试中最常见的题目集合,涵盖了数据结构与算法的核心知识点。通过系统学习这些题目,不仅可以掌握算法思想,还能培养解决问题的思维方式。

记住以下关键原则:

  1. 理解比记忆更重要:理解算法背后的原理,而不是死记硬背代码
  2. 多画图多推演:对于复杂问题,画图有助于理解
  3. 从暴力到优化:先理解暴力解法,再思考优化方案
  4. 举一反三:掌握一类问题的解法,而不是单个题目
  5. 持续练习:算法能力需要长期积累

祝你在算法学习的道路上不断进步!

相关推荐
stanleyrain1 小时前
linux上无感操作Windows上的文件夹
linux·运维·windows
Hall_IC1 小时前
LSM6DS3TR-C现货询价丨粤科源兴ST代理商,专业FAE技术支持
c++
填满你的记忆1 小时前
《动态规划-基础篇》
算法·动态规划·力扣
进击的荆棘1 小时前
优选算法——队列+宽搜
数据结构·c++·算法·leetcode·bfs·队列
Irissgwe1 小时前
STL简介
c++·stl
黎阳之光1 小时前
虚实同源·数智治水:黎阳之光视频孪生,重构智慧水务新范式
运维·物联网·算法·安全·数字孪生
江屿风1 小时前
C++OJ题经验总结(竞赛)4
开发语言·c++·笔记·算法·dp·双指针
Deep-w1 小时前
【MATLAB】微电网四DG逆变器下垂策略与分布式MPC协同控制仿真分析
开发语言·分布式·算法·matlab
码上有光1 小时前
c++: 继承(下)
android·java·c++·多继承·菱形继承·虚继承