文章目录
- [LeetCode Hot 100](#LeetCode Hot 100)
-
- 哈希
-
- [1. 两数之和](#1. 两数之和)
- [2. 字母异位词分组](#2. 字母异位词分组)
- [3. 最长连续序列](#3. 最长连续序列)
- 双指针
-
- [4. 移动零](#4. 移动零)
- [5. 盛最多水的容器](#5. 盛最多水的容器)
- [6. 三数之和](#6. 三数之和)
- [7. 接雨水](#7. 接雨水)
- 滑动窗口
-
- [8. 无重复字符的最长子串](#8. 无重复字符的最长子串)
- [9. 找到字符串中所有字母异位词](#9. 找到字符串中所有字母异位词)
- 子串
-
- [10. 和为 K 的子数组](#10. 和为 K 的子数组)
- [11. 滑动窗口最大值](#11. 滑动窗口最大值)
- [12. 最小覆盖子串](#12. 最小覆盖子串)
- 普通数组
-
- [13. 最大子数组和](#13. 最大子数组和)
- [14. 合并区间](#14. 合并区间)
- [15. 轮转数组](#15. 轮转数组)
- [16. 除自身以外数组的乘积](#16. 除自身以外数组的乘积)
- [17. 缺失的第一个正数](#17. 缺失的第一个正数)
- 矩阵
-
- [18. 矩阵置零](#18. 矩阵置零)
- [19. 螺旋矩阵](#19. 螺旋矩阵)
- [20. 旋转图像](#20. 旋转图像)
- [21. 搜索二维矩阵 II](#21. 搜索二维矩阵 II)
- 链表
-
- [22. 相交链表](#22. 相交链表)
- [23. 反转链表](#23. 反转链表)
- [24. 回文链表](#24. 回文链表)
- [25. 环形链表](#25. 环形链表)
- [26. 环形链表 II](#26. 环形链表 II)
- [27. 合并两个有序链表](#27. 合并两个有序链表)
- [28. 两数相加](#28. 两数相加)
- [29. 删除链表的倒数第 N 个结点](#29. 删除链表的倒数第 N 个结点)
- [30. 两两交换链表中的节点](#30. 两两交换链表中的节点)
- [31. K 个一组翻转链表](#31. K 个一组翻转链表)
- [32. 随机链表的复制](#32. 随机链表的复制)
- [33. 排序链表](#33. 排序链表)
- [34. 合并 K 个升序链表](#34. 合并 K 个升序链表)
- [35. LRU 缓存](#35. LRU 缓存)
- 二叉树
-
- [36. 二叉树的中序遍历](#36. 二叉树的中序遍历)
- [37. 二叉树的最大深度](#37. 二叉树的最大深度)
- [38. 翻转二叉树](#38. 翻转二叉树)
- [39. 对称二叉树](#39. 对称二叉树)
- [40. 二叉树的直径](#40. 二叉树的直径)
- [41. 二叉树的层序遍历](#41. 二叉树的层序遍历)
- [42. 将有序数组转换为二叉搜索树](#42. 将有序数组转换为二叉搜索树)
- [43. 验证二叉搜索树](#43. 验证二叉搜索树)
- [44. 二叉搜索树中第 K 小的元素](#44. 二叉搜索树中第 K 小的元素)
- [45. 二叉树的右视图](#45. 二叉树的右视图)
- [46. 二叉树展开为链表](#46. 二叉树展开为链表)
- [47. 从前序与中序遍历序列构造二叉树](#47. 从前序与中序遍历序列构造二叉树)
- [48. 路径总和 III](#48. 路径总和 III)
- [49. 二叉树的最近公共祖先](#49. 二叉树的最近公共祖先)
- [50. 二叉树中的最大路径和](#50. 二叉树中的最大路径和)
- 图论
-
- [51. 岛屿数量](#51. 岛屿数量)
- [52. 腐烂的橘子](#52. 腐烂的橘子)
- [53. 课程表](#53. 课程表)
- [54. 实现 Trie (前缀树)](#54. 实现 Trie (前缀树))
- 回溯
-
- [55. 全排列](#55. 全排列)
- [56. 子集](#56. 子集)
- [57. 电话号码的字母组合](#57. 电话号码的字母组合)
- [58. 组合总和](#58. 组合总和)
- [59. 括号生成](#59. 括号生成)
- [60. 单词搜索](#60. 单词搜索)
- [61. 分割回文串](#61. 分割回文串)
- [62. N 皇后](#62. N 皇后)
- 二分查找
-
- [63. 搜索插入位置](#63. 搜索插入位置)
- [64. 搜索二维矩阵](#64. 搜索二维矩阵)
- [65. 在排序数组中查找元素的第一个和最后一个位置](#65. 在排序数组中查找元素的第一个和最后一个位置)
- [66. 搜索旋转排序数组](#66. 搜索旋转排序数组)
- [67. 寻找旋转排序数组中的最小值](#67. 寻找旋转排序数组中的最小值)
- [68. 寻找两个正序数组的中位数](#68. 寻找两个正序数组的中位数)
- 栈
-
- [69. 有效的括号](#69. 有效的括号)
- [70. 最小栈](#70. 最小栈)
- [71. 字符串解码](#71. 字符串解码)
- [72. 每日温度](#72. 每日温度)
- [73. 柱状图中最大的矩形](#73. 柱状图中最大的矩形)
- 堆
-
- [74. 数组中的第 K 个最大元素](#74. 数组中的第 K 个最大元素)
- [75. 前 K 个高频元素](#75. 前 K 个高频元素)
- [76. 数据流的中位数](#76. 数据流的中位数)
- 贪心算法
-
- [77. 买卖股票的最佳时机](#77. 买卖股票的最佳时机)
- [78. 跳跃游戏](#78. 跳跃游戏)
- [79. 跳跃游戏 II](#79. 跳跃游戏 II)
- [80. 划分字母区间](#80. 划分字母区间)
- 动态规划
-
- [81. 爬楼梯](#81. 爬楼梯)
- [82. 杨辉三角](#82. 杨辉三角)
- [83. 打家劫舍](#83. 打家劫舍)
- [84. 完全平方数](#84. 完全平方数)
- [85. 零钱兑换](#85. 零钱兑换)
- [86. 单词拆分](#86. 单词拆分)
- [87. 最长递增子序列](#87. 最长递增子序列)
- [88. 乘积最大子数组](#88. 乘积最大子数组)
- [89. 分割等和子集](#89. 分割等和子集)
- [90. 最长有效括号](#90. 最长有效括号)
- 多维动态规划
-
- [91. 不同路径](#91. 不同路径)
- [92. 最小路径和](#92. 最小路径和)
- [93. 最长回文子串](#93. 最长回文子串)
- [94. 最长公共子序列](#94. 最长公共子序列)
- [95. 编辑距离](#95. 编辑距离)
- 技巧
-
- [96. 只出现一次的数字](#96. 只出现一次的数字)
- [97. 多数元素](#97. 多数元素)
- [98. 颜色分类](#98. 颜色分类)
- [99. 下一个排列](#99. 下一个排列)
- [100. 寻找重复数](#100. 寻找重复数)
LeetCode Hot 100
哈希
1. 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案,且不能重复使用同一个元素。
哈希表。遍历数组,对每个
nums[i]查target - nums[i]是否已在哈希表中;在则返回下标,不在则把nums[i]和i存入。时间 O(n),空间 O(n)。
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> mp;
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
if (mp.count(complement)) {
return {mp[complement], i};
}
mp[nums[i]] = i;
}
return {};
}
};
2. 字母异位词分组
给定一个字符串数组 strs,将字母异位词 组合在一起。字母异位词是由重新排列源单词的所有字母得到的一个新单词,例如 "eat"、"tea"、"ate" 互为字母异位词。可以按任意顺序返回结果列表。
哈希表 + 特征 key。对每个字符串统计 26 个字母的出现次数,拼成唯一 key(加分隔符防混淆),用
unordered_map<key, vector<string>>分组。时间 O(n·k),空间 O(n·k)。
cpp
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp; // key -> 原字符串列表
for (string& s : strs) {
string key = getKey(s);
mp[key].push_back(s);
}
// 提取结果
vector<vector<string>> result;
for (auto& p : mp) {
result.push_back(p.second);
}
return result;
}
private:
// 生成每个字符串的唯一特征字符串(26个字母的计数 + 分隔符)
string getKey(const string& s) {
array<int, 26> count{}; // 自动初始化为0
for (char c : s) {
count[c - 'a']++;
}
string key;
for (int num : count) {
key += to_string(num);
key += '#'; // 分隔符,防止 [1,23] 和 [12,3] 混淆
}
return key;
}
};
3. 最长连续序列
给定一个未排序的整数数组 nums,找出数字连续的最长序列 (不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
哈希集合 + 只从序列起点扩展。把所有数放进
unordered_set,遍历每个数num,若num-1不存在(说明是起点),则不断查num+1并计数。每个数最多被访问两次,时间 O(n)。
cpp
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> st(nums.begin(), nums.end());
int ans = 0;
for (int num : st) { // 遍历去重后的集合
// 只从连续序列的起点开始(num-1 不存在)
if (!st.count(num - 1)) {
int curNum = num;
int curLen = 1;
// 向右枚举
while (st.count(curNum + 1)) {
curNum++;
curLen++;
}
ans = max(ans, curLen);
}
}
return ans;
}
};
双指针
4. 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。必须在不复制数组的情况下原地对数组进行操作。
双指针(读写指针)。读指针遍历数组,遇到非零就写到写指针位置并让写指针后移,同时把读指针位置置 0。保持非零相对顺序,原地操作,时间 O(n)。
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int cnt=0;
for(int i=0;i<nums.size();i++)
{
if(nums[i]!=0)
{
nums[cnt++]=nums[i];
nums[i]=0;
}
}
}
};
5. 盛最多水的容器
给定一个长度为 n 的整数数组 height。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水,返回容器最大容量。
对撞双指针。左右指针从两端向中间移动,每次计算面积
(j-i)*min(height[i],height[j]),然后移动较矮的那一端。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
int maxArea(vector<int>& height) {
int maxn=0;
int i=0,j=height.size()-1;
while(i<j)
{
maxn=max(maxn,(j-i)*min(height[i],height[j]));
if(height[i]<=height[j]) i++;
else j--;
}
return maxn;
}
};
6. 三数之和
给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k,同时还满足 nums[i] + nums[j] + nums[k] == 0。请返回所有和为 0 且不重复的三元组。
排序 + 固定一个数 + 双指针。先排序,外层枚举
i(跳过重复),内层用j、k双指针找nums[j]+nums[k] = -nums[i],找到后跳过重复元素。时间 O(n²)。
cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int len = nums.size();
vector<vector<int>> ans;
sort(nums.begin(), nums.end());
for (int i = 0; i < len - 2; i++) {
// 跳过重复的 nums[i]
if (i > 0 && nums[i] == nums[i - 1]) continue;
int j = i + 1, k = len - 1;
while (j < k) {
int sum = nums[i] + nums[j] + nums[k];
if (sum < 0) {
j++;
} else if (sum > 0) {
k--;
} else {
ans.push_back({nums[i], nums[j], nums[k]});
// 跳过重复的 nums[j] 和 nums[k]
while (j < k && nums[j] == nums[j + 1]) j++;
while (j < k && nums[k] == nums[k - 1]) k--;
j++;
k--;
}
}
}
return ans;
}
};
7. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。柱子之间凹陷的部分可以蓄水。
从左找每个右边界,计算左半边蓄水;再从右找每个左边界,计算右半边蓄水,两遍相加。核心是维护当前最高柱和中间累计高度。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
int trap(vector<int>& height) {
int len=height.size();
int h=height[0],temp=0,ans=0,mark=0;
for(int i=1;i<len;i++)
{
if(height[i]>=h)
{
ans+=((i-mark-1)*h-temp);
h=height[i],mark=i,temp=0;
}
else temp+=height[i];
}
h=height[len-1],temp=0,mark=len-1;
for(int i=len-2;i>=0;i--)
{
if(height[i]>h)
{
ans+=((mark-i-1)*h-temp);
h=height[i],mark=i,temp=0;
}
else temp+=height[i];
}
return ans;
}
};
滑动窗口
8. 无重复字符的最长子串
给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。注意是子串(连续),不是子序列。
滑动窗口 + 哈希表计数。右指针扩张,遇到窗口内已存在的字符就移动左指针收缩,直到该字符计数为 0;每次更新窗口长度。时间 O(n)。
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char,int> mp; // 记录字符出现的次数
int start = 0, end = 0, maxn = 0;
while(end < s.size()) {
// 如果当前字符已经存在于当前窗口
if(mp[s[end]] > 0) {
mp[s[start]]--; // 从窗口中移除start位置的字符
start++; // 移动窗口左边界
}
else {
mp[s[end]]++; // 将当前字符加入窗口
maxn = max(maxn, end - start + 1); // 更新最大长度
end++; // 移动窗口右边界
}
}
return maxn;
}
};
9. 找到字符串中所有字母异位词
给定两个字符串 s 和 p,找到 s 中所有 p 的字母异位词 的子串,返回这些子串的起始索引。字母异位词指由相同字母重排列形成的字符串,且子串长度与 p 相同。
定长滑动窗口 + 字符计数数组。先统计
p的 26 字母计数,再用同样大小的窗口在s上滑动,每次移入一个、移出一个,比较两个计数数组是否相同(memcmp)。时间 O(n·26),可视为 O(n)。
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int mp1[26] = {0}, mp2[26] = {0};
vector<int> ans;
int m = p.size();
if (s.size() < p.size()) return {};
for (int i = 0; i < m; i++) {
mp1[s[i] - 'a']++;
mp2[p[i] - 'a']++;
}
if (memcmp(mp1, mp2, sizeof(mp1)) == 0) ans.push_back(0); // 改这里
for (int i = m; i < s.size(); i++) {
mp1[s[i - m] - 'a']--;
mp1[s[i] - 'a']++;
if (memcmp(mp1, mp2, sizeof(mp1)) == 0) ans.push_back(i - m + 1); // 改这里
}
return ans;
}
};
子串
10. 和为 K 的子数组
给你一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的连续子数组的个数。子数组是数组中元素的连续非空序列。
前缀和 + 哈希表。遍历时累加前缀和
sum,若sum - k在哈希表中出现过,说明存在以当前位置结尾、和为k的子数组,累加其出现次数。初始mp[0] = 1处理从下标 0 开始的子数组。时间 O(n)。
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;
mp[0] = 1; // 前缀和为0出现1次
int sum = 0, ans = 0;
for (int x : nums) {
sum += x;
if (mp.count(sum - k)) ans += mp[sum - k];
mp[sum]++;
}
return ans;
}
};
11. 滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字,滑动窗口每次只向右移动一位。返回滑动窗口中的最大值数组。
单调递减双端队列。队列存
{值, 下标},新元素入队前从队尾弹出所有比它小的元素(它们永远不可能成为最大值);队首滑出窗口时弹出。队首始终是当前窗口最大值。时间 O(n)。
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
struct Node { int v, pos; };
deque<Node> q;
vector<int> ans;
for (int i = 0; i < nums.size(); i++) {
// 维护单调递减(新元素大,队尾就永无出头之日)
while (!q.empty() && q.back().v < nums[i]) q.pop_back();
q.push_back({nums[i], i});
// 弹出窗口滑出的元素
if (q.front().pos <= i - k) q.pop_front();
if (i >= k - 1) ans.push_back(q.front().v);
}
return ans;
}
};
12. 最小覆盖子串
给你一个字符串 s、一个字符串 t。返回 s 中涵盖 t 所有字符的最短子串 。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""。如果存在多个最短子串,返回任意一个即可。
滑动窗口 + 计数。右指针扩张直到窗口覆盖
t所有字符(用cnt计数),然后左指针收缩去掉多余字符,记录最短窗口;不满足时继续扩张。时间 O(n)。
cpp
class Solution {
public:
string minWindow(string s, string t) {
if(s.size()<t.size()) return "";
unordered_map<char,int>smap,tmap;
string ans=s;
int l=0,r=0,cnt=0;
for(auto x:t)
{
tmap[x]++;
}
while(r<s.size())
{
smap[s[r]]++;
if(tmap.count(s[r])&&smap[s[r]]<=tmap[s[r]]) cnt++;
r++;
while(l<r&&smap[s[l]]>tmap[s[l]])
{
smap[s[l]]--;
l++;
}
if(cnt==t.size())
{
if(r-l<ans.size())
ans=s.substr(l,r-l);
}
}
return cnt==t.size()? ans:"";
}
};
普通数组
13. 最大子数组和
给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
前缀和 + 维护最小前缀。当前前缀和
pre[i]减去此前出现过的最小前缀和minn,就是以i结尾的最大子数组和。等价于 Kadane 算法。时间 O(n)。
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre[100010];
int ans=-1e9,minn=0;
pre[0]=nums[0];
for(int i=0;i<nums.size();i++)
{
if(i) pre[i]=pre[i-1]+nums[i];
ans=max(ans,pre[i]-minn);
minn=min(minn,pre[i]);
}
return ans;
}
};
14. 合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [start_i, end_i]。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
排序 + 一次遍历。按区间起点排序,遍历时若当前区间起点 ≤ 结果末尾区间的终点,则合并(更新终点为较大值);否则追加新区间。时间 O(n log n)。
cpp
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.empty()) return {};
vector<vector<int>> ans;
sort(intervals.begin(), intervals.end(), cmp);
ans.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] <= ans.back()[1]) {
ans.back()[1] = max(ans.back()[1], intervals[i][1]);
} else {
ans.push_back(intervals[i]);
}
}
return ans;
}
};
15. 轮转数组
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。要求使用空间复杂度为 O(1) 的原地算法。
取模 + 辅助数组。
k %= n,新位置(i + k) % n存放原nums[i]。你代码用了额外数组,不满足 O(1) 空间;原地解法是"三次反转":整体反转 → 前 k 个反转 → 后 n-k 个反转。
cpp
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int len = nums.size();
k %= len;
vector<int> ans(len); // 分配 len 个空间
for (int i = 0; i < len; ++i)
ans[(i + k) % len] = nums[i];
nums = ans;
}
};
16. 除自身以外数组的乘积
给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。要求不使用除法,且在 O(n) 时间复杂度内完成。
前缀积 + 后缀积。
pre[i]为[0, i]的乘积,post[i]为[i, n-1]的乘积,answer[i] = pre[i-1] * post[i+1],边界单独处理。时间 O(n),空间 O(n)(可优化到 O(1) 用输出数组存前缀积)。
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int len=nums.size();
int pre[100010],post[100010];
vector<int>ans;
pre[0]=nums[0],post[len-1]=nums[len-1];
for(int i=1;i<len;i++)
pre[i]=pre[i-1]*nums[i];
for(int i=len-2;i>=0;i--)
post[i]=post[i+1]*nums[i];
ans.push_back(post[1]);
for(int i=1;i<len-1;i++)
ans.push_back(pre[i-1]*post[i+1]);
ans.push_back(pre[len-2]);
return ans;
}
};
17. 缺失的第一个正数
给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数 。请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
原地哈希(把数放到正确位置)。遍历数组,若
nums[i]在[1, n]且nums[nums[i]-1] != nums[i],就交换到正确位置,用while持续处理换过来的数。第二次遍历找第一个nums[i] != i+1的位置,返回i+1;全对则返回n+1。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
// 第一次遍历:把每个数放到正确的位置
for (int i = 0; i < n; i++) {
// nums[i] 应该在 nums[i]-1 的位置
// 注意:交换后新换过来的数也要继续处理,所以用 while
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
swap(nums[i], nums[nums[i] - 1]);
}
}
// 第二次遍历:找第一个位置不对的地方
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 如果 [1, n] 都齐全,答案是 n+1
return n + 1;
}
};
矩阵
18. 矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
标记行列。遍历矩阵,用
set记录需要置零的行和列,再按标记置零。时间 O(mn),空间 O(m+n)。进阶 O(1) 空间:用矩阵第一行/第一列本身作标记,额外两个变量记录第一行/第一列是否需置零。
cpp
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
set<int>row,col;
for(int i=0;i<matrix.size();i++){
for(int j=0;j<matrix[0].size();j++){
if(!matrix[i][j]){
row.insert(i);
col.insert(j);
}
}
}
for(int x:row)
for(int j=0;j<matrix[0].size();j++)
matrix[x][j]=0;
for(int i=0;i<matrix.size();i++)
for(int y:col)
matrix[i][y]=0;
}
};
19. 螺旋矩阵
给你一个 m 行 n 列的矩阵 matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
方向数组 + 访问标记。按"右、下、左、上"循环,每走一步标记已访问,下一步越界或已访问就顺时针转向,直到走完
m*n个元素。时间 O(mn),空间 O(mn)(可优化为按边界收缩,空间 O(1))。
cpp
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> ans;
if (matrix.empty()) return ans; // 处理空矩阵
int m = matrix.size(), n = matrix[0].size();
// 方向数组:右,下,左,上
vector<vector<int>> dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
vector<vector<bool>> vis(m, vector<bool>(n, false));
int i = 0, j = 0, dir = 0; // dir 指向当前方向
for (int k = 0; k < m * n; ++k) {
ans.push_back(matrix[i][j]);
vis[i][j] = true;
// 计算下一步的位置
int ni = i + dirs[dir][0];
int nj = j + dirs[dir][1];
// 如果下一步越界或已访问,则顺时针转向
if (ni < 0 || ni >= m || nj < 0 || nj >= n || vis[ni][nj]) {
dir = (dir + 1) % 4;
ni = i + dirs[dir][0];
nj = j + dirs[dir][1];
}
i = ni;
j = nj;
}
return ans;
}
};
20. 旋转图像
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度 。你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵,不要使用另一个矩阵来旋转图像。
转置 + 翻转。先沿主对角线转置(
swap(matrix[i][j], matrix[j][i]),j从i开始),再反转每一行。原地操作,时间 O(n²),空间 O(1)。
cpp
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n=matrix.size(), m=matrix[0].size();
for(int i=0;i<n;i++)
for(int j=i;j<m;j++)
swap(matrix[i][j],matrix[j][i]);
for(int i=0;i<n;i++)
reverse(matrix[i].begin(),matrix[i].end());
}
};
21. 搜索二维矩阵 II
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:每行的元素从左到右升序排列;每列的元素从上到下升序排列。
从右上角开始搜索(或二分每一行)。从右上角出发:若当前值等于
target返回 true;若大于target左移一列;若小于target下移一行。利用行列有序性质,时间 O(m+n),空间 O(1)。你代码逐行二分是 O(m log n),也可行但不是最优。
cpp
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
for (const auto& row : matrix) {
if (binary_search(row.begin(), row.end(), target)) {
return true;
}
}
return false;
}
};
链表
22. 相交链表
给你两个单链表的头节点 headA 和 headB,请你找出并返回两个单链表相交的起始节点 。如果两个链表不存在相交节点,返回 null。
双指针消除长度差。指针 A 遍历完 A 后转到 B 头,指针 B 遍历完 B 后转到 A 头;两指针走的总步数相同,最终会在相交节点相遇。若无交点,则同时走到
nullptr。时间 O(m+n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *A = headA, *B = headB;
while (A != B) {
A = A != nullptr ? A->next : headB;
B = B != nullptr ? B->next : headA;
}
return A;
}
};
23. 反转链表
给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
迭代三指针。用
prev、curr、next,每次把curr->next指向prev,然后三个指针整体后移。最终prev就是新头。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre=nullptr;
ListNode* cur=head;
while(cur){
ListNode* next=cur->next;
cur->next=pre;
pre=cur;
cur=next;
}
return pre;
}
};
24. 回文链表
给你一个单链表的头节点 head,请你判断该链表是否为回文链表 。如果是,返回 true;否则,返回 false。
快慢指针找中点 + 反转后半 + 比较。快慢指针找到中点后,反转后半部分,再与前半部分逐一比较;比较完可再反转回来恢复原链表(可选)。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (!head || !head->next) return true;
// 1. 快慢指针找中点
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
// 2. 反转后半部分
ListNode* secondHalf = reverseList(slow);
// 3. 比较前半部分和后半部分
ListNode* firstHalf = head;
while (secondHalf) {
if (firstHalf->val != secondHalf->val) {
return false;
}
firstHalf = firstHalf->next;
secondHalf = secondHalf->next;
}
return true;
}
private:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
};
25. 环形链表
给你一个链表的头节点 head,判断链表中是否有环 。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
快慢指针判环。慢指针走一步,快指针走两步;若相遇则有环,若快指针走到
nullptr则无环。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head==nullptr||head->next==nullptr){
return false;
}
ListNode* slow=head;
ListNode* fast=head->next;
while(slow!=fast){
if(fast==nullptr||fast->next==nullptr)
return false;
slow=slow->next;
fast=fast->next->next;
}
return true;
}
};
26. 环形链表 II
给定一个链表的头节点 head,返回链表开始入环的第一个节点 。如果链表无环,则返回 null。
快慢指针相遇 + 双指针找入口。快慢指针相遇后,一个指针从
head出发,另一个从相遇点出发,同速前进,再次相遇即为环入口。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while (fast != nullptr) {
slow = slow->next;
if (fast->next == nullptr) {
return nullptr;
}
fast = fast->next->next;
if (fast == slow) {
ListNode *ptr = head;
while (ptr != slow) {
ptr = ptr->next;
slow = slow->next;
}
return ptr;
}
}
return nullptr;
}
};
27. 合并两个有序链表
将两个升序 链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
哑节点 + 双指针。用
dummy简化头节点处理,比较两链表当前节点,较小者接到结果尾部;某条链表走完后,把另一条剩余部分直接接上。时间 O(m+n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dum = new ListNode(0);
ListNode* cur = dum;
while (list1 != nullptr && list2 != nullptr) {
if (list1->val < list2->val) {
cur->next = list1;
list1 = list1->next;
}
else {
cur->next = list2;
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1 != nullptr ? list1 : list2;
return dum->next;
}
};
28. 两数相加
给你两个非空 的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。
模拟竖式加法。用
carry记录进位,循环条件为l1 || l2 || carry,每次求和取模得当前位、除以 10 得进位;用哑节点简化链表构建。时间 O(max(m,n)),空间 O(max(m,n))。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
int carry = 0;
while (l1 || l2 || carry) {
int sum = carry;
if (l1) {
sum += l1->val;
l1 = l1->next;
}
if (l2) {
sum += l2->val;
l2 = l2->next;
}
carry = sum / 10;
cur->next = new ListNode(sum % 10);
cur = cur->next;
}
return dummy->next;
}
};
29. 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
哑节点 + 快慢指针。
fast先走n+1步,然后fast和slow同步前进;fast到nullptr时,slow正好在待删节点的前一个,执行slow->next = slow->next->next。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
ListNode* fast = dummy;
ListNode* slow = dummy;
// fast 先走 n+1 步,让 slow 和 fast 之间间隔 n 个节点
for (int i = 0; i <= n; i++) {
fast = fast->next;
}
// 然后一起走,fast 到末尾时,slow 就在倒数第 n 个的前一个
while (fast) {
fast = fast->next;
slow = slow->next;
}
// 删除
slow->next = slow->next->next;
return dummy->next;
}
};
30. 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即只能进行节点交换)。
哑节点 + 迭代交换。用
pre指向每对节点的前驱,first、second为待交换的两个节点;交换三步:pre->next = second、first->next = second->next、second->next = first,然后pre = first进入下一对。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummy = new ListNode(0, head);
ListNode* pre = dummy;
while (pre->next && pre->next->next) {
ListNode* first = pre->next; // 第一个节点
ListNode* second = pre->next->next; // 第二个节点
// 开始交换
pre->next = second; // 前驱指向第二个(新的头)
first->next = second->next; // 第一个指向第三个
second->next = first; // 第二个指向第一个
// 前驱前进两个位置
pre = first;
}
return dummy->next;
}
};
31. K 个一组翻转链表
给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
分组 + 头插法翻转。先检查剩余节点是否够
k个,不够则直接返回;用pre作为每组的前驱,tail为第k个节点,nxt = tail->next为下一组开头;用头插法把pre->next到tail之间的节点逐个插到pre后面,完成后pre = cur(原组头,翻转后变组尾)继续下一组。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* dummy = new ListNode(0, head);
ListNode* pre = dummy; // 每次要翻转的 k 个节点的前一个
while (true) {
// 检查是否够 k 个
ListNode* tail = pre;
for (int i = 0; i < k; i++) {
tail = tail->next;
if (!tail) return dummy->next;
}
// 头插法翻转 k 个节点
ListNode* cur = pre->next; // 当前段的头(翻转后变尾)
ListNode* nxt = tail->next; // 下一段的头
// 把 cur 后面的节点一个个插到 pre 后面
while (cur->next != nxt) {
ListNode* temp = cur->next;
cur->next = temp->next;
temp->next = pre->next;
pre->next = temp;
}
pre = cur; // pre 跳到尾部,准备下一段
}
}
};
32. 随机链表的复制
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random,该指针可以指向链表中的任何节点或空节点。构造这个链表的深拷贝。
哈希表映射。第一遍遍历原链表,为每个节点创建新节点,建立
原节点 -> 新节点的映射;第二遍遍历,根据映射设置新节点的next和random。时间 O(n),空间 O(n)。进阶 O(1) 空间:把新节点插在原节点后面,再拆分。
cpp
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
unordered_map<Node*, Node*> mp;
// 第一遍:创建所有新节点,建立映射
Node* cur = head;
while (cur) {
mp[cur] = new Node(cur->val);
cur = cur->next;
}
// 第二遍:连接 next 和 random
cur = head;
while (cur) {
mp[cur]->next = mp[cur->next]; // 新节点的 next
mp[cur]->random = mp[cur->random]; // 新节点的 random
cur = cur->next;
}
return mp[head];
}
};
33. 排序链表
给你链表的头结点 head,请将其按升序 排列并返回排序后的链表。要求时间复杂度 O(n log n)。
归并排序(自顶向下)。快慢指针找中点并断开,递归排序左右两半,再合并两个有序链表。时间 O(n log n),空间 O(log n)(递归栈)。也可自底向上归并做到 O(1) 空间。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
private:
// 找中点(返回左半部分的尾节点,方便断开)
ListNode* findMiddle(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head->next; // fast 先走一步,slow 最终停在左半部分末尾
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// 合并两个有序链表
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(-1);
ListNode* curr = dummy;
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
curr->next = l1;
l1 = l1->next;
} else {
curr->next = l2;
l2 = l2->next;
}
curr = curr->next;
}
if (l1 != nullptr) curr->next = l1;
if (l2 != nullptr) curr->next = l2;
return dummy->next;
}
public:
ListNode* sortList(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}
// 找到中点并断开
ListNode* mid = findMiddle(head);
ListNode* rightHead = mid->next;
mid->next = nullptr; // 断开左右两部分
// 递归排序
ListNode* left = sortList(head);
ListNode* right = sortList(rightHead);
// 合并
return mergeTwoLists(left, right);
}
};
34. 合并 K 个升序链表
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
优先队列(小根堆)。把所有链表头节点入堆,每次弹出最小节点接到结果尾部,若该节点有
next则入堆。时间 O(N log k)(N 为总节点数,k 为链表数),空间 O(k)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
struct comp {
bool operator()(ListNode* a, ListNode* b) {
return a->val > b->val;
}
};
priority_queue<ListNode*, vector<ListNode*>, comp> q;
ListNode* mergeKLists(vector<ListNode*>& lists) {
for (auto node: lists) {
if (node) q.push(node);
}
ListNode* head = new ListNode();
ListNode* tail = head;
while (!q.empty()) {
ListNode* node = q.top();
q.pop();
tail->next = node;
tail = tail->next;
if (node->next) q.push(node->next);
}
return head->next;
}
};
35. LRU 缓存
请你设计并实现一个满足 LRU(最近最少使用)缓存 约束的数据结构。实现 LRUCache 类:get(key) 如果关键字存在则返回其值,否则返回 -1;put(key, value) 如果关键字已存在则变更其值,不存在则插入。当容量满时,淘汰最久未使用的关键字。要求 get 和 put 以 O(1) 平均时间复杂度运行。
哈希表 + 双向链表。哈希表实现 O(1) 查找,双向链表维护访问顺序:头部为最近使用,尾部为最久未使用。
get命中后把节点移到头部;put已存在则更新值并移到头部,不存在则插入头部,容量满时删除尾部节点并同步删除哈希表记录。get和put均为 O(1)。
cpp
struct Node {
int key, value;
Node* prev;
Node* next;
Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
int cap;
unordered_map<int, Node*> map;
Node* head;
Node* tail;
void remove(Node* p) {
p->prev->next = p->next;
p->next->prev = p->prev;
}
void insertToHead(Node* p) {
p->next = head->next;
p->prev = head;
head->next->prev = p;
head->next = p;
}
public:
LRUCache(int capacity) {
cap = capacity;
head = new Node(0, 0);
tail = new Node(0, 0);
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (map.find(key) == map.end()) return -1;
Node* node = map[key];
remove(node);
insertToHead(node);
return node->value;
}
void put(int key, int value) {
if (map.find(key) != map.end()) {
Node* node = map[key];
node->value = value;
remove(node);
insertToHead(node);
} else {
if ((int)map.size() == cap) {
Node* old = tail->prev;
remove(old);
map.erase(old->key);
delete old;
}
Node* node = new Node(key, value);
insertToHead(node);
map[key] = node;
}
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
二叉树
36. 二叉树的中序遍历
给定一个二叉树的根节点 root,返回它的中序遍历结果(左 → 根 → 右)。
递归。按"左 → 根 → 右"顺序递归遍历,把节点值加入结果数组。时间 O(n),空间 O(h)(递归栈,h 为树高)。也可用栈迭代模拟。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
void inorder(TreeNode* node, vector<int>& res) {
if (!node) return;
inorder(node->left, res); // 左
res.push_back(node->val); // 根
inorder(node->right, res); // 右
}
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
inorder(root,ans);
return ans;
}
};
37. 二叉树的最大深度
给定一个二叉树 root,返回其最大深度。最大深度是指从根节点到最远叶子节点的最长路径上的节点数。
递归。空节点返回 0,否则返回
max(左子树深度, 右子树深度) + 1。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr) {
return 0;
}
int left_depth = maxDepth(root->left);
int right_depth = maxDepth(root->right);
return max(left_depth, right_depth) + 1;
}
};
38. 翻转二叉树
给你一棵二叉树的根节点 root,翻转这棵二叉树(左右子树交换),并返回其根节点。
递归交换。先递归翻转左右子树,再交换当前节点的左右指针。时间 O(n),空间 O(h)。也可用队列做层序翻转。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr) return nullptr;
invertTree(root -> left);
invertTree(root -> right);
TreeNode* temp = root ->left;
root -> left = root ->right;
root ->right = temp;
return root;
}
};
39. 对称二叉树
给你一个二叉树的根节点 root,检查它是否轴对称。
双指针递归比较。写一个辅助函数
isMirror(left, right),判断两棵树是否镜像:两节点都空返回 true,一个空返回 false,值相等且isMirror(left->left, right->right)和isMirror(left->right, right->left)都成立。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
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);
}
};
40. 二叉树的直径
给你一棵二叉树的根节点,返回该树的直径 。二叉树的直径是指树中任意两个节点之间最长路径的长度,这条路径可能经过也可能不经过根节点。直径的长度以路径上的边数表示。
后序 DFS + 全局最大值。递归返回以当前节点为端点向下的最长链(边数),同时用
left + right更新全局最大直径(经过当前节点的路径)。空节点返回 -1,使叶子节点返回 0。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int ans = 0;
int dfs(TreeNode* node) {
if (!node) return -1; // 空子树不贡献边
int left = dfs(node->left) + 1;
int right = dfs(node->right) + 1;
// 经过 node 的路径长度 = left + right
ans = max(ans, left + right);
// 返回以 node 为端点向下的最长链
return max(left, right);
}
int diameterOfBinaryTree(TreeNode* root) {
dfs(root);
return ans;
}
};
41. 二叉树的层序遍历
给你二叉树的根节点 root,返回其节点值的层序遍历(即逐层地,从左到右访问所有节点)。
BFS + 队列。队列存节点,每轮先记录当前层节点数
levelSize,循环levelSize次弹出节点、收集值、把左右孩子入队,得到当前层结果。时间 O(n),空间 O(w)(w 为最大层宽)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if (!root) return res; // 空树直接返回
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int levelSize = q.size(); // 当前层的节点数
vector<int> level; // 存放当前层的值
for (int i = 0; i < levelSize; i++) {
TreeNode* node = q.front();
q.pop();
level.push_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
res.push_back(level); // 把当前层加入结果
}
return res;
}
};
42. 将有序数组转换为二叉搜索树
给你一个整数数组 nums,其中元素已经按升序 排列,请你将其转换为一棵平衡二叉搜索树。平衡二叉树是指每个节点的左右两个子树的高度差的绝对值不超过 1。
分治 + 取中点。区间
[left, right]的中点作为根,递归构建左子树[left, mid-1]和右子树[mid+1, right]。取中点保证平衡。时间 O(n),空间 O(log n)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return build(nums, 0, nums.size() - 1);
}
private:
// 左闭右闭区间 [left, right]
TreeNode* build(vector<int>& nums, int left, int right) {
if (left > right) return nullptr; // 区间为空,返回空节点
int mid = (left + right) / 2;
TreeNode* root = new TreeNode(nums[mid]);
// 递归构建左右子树
root->left = build(nums, left, mid - 1);
root->right = build(nums, mid + 1, right);
return root;
}
};
43. 验证二叉搜索树
给你一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树。有效 BST 定义:左子树所有节点小于根节点,右子树所有节点大于根节点,且左右子树也必须是 BST。
中序遍历检查严格递增。对 BST 中序遍历得到严格递增序列,遍历结果数组,若出现
pa[i] <= pa[i-1]则不是 BST。时间 O(n),空间 O(n)。也可用递归传上下界(min, max)做到 O(h) 空间。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void inorder(TreeNode *root, vector<int>& path)
{
if(root==nullptr)return;
inorder(root->left, path);
path.push_back(root->val);
inorder(root->right, path);
}
bool isValidBST(TreeNode* root) {
vector<int> pa;
inorder(root,pa);
for(int i=1;i<pa.size();i++)
{
if(pa[i]<=pa[i-1])return false;
}
return true;
}
};
44. 二叉搜索树中第 K 小的元素
给定一个二叉搜索树的根节点 root,和一个整数 k,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。
中序遍历取第 k 个。BST 中序遍历是升序序列,遍历到第 k 个节点即为答案。你代码用数组存全部再取
vec[k-1],时间 O(n)、空间 O(n);可优化为计数到 k 就提前返回,空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
void inorder(TreeNode* node,vector<int> &vec){
if(!node) return;
inorder(node->left,vec);
vec.push_back(node->val);
inorder(node->right,vec);
}
public:
int kthSmallest(TreeNode* root, int k) {
vector<int>vec;
inorder(root,vec);
return vec[k-1];
}
};
45. 二叉树的右视图
给定一个二叉树的根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
DFS 先右后左 + 按层记录第一个。
depth == ans.size()时说明该层还没记录过节点,当前节点就是该层最右侧节点。先递归右子树再左子树。时间 O(n),空间 O(h)。也可用 BFS 取每层最后一个。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> ans;
void dfs(TreeNode* root, int depth) {
if (!root) return;
// 如果当前节点所在的这一层,还没有记录过节点,就记录当前节点。
if (depth == ans.size()) {
ans.push_back(root->val);
}
dfs(root->right, depth + 1);
dfs(root->left, depth + 1);
}
vector<int> rightSideView(TreeNode* root) {
dfs(root, 0);
return ans;
}
};
46. 二叉树展开为链表
给你二叉树的根结点 root,请你将它展开为一个单链表 :展开后的单链表应该同样使用 TreeNode,其中 right 子指针指向链表中下一个结点,而 left 子指针始终为 null。展开后的单链表应该与二叉树先序遍历顺序相同。
先序遍历 + 重新连接。先序遍历收集所有节点到数组,再依次把
nodes[i-1]->right = nodes[i]、left = nullptr。时间 O(n),空间 O(n)。进阶 O(1) 空间:对每个节点,把左子树接到右子树前面(找左子树最右节点接原右子树)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void flatten(TreeNode* root) {
if (!root) return;
vector<TreeNode*> nodes;
preorder(root, nodes);
for (int i = 1; i < nodes.size(); i++) {
nodes[i-1]->left = nullptr;
nodes[i-1]->right = nodes[i];
}
nodes.back()->left = nullptr;
nodes.back()->right = nullptr;
}
private:
void preorder(TreeNode* node, vector<TreeNode*>& nodes) {
if (!node) return;
nodes.push_back(node);
preorder(node->left, nodes);
preorder(node->right, nodes);
}
};
47. 从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder,其中 preorder 是二叉树的先序遍历 ,inorder 是同一棵树的中序遍历。请构造二叉树并返回其根节点。
分治 + 前序定根、中序分左右。前序第一个是根,在中序中找到根的位置
i,左子树长度len = i - inl,递归构建左子树(前序[prel+1, prel+len],中序[inl, i-1])和右子树(前序[prel+len+1, prer],中序[i+1, inr])。时间 O(n²)(每次线性找根),可用哈希表存中序下标优化到 O(n)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
return build(preorder, 0, preorder.size() - 1, inorder, 0, inorder.size() - 1);
}
TreeNode* build(vector<int>& preorder,int prel,int prer,vector<int>& inorder,int inl,int inr)
{
if (prel > prer || inl > inr) {
return nullptr;
}
TreeNode* root = new TreeNode(preorder[prel]);
for(int i=inl;i<=inr;i++)
{
if(inorder[i]==preorder[prel])
{
int len=i-inl;
root->left=build(preorder,prel+1,prel+len,inorder,inl,i-1);
root->right=build(preorder,prel+len+1,prer,inorder,i+1,inr);
break;
}
}
return root;
}
};
48. 路径总和 III
给定一个二叉树的根节点 root,和一个整数 targetSum,求该二叉树里节点值之和等于 targetSum 的路径的数目。路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
前缀和 + 哈希表 + DFS 回溯。DFS 时维护从根到当前节点的前缀和
curSum,若curSum - target在哈希表中出现过,说明存在以当前节点结尾、和为target的路径。进入节点时prefixCount[curSum]++,离开时--(回溯)。初始prefixCount[0] = 1处理从根开始的路径。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int pathSum(TreeNode* root, int targetSum) {
unordered_map<long long, int> prefixCount;
prefixCount[0] = 1; // 前缀和为 0 的"虚拟节点",处理从根开始的路径
return dfs(root, 0, targetSum, prefixCount);
}
private:
int dfs(TreeNode* node, long long curSum, int target,
unordered_map<long long, int>& prefixCount) {
if (!node) return 0;
curSum += node->val;
// 查找是否存在前缀和为 curSum - target 的祖先
int res = prefixCount.count(curSum - target) ? prefixCount[curSum - target] : 0;
// 把当前前缀和加入哈希表,继续向下
prefixCount[curSum]++;
res += dfs(node->left, curSum, target, prefixCount);
res += dfs(node->right, curSum, target, prefixCount);
// 回溯:离开当前节点时,把它从哈希表中移除
prefixCount[curSum]--;
return res;
}
};
49. 二叉树的最近公共祖先
给定一个二叉树,找到该树中两个指定节点的最近公共祖先。最近公共祖先定义为:对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大。
后序 DFS。递归判断左右子树是否包含 p 或 q:若左右都包含,当前节点就是 LCA;若只有一边包含,返回那一边的结果;若当前节点就是 p 或 q,返回当前节点。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* ans;
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
dfs(root,p,q);
return ans;
}
bool dfs(TreeNode* root,TreeNode* p,TreeNode* q)
{
if(root==nullptr) return false;
bool lson=dfs(root->left,p,q);
bool rson=dfs(root->right,p,q);
if((lson&&rson)||(root->val==p->val||root->val==q->val)&&(lson||rson))
ans=root;
//(lson&&rson):表示当前子树是否包含 p 或 q
//(root->val == p->val || root->val == q->val) && (lson || rson):表示当前节点 root 本身就是 p 或 q 之一,并且另一个目标节点位于 root 的左子树或右子树中。
return lson||rson||root->val==p->val||root->val==q->val;//表示当前子树是否包含 p 或 q
}
};
50. 二叉树中的最大路径和
二叉树中的路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。路径和是路径中各节点值的总和。给你一个二叉树的根节点 root,返回其最大路径和。
后序 DFS + 全局最大值。递归返回以当前节点为端点向下的最大路径和(负贡献取 0),同时用
root->val + left + right更新全局最大路径和(经过当前节点的路径)。maxn初始为INT_MIN以处理全负树。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxn = INT_MIN;
int maxPathSum(TreeNode* root) {
pathsum(root);
return maxn;
}
int pathsum(TreeNode* root) {
if (!root) return 0;
int left = max(0, pathsum(root->left));
int right = max(0, pathsum(root->right));
maxn = max(maxn, root->val + left + right);
return root->val + max(left, right);
}
};
图论
51. 岛屿数量
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
DFS 遍历 + 访问标记。遍历网格,遇到
'1'且未访问时,岛屿数 +1,并从该点 DFS 把所有相连的陆地标记为已访问。时间 O(mn),空间 O(mn)(可原地把'1'改成'0'省掉 vis 数组)。
cpp
class Solution {
public:
int cnt = 0;
int vis[350][350] = {0}; // 显式初始化,更安全
int numIslands(vector<vector<char>>& grid) {
int m = grid.size(), n = grid[0].size();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 关键:必须是陆地,且未访问
if (grid[i][j] == '1' && !vis[i][j]) {
dfs(grid, i, j);
cnt++;
}
}
}
return cnt; // 关键:要有返回值
}
private:
void dfs(vector<vector<char>>& grid, int i, int j) {
int m = grid.size(), n = grid[0].size();
// 越界 或 不是陆地 或 已访问,都返回
if (i < 0 || i >= m || j < 0 || j >= n) return;
if (grid[i][j] != '1' || vis[i][j]) return;
vis[i][j] = 1;
dfs(grid, i - 1, j);
dfs(grid, i + 1, j);
dfs(grid, i, j - 1);
dfs(grid, i, j + 1);
}
};
52. 腐烂的橘子
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:0 代表空单元格,1 代表新鲜橘子,2 代表腐烂的橘子。每分钟,腐烂的橘子会使其上下左右相邻的新鲜橘子腐烂。返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数 。如果不可能,返回 -1。
多源 BFS。先把所有腐烂橘子入队,统计新鲜橘子数
fresh;每轮 BFS 传播一层(每分钟),把相邻新鲜橘子变腐烂并计数;若 BFS 结束时fresh > 0返回 -1,否则返回分钟数。时间 O(mn),空间 O(mn)。
cpp
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
queue<pair<int, int>> q;
int fresh = 0;
// 1. 初始化:统计新鲜橘子,把腐烂橘子入队
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
fresh++;
} else if (grid[i][j] == 2) {
q.push({i, j});
}
}
}
int minutes = 0;
int dir[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
// 2. 多源 BFS
while (!q.empty() && fresh > 0) {
int size = q.size();
for (int k = 0; k < size; k++) {
auto [x, y] = q.front();
q.pop();
for (int d = 0; d < 4; d++) {
int nx = x + dir[d][0];
int ny = y + dir[d][1];
if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
if (grid[nx][ny] != 1) continue; // 只感染新鲜橘子
grid[nx][ny] = 2; // 变成腐烂
q.push({nx, ny});
fresh--; // 新鲜橘子少一个
}
}
minutes++; // 这一层传播完,分钟数 +1
}
// 3. 如果还有新鲜橘子,说明无法全部腐烂
return fresh == 0 ? minutes : -1;
}
};
53. 课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。在选修某些课程之前需要一些先修课程,先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [a_i, b_i] 表示如果要学习课程 a_i 则必须先学习课程 b_i。请你判断是否可能完成所有课程的学习。
拓扑排序(BFS + 入度)。构建邻接表和入度数组,所有入度为 0 的课程入队;依次出队并"删除"其出边(入度减 1),入度变 0 的入队;最后判断访问过的课程数是否等于
numCourses。时间 O(V+E),空间 O(V+E)。
cpp
class Solution {
private:
vector<vector<int>> edges;
vector<int> indeg;
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
edges.resize(numCourses);
indeg.resize(numCourses);
for (const auto& info: prerequisites) {
edges[info[1]].push_back(info[0]);
++indeg[info[0]];
}
queue<int> q;
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
q.push(i);
}
}
int visited = 0;
while (!q.empty()) {
++visited;
int u = q.front();
q.pop();
for (int v: edges[u]) {
--indeg[v];
if (indeg[v] == 0) {
q.push(v);
}
}
}
return visited == numCourses;
}
};
54. 实现 Trie (前缀树)
Trie(发音类似 "try")或者说前缀树是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。请你实现 Trie 类:insert(word) 向前缀树中插入字符串;search(word) 如果字符串在前缀树中返回 true;startsWith(prefix) 如果前缀树中存在以 prefix 为前缀的字符串返回 true。
多叉树 + 字母映射。每个节点有 26 个孩子指针和一个
isEnd标记;insert逐字符向下建节点,末尾标记isEnd;search逐字符向下走,走到末尾且isEnd为 true 才算找到;startsWith逐字符走通即可,不要求isEnd。时间 O(L)(L 为字符串长度),空间 O(总字符数)。
cpp
class Trie {
struct Node {
Node* ch[26]{};
bool isEnd = false;
};
Node* root;
public:
Trie() {
root = new Node();
}
void insert(string word) {
Node* p = root;
for (char c : word) {
int i = c - 'a';
if (!p->ch[i]) p->ch[i] = new Node();
p = p->ch[i];
}
p->isEnd = true;
}
bool search(string word) {
Node* p = root;
for (char c : word) {
int i = c - 'a';
if (!p->ch[i]) return false;
p = p->ch[i];
}
return p->isEnd;
}
bool startsWith(string prefix) {
Node* p = root;
for (char c : prefix) {
int i = c - 'a';
if (!p->ch[i]) return false;
p = p->ch[i];
}
return true;
}
};
/**
* Your Trie object will be instantiated and called as such:
* Trie* obj = new Trie();
* obj->insert(word);
* bool param_2 = obj->search(word);
* bool param_3 = obj->startsWith(prefix);
*/
回溯
55. 全排列
给定一个不含重复数字的数组 nums,返回其所有可能的全排列。你可以按任意顺序返回答案。
回溯 + 访问标记。用
vis数组标记已选元素,每层从0到n-1枚举未选元素,选入路径后递归,返回时撤销标记和路径。时间 O(n·n!),空间 O(n)。
cpp
class Solution {
public:
vector<vector<int>> vec;
vector<vector<int>> permute(vector<int>& nums) {
int cnt = nums.size();
vector<int> ans;
vector<bool> vis(cnt, false); // 每次调用重新初始化
dfs(nums, ans, vis, 0, cnt);
return vec;
}
private:
void dfs(vector<int>& nums, vector<int>& ans, vector<bool>& vis,
int now, int cnt) {
if (now == cnt) {
vec.push_back(ans);
return;
}
for (int i = 0; i < cnt; i++) {
if (vis[i]) continue;
vis[i] = true;
ans.push_back(nums[i]);
dfs(nums, ans, vis, now + 1, cnt);
ans.pop_back();
vis[i] = false;
}
}
};
56. 子集
给你一个整数数组 nums,数组中的元素互不相同 。返回该数组所有可能的子集(幂集)。解集不能包含重复的子集,可以按任意顺序返回。
回溯(选/不选)。对每个元素分两支:选它递归,再撤销,然后不选它递归。走到末尾收集路径。时间 O(n·2ⁿ),空间 O(n)。
cpp
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> subsets(vector<int>& nums) {
vector<int>vec;
dfs(nums,vec,0,nums.size());
return ans;
}
private:
void dfs(vector<int>& nums,vector<int>& vec,int now,int cnt){
if(now==cnt){
ans.push_back(vec);
return;
}
vec.push_back(nums[now]);
dfs(nums,vec,now+1,cnt);
vec.pop_back();
dfs(nums,vec,now+1,cnt);
}
};
57. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。数字到字母的映射与电话按键相同。
回溯 + 映射表。用数组映射数字到字母串,逐位递归:对当前数字的每个字母,加入路径后递归下一位,返回时撤销。时间 O(4ⁿ),空间 O(n)。
cpp
class Solution {
public:
vector<string> letterCombinations(string digits) {
vector<string> res;
if (digits.empty()) return res; // 空字符串直接返回
string path;
dfs(digits, 0, path, res);
return res;
}
private:
// 数字到字母的映射,下标直接对应数字字符
const string mapping[10] = {
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
};
void dfs(string& digits, int index, string& path, vector<string>& res) {
// 终止条件:已经选完所有数字
if (index == digits.size()) {
res.push_back(path);
return;
}
// 当前数字对应的字母串
string letters = mapping[digits[index] - '0'];
// 遍历每个可选字母
for (char c : letters) {
path.push_back(c); // 做选择
dfs(digits, index + 1, path, res); // 进入下一层
path.pop_back(); // 撤销选择
}
}
};
58. 组合总和
给你一个无重复元素 的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target 的所有不同组合 ,并以列表形式返回。candidates 中的同一个数字可以无限制重复被选取。
回溯(可重复选取)。每层从当前位置枚举,选当前数后递归时
now不变(允许重复),不选则now+1进入下一数;sum > target剪枝,sum == target收集。时间 O(指数级),空间 O(target)。
cpp
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<int> vec;
dfs(candidates, vec, 0, target, 0);
return ans;
}
private:
void dfs(vector<int>& candidates, vector<int>& vec,
int now, int target, int sum) {
// 越界 或 和已经超过 target,直接返回
if (now >= candidates.size() || sum > target) return;
// 恰好等于 target,记录结果
if (sum == target) {
ans.push_back(vec);
return;
}
// 选择当前数字(可以重复选,所以 now 不变)
vec.push_back(candidates[now]);
dfs(candidates, vec, now, target, sum + candidates[now]);
vec.pop_back(); // 撤销
// 不选当前数字,进入下一个数字
dfs(candidates, vec, now + 1, target, sum);
}
};
59. 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。
回溯 + 合法性剪枝。维护已用左括号数
left和右括号数right,只有left < n才能加左括号,只有right < left才能加右括号,左右都用完时收集结果。时间 O(4ⁿ/√n),空间 O(n)。
cpp
class Solution {
public:
vector<string> ans;
vector<string> generateParenthesis(int n) {
string s;
dfs(s, 0, 0, n);
return ans;
}
private:
void dfs(string& s, int left, int right, int n) {
// 终止条件:左右括号都用完了
if (left == n && right == n) {
ans.push_back(s);
return;
}
// 还能加左括号
if (left < n) {
s.push_back('(');
dfs(s, left + 1, right, n);
s.pop_back(); // 撤销
}
// 右括号数量小于左括号时,才能加右括号
if (right < left) {
s.push_back(')');
dfs(s, left, right + 1, n);
s.pop_back(); // 撤销
}
}
};
60. 单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word。如果 word 存在于网格中,返回 true;否则,返回 false。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
DFS + 回溯标记。遍历网格每个起点,DFS 按单词字符逐个匹配,每次标记当前格已访问,向四方向递归,返回时撤销标记。匹配到最后一个字符即成功。时间 O(mn·4^L),空间 O(L)。
cpp
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
int m = board.size(), n = board[0].size();
vector<vector<bool>> vis(m, vector<bool>(n, false));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == word[0]) {
if (dfs(board, vis, i, j, word, 0)) {
return true;
}
}
}
}
return false;
}
private:
bool dfs(vector<vector<char>>& board, vector<vector<bool>>& vis,
int i, int j, string& word, int index) {
// 越界 或 已访问 或 字符不匹配
if (i < 0 || i >= board.size() || j < 0 || j >= board[0].size()) return false;
if (vis[i][j] || board[i][j] != word[index]) return false;
// 匹配到最后一个字符,成功
if (index == word.size() - 1) return true;
vis[i][j] = true; // 标记已访问
// 向四个方向搜索
bool found = dfs(board, vis, i - 1, j, word, index + 1) ||
dfs(board, vis, i + 1, j, word, index + 1) ||
dfs(board, vis, i, j - 1, word, index + 1) ||
dfs(board, vis, i, j + 1, word, index + 1);
vis[i][j] = false; // 回溯,撤销标记
return found;
}
};
61. 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串 。返回 s 所有可能的分割方案。
DP 预处理回文 + 回溯切割。先用区间 DP(按长度递推)判断所有子串是否回文,再从起点
start枚举每个回文子串的结束位置,切割后递归剩余部分,返回时撤销。时间 O(n·2ⁿ),空间 O(n²)。
cpp
class Solution {
public:
vector<vector<string>> partition(string s) {
int n = s.size();
// ---------- 1. 动态规划预处理:判断任意子串是否为回文 ----------
// f[i][j] = true 表示子串 s[i..j](闭区间)是回文串
vector<vector<bool>> f(n, vector<bool>(n, false));
// 必须从 i = n-1 递减到 0,因为 f[i][j] 依赖 f[i+1][j-1]
// 即:行下标更大的先算好,才能算当前行
for (int i = n - 1; i >= 0; i--) {
// j 从 i 递增到 n-1,保证同一行内 j-1 先算好
for (int j = i; j < n; j++) {
// 首尾字符相同,才有可能是回文
if (s[i] == s[j]) {
// 长度 1、2、3 的子串,首尾相同即可判定为回文
// 长度 1: i == j
// 长度 2: j - i == 1
// 长度 3: j - i == 2(中间只有一个字符,不影响)
if (j - i <= 2)
f[i][j] = true;
else
// 长度 > 3,需要看去掉首尾后的中间部分
// 即 s[i+1..j-1] 是否为回文
f[i][j] = f[i + 1][j - 1];
}
// 如果 s[i] != s[j],f[i][j] 保持初始值 false,无需赋值
}
}
// ---------- 2. 回溯搜索所有分割方案 ----------
vector<vector<string>> res; // 存放所有合法分割方案
vector<string> path; // 当前正在构建的分割方案
dfs(s, 0, f, path, res); // 从下标 0 开始切割
return res;
}
private:
// 回溯函数
// s: 原字符串
// start: 当前切割的起始下标(从 s[start] 开始切)
// f: 预处理好的回文判断表
// path: 当前已经切出的回文子串序列
// res: 最终结果集
void dfs(const string& s, int start, vector<vector<bool>>& f,
vector<string>& path, vector<vector<string>>& res) {
// 终止条件:start 走到字符串末尾,说明已经切完
// 此时 path 中保存的就是一个完整的分割方案
if (start == s.size()) {
res.push_back(path); // 将当前方案加入结果集
return;
}
// 枚举当前子串的结束位置 i
// 子串为 s[start..i],长度 = i - start + 1
for (int i = start; i < s.size(); i++) {
// 如果 s[start..i] 不是回文,直接跳过,尝试更长的子串
if (!f[start][i]) continue;
// 做选择:将当前回文子串加入 path
// substr(pos, len):从 pos 开始,截取 len 个字符
// 这里 pos = start,len = i - start + 1
path.push_back(s.substr(start, i - start + 1));
// 递归:从 i+1 开始继续切割剩余部分
dfs(s, i + 1, f, path, res);
// 撤销选择:回溯,把刚才加入的子串弹出
// 以便尝试以 start 开头的其他分割方式
path.pop_back();
}
}
};
62. N 皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n,返回所有不同的 n 皇后问题的解决方案。
回溯 + 三个集合剪枝。逐行放置皇后,用三个集合分别记录已占用的列、主对角线(
row-col)、次对角线(row+col);每行枚举列,若三个集合都未被占用则放置并递归下一行,返回时撤销。时间 O(n!),空间 O(n)。
cpp
class Solution {
public:
// 成员变量,相当于"全局变量"
unordered_set<int> cols; // 已占用的列
unordered_set<int> diag1; // 已占用的主对角线 (row - col)
unordered_set<int> diag2; // 已占用的次对角线 (row + col)
vector<vector<string>> res;
vector<vector<string>> solveNQueens(int n) {
// 关键:每次调用前清空,防止上次残留
cols.clear();
diag1.clear();
diag2.clear();
res.clear();
vector<string> board(n, string(n, '.'));
backtrack(board, 0, n);
return res;
}
private:
void backtrack(vector<string>& board, int row, int n) {
if (row == n) {
res.push_back(board);
return;
}
for (int col = 0; col < n; col++) {
int d1 = row - col;
int d2 = row + col;
if (cols.count(col) || diag1.count(d1) || diag2.count(d2)) {
continue;
}
board[row][col] = 'Q';
cols.insert(col);
diag1.insert(d1);
diag2.insert(d2);
backtrack(board, row + 1, n);
board[row][col] = '.';
cols.erase(col);
diag1.erase(d1);
diag2.erase(d2);
}
}
};
二分查找
63. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。
二分查找第一个
>= target的位置。target <= nums[mid]时记录ans = mid并收缩右边界,否则左边界右移;循环结束返回ans(初始为n,处理 target 比所有元素大的情况)。时间 O(log n)。
cpp
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0, right = n - 1, ans = n;
while (left <= right) {
int mid = ((right - left) >> 1) + left;
if (target <= nums[mid]) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
};
64. 搜索二维矩阵
给你一个满足下述两条属性的 m x n 整数矩阵 matrix:每行中的整数从左到右按非严格递增顺序排列;每行的第一个整数大于前一行的最后一个整数。给你一个整数 target,如果 target 在矩阵中,返回 true,否则返回 false。
二维转一维 + 二分。矩阵整体按行展开是升序序列,用一维下标
mid二分,通过matrix[mid / n][mid % n]映射回二维坐标比较。时间 O(log(mn))。
cpp
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int left = 0, right = m * n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
// 把一维下标 mid 映射回二维坐标
int x = matrix[mid / n][mid % n];
if (x == target) return true;
if (x < target) left = mid + 1;
else right = mid - 1;
}
return false;
}
};
65. 在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置 。如果数组中不存在目标值,返回 [-1, -1]。必须设计并实现时间复杂度为 O(log n) 的算法。
两次二分。
findLeft找第一个>= target的位置,findRight找最后一个<= target的位置,最后验证nums[left] == target是否成立。也可直接用lower_bound/upper_bound。时间 O(log n)。
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty()) return {-1, -1};
return {findLeft(nums, target), findRight(nums, target)};
}
private:
// 找第一个 >= target 的位置
int findLeft(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) right = mid - 1;
else left = mid + 1;
}
// left 是第一个 >= target 的位置
if (left < nums.size() && nums[left] == target) return left;
return -1;
}
// 找最后一个 <= target 的位置
int findRight(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) left = mid + 1;
else right = mid - 1;
}
// right 是最后一个 <= target 的位置
if (right >= 0 && nums[right] == target) return right;
return -1;
}
};
// class Solution {
//public:
// vector<int> searchRange(vector<int>& nums, int target) {
// // 第一个 >= target 的位置
// int left = lower_bound(nums.begin(), nums.end(), target) - nums.begin();
// // 第一个 > target 的位置
// int right = upper_bound(nums.begin(), nums.end(), target) - nums.begin() - 1;
//
// // 如果 left 越界,或者 nums[left] != target,说明 target 不存在
// if (left == nums.size() || nums[left] != target) {
// return {-1, -1};
// }
// return {left, right};
// }
//};
66. 搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值互不相同。数组在某个下标处被旋转后,给你旋转后的数组 nums 和一个整数 target,如果 nums 中存在这个目标值 target,则返回它的下标,否则返回 -1。必须设计一个时间复杂度为 O(log n) 的算法。
二分 + 判断哪半段有序。每次取
mid,若nums[left] <= nums[mid]说明左半段有序,再看 target 是否落在左半段范围内,决定收缩哪边;否则右半段有序,同理判断。时间 O(log n)。
cpp
class Solution {
public:
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]) {
// target 在左半段范围内
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 右半段有序
else {
// target 在右半段范围内
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
};
67. 寻找旋转排序数组中的最小值
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次旋转后得到输入数组。给你一个元素值互不相同的数组 nums,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的最小元素 。必须设计一个时间复杂度为 O(log n) 的算法。
二分收缩。比较
nums[mid]和nums[right]:若nums[mid] > nums[right],最小值在右半边,left = mid + 1;否则最小值在左半边(含 mid),right = mid。循环结束时left == right,返回nums[left]。时间 O(log n)。
cpp
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
// mid 在左半段,最小值在右边
left = mid + 1;
} else {
// mid 在右半段,最小值在左边或就是 mid
right = mid;
}
}
return nums[left];
}
};
68. 寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2,请你找出并返回这两个正序数组的中位数 。算法的时间复杂度应该为 O(log (m+n))。
二分分割线。保证
nums1较短,在nums1上二分分割点i,j = totalLeft - i为nums2的分割点;取分割线两侧的L1, R1, L2, R2(越界用 ±INF),满足L1 <= R2 && L2 <= R1即找到正确分割线,按奇偶返回中位数;否则根据L1 > R2或L2 > R1调整i。时间 O(log(min(m,n)))。
cpp
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// 保证 nums1 更短,减少二分次数
if (nums1.size() > nums2.size()) swap(nums1, nums2);
int m = nums1.size(), n = nums2.size();
int totalLeft = (m + n + 1) / 2; // 左半部分元素总数
int left = 0, right = m;
while (left <= right) {
int i = left + (right - left) / 2; // nums1 左边取 i 个
int j = totalLeft - i; // nums2 左边取 j 个
// 取分割线两侧的值,越界时用 ±INF 代替
int L1 = (i == 0) ? INT_MIN : nums1[i - 1];
int R1 = (i == m) ? INT_MAX : nums1[i];
int L2 = (j == 0) ? INT_MIN : nums2[j - 1];
int R2 = (j == n) ? INT_MAX : nums2[j];
if (L1 <= R2 && L2 <= R1) {
// 找到正确分割线
int leftMax = max(L1, L2);
if ((m + n) % 2 == 1) return leftMax; // 奇数
int rightMin = min(R1, R2);
return (leftMax + rightMin) / 2.0; // 偶数
}
else if (L1 > R2) {
right = i - 1; // i 太大,往左移
}
else {
left = i + 1; // i 太小,往右移
}
}
return 0.0;
}
};
栈
69. 有效的括号
给定一个只包括 '('、')'、'{'、'}'、'['、']' 的字符串 s,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合;左括号必须以正确的顺序闭合;每个右括号都有一个对应的相同类型的左括号。
栈匹配。左括号入栈;右括号检查栈顶是否为对应左括号,不匹配或栈空则返回 false,匹配则弹出。遍历结束栈空说明全部闭合。时间 O(n),空间 O(n)。
cpp
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for (char c : s) {
// 左括号入栈
if (c == '(' || c == '[' || c == '{') {
st.push(c);
}
// 右括号:检查栈顶是否匹配
else {
if (st.empty()) return false; // 没有对应的左括号
char top = st.top();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
st.pop();
}
}
return st.empty(); // 栈空说明所有左括号都闭合了
}
};
70. 最小栈
设计一个支持 push、pop、top 操作,并能在常数时间 内检索到最小元素的栈。实现 MinStack 类:push(val) 将元素压入栈;pop() 删除栈顶元素;top() 获取栈顶元素;getMin() 检索栈中的最小元素。
双栈(数据栈 + 最小值栈)。
minSt栈顶始终是当前栈内最小值,push时压入min(value, minSt.top()),pop时两栈同步弹出;minSt初始压入INT_MAX作哨兵避免判空。getMin直接返回minSt.top(),所有操作 O(1)。
cpp
class MinStack {
private:
stack<int> st; // 数据栈
stack<int> minSt; // 最小值栈
public:
MinStack() {
minSt.push(INT_MAX); // 哨兵,避免 push 时判空
}
void push(int value) {
st.push(value);
minSt.push(min(value, minSt.top()));
}
void pop() {
st.pop();
minSt.pop();
}
int top() {
return st.top();
}
int getMin() {
return minSt.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(value);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
71. 字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。编码规则为 k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。可以认为输入字符串总是有效的,且原字符串中不含有额外的空格。
双栈(数字栈 + 字符串栈)。遇到数字累积
num;遇到[把num和当前字符串分别压栈,然后清空;遇到]弹出次数和之前的字符串,把当前字符串重复times次拼到之前字符串后面;普通字符追加到当前字符串。时间 O(n + 输出长度),空间 O(n)。
cpp
class Solution {
public:
string decodeString(string s) {
stack<int> numStk;
stack<string> strStk;
string cur = "";
int num = 0;
for (char c : s) {
if (isdigit(c)) {
num = num * 10 + (c - '0');
} else if (c == '[') {
numStk.push(num);
strStk.push(cur);
num = 0;
cur = "";
} else if (c == ']') {
int times = numStk.top(); numStk.pop();
string prev = strStk.top(); strStk.pop();
string tmp = "";
for (int i = 0; i < times; i++) tmp += cur;
cur = prev + tmp;
} else {
cur += c;
}
}
return cur;
}
};
72. 每日温度
给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
单调递减栈(存下标)。遍历温度,当前温度大于栈顶温度时,栈顶下标的答案就是
i - idx,弹出;否则入栈。栈内温度单调递减,每个下标最多入栈出栈一次。时间 O(n),空间 O(n)。
cpp
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> ans(n, 0); // 初始化,默认 0
stack<int> st; // 只存索引
for (int i = 0; i < n; i++) {
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
int idx = st.top();
st.pop();
ans[idx] = i - idx;
}
st.push(i);
}
return ans;
}
};
73. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。求在该柱状图中,能够勾勒出来的矩形的最大面积。
单调递增栈 + 首尾哨兵。首尾各加高度 0 的哨兵,保证栈不空且所有柱子最终被弹出计算。遍历高度,当前高度小于栈顶高度时弹出栈顶,以弹出柱子的高度为矩形高,宽度为
i - 新栈顶 - 1,更新最大面积。时间 O(n),空间 O(n)。
cpp
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
heights.insert(heights.begin(), 0); // 首部哨兵
heights.push_back(0); // 尾部哨兵
stack<int>st;
int ans=0;
for(int i=0;i<heights.size();i++)
{
while(!st.empty()&&heights[i]<heights[st.top()])
{
int h=heights[st.top()];
st.pop();
int w=i-st.top()-1;
ans=max(ans,h*w);
}
st.push(i);
}
return ans;
}
};
堆
74. 数组中的第 K 个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大 的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
快速选择(Quickselect)。第
k大在升序数组中的下标是n - k。用快排的 partition 思想,每次选基准分区后,只递归目标所在的那一半,平均时间 O(n)。随机选基准避免有序数组退化成 O(n²)。空间 O(1)(迭代)或 O(log n)(递归栈)。
cpp
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int n = nums.size();
int target = n - k; // 第 k 大在升序数组中的下标
return quickSelect(nums, 0, n - 1, target);
}
private:
// 快速选择:只递归目标所在的那一半
int quickSelect(vector<int>& a, int l, int r, int target) {
if (l == r) return a[l]; // 区间只剩一个元素,直接返回
int idx = partition(a, l, r); // 分区,返回基准最终位置
if (idx == target) return a[idx];
else if (idx < target) return quickSelect(a, idx + 1, r, target);
else return quickSelect(a, l, idx - 1, target);
}
// 分区函数:完全模仿你给的 qsort 分区逻辑,改为 0-based
int partition(vector<int>& a, int l, int r) {
// 随机选基准,避免有序数组退化成 O(n^2)
int randomIdx = l + rand() % (r - l + 1);
swap(a[l], a[randomIdx]);
int i = l, j = r;
int t = a[l]; // 基准值
while (i < j) {
while (i < j && a[j] > t) j--;
while (i < j && a[i] <= t) i++;
swap(a[i], a[j]);
}
swap(a[i], a[l]); // 基准归位
return i;
}
};
75. 前 K 个高频元素
给你一个整数数组 nums 和一个整数 k,请你返回其中出现频率前 k 高的元素。你可以按任意顺序返回答案。
哈希计数 + 桶排序。先用哈希表统计频率,再建
n+1个桶,bucket[cnt]存频率为cnt的元素;从高频桶往低频桶遍历,收集前 k 个元素。时间 O(n),空间 O(n)。也可用小根堆做到 O(n log k)。
cpp
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> freq;
for (int x : nums) freq[x]++;
int n = nums.size();
vector<vector<int>> bucket(n + 1); // bucket[i] 存频率为 i 的元素
for (auto& [val, cnt] : freq) {
bucket[cnt].push_back(val);
}
vector<int> ans;
for (int i = n; i >= 1 && ans.size() < k; i--) {
for (int val : bucket[i]) {
ans.push_back(val);
if (ans.size() == k) break;
}
}
return ans;
}
};
76. 数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。实现 MedianFinder 类:addNum(num) 从数据流中添加一个整数;findMedian() 返回目前所有元素的中位数。
对顶堆(大根堆 + 小根堆)。大根堆存较小的一半,小根堆存较大的一半,固定
maxHeap.size() >= minHeap.size()且差值不超过 1。addNum三步:先入大根堆 → 把大根堆堆顶移到小根堆 → 若小根堆更大则移回。findMedian:大小相等取两堆顶平均,否则取大根堆堆顶。addNumO(log n),findMedianO(1)。
cpp
class MedianFinder {
private:
priority_queue<int> maxHeap; // 大根堆,存较小的一半
priority_queue<int, vector<int>, greater<int>> minHeap; // 小根堆,存较大的一半
public:
MedianFinder() {}
void addNum(int num) {
// 1. 先放入 maxHeap
maxHeap.push(num);
// 2. 把 maxHeap 的堆顶(左半最大值)移到 minHeap
minHeap.push(maxHeap.top());
maxHeap.pop();
// 3. 如果 minHeap 比 maxHeap 大了,把 minHeap 堆顶移回 maxHeap
if (minHeap.size() > maxHeap.size()) {
maxHeap.push(minHeap.top());
minHeap.pop();
}
}
double findMedian() {
if (maxHeap.size() > minHeap.size()) {
return maxHeap.top();
}
return (maxHeap.top() + minHeap.top()) / 2.0;
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
贪心算法
77. 买卖股票的最佳时机
给定一个数组 prices,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择某一天 买入这只股票,并选择在未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润 。如果你不能获取任何利润,返回 0。
一次遍历维护历史最低价。遍历价格,用
maxn更新prices[i] - minn(当前价减历史最低价),再用minn更新历史最低价。时间 O(n),空间 O(1)。注意minn初始值要足够大(如INT_MAX)。
cpp
class Solution {
public:
int maxProfit(vector<int>& prices){
int minn=10e4,maxn=0,mark=0;
for(int i=0;i<prices.size();i++)
{
maxn=max(maxn,prices[i]-minn);
minn=min(minn,prices[i]);
}
return maxn;
}
};
78. 跳跃游戏
给你一个非负整数数组 nums,你最初位于数组的第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
贪心维护最远可达距离。遍历数组,维护
maxReach = max(maxReach, i + nums[i]);若当前下标i > maxReach,说明该位置不可达,返回 false;若maxReach >= n-1提前返回 true。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxReach = 0;
int n = nums.size();
for (int i = 0; i < n; ++i) {
if (i > maxReach) return false; // 当前位置都到不了
maxReach = max(maxReach, i + nums[i]);
if (maxReach >= n - 1) return true;
}
return true;
}
};
79. 跳跃游戏 II
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。每个元素 nums[i] 表示从下标 i 向前跳转的最大长度。返回到达 nums[n - 1] 的最小跳跃次数。
贪心 + BFS 分层。维护
currentEnd(当前步数能到的最远边界)和farthest(下一步能到的最远位置)。遍历时不断更新farthest;当i == currentEnd时,说明当前步数用尽,必须跳一步,jumps++、currentEnd = farthest;若currentEnd >= n-1提前退出。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
int jumps = 0, currentEnd = 0, farthest = 0;
for (int i = 0; i < n - 1; i++) {
farthest = max(farthest, i + nums[i]);
if (i == currentEnd) { // 必须跳跃
jumps++;
currentEnd = farthest;
if (currentEnd >= n - 1) break; // 提前结束
}
}
return jumps;
}
};
80. 划分字母区间
给你一个字符串 s。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中 。注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s。返回一个表示每个字符串片段的长度的列表。
贪心 + 记录最后出现位置。先遍历一遍记录每个字母最后出现的位置;再遍历一次,维护当前片段的右边界
end = max(end, last[s[i]-'a']);当i == end时,说明当前片段内所有字母的最后出现位置都不超过end,可以切出一段,记录长度并更新start = i + 1。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
vector<int> partitionLabels(string s) {
int last[26] = {0};
// 第一步:记录每个字母最后出现的位置
for (int i = 0; i < s.size(); i++) {
last[s[i] - 'a'] = i;
}
vector<int> ans;
int start = 0, end = 0;
// 第二步:遍历,动态扩展右边界
for (int i = 0; i < s.size(); i++) {
end = max(end, last[s[i] - 'a']);
// 第三步:到达边界,切出一段
if (i == end) {
ans.push_back(end - start + 1);
start = i + 1;
}
}
return ans;
}
};
动态规划
81. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
斐波那契递推。
dp[i] = dp[i-1] + dp[i-2],初始化dp[1] = 1, dp[2] = 2,从i=3递推到n。时间 O(n),空间 O(n)(可滚动变量优化到 O(1))。
cpp
class Solution {
public:
int climbStairs(int n) {
if (n <= 1) return 1;
int dp[50] = {0};
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
82. 杨辉三角
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。杨辉三角中,每个数是它左上方和右上方的数的和。
逐行构造。第
i行有i+1个元素,首尾为 1,中间ans[i][j] = ans[i-1][j-1] + ans[i-1][j]。时间 O(numRows²),空间 O(numRows²)。
cpp
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> ans(numRows); // 预分配 numRows 行
for (int i = 0; i < numRows; i++) {
ans[i].resize(i + 1); // 第 i 行有 i+1 个元素
ans[i][0] = ans[i][i] = 1; // 首尾固定为 1
for (int j = 1; j < i; j++) {
ans[i][j] = ans[i - 1][j - 1] + ans[i - 1][j];
}
}
return ans;
}
};
83. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
二维 DP(偷/不偷)。
dp[i][0]表示不偷第 i 间(max(dp[i-1][0], dp[i-1][1])),dp[i][1]表示偷第 i 间(dp[i-1][0] + nums[i])。答案取max(dp[n-1][0], dp[n-1][1])。时间 O(n),空间 O(n)。一维版:dp[i] = max(dp[i-1], dp[i-2] + nums[i]),可优化到 O(1)。
cpp
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if (n == 1) return nums[0];
int dp[110][2] = {0};
dp[0][0] = 0; // 不偷第 0 间
dp[0][1] = nums[0]; // 偷第 0 间
for (int i = 1; i < n; i++) {
dp[i][0] = max(dp[i-1][0], dp[i-1][1]); // 不偷 i,i-1 可偷可不偷
dp[i][1] = dp[i-1][0] + nums[i]; // 偷 i,i-1 必须不偷
}
return max(dp[n-1][0], dp[n-1][1]);
}
};
84. 完全平方数
给你一个整数 n,返回和为 n 的完全平方数的最少数量 。完全平方数是一个整数,其值等于另一个整数的平方,例如 1、4、9、16 等。
完全背包(求最小物品数)。
dp[i]表示凑出i的最少平方数个数,dp[i] = min(dp[i], dp[i - j*j] + 1),j*j <= i。时间 O(n√n),空间 O(n)。
cpp
class Solution {
public:
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];
}
};
85. 零钱兑换
给你一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1。每种硬币的数量是无限的。
完全背包(求最小硬币数)。
dp[j] = min(dp[j], dp[j - coins[i]] + 1),外层枚举硬币、内层正序 枚举金额(保证可重复取)。初始化为amount+1当无穷大,最后判断是否仍为无穷大。时间 O(amount × 硬币数),空间 O(amount)。
cpp
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, amount + 1); // amount+1 当无穷大,不会溢出
dp[0] = 0;
for (int i = 0; i < coins.size(); i++) {
for (int j = coins[i]; j <= amount; j++) { // 正序!完全背包
dp[j] = min(dp[j], dp[j - coins[i]] + 1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
};
86. 单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s,则返回 true。注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
完全背包(可行性)。
dp[i]表示s前i个字符能否被拆分,dp[i] = dp[j] && dict.count(s.substr(j, i-j)),枚举切分点j。时间 O(n² × 子串长度),空间 O(n)。
cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> dict(wordDict.begin(), wordDict.end());
int n = s.size();
vector<bool> dp(n + 1, false);
dp[0] = true; // 空串可以被"拆分"
for (int i = 1; i <= n; i++) { // 枚举前缀长度 i
for (int j = 0; j < i; j++) { // 枚举切分点 j
if (dp[j] && dict.count(s.substr(j, i - j))) {
dp[i] = true;
break; // 找到一种即可,提前退出
}
}
}
return dp[n];
}
};
87. 最长递增子序列
给你一个整数数组 nums,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。
贪心 + 二分(耐心排序)。维护
d数组,d[i]表示长度为i+1的递增子序列的最小结尾。对每个x,用lower_bound找第一个>= x的位置:没找到则追加(长度 +1),找到则替换(让结尾更小,为后续创造机会)。时间 O(n log n),空间 O(n)。另有 O(n²) 的经典 DP:dp[i] = max(dp[j] + 1)(j < i且nums[j] < nums[i])。
cpp
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// d 数组:d[i] 表示"长度为 i+1 的递增子序列"的最小结尾元素
// 这个数组始终严格递增,长度就是当前找到的 LIS 长度
vector<int> d;
// 遍历原数组中的每一个数字 x
for (int x : nums) {
// 在 d 中二分查找第一个 >= x 的元素位置
// lower_bound 返回迭代器:
// - 如果所有元素都 < x,返回 d.end()
// - 否则返回第一个 >= x 的位置
auto it = lower_bound(d.begin(), d.end(), x);
// 情况 1:x 比 d 中所有结尾元素都大
// 说明 x 可以接在当前最长递增子序列的后面,直接追加,长度 +1
if (it == d.end()) {
d.push_back(x);
}
// 情况 2:找到了第一个 >= x 的元素
// 用 x 替换它,不会改变 d 的长度,但能让该长度下的结尾元素变小
// 这样后续有更多机会接上更大的数字,贪心地为更长子序列创造可能
else {
*it = x;
}
}
// d 的长度就是整个数组的最长递增子序列长度
return d.size();
}
};
88. 乘积最大子数组
给你一个整数数组 nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
同时维护最大和最小乘积。因为负数的符号翻转效应,
maxF[i] = max(nums[i], maxF[i-1]*nums[i], minF[i-1]*nums[i]),minF[i]同理取min。答案取所有maxF[i]的最大值。时间 O(n),空间 O(n)(可优化到 O(1))。
cpp
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
vector<int> maxF(nums), minF(nums);
int ans = nums[0];
for (int i = 1; i < n; i++) {
maxF[i] = max({nums[i], maxF[i - 1] * nums[i], minF[i - 1] * nums[i]});
minF[i] = min({nums[i], maxF[i - 1] * nums[i], minF[i - 1] * nums[i]});
ans = max(ans, maxF[i]);
}
return ans;
}
};
89. 分割等和子集
给你一个只包含正整数 的非空 数组 nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
0-1 背包(可行性)。总和为奇数直接返回 false;目标为
sum/2,dp[j]表示能否凑出和j,dp[j] = dp[j] || dp[j - num]。外层枚举数字,内层倒序枚举容量(保证每个数字只用一次)。时间 O(n × target),空间 O(target)。
cpp
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 == 1) 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];
}
};
90. 最长有效括号
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
栈 + 哨兵。栈底存"最后一个未匹配的右括号下标",初始压入 -1。遇
'('压栈;遇')'弹出,若栈空则把当前下标压入作新边界,否则ans = max(ans, i - st.top())。时间 O(n),空间 O(n)。另有 O(1) 空间的双指针两次遍历解法。
cpp
class Solution {
public:
int longestValidParentheses(string s) {
stack<int> st;
st.push(-1); // 哨兵:最后一个未匹配的右括号下标
int ans = 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 {
ans = max(ans, i - st.top());
}
}
}
return ans;
}
};
多维动态规划
91. 不同路径
一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下 或者向右移动一步。机器人试图达到网格的右下角。问总共有多少条不同的路径?
二维 DP。
dp[i][j] = dp[i-1][j] + dp[i][j-1](上方路径数 + 左方路径数)。起点dp[1][1] = 1,第一行和第一列自动推导为 1。时间 O(mn),空间 O(mn)(可滚动数组优化到 O(n))。
cpp
class Solution {
public:
int uniquePaths(int m, int n) {
int dp[110][110] = {0};
dp[1][1] = 1; // 起点只有一种到达方式
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (i == 1 && j == 1) continue; // 起点已初始化
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m][n];
}
};
92. 最小路径和
给定一个包含非负整数的 m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。
二维 DP 求最小。
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]。用memset(dp, 0x3f, sizeof(dp))初始化无穷大,起点dp[0][0] = grid[0][0],边界用if(i>0)/if(j>0)处理。时间 O(mn),空间 O(mn)。
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m=grid.size(),n=grid[0].size();
int dp[210][210];
memset(dp, 0x3f, sizeof(dp)); // 每个 int 变成 0x3f3f3f3f
dp[0][0]=grid[0][0];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i>0) dp[i][j]=min(dp[i][j],dp[i-1][j]+grid[i][j]);
if(j>0) dp[i][j]=min(dp[i][j],dp[i][j-1]+grid[i][j]);
}
}
return dp[m-1][n-1];
}
};
93. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
区间 DP。
dp[i][j]表示s[i..j]是否回文。首尾相等且(长度 ≤ 2 或dp[i+1][j-1]为真)时回文。i从后往前、j从i往后遍历,保证dp[i+1][j-1]已计算。记录最长回文的起点和长度。时间 O(n²),空间 O(n²)。另有 O(1) 空间的中心扩展法。
cpp
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n, false));
int start = 0, maxLen = 1;
//dp[i][j] 依赖 dp[i+1][j-1], i 从后往前,j 从 i 往后,保证 dp[i+1][j-1] 已经计算过
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i+1][j-1])) {
dp[i][j] = true;
if (j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i;
}
}
}
}
return s.substr(start, maxLen);
}
};
94. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列 的长度。如果不存在公共子序列,返回 0。子序列是指在不改变字符相对顺序的情况下,删除某些字符后组成的新字符串。
二维 DP。
dp[i][j]表示text1前 i 个字符和text2前 j 个字符的 LCS 长度。字符相等时dp[i][j] = dp[i-1][j-1] + 1;不等时dp[i][j] = max(dp[i-1][j], dp[i][j-1])。注意下标偏移(text1[i-1])。时间 O(mn),空间 O(mn)。
cpp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(), n = text2.size();
// dp[i][j]:text1 前 i 个字符和 text2 前 j 个字符的 LCS 长度
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
};
95. 编辑距离
给你两个单词 word1 和 word2,请返回将 word1 转换成 word2 所使用的最少操作数。你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符。
二维 DP。
dp[i][j]表示word1前 i 个字符转成word2前 j 个字符的最少操作数。字符相同时dp[i][j] = dp[i-1][j-1];不同时dp[i][j] = min(删除, 插入, 替换) + 1,即min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1。初始化dp[i][0] = i、dp[0][j] = j。时间 O(mn),空间 O(mn)。
cpp
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
// dp[i][j]:word1 前 i 个字符转成 word2 前 j 个字符的最少操作数
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
// 初始化:空串转换
for (int i = 0; i <= m; i++) dp[i][0] = i; // 删除 i 个字符
for (int j = 0; j <= n; j++) dp[0][j] = 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], // 删除
dp[i][j - 1], // 插入
dp[i - 1][j - 1]}) + 1; // 替换
}
}
}
return dp[m][n];
}
};
技巧
96. 只出现一次的数字
给你一个非空 整数数组 nums,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
异或运算。
a ^ a = 0、a ^ 0 = a,所有数字异或起来,成对的抵消为 0,剩下就是只出现一次的数字。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans = 0;
for (int x : nums) {
ans ^= x; // 异或累积,成对的会抵消为 0
}
return ans;
}
};
97. 多数元素
给定一个大小为 n 的数组 nums,返回其中的多数元素 。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法。
Boyer-Moore 投票算法。维护候选人和计数器,相同则加票、不同则抵消,计数归零时换候选人。因为多数元素超过一半,最终剩下的候选人一定是它。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = 0, count = 0;
for (int x : nums) {
if (count == 0) {
candidate = x; // 换候选人
}
count += (x == candidate) ? 1 : -1;
}
return candidate;
}
};
98. 颜色分类
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。我们使用整数 0、1 和 2 分别表示红色、白色和蓝色。必须在不使用库内置的 sort 函数的情况下解决这个问题。
三指针(荷兰国旗)。
p0指向 0 区右边界,p2指向 2 区左边界,i遍历。遇 0 换到左边(p0++、i++),遇 1 跳过(i++),遇 2 换到右边(p2--,i不动,因为换过来的元素还没检查)。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
void sortColors(vector<int>& nums) {
int n = nums.size();
int p0 = 0, i = 0, p2 = n - 1;
while (i <= p2) {
if (nums[i] == 0) {
swap(nums[i], nums[p0]);
p0++;
i++;
} else if (nums[i] == 1) {
i++;
} else { // nums[i] == 2
swap(nums[i], nums[p2]);
p2--;
// i 不动,因为换过来的元素还没检查
}
}
}
};
99. 下一个排列
整数数组的一个排列 就是将其所有成员以序列或线性顺序排列。整数数组的下一个排列 是指其整数的下一个字典序更大的排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即升序排列)。必须原地修改,只允许使用额外常数空间。
两次扫描 + 反转。从右往左找第一个升序对
nums[i] < nums[i+1];再从右往左找第一个比nums[i]大的元素交换;最后反转i+1到末尾(降序变升序)。找不到升序对时直接反转整个数组。时间 O(n),空间 O(1)。
cpp
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n = nums.size();
int i = n - 2;
// 第 1 步:从右往左找第一个升序对 nums[i] < nums[i+1]
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
// 第 2 步:从右往左找第一个比 nums[i] 大的元素
int j = n - 1;
while (nums[j] <= nums[i]) {
j--;
}
swap(nums[i], nums[j]);
}
// 第 3 步:反转 i+1 到末尾
reverse(nums.begin() + i + 1, nums.end());
}
};
100. 寻找重复数
给定一个包含 n + 1 个整数的数组 nums,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。假设 nums 只有一个重复的整数 ,返回这个重复的数。你设计的解决方案必须不修改 数组 nums 且只用常量级 O(1) 的额外空间。
Floyd 判圈(快慢指针)。把数组看成链表
i -> nums[i],重复数字导致环。快慢指针先找相遇点,再把快指针重置到 0,两者同速前进,再次相遇即环入口(重复数字)。不修改数组,时间 O(n),空间 O(1)。
cpp
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = 0, fast = 0;
// 第 1 步:快慢指针找相遇点
do {
slow = nums[slow]; // 慢指针走一步
fast = nums[nums[fast]]; // 快指针走两步
} while (slow != fast);
// 第 2 步:快指针回到起点,两者同速前进,相遇点即环入口
fast = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
};