1.leetcode hot100
1.哈希表
1.两数之和
题目:
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
解答:哈希映射,
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> mp; // 用于存储数值和索引的映射
int n = nums.size();
for (int i = 0; i < n; i++) {
// 检查是否存在对应的数值,使得 nums[i] + 对应的数值 = target
if (mp.count(nums[i])) {
// 如果存在,返回对应的索引和当前索引
return {mp[nums[i]], i};
}
// 否则,将当前数值所需的目标差值存储到哈希表中
mp[target - nums[i]] = i;
}
return {}; // 如果没有找到,返回空的向量
}
};
2.字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
哈希 unordered_map
使用string类型为键,vector类型为值
使用string的sort方法,可以将strs数组中的每一个字符串排序,作为键,排序后一样的,就push_back到该键对应的数组中去。
创建 vector<vector<string>> vv,
因为遍历哈希表mp得到的是键值对,所以需要访问s.second,所以vv.push_back(s.second)
代码:
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<string>>mp;
for(auto&str:strs)
{
string sortedStr=str;
sort(sortedStr.begin(),sortedStr.end());
mp[sortedStr].push_back(str);
}
vector<vector<string>> vv;
for(auto& s:mp)
{
vv.push_back(s.second);
}
return vv;
}
};
3.最长连续序列
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
emplace
是 C++ 标准库中用于在容器中原地构造元素的方法。与传统的插入方法相比,它能避免不必要的复制或移动,从而提高性能。emplace
方法在多个容器中都有所支持,包括 std::vector
、std::list
、std::map
、std::set
等。通过 emplace
方法,你可以直接在容器内部构造元素,而不需要先构造元素然后再插入到容器中。
emplace
的基本用法
emplace
方法在不同容器中的用法大同小异。下面我们来看一些常用容器中 emplace
的使用。
1. std::vector
中的 emplace_back
emplace_back
是 std::vector
特有的一个方法,它用来在容器末尾原地构造元素。
#include <vector>
#include <iostream>
struct MyStruct {
int x, y;
MyStruct(int a, int b) : x(a), y(b) {
std::cout << "MyStruct constructor called.\n";
}
};
int main() {
std::vector<MyStruct> vec;
// 使用 emplace_back 原地构造元素
vec.emplace_back(1, 2);
vec.emplace_back(3, 4);
for (const auto& elem : vec) {
std::cout << "x: " << elem.x << ", y: " << elem.y << "\n";
}
return 0;
}
输出:
MyStruct constructor called.
MyStruct constructor called.
x: 1, y: 2
x: 3, y: 4
解释:emplace_back
直接使用构造函数的参数来创建 MyStruct
对象,而不是先创建对象再复制到容器中。
2.std::set
中的 emplace
在 std::set
中,emplace
可以用于原地插入元素,同时确保元素的唯一性和排序。
#include <set>
#include <iostream>
int main() {
std::set<int> mySet;
// 使用 emplace 插入元素
mySet.emplace(5);
mySet.emplace(3);
mySet.emplace(8);
for (const auto& num : mySet) {
std::cout << num << " "; // 输出:3 5 8
}
return 0;
}
解释:emplace
插入元素到 std::set
,并确保 std::set
保持元素的唯一性和排序。
3. std::map
中的 emplace
在 std::map
中,emplace
可以用于原地构造键值对。
#include <map>
#include <iostream>
int main() {
std::map<int, std::string> myMap;
// 使用 emplace 原地构造键值对
myMap.emplace(1, "One");
myMap.emplace(2, "Two");
for (const auto& pair : myMap) {
std::cout << pair.first << ": " << pair.second << "\n";
}
return 0;
}
解释:emplace
直接构造键值对 (1, "One")
和 (2, "Two")
,避免了不必要的复制。
emplace
的优点
- 性能优化:
emplace
直接在容器内部构造元素,避免了构造后再移动或复制,从而提高了性能。 - 避免冗余构造和复制:在插入复杂对象时,
emplace
可以避免额外的临时对象的创建。 - 使用方便:
emplace
可以直接使用构造函数参数进行插入,代码更简洁。
使用注意事项
- 适合构造复杂对象:
emplace
在插入简单数据类型(如int
)时,与insert
差别不大,但在插入复杂对象时,性能优势明显。 - 适用范围:并不是所有容器都支持
emplace
,但多数标准容器(如vector
,list
,set
,map
)都支持。
与 insert
的区别
insert
:需要传入一个已经构造好的对象,可能涉及到对象的复制或移动。emplace
:直接在容器内原地构造对象,避免了对象的复制或移动。
示例对比
#include <vector>
#include <iostream>
#include <string>
int main() {
std::vector<std::string> vec;
// 使用 insert
vec.insert(vec.end(), "Hello"); // 复制已构造好的字符串 "Hello"
// 使用 emplace_back
vec.emplace_back("World"); // 直接原地构造字符串 "World"
for (const auto& str : vec) {
std::cout << str << "\n";
}
return 0;
}
总结:
- 使用
emplace
可以避免对象的额外复制和移动,尤其在构造复杂对象时,可以明显提高性能。 emplace
直接使用构造函数参数进行原地构造,适用于几乎所有常见容器,包括std::vector
、std::set
、std::map
等。
最长子序列代码:
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
// 如果数组长度小于2,则直接返回数组长度,因为最长连续序列至少为1
if (nums.size() < 2) {
return nums.size();
}
// 使用集合来存储数组中的元素,保证元素的唯一性
set<int> s;
s.insert(nums.begin(), nums.end());
int res = 1; // 初始化最长连续序列的长度为1
// 遍历集合中的每个元素
for (auto& num : s) {
int cur = num;
// 如果当前元素的前一个元素不在集合中,说明它是一个连续序列的起点
if (!s.contains(cur - 1)) {
int count = 1; // 当前连续序列的长度
// 继续向后查找连续的元素,更新连续序列的长度
while (s.contains(cur + 1)) {
count++;
cur++;
}
// 更新最长连续序列的长度
res = max(count, res);
}
}
return res; // 返回最长连续序列的长度
}
};
2.双指针
1.移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
一个指针 lastNonZeroIndex 用于记录非零元素应该放置的位置。
遍历数组,将所有非零元素移到数组的前部,lastNonZeroIndex 随每次移动递增。
用零填充:遍历完成后,从 lastNonZeroIndex 开始到数组末尾,用零填充剩余的部分。
代码:
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n=nums.size();
int LastNonZeroIndex=0;
for(int i=0;i<n;i++)
{
if(nums[i])
{
nums[LastNonZeroIndex++]=nums[i];
}
}
for(int i=LastNonZeroIndex;i<n;i++)
{
nums[i]=0;
}
}
};
2.盛最多水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
示例 1:
代码:
class Solution {
public:
int maxArea(vector<int>& height) {
int res = 0; // 初始化最大面积为0
int left = 0; // 左指针指向数组的开头
int right = height.size() - 1; // 右指针指向数组的末尾
// 当左指针小于右指针时继续循环
while (left < right) {
// 计算当前区域的面积,取当前左右指针指向的高度中的较小值乘以宽度
res = max(res, min(height[left], height[right]) * (right - left));
// 移动高度较小的指针,以期望找到更高的高度来获取更大的面积
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return res; // 返回最大的面积
}
};
3.三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
代码:
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// 排序数组,方便使用双指针
sort(nums.begin(), nums.end());
int n = nums.size();
vector<vector<int>> v; // 存储结果的二维向量
// 遍历数组,每次固定一个数,查找其后的两数之和为目标值的组合
for (int i = 0; i < n; i++) {
// 如果当前数与前一个数相同,跳过以避免重复结果
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 使用双指针在剩余数组中查找两数之和为目标值的组合
int left = i + 1, right = n - 1;
// 如果左指针超出范围或左右指针相等,退出循环
if (left >= n || left == right) {
break;
}
while (left < right) {
// 如果当前左指针指向的数与前一个数相同,跳过以避免重复结果
if (left > i + 1 && nums[left] == nums[left - 1]) {
left++;
continue;
}
// 如果三数之和大于0,右指针左移以减小和
if (nums[i] + nums[left] + nums[right] > 0) {
right--;
}
// 如果三数之和等于0,找到一组结果并存入结果向量中
else if (nums[i] + nums[left] + nums[right] == 0) {
v.push_back({nums[i], nums[left], nums[right]});
left++;
}
// 如果三数之和小于0,左指针右移以增加和
else {
left++;
}
}
}
return v; // 返回结果
}
};
4.接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
方法一:二重循环双指针but超时:
class Solution {
public:
int trap(vector<int>& height) {
auto h=*max_element(height.begin(),height.end());
auto low=*min_element(height.begin(),height.end());
int ans=0;
int n=height.size();
for(int i=0;i<h;i++)//每一层单独计算蓄水
{
int left=0,right=n-1;
if(left<right)
{
while(left<right&&height[left]<=low)
{
left++;
}
while(left<right&&height[right]<=low)
{
right--;
}
for(int j=left+1;j<right;j++)
{
if(height[j]<=low)
{
ans++;
}
}
}
low++;
}
return ans;
}
};
方法二:
#include <vector>
using namespace std;
class Solution {
public:
int trap(vector<int>& height) {
// 初始化左右最大值和答案
int leftmax = 0, rightmax = 0;
int ans = 0;
int n = height.size();
int left = 0, right = n - 1; // 左右指针初始化
// 当左指针小于右指针时进行循环
while (left < right) {
// 如果左侧高度小于右侧高度
if (height[left] < height[right]) {
// 更新左侧最大高度
if (height[left] > leftmax) {
leftmax = height[left];
} else {
// 累加可以存储的雨水量
ans += leftmax - height[left];
}
// 移动左指针
left++;
} else {
// 如果右侧高度小于或等于左侧高度
// 更新右侧最大高度
if (height[right] > rightmax) {
rightmax = height[right];
} else {
// 累加可以存储的雨水量
ans += rightmax - height[right];
}
// 移动右指针
right--;
}
}
return ans; // 返回总的雨水量
}
};
3.滑动窗口
滑动窗口的应用场景
-
查找固定长度子数组的最大或最小值。
-
查找和为某个值的最长子数组。
-
字符串的模式匹配问题。
-
查找子数组的最大和最小和。
滑动窗口的实现
滑动窗口有两种常见的实现方式:固定长度窗口和可变长度窗口。
固定长度滑动窗口
假设我们要找到一个数组中所有长度为
k
的子数组的最大和。可以用滑动窗口来实现:#include <iostream> #include <vector> using namespace std; int maxSumSubarray(vector<int>& nums, int k) { int n = nums.size(); if (n < k) return -1; int max_sum = 0; int window_sum = 0; // 计算第一个窗口的和 for (int i = 0; i < k; i++) { window_sum += nums[i]; } max_sum = window_sum; // 滑动窗口 for (int i = k; i < n; i++) { window_sum += nums[i] - nums[i - k]; // 移动窗口 max_sum = max(max_sum, window_sum); } return max_sum; } int main() { vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9}; int k = 3; cout << "Maximum sum of subarrays of size " << k << " is " << maxSumSubarray(nums, k) << endl; return 0; }
可变长度滑动窗口
假设我们要找到一个数组中和为
s
的最小长度的子数组。可以用滑动窗口来实现:#include <iostream> #include <vector> using namespace std; int minSubArrayLen(int s, vector<int>& nums) { int n = nums.size(); int left = 0, right = 0; int sum = 0; int min_len = n + 1; while (right < n) { sum += nums[right++]; while (sum >= s) { min_len = min(min_len, right - left); sum -= nums[left++]; } } return min_len == n + 1 ? 0 : min_len; } int main() { vector<int> nums = {2, 3, 1, 2, 4, 3}; int s = 7; cout << "Minimum length of subarray with sum >= " << s << " is " << minSubArrayLen(s, nums) << endl; return 0; }
滑动窗口的优点
- 时间复杂度低:多数情况下,滑动窗口能够将时间复杂度降低到 O(n),比传统的 O(n^2) 要快很多。
- 代码简洁:滑动窗口的代码往往比较简洁,便于理解和实现。
1.无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长
子串
的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
代码一:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// 初始化左右指针 i 和 j,初始最长子串长度 ans
int i = 0, j = 0, ans = 0;
// 创建一个大小为128的向量 table 用于记录字符出现的次数
vector<int> table(128);
// 遍历字符串 s
for (; j < s.size(); j++) {
// 如果当前字符 s[j] 没有在窗口中出现
if (table[s[j]] == 0) {
table[s[j]]++; // 将其计数增加
} else { // 如果当前字符 s[j] 已经在窗口中出现
// 移动左指针 i,直到 s[j] 不再在窗口中
while (table[s[j]] != 0) {
table[s[i]]--; // 移除字符 s[i],并将其计数减少
i++; // 左指针右移
}
table[s[j]] = 1; // 将当前字符 s[j] 计数重置为1
}
// 更新最长子串的长度
ans = max(ans, j - i + 1);
}
return ans; // 返回最长子串的长度
}
};
代码二:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0, right = 0; // 初始化滑动窗口的左右指针
int maxlen = 0; // 记录最长子串长度
int n = s.size(); // 获取字符串长度
// 如果字符串长度小于等于1,直接返回字符串长度
if (n <= 1)
return n;
// 遍历字符串
while (right < n - 1) {
// 获取当前滑动窗口内的子字符串
string str = s.substr(left, right - left + 1);
// 查找即将加入窗口的字符在当前窗口中的位置
int pos = str.find(s[right + 1]);
// 如果找到了重复字符
if (pos >= 0) {
// 更新最长子串长度
maxlen = max(maxlen, right - left + 1);
// 移动左指针,跳过重复字符
left += pos + 1;
}
// 移动右指针
right++;
}
// 更新最长子串长度,处理最后一次更新
maxlen = max(maxlen, right - left + 1);
return maxlen; // 返回最长子串长度
}
};
区别:
第一个代码(使用了一个大小为 128 的向量 table
来记录每个字符的出现次数(哈希),滑动窗口的实现)相对于第二个代码(使用 substr
和 find
)在时间复杂度和效率上更优。
时间复杂度分析
代码一
-
时间复杂度
:O(n)
- 使用两个指针
i
和j
遍历字符串,每个字符最多访问两次(一次通过j
指针,一次通过i
指针)。查找和更新table
向量的操作是 O(1) 的,所以整体时间复杂度是 O(n)
- 使用两个指针
-
空间复杂度
:O(1)
table
向量的大小是固定的 128,不随输入字符串的长度变化。
代码二
-
时间复杂度
:O(n^2)
- 每次移动
right
指针时,都会调用substr
方法创建一个子字符串,并在该子字符串上调用find
方法。substr
和find
都是 O(n) 操作,这使得整体时间复杂度是 O(n^2)。
- 每次移动
-
空间复杂度
:O(n)
- 由于每次都要创建一个子字符串,因此需要额外的空间来存储子字符串。
关键差异
- 时间复杂度:
- 代码一的时间复杂度是 O(n),因为它在最坏情况下最多遍历字符串两次。
- 代码二的时间复杂度是 O(n^2),因为每次
substr
和find
操作都是 O(n) 的,这会导致嵌套循环。
- 空间复杂度:
- 代码一的空间复杂度是 O(1),因为
table
的大小是固定的。 - 代码二的空间复杂度是 O(n),因为每次都要创建新的子字符串。
- 代码一的空间复杂度是 O(1),因为
- 效率:
- 代码一效率更高,因为它通过
unordered_set
和滑动窗口直接在原字符串上操作,避免了重复的字符串创建和查找操作。 - 代码二效率较低,因为每次都要创建子字符串,并在子字符串上进行查找,操作冗余且耗时。
- 代码一效率更高,因为它通过
2.找到字符串中所有字母异位词
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
暴力搜索:超时
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int>ans;
sort(p.begin(),p.end());
int p_len=p.size(),s_len=s.size();
if(s_len<p_len)return ans;
for(int i=0;i<=s_len-p_len;i++)
{
string str=s.substr(i,p_len);
sort(str.begin(),str.end());
if(str==p)
{
ans.push_back(i);
}
}
return ans;
}
};
滑动窗口:
初始化频率数组:
p_count
:记录字符串p
中每个字符的频率。s_count
:记录当前窗口(初始为s
的前p_len
个字符)中每个字符的频率。
初始窗口比较:
- 比较
p_count
和s_count
,如果相同,则说明初始窗口是一个异位词。
滑动窗口:
- 从第
p_len
个字符开始,逐个字符滑动窗口。 - 每次移动窗口时,更新
s_count
数组,增加新进入窗口的字符频率,减少移出窗口的字符频率。 - 比较更新后的
s_count
和p_count
,如果相同,则记录窗口的起始索引。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int>ans;
int p_len=p.size(),s_len=s.size();
if(s_len<p_len)return ans;
vector<int>p_count(26,0),s_count(26,0);
for(int i=0;i<p_len;i++)
{
p_count[p[i]-'a']++;
s_count[s[i]-'a']++;
}
if(p_count==s_count)ans.push_back(0);
for(int i=0,j=p_len;j<s_len;i++,j++)
{
s_count[s[i]-'a']--;
s_count[s[j]-'a']++;
if(s_count==p_count)ans.push_back(i+1);
}
return ans;
}
};
4.子串
1.和为K的子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
前缀和,哈希表:
初始化哈希表:
prefixSumCount[0] = 1
,表示前缀和为0出现1次。这是为了处理从数组开头到某个位置的子数组和为k
的情况。
遍历数组:
- 计算当前前缀和
prefixSum
。 - 检查
prefixSum - k
是否在哈希表中。如果存在,说明有之前某个位置到当前的位置之间的子数组和为k
。 - 更新哈希表,记录当前前缀和
prefixSum
的出现次数。
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> prefixSumCount;
prefixSumCount[0] = 1; // 初始化前缀和为0出现1次
int count = 0;
int prefixSum = 0;
for (int num : nums) {
prefixSum += num; // 计算当前前缀和
// 检查当前前缀和减去k是否在哈希表中
if (prefixSumCount.find(prefixSum - k) != prefixSumCount.end()) {
count += prefixSumCount[prefixSum - k];
}
// 更新哈希表中当前前缀和的计数
prefixSumCount[prefixSum]++;
}
return count;
}
};
2.滑动窗口的最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
双端队列deque
- 双端队列
dq
:用来存储数组的索引,确保队列中的索引对应的元素值按照递减的顺序排列。 - 移除不在窗口内的索引:当索引超出了窗口范围时,从队列的头部移除。
- 保持队列递减:在将当前元素的索引加入队列之前,从队列的尾部开始移除比当前元素值小的索引,以保持队列的递减性质。
- 窗口形成后的处理:当索引达到窗口大小时(即
i >= k - 1
),将队列头部的元素加入结果数组中作为当前窗口的最大值。
通过这种方法,可以在 O(n)的时间复杂度内找到每个滑动窗口的最大值,是一个高效的解决方案。
#include <vector>
#include <deque>
using namespace std;
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> result;
deque<int> dq; // 双端队列,存储数组索引
for (int i = 0; i < nums.size(); ++i) {
// 移除不在窗口内的索引
if (!dq.empty() && dq.front() == i - k) {
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;
}
};
3.最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
滑动窗口+哈希
(感觉这个代码写的太妙了,leetcode上的朋友写的)
class Solution {
public:
string minWindow(string s, string t) {
// baseHash 记录 t 中每个字符出现的次数
// curHash 记录当前窗口中每个字符出现的次数
vector<int> baseHash(128, 0), curHash(128, 0);
// 填充 baseHash
for(auto& ch : t) {
++baseHash[ch];
}
int pos = 0, minlen = INT_MAX; // 子串起始位置和最小长度
int left = 0, right = 0; // 左右指针
int s_len = s.size(), t_len = t.size();
int count = 0; // 记录当前窗口中有效字符的个数
// 扩展右指针
while (right < s_len) {
char in = s[right++]; // 当前右指针指向的字符
// 更新当前窗口中字符的计数
if (++curHash[in] <= baseHash[in]) {
++count; // 如果当前字符属于 t 中且未超过需求计数,则计数加一
}
// 当当前窗口包含所有 t 中字符时,收缩左指针
while (count == t_len) {
if (right - left < minlen) {
pos = left; // 更新子串起始位置
minlen = right - left; // 更新最小长度
}
char out = s[left++]; // 当前左指针指向的字符
// 更新当前窗口中字符的计数
if (curHash[out]-- <= baseHash[out]) {
--count; // 如果当前字符属于 t 中且计数减少后未满足需求,则计数减一
}
}
}
// 如果找不到符合条件的子串,返回空字符串;否则返回最小子串
return minlen == INT_MAX ? "" : s.substr(pos, minlen);
}
};
5.普通数组
1.最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组
是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
动态规划
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size(); // 获取数组 nums 的长度
vector<int> prefix_sum(n + 1, 0); // 前缀和数组,长度为 n+1,初始化为0
prefix_sum[0] = 0; // 第一个前缀和为0,即空子数组的和为0
int max_sum = INT_MIN; // 初始最大和设为最小整数
for (int i = 0; i < n; i++) {
// 计算当前位置的前缀和,选择当前元素加入前一个元素的前缀和或者从当前元素开始新的子数组
prefix_sum[i + 1] = max(prefix_sum[i] + nums[i], nums[i]);
// 更新最大和
max_sum = max(max_sum, prefix_sum[i + 1]);
}
return max_sum; // 返回最大和
}
};
前缀和数组 与最小前缀和
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0; // 如果数组为空,直接返回0
vector<int> prefix_sum(n + 1, 0); // 前缀和数组,长度为 n+1,初始化为0
int max_sum = INT_MIN; // 最大子数组和初始为最小整数
int min_prefix_sum = 0; // 最小前缀和初始为0,因为前缀和数组第一位是0
for (int i = 0; i < n; ++i) {
prefix_sum[i + 1] = prefix_sum[i] + nums[i]; // 计算前缀和数组
max_sum = max(max_sum, prefix_sum[i + 1] - min_prefix_sum); // 更新最大子数组和
min_prefix_sum = min(min_prefix_sum, prefix_sum[i + 1]); // 更新最小前缀和
}
return max_sum; // 返回最大子数组和
}
};
2.合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
左端点排序,逐个比较:
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
// 对区间按照左端点排序
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
});
int n = intervals.size();
if (n < 1) return intervals; // 如果区间数小于1,直接返回intervals
vector<vector<int>> merge; // 存储合并后的区间结果
for (int i = 0; i < n; ++i) {
if (merge.empty() || merge.back()[1] < intervals[i][0]) {
// 如果merge为空,或者merge的最后一个区间的右端点小于当前区间的左端点
merge.push_back(intervals[i]); // 直接将当前区间加入merge
} else {
// 否则,说明当前区间与merge的最后一个区间有重叠,更新merge的最后一个区间的右端点
merge.back()[1] = max(merge.back()[1], intervals[i][1]);
}
}
return merge; // 返回合并后的区间结果
}
};
3.轮转数组
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
定义新数组ans,最终复制给nums
class Solution {
public:
void rotate(vector<int>& nums, int k) {
vector<int> ans;
int n=nums.size();
k=k%n;
for(int i=n-k;i<n;i++)
{
ans.push_back(nums[i]);
}
for(int i=0;i<n-k;i++)
{
ans.push_back(nums[i]);
}
nums=ans;
}
};
暂存在队列中
class Solution {
public:
void rotate(vector<int>& nums, int k) {
queue<int> q;
int n=nums.size();
k=k%n;
for(int i=0;i<n-k;i++)
{
q.push(nums[i]);
}
for(int i=n-k,j=0;i<n;i++,j++)
{
nums[j]=nums[i];
}
for(int i=k;i<n;i++)
{
nums[i]=q.front();
q.pop();
}
}
};
反转reverse
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n=nums.size();
k=k%n;
reverse(nums.begin(),nums.end());
reverse(nums.begin(),nums.begin()+k);
reverse(nums.begin()+k,nums.end());
}
};
Lambda 表达式:
auto reverse = [&](int i, int j) { ... };
定义了一个名为reverse
的 lambda 表达式。auto
关键字用于自动推断 lambda 表达式的类型。&
表示捕获外部作用域中的所有变量的引用,这样可以在 lambda 表达式内部访问并修改nums
数组。
参数:
(int i, int j)
是 lambda 表达式的参数,表示需要反转的数组的起始索引i
和结束索引j
。
lambda写reverse
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size(); // 获取数组 nums 的长度
k = k % n; // 计算实际需要旋转的步数,取余确保 k 在 [0, n-1] 范围内
// Lambda 函数,用于反转数组中指定范围的元素
auto reverse = [&](int i, int j) {
while (i < j) {
swap(nums[i++], nums[j--]); // 反转元素
}
};
reverse(0, n - 1); // 先将整个数组反转
reverse(0, k - 1); // 再反转前 k 个元素
reverse(k, n - 1); // 最后反转剩余的元素,即后 n-k 个元素
}
};
4 除自身以外数组的累乘
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n) 时间复杂度内完成此题。
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
左右累乘
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size(); // 获取数组 nums 的长度
vector<int> ans(n, 1); // 初始化结果数组 ans,全部设为 1
int l = 1, r = 1; // 初始化左乘积和右乘积为 1
// 第一轮循环计算左乘积
for (int i = 0; i < n; i++) {
ans[i] *= l; // 计算当前位置的结果数组元素乘以左乘积
l *= nums[i]; // 更新左乘积
}
// 第二轮循环计算右乘积
for (int i = n - 1; i >= 0; i--) {
ans[i] *= r; // 计算当前位置的结果数组元素乘以右乘积
r *= nums[i]; // 更新右乘积
}
return ans; // 返回结果数组
}
};
前缀积和后缀积
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size(); // 获取数组 nums 的长度
vector<int> ans(n, 1); // 初始化结果数组 ans,全部设为 1
vector<int> prefix(n, 1), surfix(n, 1); // 定义前缀数组和后缀数组,初始值均为 1
int l = 1, r = 1; // 初始化左右乘积为 1
// 计算前缀乘积数组和后缀乘积数组
for (int i = 1, j = n - 2; i < n && j >= 0; i++, j--) {
prefix[i] = prefix[i - 1] * nums[i - 1]; // 计算前缀乘积
surfix[j] = surfix[j + 1] * nums[j + 1]; // 计算后缀乘积
}
// 计算结果数组 ans
for (int i = 0; i < n; i++) {
ans[i] = prefix[i] * surfix[i]; // 结果数组的每个元素等于前缀乘积乘以后缀乘积
}
return ans; // 返回结果数组
}
};
5.缺失的第一个正数
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。
示例 2:
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
解释:最小的正数 1 没有出现。
暴力求解:
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
sort(nums.begin(), nums.end()); // 将数组 nums 排序
int ans = 1; // 初始化要返回的最小正整数为 1
int n = nums.size(); // 获取数组的长度
// 如果排序后的数组中最大的数都小于 1,则直接返回 1
if (nums[n - 1] < ans) {
return 1;
}
// 遍历排序后的数组 nums
for (int i = 0; i < n; i++) {
// 跳过非正整数
while (nums[i] <= 0) {
i++; // 向后移动直到找到第一个正整数
}
// 如果当前数大于要返回的最小正整数 ans,直接退出循环
if (nums[i] > ans) {
break;
}
// 如果当前数等于 ans,则更新 ans 为下一个正整数
if (nums[i] == ans) {
ans++;
}
}
return ans; // 返回找到的最小正整数
}
};
但是排序的实间复杂度为O(nlogn),不符合
题解的意思是:长度为n的数组,最小不出现的正数只可能在1-n+1之间
索引标记
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size(); // 获取数组的长度
// 第一步:将所有小于等于0的数字替换为n+1
// 这一步是为了将不可能成为第一个缺失的正整数的数字排除在外
for (int i = 0; i < n; ++i) {
if (nums[i] <= 0) {
nums[i] = n + 1;
}
}
// 第二步:利用索引标记存在的数字
// 对于每一个数字num,如果它在[1, n]范围内,就将索引num-1位置的数字变为负数,表示num存在
for (int i = 0; i < n; ++i) {
int num = abs(nums[i]);
if (num <= n) {
nums[num - 1] = -abs(nums[num - 1]);
}
}
// 第三步:找到第一个值为正数的索引,该索引+1即为第一个缺失的正整数
for (int i = 0; i < n; ++i) {
if (nums[i] > 0) {
return i + 1;
}
}
// 如果所有1到n的数字都存在,则返回n+1
return n + 1;
}
};
哈希表也是O(n)
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size(); // 获取数组的长度
unordered_map<int, bool> mp; // 使用无序映射记录数组中出现的数字
// 第一次遍历数组,将每个数字放入 unordered_map 中
for (int i = 0; i < n; ++i) {
mp[nums[i]] = true; // 将 nums[i] 放入映射中,值为 true
}
// 第二次遍历,从 1 开始逐个查找,找到第一个未出现在映射中的正整数
for (int i = 1; i <= n; ++i) {
if (!mp.count(i)) { // 如果 i 不在映射中
return i; // 返回第一个未出现的正整数
}
}
// 如果所有的正整数都在映射中,则返回下一个正整数
return n + 1;
}
};
力扣上还有一个题解是置换法,把对应数字送到数组的对应索引位置,比如2换到索引为1的地方
置换
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size(); // 获取数组的长度
// 第一次遍历,将每个正整数放到它应该在的位置上
for (int i = 0; i < n; ++i) {
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
// 如果 nums[i] 在 (1, n] 范围内,并且 nums[nums[i]-1] 不等于 nums[i],则交换位置
swap(nums[nums[i] - 1], nums[i]);
}
}
// 第二次遍历,查找第一个不在正确位置的正整数
for (int i = 0; i < n; ++i) {
if (nums[i] != i + 1) { // 如果当前位置不是应该的正整数
return i + 1; // 返回第一个缺失的正整数
}
}
// 如果数组中所有的正整数都在正确的位置上,则返回下一个正整数
return n + 1;
}
};
6.矩阵
1.矩阵置0
给定一个 *m* x *n*
的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法**。**
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
额外使用rows和cols数组记录要置零的行和列
空间O(m+n)
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size(); // 获取矩阵的行数
vector<int> rows, cols; // 定义两个向量用来记录值为0的元素所在的行和列
// 遍历矩阵,记录值为0的元素所在的行和列
for (int i = 0; i < m; i++) {
int n = matrix[i].size(); // 获取当前行的列数
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
rows.push_back(i); // 记录行号
cols.push_back(j); // 记录列号
}
}
}
// 将记录的行号对应的行全部置为0
for (int row : rows) {
int n = matrix[row].size(); // 获取该行的列数
for (int j = 0; j < n; j++) {
matrix[row][j] = 0;
}
}
// 将记录的列号对应的列全部置为0
for (int col : cols) {
for (int j = 0; j < m; j++) {
matrix[j][col] = 0;
}
}
}
};
使用第一个0所在行列标记
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size(); // 获取矩阵的行数
int n = matrix[0].size(); // 获取矩阵的列数
int row = -1, col = -1; // 初始化标记行和列为-1,表示未找到值为0的元素的位置
// 找到第一个值为0的元素的位置
for (int i = 0; i < m; i++) {
if (row != -1 && col != -1) {
break; // 如果已经找到了标记的行列位置,跳出循环
}
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
row = i; // 记录第一个值为0的行
col = j; // 记录第一个值为0的列
break; // 找到后跳出内层循环
}
}
}
// 如果未找到任何值为0的元素,直接返回
if (row == -1 || col == -1) {
return;
}
// 遍历矩阵,将值为0的元素所在的行和列的标记位置置为0
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[row][j] = 0; // 将同列的元素置为0
matrix[i][col] = 0; // 将同行的元素置为0
}
}
}
// 将标记的行置为0
for (int i = 0; i < m; i++) {
if (i != row && matrix[i][col] == 0) {
for (int j = 0; j < n; j++) {
matrix[i][j] = 0; // 第i行全部置为0
}
}
}
// 将标记的列置为0
for (int i = 0; i < n; i++) {
if (i != col && matrix[row][i] == 0) {
for (int j = 0; j < m; j++) {
matrix[j][i] = 0; // 第i列全部置为0
}
}
}
// 将标记的行和列自身置为0
for (int i = 0; i < m; i++) {
matrix[i][col] = 0; // 第col列全部置为0
}
for (int i = 0; i < n; i++) {
matrix[row][i] = 0; // 第row行全部置为0
}
}
};
2.螺旋矩阵
给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
设置四个边界
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> ans; // 用于存储结果的一维向量
if (matrix.empty() || matrix[0].empty()) return ans; // 如果矩阵为空或者第一行为空,则直接返回空结果
int left = 0, top = 0; // 左边界、上边界的初始位置
int bottom = matrix.size() - 1; // 下边界的初始位置(矩阵的行数)
int right = matrix[0].size() - 1; // 右边界的初始位置(矩阵的列数)
// 循环直到左边界大于右边界或者上边界大于下边界
while (left <= right && top <= bottom) {
// 从左到右遍历当前的上边界行
for (int j = left; j <= right; j++) {
ans.push_back(matrix[top][j]);
}
top++; // 上边界向下移动一行
// 从上到下遍历当前的右边界列
for (int i = top; i <= bottom; i++) {
ans.push_back(matrix[i][right]);
}
right--; // 右边界向左移动一列
// 检查是否还有未遍历的行和列,从右到左遍历当前的下边界行
if (left <= right && top <= bottom) {
for (int j = right; j >= left; j--) {
ans.push_back(matrix[bottom][j]);
}
bottom--; // 下边界向上移动一行
}
// 检查是否还有未遍历的行和列,从下到上遍历当前的左边界列
if (left <= right && top <= bottom) {
for (int i = bottom; i >= top; i--) {
ans.push_back(matrix[i][left]);
}
left++; // 左边界向右移动一列
}
}
return ans; // 返回螺旋顺序遍历的结果
}
};
3旋转图像
给定一个 n × n 的二维矩阵 matrix
表示一个图像。请你将图像顺时针旋转 90 度。
你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
设置边界参数
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size(); // 获取矩阵的大小,假设为 n × n
int count; // 定义计数变量
// 如果 n 是偶数,计数 count 就是 n 的一半;如果 n 是奇数,计数 count 就是 n 的一半加一
if (n % 2 == 0) {
count = n / 2;
} else {
count = n / 2 + 1;
}
// 遍历每一层环状区域
for (int i = 0; i < count; i++) {
// 定义当前层的四个边界的起始和结束位置
int l_begin = i, l_end = i;
int t_begin = i, t_end = i;
int r_begin = n - 1 - i, r_end = n - 1 - i;
int b_begin = n - 1 - i, b_end = n - 1 - i;
// 当前层的旋转操作,直到达到中心位置
while (b_end > t_begin) {
// 顺时针旋转四个位置的元素
int temp = matrix[t_begin][l_end];
matrix[t_begin][l_end] = matrix[b_end][l_begin];
matrix[b_end][l_begin] = matrix[b_begin][r_end];
matrix[b_begin][r_end] = matrix[t_end][r_begin];
matrix[t_end][r_begin] = temp;
// 更新四个边界的起始和结束位置,完成一次旋转
l_end++;
t_end++;
r_end--;
b_end--;
}
}
}
};
直接利用循环参数
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size(); // 获取矩阵的大小,假设为 n × n
int count; // 定义计数变量
// 如果 n 是偶数,计数 count 就是 n 的一半;如果 n 是奇数,计数 count 就是 n 的一半加一
if (n % 2 == 0) {
count = n / 2;
} else {
count = n / 2 + 1;
}
// 逐层旋转,从外向内,每次旋转一个环
for (int i = 0; i < count; i++) {
for (int j = i; j < n - 1 - i; j++) {
int temp = matrix[i][j]; // 临时变量存储当前位置的值
// 进行四个位置的元素交换,实现旋转操作
matrix[i][j] = matrix[n - 1 - j][i];
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
matrix[j][n - 1 - i] = temp;
}
}
}
};
4.搜索二维矩阵||
编写一个高效的算法来搜索 *m* x *n*
矩阵 matrix
中的一个目标值 target
。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
示例 1:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true
示例 2:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false
递归
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(); // 获取矩阵的行数
int n = matrix[0].size(); // 获取矩阵的列数
return searchRec(matrix, target, 0, m - 1, 0, n - 1); // 调用递归搜索函数
}
private:
bool searchRec(vector<vector<int>>& matrix, int target, int top, int bottom, int left, int right) {
// 如果左边界大于右边界或者上边界大于下边界,表示搜索范围为空,返回false
if (left > right || top > bottom) {
return false;
}
// 计算中间行和中间列的索引
int midrow = (top + bottom) / 2;
int midcol = (left + right) / 2;
int midval = matrix[midrow][midcol]; // 获取中间元素的值
if (midval == target) {
return true; // 如果中间元素等于目标值,找到目标值,返回true
} else if (midval > target) {
// 如果中间元素大于目标值,递归搜索左上和左下两个子矩阵
return searchRec(matrix, target, top, midrow - 1, left, right) || searchRec(matrix, target, midrow, bottom, left, midcol - 1);
} else {
// 如果中间元素小于目标值,递归搜索右上和右下两个子矩阵
return searchRec(matrix, target, midrow + 1, bottom, left, right) || searchRec(matrix, target, top, bottom, midcol + 1, right);
}
}
};
z型查找
Z型查找(又称为“从右上角或左下角开始的查找”)是一种有效的解决方案,可以避免递归并且在O(m + n)时间内完成查找
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(); // 获取矩阵的行数
if (m == 0) return false; // 如果矩阵行数为0,则直接返回false,表示矩阵为空
int n = matrix[0].size(); // 获取矩阵的列数
if (n == 0) return false; // 如果矩阵列数为0,则直接返回false,表示矩阵为空
int row = 0; // 从矩阵的第一行开始
int col = n - 1; // 从矩阵的最后一列开始
while (row < m && col >= 0) {
int current = matrix[row][col]; // 获取当前位置的值
if (current == target) { // 如果当前值等于目标值,返回true
return true;
} else if (current > target) { // 如果当前值大于目标值,向左移动一列
col--;
} else { // 如果当前值小于目标值,向下移动一行
row++;
}
}
return false; // 循环结束没有找到目标值,返回false
}
};
7.链表
1.相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
图示两个链表在节点 c1
开始相交**:**
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
双指针比较长度消除长度差(暴力)
/**
* 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) {
if(headA==nullptr||headB==nullptr)
{
return nullptr;
}
int a=0,b=0;
ListNode *pA=headA,*pB=headB;
while(pA!=nullptr)
{
a++;
pA=pA->next;
}
while(pB!=nullptr)
{
b++;
pB=pB->next;
}
pA=headA;
pB=headB;
if(a>b)
{
for(int i=0;i<a-b;i++)
{
pA=pA->next;
}
}
else if(a<b)
{
for(int i=0;i<b-a;i++)
{
pB=pB->next;
}
}
while(pA!=nullptr&&pB!=nullptr)
{
if(pA==pB)
return pA;
pA=pA->next;
pB=pB->next;
}
return nullptr;
}
};
双指针都依次遍历两条链表
两指针到达链表末端后从另一链表头节点出发遍历,如果相交,那么pA和pB会同时到达相交节点,不相交,那么会同时到达空节点
/**
* 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) {
// 如果其中一个链表为空,直接返回空,因为不可能有交点
if (headA == nullptr || headB == nullptr) {
return nullptr;
}
ListNode *pA = headA; // 指针pA指向链表A的头节点
ListNode *pB = headB; // 指针pB指向链表B的头节点
// 如果两个指针没有相遇(即没有找到交点),则继续遍历
while (pA != pB) {
// 如果pA遍历到链表A的末尾,则从链表B的头节点开始继续遍历
pA = pA == nullptr ? headB : pA->next;
// 如果pB遍历到链表B的末尾,则从链表A的头节点开始继续遍历
pB = pB == nullptr ? headA : pB->next;
}
// 返回相遇的节点,如果没有交点则返回nullptr
return pA;
}
};
2.反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
栈,迭代,空间开销O(n)
/**
* 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) {
if(head==nullptr)return head;
stack<ListNode*>s;
while(head!=nullptr)
{
s.push(head);
head=head->next;
}
ListNode *newnode=s.top();
ListNode *newhead=newnode;
s.pop();
while(!s.empty())
{
newnode->next=s.top();
newnode=newnode->next;
s.pop();
}
newnode->next=nullptr;
return newhead;
}
};
prev curr
/**
* 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) {
// 如果链表为空,直接返回空
if (head == nullptr) {
return nullptr;
}
ListNode *prev = nullptr; // 前一个节点初始化为nullptr,因为反转后当前头节点会变成尾节点
ListNode *curr = head; // 当前节点从头节点开始
while (curr != nullptr) {
ListNode *nextnode = curr->next; // 暂存下一个节点
curr->next = prev; // 反转当前节点的指针指向前一个节点
prev = curr; // 更新前一个节点为当前节点
curr = nextnode; // 当前节点移动到下一个节点
}
return prev; // 返回新的头节点,即原链表的尾节点
}
};
递归
代码
/**
* 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) {
// 如果链表为空或只有一个节点,直接返回头节点
if (head == nullptr || head->next == nullptr) return head;
// 递归反转子链表,newHead是新链表的头节点
ListNode *newHead = reverseList(head->next);
// 将当前节点的下一个节点的next指向当前节点,实现反转
head->next->next = head;
// 当前节点的next指向nullptr,断开当前节点和其下一个节点的连接
head->next = nullptr;
// 返回新的头节点
return newHead;
}
};
解释过程
我们假设链表为 1->2->3->4->5->nullptr
。
-
初始调用:
head = 1
newhead_1 = reverseList(2);
-
第二次调用:
head = 2
newhead_2 = reverseList(3);
-
第三次调用:
head = 3
newhead_3 = reverseList(4);
-
第四次调用:
head = 4
newhead_4 = reverseList(5);
-
第五次调用:
head = 5
- 因为
head->next == nullptr
,返回head
本身,也就是5
return head; // 返回 5
- 因为
接下来是回溯过程,从最深的调用处开始:
-
回溯到第四次调用:
newhead_4 = 5; // newhead_4 指向 5 head = 4; head->next->next = head; // 5->next = 4 head->next = nullptr; // 4->next = nullptr return newhead_4; // 返回 5
此时链表部分变为:
4 <- 5
(4 的 next 为 nullptr) -
回溯到第三次调用:
newhead_3 = 5; // newhead_3 指向 5 head = 3; head->next->next = head; // 4->next = 3 head->next = nullptr; // 3->next = nullptr return newhead_3; // 返回 5
此时链表部分变为:
3 <- 4 <- 5
-
回溯到第二次调用:
newhead_2 = 5; // newhead_2 指向 5 head = 2; head->next->next = head; // 3->next = 2 head->next = nullptr; // 2->next = nullptr return newhead_2; // 返回 5
此时链表部分变为:
2 <- 3 <- 4 <- 5
-
回溯到第一次调用:
newhead_1 = 5; // newhead_1 指向 5 head = 1; head->next->next = head; // 2->next = 1 head->next = nullptr; // 1->next = nullptr return newhead_1; // 返回 5
此时链表完全反转:
1 <- 2 <- 3 <- 4 <- 5
-
每次递归调用都会处理链表的一个节点,直到最后一个节点
5
。 -
在回溯过程中,依次反转节点的
next
指针。 -
最终将最初的头节点
1
指向nullptr
,并返回新的头节点5
。
3.回文链表
给你一个单链表的头节点 head
,请你判断该链表是否为
回文链表
。如果是,返回 true
;否则,返回 false
。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
快慢指针+后半链表反向(基于上一题的代码)
/**
* 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 == nullptr || head->next == nullptr) {
return true;
}
// 如果链表只有两个节点,直接比较这两个节点的值
if (head->next->next == nullptr) {
return head->val == head->next->val;
}
ListNode *fast = head, *slow = head;
ListNode *halfReversed; // 用于存储反转后半部分的头节点
// 使用快慢指针找到链表的中间节点
while (true) {
if (fast->next == nullptr) {
// 链表长度为奇数
halfReversed = reverseHalf(slow);
break;
} else if (fast->next->next == nullptr) {
// 链表长度为偶数
halfReversed = reverseHalf(slow->next);
break;
} else {
fast = fast->next->next;
slow = slow->next;
}
}
// 比较链表前半部分和反转后的后半部分
while (head != slow->next) {
if (head->val != halfReversed->val) {
return false;
}
head = head->next;
halfReversed = halfReversed->next;
}
return true;
}
private:
// 递归反转链表的后半部分
ListNode* reverseHalf(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* newHead = reverseHalf(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
};
官方题解的递归大致看懂了
递归每次看懂了就觉得好巧妙,但是自己写感觉好难
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
// 指向链表头部的指针
ListNode* frontPointer;
public:
// 递归函数用于检查链表是否为回文
bool recursivelyCheck(ListNode* currentNode) {
if (currentNode != nullptr) {
// 递归检查后续节点,如果有一个返回 false,则整个函数返回 false
if (!recursivelyCheck(currentNode->next)) {
return false;
}
// 检查当前节点和前面的节点是否相等
if (currentNode->val != frontPointer->val) {
return false;
}
// 移动前指针到下一个节点
frontPointer = frontPointer->next;
}
// 如果到达链表末端或所有比较都相等,返回 true
return true;
}
// 检查链表是否为回文的主函数
bool isPalindrome(ListNode* head) {
// 初始化前指针
frontPointer = head;
// 调用递归检查函数
return recursivelyCheck(head);
}
};
4.环形链表
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
集合存链表节点
空间复杂度不满足O(1)
/**
* 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) {
unordered_set<ListNode*>s;
while(head!=nullptr)
{
if(s.find(head)!=s.end())
{
return true;
}
s.insert(head);
head=head->next;
}
return false;
}
};
Floyd 判圈算法(又称龟兔赛跑算法)
我们定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。(官方题解)
/**
* 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*fast=head->next,*slow=head;
while(fast!=slow)
{
if(fast==nullptr||fast->next==nullptr)
{
return false;
}
fast=fast->next->next;//fast走两步
slow=slow->next;//slow走一步
}
if(fast==slow)
{
return true;
}
return false;
}
};
5.环形链表||
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
哈希表
哈希表真的不要思考太多,就是空间复杂度还是不满足O(1)
/**
* 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) {
unordered_set<ListNode*>s;
while(head!=nullptr)
{
if(s.find(head)!=s.end())
{
return head;
}
s.insert(head);
head=head->next;
}
return nullptr;
}
};
快慢指针
(leetcode上的解释)
/**
* 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) {
// 如果链表为空或者只有一个节点,则不可能有环
if (head == nullptr || head->next == nullptr) {
return nullptr;
}
// 初始化快慢指针,都从头节点开始
ListNode* fast = head;
ListNode* slow = head;
// 使用do-while循环确保快指针先移动
do {
// 如果快指针或者快指针的下一个节点为空,说明没有环
if (fast == nullptr || fast->next == nullptr) {
return nullptr;
}
// 快指针每次移动两步,慢指针每次移动一步
fast = fast->next->next;
slow = slow->next;
} while (fast != slow);
// 当快慢指针相遇时,说明链表中存在环
if (fast == slow) {
ListNode *p = head; // 新指针p从头开始
// 慢指针和新指针每次都向前移动一步,直到相遇
while (p != slow) {
p = p->next;
slow = slow->next;
}
// 返回相遇的节点,即环的起始节点
return p;
}
// 如果不存在环,返回nullptr
return nullptr;
}
};
6.合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
递归 空间复杂度O(m+n)
/**
* 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) {
// 基准条件:如果其中一个链表为空,则返回另一个链表
if (list1 == nullptr) return list2;
if (list2 == nullptr) return list1;
// 递归处理:
if (list1->val < list2->val) {
// 如果 list1 的当前值小于 list2 的当前值,递归合并 list1 的下一个节点和 list2
list1->next = mergeTwoLists(list1->next, list2);
return list1; // 返回较小值的节点作为结果链表的当前节点
} else {
// 如果 list2 的当前值小于等于 list1 的当前值,递归合并 list1 和 list2 的下一个节点
list2->next = mergeTwoLists(list1, list2->next);
return list2; // 返回较小值的节点作为结果链表的当前节点
}
}
};
迭代 虚拟头节点解决边界问题 O(1)
/**
* 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 *prev = new ListNode();
ListNode *prehead = prev; // 用于返回合并后的链表的头部
// 当 list1 和 list2 都不为空时,循环遍历它们
while (list1 != nullptr && list2 != nullptr) {
// 如果 list1 当前节点的值小于 list2 当前节点的值
if (list1->val < list2->val) {
prev->next = list1; // 将 list1 当前节点连接到结果链表
prev = list1; // 更新 prev 指针
list1 = list1->next; // 移动 list1 指针到下一个节点
} else {
// 否则,将 list2 当前节点连接到结果链表
prev->next = list2;
prev = list2; // 更新 prev 指针
list2 = list2->next; // 移动 list2 指针到下一个节点
}
}
// 当 list1 和 list2 其中一个为空时,将另一个链表的剩余部分连接到结果链表
prev->next = (list1 == nullptr) ? list2 : list1;
// 返回合并后的链表,跳过虚拟头节点
return prehead->next;
}
};
7.两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
代码
/**
* 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) {
// 创建一个虚拟头节点,初始值为0
ListNode *dummy = new ListNode(0);
// 初始化进位值
int carry = 0;
// 指针用于遍历结果链表
ListNode* ptr = dummy;
// 当l1、l2或进位不为0时,循环继续
while (l1 != nullptr || l2 != nullptr || carry) {
// 初始化当前和为进位值
int sum = carry;
// 如果l1不为空,加上l1的值,并将l1指向下一个节点
if (l1 != nullptr) {
sum += l1->val;
l1 = l1->next;
}
// 如果l2不为空,加上l2的值,并将l2指向下一个节点
if (l2 != nullptr) {
sum += l2->val;
l2 = l2->next;
}
// 计算新的进位值
carry = sum / 10;
// 创建新节点,值为sum % 10,并将其链接到结果链表
ptr->next = new ListNode(sum % 10);
// 移动指针到下一个节点
ptr = ptr->next;
}
// 返回实际头节点的下一个节点,即去掉虚拟头节点
return dummy->next;
}
};
8.删除倒数第n个结点
递归
/**
* 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:
// 主函数,删除链表的倒数第 N 个节点
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (head == nullptr || n == 0) {
return head;
}
// 创建一个虚拟头节点,指向原始头节点
ListNode *dummy = new ListNode(0, head);
// 递归移除倒数第 N 个节点
int count = Remove(dummy, n);
// 返回新的头节点
return dummy->next;
}
private:
// 递归函数,删除倒数第 N 个节点
int Remove(ListNode* node, int n) {
if (node == nullptr) {
return 0;
}
// 递归调用,返回节点数
int count = Remove(node->next, n) + 1;
// 如果当前节点是倒数第 N + 1 个节点
if (count == n + 1) {
// 跳过下一个节点
node->next = node->next->next;
}
return count;
}
};
栈
/**
* 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:
// 主函数,删除链表的倒数第 N 个节点
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 如果链表为空或者 n 小于等于 0,直接返回头节点
if (head == nullptr || n <= 0) {
return head;
}
stack<ListNode*> s; // 定义一个栈来存储链表节点
ListNode* ptr = new ListNode(0, head); // 创建一个虚拟头节点
ListNode* dummy = ptr; // 保存虚拟头节点指针
// 遍历链表并将所有节点压入栈中
while (ptr != nullptr) {
s.push(ptr);
ptr = ptr->next;
}
// 弹出栈顶 n 次,找到倒数第 N+1 个节点
while (n--) {
s.pop();
}
// 现在栈顶是倒数第 N+1 个节点,调整其 next 指针跳过下一个节点
ptr = s.top();
ptr->next = ptr->next->next;
// 返回新的头节点
return dummy->next;
}
};
双指针
/**
* 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:
// 主函数,删除链表的倒数第 N 个节点
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 如果链表为空或者 n 小于等于 0,直接返回头节点
if (head == nullptr || n <= 0) {
return head;
}
ListNode* p1 = head;
ListNode* p2 = head;
ListNode* dummy = new ListNode(0, head); // 创建一个虚拟头节点
// 将 p1 向前移动 n 步
while (n--) {
p1 = p1->next;
}
// 如果 p1 为空,说明要删除的是头节点
if (p1 == nullptr) {
dummy->next = dummy->next->next;
return dummy->next;
}
// 同时移动 p1 和 p2,直到 p1 到达链表末尾
while (p1 != nullptr) {
p1 = p1->next;
// 仅当 p1 不为空时,p2 才向前移动
if (p1 != nullptr) {
p2 = p2->next;
}
}
// 调整 p2 的 next 指针,跳过下一个节点
p2->next = p2->next->next;
// 返回新的头节点
return dummy->next;
}
};
9.两两交换链表中的结点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
队列
(空间复杂度较高,待优化)
看到一个题解用栈,push两个节点就pop出来,比队列好。
/**
* 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) {}
* };
*/
#include <queue>
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
// 如果链表为空或者只有一个节点,直接返回头节点
if (head == nullptr || head->next == nullptr) {
return head;
}
// 使用队列来存储链表节点
std::queue<ListNode*> q;
// 创建一个虚拟头节点,并将其指向原始头节点
ListNode* dummy = new ListNode(0, head);
// 指针 ptr 用于遍历链表
ListNode* ptr = head;
// 将链表的每个节点压入队列
while (ptr != nullptr) {
q.push(ptr);
ptr = ptr->next;
}
// 重置 ptr 为虚拟头节点
ptr = dummy;
// 依次出队并交换节点
while (!q.empty()) {
// 出队第一个节点
ListNode* temp1 = q.front();
q.pop();
// 如果队列不为空,出队第二个节点并交换
if (!q.empty()) {
ListNode* temp2 = q.front();
q.pop();
ptr->next = temp2; // 连接第二个节点
ptr = ptr->next; // 移动指针到第二个节点
}
// 连接第一个节点
ptr->next = temp1;
ptr = ptr->next; // 移动指针到第一个节点
}
// 最后处理最后一个节点的 next 指针,确保链表正确终止
ptr->next = nullptr;
// 返回新的头节点
return dummy->next;
}
};
迭代 O(1)
/**
* 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) {
// 如果链表为空或者只有一个节点,直接返回头节点
if (head == nullptr || head->next == nullptr) {
return head;
}
// 创建一个虚拟头节点,并将其指向原始头节点
ListNode* dummy = new ListNode(0, head);
ListNode* ptr = dummy;
// 遍历链表,每次交换相邻的两个节点
while (ptr->next != nullptr && ptr->next->next != nullptr) {
ListNode* node1 = ptr->next; // 第一个要交换的节点
ListNode* node2 = ptr->next->next; // 第二个要交换的节点
ptr->next = node2; // 连接第二个节点到当前位置
node1->next = node2->next; // 将第一个节点连接到第三个节点(可能为空)
node2->next = node1; // 将第二个节点连接到第一个节点
ptr = node1; // 将 ptr 移动到下一对要交换的节点的前一个节点
}
// 返回虚拟头节点的下一个节点,即新的头节点
return dummy->next;
}
};
递归
这个递归比较好理解
/**
* 定义单链表节点结构
* 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) {
// 基础情况:如果头节点为空或只有一个节点
if (head == nullptr || head->next == nullptr) {
return head;
}
// 将下一个节点存储在变量next中
ListNode* next = head->next;
// 递归调用swapPairs处理剩余的节点
head->next = swapPairs(next->next);
// 交换当前一对节点的位置
next->next = head;
// 返回交换后的新头节点
return next;
}
};
10.k个一组翻转链表
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
第一版(写的好难受,像坨屎><)
/**
* 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) {
if(head==nullptr||head->next==nullptr||k<=1)
{
return head;
}
ListNode* dummy=new ListNode(0,head);
ListNode* prev=dummy,*oritail=dummy;
ListNode* next=head;
while(next!=nullptr)
{
for(int i=0;i<k;i++)
{
oritail=oritail->next;
if(oritail==nullptr)
{
return dummy->next;
}
}
next=oritail->next;
ListNode*orihead=prev->next;
ListNode*newhead=reverse_k(prev->next,k);
prev->next=newhead;
orihead->next=next;
prev=orihead;
oritail=orihead;
}
return dummy->next;
}
private:
ListNode* reverse_k(ListNode* head,int k)
{
if(head==nullptr||head->next==nullptr)return head;
ListNode* prev=nullptr;
ListNode*curr=head;
while(k--&&curr!=nullptr)
{
ListNode* nextnode=curr->next;
curr->next=prev;
prev=curr;
curr=nextnode;
}
return prev;
}
};
第二版(循环中翻转)
/**
* 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) {
if (head == nullptr || head->next == nullptr || k <= 1) {
return head;
}
ListNode dummy(0, head);
ListNode* ptr = &dummy;
ListNode* start = head;
ListNode* end = ptr;
while (true) {
for (int i = 0; i < k; ++i) {
end = end->next;
if (end == nullptr) {
return dummy.next;
}
}
ListNode* next = end->next;
ListNode* prev = next;
ListNode* curr = start;
while (curr != next) {
ListNode* tmp = curr->next;
curr->next = prev;
prev = curr;
curr = tmp;
}
ListNode* temp = ptr->next;
ptr->next = prev;
ptr = temp;
start = next;
end = ptr;
}
return dummy.next;
}
};
11.随机链表的复制
给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next
指针和 random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X
和 Y
两个节点,其中 X.random --> Y
。那么在复制链表中对应的两个节点 x
和 y
,同样有 x.random --> y
。
返回复制链表的头节点。
用一个由 n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。
你的代码 只 接受原链表的头节点 head
作为传入参数。
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
哈希表+两遍迭代
/*
// 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 == nullptr) return nullptr;
// 创建新链表的头节点
Node* newhead = new Node(head->val);
Node* ptr = head;
Node* newptr = newhead;
// 使用哈希表存储旧节点和新节点的映射
unordered_map<Node*, Node*> mp;
mp[head] = newhead;
// 遍历原链表,创建新节点,并将它们链接起来
ptr = ptr->next;
while (ptr != nullptr) {
newptr->next = new Node(ptr->val); // 创建新节点并链接到新链表
newptr = newptr->next; // 移动到新链表的下一个节点
mp[ptr] = newptr; // 将旧节点和新节点的映射存储到哈希表中
ptr = ptr->next; // 移动到原链表的下一个节点
}
newptr->next = nullptr; // 确保新链表的最后一个节点的 next 指针为 nullptr
// 再次遍历原链表,设置新链表的 random 指针
ptr = head;
newptr = newhead;
while (ptr != nullptr) {
if (mp.find(ptr->random) != mp.end()) {
newptr->random = mp[ptr->random]; // 设置新节点的 random 指针
} else {
newptr->random = nullptr; // 如果原节点的 random 指针为空,设置新节点的 random 指针为空
}
ptr = ptr->next; // 移动到原链表的下一个节点
newptr = newptr->next; // 移动到新链表的下一个节点
}
// 返回新链表的头节点
return newhead;
}
};
12.排序链表
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
示例 2:
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
示例 3:
输入:head = []
输出:[]
优先队列
时间复杂度:O(nlogn)
空间复杂度:由于优先队列需要存储所有节点,因此空间复杂度为 O(n),不满足题目要求
/**
* 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) {}
* };
*/
// 自定义比较器,用于 priority_queue 将节点按 val 进行升序排序
struct CompareListNode {
bool operator()(ListNode* const& a, ListNode* const& b) {
// 返回 true 表示 a 应该在 b 之后(升序排列)
return a->val > b->val;
}
};
class Solution {
public:
ListNode* sortList(ListNode* head) {
// 如果链表为空,直接返回空
if (head == nullptr) {
return head;
}
// 创建一个优先队列,节点按照 val 升序排序
priority_queue<ListNode*, vector<ListNode*>, CompareListNode> pq;
// 遍历链表,将所有节点加入优先队列
ListNode* ptr = head;
while (ptr != nullptr) {
pq.push(ptr);
ptr = ptr->next;
}
// 创建一个虚拟头节点,便于操作
ListNode* dummy = new ListNode(0);
ptr = dummy;
// 依次从优先队列中取出节点,并重建排序后的链表
while (!pq.empty()) {
ptr->next = pq.top(); // 将队头节点链接到新链表中
pq.pop(); // 弹出队头节点
ptr = ptr->next; // 移动到新链表的下一个位置
}
ptr->next = nullptr; // 确保最后一个节点的 next 指针为 nullptr
// 返回排序后的链表头节点
return dummy->next;
}
};
13.合并k个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists = [[]]
输出:[]
在6的基础上
/**
* Definition for singly-linked list.
* 链表节点的定义
* struct ListNode {
* int val; // 节点的值
* ListNode *next; // 指向下一个节点的指针
* ListNode() : val(0), next(nullptr) {} // 默认构造函数,初始化值为0,指针为空
* ListNode(int x) : val(x), next(nullptr) {} // 带参数构造函数,初始化值为x,指针为空
* ListNode(int x, ListNode *next) : val(x), next(next) {} // 带参数构造函数,初始化值为x,指针指向传入的节点
* };
*/
class Solution {
public:
// 合并 k 个排序链表
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode* ans = nullptr; // 初始化结果链表为空
for(ListNode* l : lists) {
ans = mergeList(ans, l); // 将当前结果链表与新的链表合并
}
return ans; // 返回合并后的结果链表
}
private:
// 合并两个排序链表
ListNode* mergeList(ListNode* l1, ListNode* l2) {
if(l1 == nullptr) return l2; // 如果第一个链表为空,直接返回第二个链表
if(l2 == nullptr) return l1; // 如果第二个链表为空,直接返回第一个链表
ListNode* dummy = new ListNode(0); // 创建一个虚拟头节点,方便操作
ListNode* ptr = dummy; // 指针指向虚拟头节点
// 遍历两个链表,直到其中一个为空
while(l1 != nullptr && l2 != nullptr) {
if(l1->val < l2->val) {
ptr->next = l1; // 如果 l1 的值小于 l2 的值,将 l1 连接到结果链表上
l1 = l1->next; // l1 指针后移
} else {
ptr->next = l2; // 如果 l2 的值小于等于 l1 的值,将 l2 连接到结果链表上
l2 = l2->next; // l2 指针后移
}
ptr = ptr->next; // 结果链表指针后移
}
// 如果 l1 还没遍历完,直接连接到结果链表上
// 如果 l2 还没遍历完,直接连接到结果链表上
ptr->next = (l1 == nullptr) ? l2 : l1;
// 返回去掉虚拟头节点的结果链表
return dummy->next;
}
};
归并排序
时间复杂度:O(nlogk)
空间复杂度:O(1)
/**
* Definition for singly-linked list.
* 链表节点的定义
* struct ListNode {
* int val; // 节点的值
* ListNode *next; // 指向下一个节点的指针
* ListNode() : val(0), next(nullptr) {} // 默认构造函数,初始化值为0,指针为空
* ListNode(int x) : val(x), next(nullptr) {} // 带参数构造函数,初始化值为x,指针为空
* ListNode(int x, ListNode *next) : val(x), next(next) {} // 带参数构造函数,初始化值为x,指针指向传入的节点
* };
*/
class Solution {
public:
// 合并 k 个排序链表
ListNode* mergeKLists(vector<ListNode*>& lists) {
// 如果链表数组为空,返回 nullptr
if (lists.empty()) {
return nullptr;
}
// 调用辅助函数进行归并排序
return mergeKListsHelper(lists, 0, lists.size() - 1);
}
private:
// 归并排序辅助函数
ListNode* mergeKListsHelper(vector<ListNode*>& lists, int start, int end) {
// 如果只有一个链表,直接返回该链表
if (start == end) return lists[start];
// 找到中间位置
int mid = (start + end) / 2;
// 递归合并左半部分
ListNode* left = mergeKListsHelper(lists, start, mid);
// 递归合并右半部分
ListNode* right = mergeKListsHelper(lists, mid + 1, end);
// 合并左右两部分
return mergeList(left, right);
}
// 合并两个排序链表
ListNode* mergeList(ListNode* l1, ListNode* l2) {
// 如果其中一个链表为空,返回另一个链表
if (l1 == nullptr) return l2;
if (l2 == nullptr) return l1;
// 创建一个虚拟头节点,方便操作
ListNode* dummy = new ListNode(0);
ListNode* ptr = dummy;
// 遍历两个链表,直到其中一个为空
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
// 如果 l1 的值小于 l2 的值,将 l1 连接到结果链表上
ptr->next = l1;
l1 = l1->next;
} else {
// 如果 l2 的值小于等于 l1 的值,将 l2 连接到结果链表上
ptr->next = l2;
l2 = l2->next;
}
ptr = ptr->next; // 结果链表指针后移
}
// 如果 l1 还没遍历完,直接连接到结果链表上
// 如果 l2 还没遍历完,直接连接到结果链表上
ptr->next = (l1 == nullptr) ? l2 : l1;
// 返回去掉虚拟头节点的结果链表
return dummy->next;
}
};
14.LRU缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
STL (list和unordered_map)
get和put的时间复杂度都是O(1)
class LRUCache {
public:
LRUCache(int capacity) : capa(capacity) {}
int get(int key) {
if (mp.find(key) != mp.end()) {
// 将键移动到链表头部
lru_list.splice(lru_list.begin(), lru_list, mp[key]);
return mp[key]->second;
}
return -1;
}
void put(int key, int value) {
if (mp.find(key) != mp.end()) {
// 更新现有的键值对,移动到链表头部
mp[key]->second = value;
lru_list.splice(lru_list.begin(), lru_list, mp[key]);
} else {
if (lru_list.size() >= capa) {
// 如果缓存已满,移除链表尾部的元素
mp.erase(lru_list.back().first);
lru_list.pop_back();
}
lru_list.push_front({key, value});
mp[key] = lru_list.begin();
}
}
private:
int capa;
std::list<std::pair<int, int>> lru_list;
std::unordered_map<int, std::list<std::pair<int, int>>::iterator> mp;
};
/**
* 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);
*/
std::list::splice
是 C++ 标准库中 std::list
容器的一个成员函数,用于在常数时间内将一个或多个元素从一个列表移动到另一个列表或同一列表中的不同位置。splice
不会创建新元素,而是直接移动现有元素,从而避免了不必要的复制或移动操作。
splice
有三种重载形式:
- 将整个列表移动到目标位置
- 将一个元素移动到目标位置
- 将一段范围内的元素移动到目标位置
splice用法详解
1. 将整个列表移动到目标位置
void splice(const_iterator pos, list& other);
-
参数:
pos
: 目标位置的迭代器,表示要将other
列表插入到当前列表中的位置。other
: 要插入的另一个列表。
-
说明: 将
other
列表中的所有元素移动到当前列表中pos
位置之前。other
列表在操作后将为空。
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6};
list1.splice(list1.end(), list2); // 将 list2 中的所有元素移动到 list1 的末尾
// list1: 1 2 3 4 5 6
// list2: (empty)
2. 将一个元素移动到目标位置
void splice(const_iterator pos, list& other, const_iterator it);
-
参数:
pos
: 目标位置的迭代器,表示要将元素插入到当前列表中的位置。other
: 要插入的另一个列表。it
:other
列表中的元素迭代器。
-
说明: 将
other
列表中的元素it
移动到当前列表中的pos
位置之前。it
必须是other
列表中的有效迭代器。
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6};
auto it = list2.begin();
std::advance(it, 1); // it 指向 list2 中的 5
list1.splice(list1.end(), list2, it); // 将 list2 中的 5 移动到 list1 的末尾
// list1: 1 2 3 5
// list2: 4 6
3. 将一段范围内的元素移动到目标位置
void splice(const_iterator pos, list& other, const_iterator first, const_iterator last);
-
参数:
pos
: 目标位置的迭代器,表示要将元素插入到当前列表中的位置。other
: 要插入的另一个列表。first
:other
列表中的第一个元素迭代器,表示要移动的范围的开始。last
:other
列表中的最后一个元素迭代器,表示要移动的范围的结束。
-
说明: 将
other
列表中[first, last)
范围内的元素移动到当前列表中的pos
位置之前。first
和last
必须是other
列表中的有效迭代器。
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6, 7, 8};
auto first = list2.begin();
auto last = list2.begin();
std::advance(first, 1); // first 指向 list2 中的 5
std::advance(last, 4); // last 指向 list2 中的 8
list1.splice(list1.end(), list2, first, last); // 将 list2 中 [5, 6, 7) 范围的元素移动到 list1 的末尾
// list1: 1 2 3 5 6 7
// list2: 4 8
总结
splice
不会创建新元素,而是直接移动现有元素,因此在时间和空间上都很高效。splice
可以将整个列表、单个元素或一段范围内的元素移动到目标位置。- 需要注意的是,被移动的元素会从原来的位置上删除,并插入到新的位置上。
8.二叉树
1.二叉树的中序遍历
给定一个二叉树的根节点 root
,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
递归
效率低,不推荐,原谅我现在只会递归
/**
* 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> inorderTraversal(TreeNode* root) {
vector<int> ans;
if(root==nullptr)
{
return ans;
}
inorderHelper(root,ans);
return ans;
}
private:
void inorderHelper(TreeNode*node,vector<int>&ans)
{
if(node==nullptr)
{
return;
}
inorderHelper(node->left,ans);
ans.push_back(node->val);
inorderHelper(node->right,ans);
}
};
2.二叉树的最大深度
给定一个二叉树 root
,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:3
示例 2:
输入:root = [1,null,2]
输出:2
递归
/**
* 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 maxdepth=max(maxDepth(root->left),maxDepth(root->right))+1;
return maxdepth;
}
};
3.翻转二叉树
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:
输入:root = [2,1,3]
输出:[2,3,1]
示例 3:
输入:root = []
输出:[]
递归
/**
* 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 root;
}
TreeNode*tmp=root->left;
root->left=invertTree(root->right);
root->right=invertTree(tmp);
return root;
}
};
4.对称二叉树
给你一个二叉树的根节点 root
, 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
递归
先左子树翻转后比较两子树是否相等
两次递归,效率低
/**
* 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;
}
root->left=invertTree(root->left);
return isequal(root->left,root->right);
}
private:
TreeNode* invertTree(TreeNode* root)
{
if(root==nullptr)
{
return root;
}
TreeNode*tmp=root->left;
root->left=invertTree(root->right);
root->right=invertTree(tmp);
return root;
}
bool isequal(TreeNode* left,TreeNode* right)
{
if(left==nullptr&&right==nullptr)return true;
else if((left&&!right)||(!left&&right))return false;
else
if(left->val==right->val)
{
return isequal(left->left,right->left)&&isequal(left->right,right->right);
}
return false;
}
};
一次递归
/**
* 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 isSymmetricHelper(root->left, root->right);
}
private:
// 辅助函数,递归检查两个子树是否对称
bool isSymmetricHelper(TreeNode* left, TreeNode* right) {
if (left == nullptr && right == nullptr) return true; // 如果两个子节点都为空,返回true
if (left == nullptr || right == nullptr) return false; // 如果只有一个子节点为空,返回false
// 如果两个子节点的值相等,递归检查左子树的左子节点与右子树的右子节点,
// 以及左子树的右子节点与右子树的左子节点是否对称
if (left->val == right->val) {
return isSymmetricHelper(left->left, right->right) && isSymmetricHelper(left->right, right->left);
}
return false; // 如果两个子节点的值不相等,返回false
}
};
迭代
版本一(太长了)
/**
* 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) {
// 定义一个队列来存储节点
std::queue<TreeNode*> q;
// 如果根节点的左右子节点都为空,返回 true
if (root->left == nullptr && root->right == nullptr) return true;
// 如果根节点的左右子节点一个为空,另一个不为空,返回 false
if (root->left == nullptr || root->right == nullptr) return false;
// 将根节点的左右子节点入队
q.push(root->left);
q.push(root->right);
// 当队列不为空时,进行循环
while (!q.empty()) {
// 从队列中取出两个节点进行比较
TreeNode* node1 = q.front();
q.pop();
if (q.empty()) return false; // 如果队列为空,说明节点不对称
TreeNode* node2 = q.front();
q.pop();
// 如果两个节点的值不相等,返回 false
if (node1->val != node2->val) {
return false;
}
// 如果两个节点的左子节点和右子节点都不为空,将它们分别入队
if (node1->left && node2->right) {
q.push(node1->left);
q.push(node2->right);
}
// 如果一个左子节点为空,另一个右子节点不为空,返回 false
else if (!(node1->left == nullptr && node2->right == nullptr)) {
return false;
}
// 如果两个节点的右子节点和左子节点都不为空,将它们分别入队
if (node1->right && node2->left) {
q.push(node1->right);
q.push(node2->left);
}
// 如果一个右子节点为空,另一个左子节点不为空,返回 false
else if (!(node1->right == nullptr && node2->left == nullptr)) {
return false;
}
}
// 如果所有节点都匹配,返回 true
return true;
}
};
版本二(更简洁)
/**
* 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) {
// 使用队列来进行层序遍历
queue<TreeNode*> q;
// 将根节点的左右子节点入队
q.push(root->left);
q.push(root->right);
// 当队列不为空时,继续处理
while (!q.empty()) {
// 从队列中取出第一个节点
TreeNode* node1 = q.front();
q.pop();
// 如果队列为空,说明左右子树的节点数量不相等,返回 false
if (q.empty())
return false;
// 从队列中取出第二个节点
TreeNode* node2 = q.front();
q.pop();
// 如果两个节点都为空,继续检查下一对节点
if (node1 == nullptr && node2 == nullptr)
continue;
// 如果只有一个节点为空,返回 false,说明不对称
if (node1 == nullptr || node2 == nullptr)
return false;
// 如果两个节点的值不相等,返回 false
if (node1->val != node2->val) {
return false;
}
// 将左子节点和右子节点入队,保持对称性检查
q.push(node1->left); // 将 node1 的左子节点入队
q.push(node2->right); // 将 node2 的右子节点入队
q.push(node1->right); // 将 node1 的右子节点入队
q.push(node2->left); // 将 node2 的左子节点入队
}
// 如果遍历结束且没有发现不对称,返回 true
return true;
}
};
5.二叉树的直径
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
。
两节点之间路径的 长度 由它们之间边数表示。
示例 1:
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
示例 2:
输入:root = [1,2]
输出:1
递归
说是简单题,但是我觉得不简单><
/**
* 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 diameterOfBinaryTree(TreeNode* root) {
ans = 1; // 初始化最大直径值为1
depth(root); // 计算根节点的深度,同时更新最大直径值
return ans - 1; // 返回最大直径值,减1是因为 ans 包含了节点数,而直径是边的数量
}
private:
int ans; // 用于存储当前最大直径值
// 辅助函数,计算节点的深度
int depth(TreeNode* root) {
if (root == nullptr) {
return 0; // 空节点的深度为0
}
// 递归计算左子树和右子树的深度
int l = depth(root->left);
int r = depth(root->right);
// 更新最大直径值,左右子树深度之和加1(当前节点)
ans = std::max(ans, l + r + 1);
// 返回节点的深度,取左右子树深度的较大值加1
return std::max(l, r) + 1;
}
};
6.二叉树的层序遍历
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
关键,levelSize
/**
* 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>> 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) {
q.push(node->left); // 将左子节点入队
}
if (node->right) {
q.push(node->right); // 将右子节点入队
}
}
result.push_back(currentLevel); // 将当前层结果加入最终结果
}
return result; // 返回最终结果
}
};
7.将有序数组转化为二叉搜索树
给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵
平衡
二叉搜索树。
示例 1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例 2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
二分法
/**
* 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) {
int left=0,right=nums.size()-1;
return sortHelper(nums,left,right);
}
TreeNode* sortHelper(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=sortHelper(nums,left,mid-1);
root->right=sortHelper(nums,mid+1,right);
return root;
}
};
8.验证二叉搜索树
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
-
节点的左
子树
只包含
小于
当前节点的数。
-
节点的右子树只包含 大于 当前节点的数。
-
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3]
输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
代码
/**
* 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 isValidBST(TreeNode* root) {
return isBSTHelper(root,LONG_MIN,LONG_MAX);
}
bool isBSTHelper(TreeNode* root,int long long lower,long long upper){
if(root==nullptr)
{
return true;
}
if(root->val<=lower||root->val>=upper)
{
return false;
}
return isBSTHelper(root->left,lower,root->val)&&isBSTHelper(root->right,root->val,upper);
}
};
9.二叉搜索树中第k小的元素
给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
小的元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
中序遍历
/**
* 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 kthSmallest(TreeNode* root, int k) {
int ans;
return kthHelper(root, k, ans);
}
int kthHelper(TreeNode*root,int& k,int& ans)
{
if(root==nullptr)
{
return ans;
}
kthHelper(root->left,k,ans);
if(k--==1)
{
ans=root->val;
}
kthHelper(root->right,k,ans);
return ans;
}
};
10.二叉树的右视图
给定一个二叉树的 根节点 root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例 1:
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
示例 2:
输入: [1,null,3]
输出: [1,3]
示例 3:
输入: []
输出: []
层次遍历
/**
* 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> rightSideView(TreeNode* root) {
vector<int> ans;
if(root==nullptr)
{
return ans;
}
queue<TreeNode*>q;
q.push(root);
while(!q.empty())
{
int size=q.size();
TreeNode* node;
while(size--)
{
node=q.front();
q.pop();
if(node->left)
{
q.push(node->left);
}
if(node->right)
{
q.push(node->right);
}
}
ans.push_back(node->val);
}
return ans;
}
};
DFS
/**
* 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> rightSideView(TreeNode* root) {
vector<int> ans;
dfs(root,0,ans);
return ans;
}
void dfs(TreeNode* node,int level,vector<int>&ans)
{
if(node==nullptr)
{
return;
}
if(level==ans.size())
{
ans.push_back(node->val);
}
dfs(node->right,level+1,ans);
dfs(node->left,level+1,ans);
}
};
11.二叉树展开为链表
给你二叉树的根结点 root
,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode
,其中right
子指针指向链表中下一个结点,而左子指针始终为null
。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
示例 1:
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [0]
输出:[0]
寻找前驱结点
1
/ \
2 5
/ \ \
3 4 6
//将 1 的左子树插入到右子树的地方
1
\
2 5
/ \ \
3 4 6
//将原来的右子树接到左子树的最右边节点
1
\
2
/ \
3 4
\
5
\
6
//将 2 的左子树插入到右子树的地方
1
\
2
\
3 4
\
5
\
6
//将原来的右子树接到左子树的最右边节点
1
\
2
\
3
\
4
\
5
\
6
前序遍历
使用数组按前序遍历存储二叉树的所有结点
/**
* 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) {
vector<TreeNode*>l;
preorderTraversal(root,l);
int n=l.size();
for(int i=1;i<n;i++)
{
TreeNode*prev=l.at(i-1),*curr=l.at(i);
prev->left=nullptr;
prev->right=curr;
}
}
void preorderTraversal(TreeNode*root,vector<TreeNode*>&l)
{
if(!root)
{
return;
}
l.push_back(root);
preorderTraversal(root->left,l);
preorderTraversal(root->right,l);
}
};
12.从前序遍历和中序遍历构造二叉树
给定两个整数数组 preorder
和 inorder
,其中 preorder
是二叉树的先序遍历, inorder
是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
递归
关键是在中序遍历中定位根结点
/**
* 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 buildTreeHelper(preorder.begin(),preorder.end(),inorder.begin(),inorder.end());
}
private:
using it = vector<int>::iterator;
TreeNode* buildTreeHelper(it preStart,it preEnd,it inStart,it inEnd)
{
if(inStart==inEnd)
{
return nullptr;
}
int rootValue=*preStart;
TreeNode* root =new TreeNode(rootValue);
// 在中序遍历中找到根节点的位置
it rootPos=find(inStart,inEnd,rootValue);
// 计算左子树的节点数量
auto leftSize = distance(inStart, rootPos);
root->left=buildTreeHelper(preStart+1,preStart+1+leftSize,inStart,rootPos);
root->right=buildTreeHelper(preStart+1+leftSize,preEnd,rootPos+1,inEnd);
return root;
}
};
迭代
暂时不理解,先放着
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.empty()) {
return nullptr; // 如果前序遍历为空,返回nullptr
}
TreeNode* root = new TreeNode(preorder[0]); // 创建根节点
stack<TreeNode*> stk; // 创建一个栈用于模拟递归
stk.push(root); // 将根节点压入栈
int inorderIndex = 0; // 初始化中序遍历索引
for (int i = 1; i < preorder.size(); ++i) {
int preorderVal = preorder[i]; // 获取当前前序遍历的值
TreeNode* node = stk.top(); // 获取栈顶节点
if (node->val != inorder[inorderIndex]) {
// 如果栈顶节点的值不等于当前中序遍历的值
node->left = new TreeNode(preorderVal); // 当前节点是左子节点
stk.push(node->left); // 将左子节点压入栈
} else {
// 如果栈顶节点的值等于当前中序遍历的值
while (!stk.empty() && stk.top()->val == inorder[inorderIndex]) {
node = stk.top(); // 弹出栈顶节点
stk.pop();
++inorderIndex; // 移动中序遍历索引
}
node->right = new TreeNode(preorderVal); // 当前节点是右子节点
stk.push(node->right); // 将右子节点压入栈
}
}
return root; // 返回根节点
}
};
13. 路径总和|||
给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
与4.1做法一致,前缀和与哈希表
/**
* 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>prefixSumCount;
prefixSumCount[0]=1;
return dfs(root,0,targetSum,prefixSumCount);
}
int dfs(TreeNode*node,long long currSum,int targetSum,unordered_map<long long,int>&prefixSumCount)
{
if(!node)
{
return 0;
}
currSum+=node->val;
int pathToCurr=prefixSumCount[currSum-targetSum];
prefixSumCount[currSum]++;
int result=pathToCurr+dfs(node->left,currSum,targetSum,prefixSumCount)+dfs(node->right,currSum,targetSum,prefixSumCount);
prefixSumCount[currSum]--;//恢复
return result;
}
};
DFS
/**
* 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, long long targetSum) {
if(root==nullptr)
{
return 0;
}
int res=dfs(root,targetSum);
res+=pathSum(root->left,targetSum);
res+=pathSum(root->right,targetSum);
return res;
}
int dfs(TreeNode*node,long long targetSum)
{
if(node==nullptr)
{
return 0;
}
int res=0;
if(node->val==targetSum)
{
res++;
}
res+=dfs(node->left,targetSum-node->val)+dfs(node->right,targetSum-node->val);
return res;
}
};
14.二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
比较好理解的递归(待优化)
/**
* 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* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root==nullptr)
{
return root;
}
if(root==p||root==q)
{
return root;
}
if(find(root->left,p)&&find(root->left,q))
{
return lowestCommonAncestor(root->left,p,q);
}
if(find(root->right,p)&&find(root->right,q))
{
return lowestCommonAncestor(root->right,p,q);
}
return root;
}
private:
bool find(TreeNode* root,TreeNode* c)
{
if(root==nullptr)
{
return false;
}
if(root==c)
{
return true;
}
return find(root->left,c)||find(root->right,c);
}
};
题解的代码(看懂了,,)
/**
* 二叉树节点的定义
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 基本情况:如果当前节点为空,或找到节点p或q,则返回当前节点
if (root == nullptr || root == p || root == q) {
return root;
}
// 递归搜索左子树中的p和q
TreeNode* left = lowestCommonAncestor(root->left, p, q);
// 递归搜索右子树中的p和q
TreeNode* right = lowestCommonAncestor(root->right, p, q);
// 如果p和q分别在左右子树中找到,则当前节点就是它们的最近公共祖先
if (left != nullptr && right != nullptr) {
return root;
}
// 如果只有一个子树中包含p或q,返回那个子树的结果
return left != nullptr ? left : right;
}
};
15.二叉树中的最大路径和
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
递归
/**
* 二叉树节点的定义
* 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 maxSum = INT_MIN; // 最大路径和的初始值为最小整数
// 主函数,计算二叉树的最大路径和
int maxPathSum(TreeNode* root) {
if (root == nullptr) {
return 0; // 如果根节点为空,返回0
}
dfs(root); // 深度优先搜索计算最大路径和
return maxSum; // 返回计算得到的最大路径和
}
// 辅助函数,执行深度优先搜索
int dfs(TreeNode* root) {
if (root == nullptr) {
return 0; // 如果节点为空,返回0
}
// 递归计算左子树和右子树的最大路径和
int left = dfs(root->left);
int right = dfs(root->right);
// 当前节点值加上左右子树路径和的总和
int sum = root->val + left + right;
// 更新最大路径和
maxSum = max(maxSum, sum);
// 计算通过当前节点的最大路径和,不包括左右子树路径和为负的情况
int outsum = root->val + max(0, max(left, right));
// 返回通过当前节点的最大路径和
return max(0, outsum);
}
};
不把maxSum定义为类成员
一开始忘记int&maxSum了
/**
* 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 maxPathSum(TreeNode* root) {
if(root==nullptr)
{
return 0;
}
int maxSum=INT_MIN;
dfs(root,maxSum);
return maxSum;
}
int dfs(TreeNode*root,int& maxSum)
{
if(root==nullptr)
{
return 0;
}
int left=dfs(root->left,maxSum);
int right=dfs(root->right,maxSum);
int sum=root->val+left+right;
maxSum=max(maxSum,sum);
int outsum=root->val+max(0,max(left,right));
return max(0,outsum);
}
};
9.图论
1.岛屿数量
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
DFS
访问过的岛屿标记为2
class Solution {
public:
// 主函数,计算岛屿的数量
int numIslands(vector<vector<char>>& grid) {
int res = 0; // 用于存储岛屿的数量
// 遍历整个网格
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
// 如果当前格子是陆地('1')
if (grid[i][j] == '1') {
dfs(grid, i, j); // 使用深度优先搜索将连通的陆地标记为访问过
res++; // 岛屿数量加1
}
}
}
return res; // 返回岛屿的数量
}
// 深度优先搜索函数
void dfs(vector<vector<char>>& grid, int i, int j) {
// 如果当前坐标不在网格范围内,直接返回
if (!inarea(grid, i, j)) {
return;
}
// 如果当前格子不是陆地(不是'1'),直接返回
if (grid[i][j] != '1') {
return;
}
grid[i][j] = '2'; // 将当前格子标记为已访问过
// 递归搜索上下左右四个方向的格子
dfs(grid, i, j + 1); // 向右
dfs(grid, i, j - 1); // 向左
dfs(grid, i + 1, j); // 向下
dfs(grid, i - 1, j); // 向上
}
// 判断坐标是否在网格范围内
bool inarea(vector<vector<char>>& grid, int i, int j) {
return i >= 0 && i < grid.size() && j >= 0 && j < grid[0].size();
}
};
BFS
代码好长
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int res = 0;
// 遍历整个网格
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
// 如果当前格子是陆地
if (grid[i][j] == '1') {
res++; // 发现一个新的岛屿
grid[i][j] = '2'; // 将其标记为已访问
queue<pair<int, int>> neighbors;
neighbors.push({i, j});
// 使用BFS遍历该岛屿
while (!neighbors.empty()) {
int r = neighbors.front().first;
int c = neighbors.front().second;
neighbors.pop();
// 访问右边的邻居
if (numIslandsHelper(grid, r, c + 1)) {
neighbors.push({r, c + 1});
}
// 访问左边的邻居
if (numIslandsHelper(grid, r, c - 1)) {
neighbors.push({r, c - 1});
}
// 访问上边的邻居
if (numIslandsHelper(grid, r - 1, c)) {
neighbors.push({r - 1, c});
}
// 访问下边的邻居
if (numIslandsHelper(grid, r + 1, c)) {
neighbors.push({r + 1, c});
}
}
}
}
}
return res; // 返回岛屿的数量
}
// 辅助函数,检查并标记访问过的陆地
bool numIslandsHelper(vector<vector<char>>& grid, int i, int j) {
if (!inarea(grid, i, j)) {
return false; // 不在网格范围内
}
if (grid[i][j] != '1') {
return false; // 不是陆地或已访问
}
grid[i][j] = '2'; // 标记为已访问
return true;
}
// 判断坐标是否在网格范围内
bool inarea(vector<vector<char>>& grid, int i, int j) {
return i >= 0 && i < grid.size() && j >= 0 && j < grid[0].size();
}
};
并查集
岛屿的数量就是并查集中连通分量的数目
class UnionFind {
public:
UnionFind(vector<vector<char>>& grid) {
count = 0;
int m = grid.size();
int n = grid[0].size();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
parent.push_back(i * n + j);
++count;
}
else {
parent.push_back(-1);
}
rank.push_back(0);
}
}
}
int find(int i) {
if (parent[i] !=i ) {
parent[i] = find(parent[i]);
}
return parent[i];
}
void unite(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] < rank[rooty]) {
swap(rootx, rooty);
}
parent[rooty] = rootx;
if (rank[rootx] == rank[rooty]) {
rank[rootx]++;
}
--count;
}
}
int getCount() const { return count; }
private:
int count;
vector<int> parent;
vector<int> rank;
};
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int nr = grid.size();
if(!nr)return 0;
int nc = grid[0].size();
UnionFind uf(grid);
for (int r = 0; r < nr; r++) {
for (int c = 0; c < nc; c++) {
if (grid[r][c] == '1') {
grid[r][c] = '2';
if (c + 1 < nc && grid[r][c + 1] == '1') {
uf.unite(r * nc + c, r * nc + c + 1);
}
if (c - 1 >= 0 && grid[r][c - 1] == '1') {
uf.unite(r * nc + c, r * nc + c - 1);
}
if (r - 1 >= 0 && grid[r - 1][c] == '1') {
uf.unite(r * nc + c, (r - 1) * nc + c);
}
if (r + 1 < nr && grid[r + 1][c] == '1') {
uf.unite(r * nc + c, (r + 1) * nc + c);
}
}
}
}
return uf.getCount();
}
};
2.腐烂的橘子
在给定的 m x n
网格 grid
中,每个单元格可以有以下三个值之一:
- 值
0
代表空单元格; - 值
1
代表新鲜橘子; - 值
2
代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1
。
示例 1:
输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4
示例 2:
输入:grid = [[2,1,1],[0,1,1],[1,0,1]]
输出:-1
解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。
示例 3:
输入:grid = [[0,2]]
输出:0
解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。
BFS
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
int ret = 0;
int count = 0;
queue<pair<int, int>> q;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 2) {
q.push({i, j});
} else if (grid[i][j] == 1) {
count++;
}
}
}
if (count == 0) return 0;
while (!q.empty()) {
int size = q.size();
bool rottenThisStep = false;
while (size--) {
pair<int, int> fp = q.front();
q.pop();
int row = fp.first, col = fp.second;
rottenThisStep |= stale(grid, row + 1, col, count, q);
rottenThisStep |= stale(grid, row - 1, col, count, q);
rottenThisStep |= stale(grid, row, col + 1, count, q);
rottenThisStep |= stale(grid, row, col - 1, count, q);
}
if (rottenThisStep) ret++;
}
return count ? -1 : ret;
}
bool stale(vector<vector<int>>& grid, int row, int col, int& count, queue<pair<int, int>>& q) {
int m = grid.size();
int n = grid[0].size();
if (row >= 0 && row < m && col >= 0 && col < n) {
if (grid[row][col] == 1) {
grid[row][col] = 2;
count--;
q.push({row, col});
return true;
}
}
return false;
}
};
简化代码
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
int ret = 0;
int freshCount = 0;
queue<pair<int, int>> q;
// 初始化腐烂橙子的位置和新鲜橙子的计数
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 2) {
q.push({i, j});
} else if (grid[i][j] == 1) {
freshCount++;
}
}
}
// 如果没有新鲜橙子,直接返回0
if (freshCount == 0) return 0;
// 四个方向的坐标偏移量
vector<pair<int, int>> directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
// 广度优先搜索腐烂橙子
while (!q.empty()) {
int size = q.size();
bool rottenThisRound = false;
while (size--) {
auto [row, col] = q.front(); q.pop();
for (auto [dr, dc] : directions) {
int newRow = row + dr;
int newCol = col + dc;
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && grid[newRow][newCol] == 1) {
grid[newRow][newCol] = 2;
freshCount--;
q.push({newRow, newCol});
rottenThisRound = true;
}
}
}
if (rottenThisRound) ret++;
}
// 如果还有新鲜橙子无法腐烂,返回 -1
return freshCount == 0 ? ret : -1;
}
};
3.课程表
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
- 例如,先修课程对
[0, 1]
表示:想要学习课程0
,你需要先完成课程1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
DFS
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
if (prerequisites.empty()) return true;
vector<vector<int>> adjList(numCourses);//建立邻接表
for (const auto& prereq : prerequisites) {
adjList[prereq[1]].push_back(prereq[0]);
}
vector<int> visitStatus(numCourses, 0); // 0: 未访问, 1: 正在访问, 2: 访问完成
for (int i = 0; i < numCourses; i++) {
if (visitStatus[i] == 0) {
if (isRing(adjList, visitStatus, i)) return false;
}
}
return true;
}
private:
bool isRing(const vector<vector<int>>& adjList, vector<int>& visitStatus, int course) {
if (visitStatus[course] == 1) return true; // 正在访问,检测到环
if (visitStatus[course] == 2) return false; // 访问完成,无环
visitStatus[course] = 1; // 标记为正在访问
for (int nextCourse : adjList[course]) {
if (isRing(adjList, visitStatus, nextCourse)) return true;
}
visitStatus[course] = 2; // 标记为访问完成
return false;
}
};
BFS
入度为0的结点入队,计算总入队数是否为全部课程数
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> adjList(numCourses);
vector<int> inDegree(numCourses, 0);
queue<int> zeroDegreeQueue;
for (const auto& prereq : prerequisites) {
adjList[prereq[1]].push_back(prereq[0]);
inDegree[prereq[0]]++;
}
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
zeroDegreeQueue.push(i);
}
}
int visitedCourses = 0;
while (!zeroDegreeQueue.empty()) {
int course = zeroDegreeQueue.front();
zeroDegreeQueue.pop();
visitedCourses++;
for (int nextCourse : adjList[course]) {
inDegree[nextCourse]--;
if (inDegree[nextCourse] == 0) {
zeroDegreeQueue.push(nextCourse);
}
}
}
return visitedCourses == numCourses;
}
};
4.实现Tire(前缀树)
Tire发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
代码
指向子节点的指针数组 children。对于本题而言,数组长度为 26,即小写英文字母的数量。此时 children[0] 对应小写字母 a,children[1] 对应小写字母 b,…,children[25] 对应小写字母 z。
布尔字段 isword,表示该节点是否为字符串的结尾
struct TireNode{
public:
bool isword;
vector<TireNode*> children;
TireNode() : isword(false), children(26, nullptr) {}
};
class Trie {
public:
Trie() {
root=new TireNode();
}
void insert(string word) {
TireNode*curr=root;
for(char ch:word)
{
if(curr->children[ch-'a']==nullptr)
{
curr->children[ch-'a']=new TireNode();
}
curr=curr->children[ch-'a'];
}
curr->isword=true;
}
bool search(string word) {
TireNode*curr=root;
for(char ch:word)
{
if(curr->children[ch-'a']==nullptr)return false;
curr=curr->children[ch-'a'];
}
return curr->isword;
}
bool startsWith(string prefix) {
TireNode*curr=root;
for(char ch:prefix)
{
if(curr->children[ch-'a']==nullptr)
{
return false;
}
curr=curr->children[ch-'a'];
}
return true;
}
private:
TireNode* root;
};
/**
* 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);
*/
5.新增道路查询后的最短路径(409周赛||)
给你一个整数 n
和一个二维整数数组 queries
。
有 n
个城市,编号从 0
到 n - 1
。初始时,每个城市 i
都有一条单向道路通往城市 i + 1
( 0 <= i < n - 1
)。
queries[i] = [ui, vi]
表示新建一条从城市 ui
到城市 vi
的单向道路。每次查询后,你需要找到从城市 0
到城市 n - 1
的最短路径的长度。
返回一个数组 answer
,对于范围 [0, queries.length - 1]
中的每个 i
,answer[i]
是处理完前 i + 1
个查询后,从城市 0
到城市 n - 1
的最短路径的长度。
示例 1:
输入: n = 5, queries = [[2, 4], [0, 2], [0, 4]]
输出: [3, 2, 1]
解释:
新增一条从 2 到 4 的道路后,从 0 到 4 的最短路径长度为 3。
新增一条从 0 到 2 的道路后,从 0 到 4 的最短路径长度为 2。
新增一条从 0 到 4 的道路后,从 0 到 4 的最短路径长度为 1。
示例 2:
输入: n = 4, queries = [[0, 3], [0, 2]]
输出: [1, 1]
解释:
新增一条从 0 到 3 的道路后,从 0 到 3 的最短路径长度为 1。
新增一条从 0 到 2 的道路后,从 0 到 3 的最短路径长度仍为 1。
提示:
3 <= n <= 500
1 <= queries.length <= 500
queries[i].length == 2
0 <= queries[i][0] < queries[i][1] < n
1 < queries[i][1] - queries[i][0]
- 查询中没有重复的道路。
bfs
建立邻接表
建立可达矩阵会超时
class Solution {
public:
vector<int> shortestDistanceAfterQueries(int n, vector<vector<int>>& queries) {
// 初始化邻接矩阵
vector<vector<int>> adj(n);
for (int i = 0; i < n-1; ++i) {
adj[i].push_back(i+1);
}
vector<int> ans;
for (auto& query : queries) {
int start = query[0], end = query[1];
if (start > end)
swap(start, end);
adj[start].push_back(end);
int path = bfs(adj, n, 0, n - 1);
ans.push_back(path);
}
return ans;
}
private:
int bfs(vector<vector<int>>& adj, int n, int start, int end) {
if (start == end)
return 0;
vector<int> dist(n, INT_MAX);
queue<int> q;
dist[start] = 0;
q.push(start);
while (!q.empty()) {
int node = q.front();
q.pop();
for (int neighbour:adj[node]) {
if (dist[neighbour] > dist[node]+1) {
dist[neighbour] = dist[node] + 1;
q.push(neighbour);
}
}
}
return dist[end];
}
};
6.新增道路查询后的最短路径||(409周赛|||)
给你一个整数 n
和一个二维整数数组 queries
。
有 n
个城市,编号从 0
到 n - 1
。初始时,每个城市 i
都有一条单向道路通往城市 i + 1
( 0 <= i < n - 1
)。
queries[i] = [ui, vi]
表示新建一条从城市 ui
到城市 vi
的单向道路。每次查询后,你需要找到从城市 0
到城市 n - 1
的最短路径的长度。
所有查询中不会存在两个查询都满足 queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]
。
返回一个数组 answer
,对于范围 [0, queries.length - 1]
中的每个 i
,answer[i]
是处理完前 i + 1
个查询后,从城市 0
到城市 n - 1
的最短路径的长度。
示例 1:
输入: n = 5, queries = [[2, 4], [0, 2], [0, 4]]
输出: [3, 2, 1]
解释:
新增一条从 2 到 4 的道路后,从 0 到 4 的最短路径长度为 3。
新增一条从 0 到 2 的道路后,从 0 到 4 的最短路径长度为 2。
新增一条从 0 到 4 的道路后,从 0 到 4 的最短路径长度为 1。
示例 2:
输入: n = 4, queries = [[0, 3], [0, 2]]
输出: [1, 1]
解释:
新增一条从 0 到 3 的道路后,从 0 到 3 的最短路径长度为 1。
新增一条从 0 到 2 的道路后,从 0 到 3 的最短路径长度仍为 1。
set
关键句:所有查询中不会存在两个查询都满足 queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]
。
class Solution {
public:
vector<int> shortestDistanceAfterQueries(int n, vector<vector<int>>& queries) {
typedef pair<int, int> pii;
// 用一个 set 维护当前的最短路包含哪些区间,set有序,unordered_map无序
set<pii> st;
// 先把初始的 i - 1 -> i 都加进来
for (int i = 1; i < n; i++) st.insert(pii(i - 1, i));
vector<int> ans;
for (auto &qry : queries) {
int l = qry[0], r = qry[1];
auto it = st.lower_bound(pii(l, -1));
if (it != st.end() && it->first == l && it->second < r) {
// 踢掉所有新区间 [l, r) 包含的老区间
while (it != st.end() && it->first < r) it = st.erase(it);
st.insert(pii(l, r));
}
ans.push_back(st.size());
}
return ans;
}
};
10.回溯
1.全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
递归
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
backtrace(res,nums,0,nums.size());
return res;
}
void backtrace(vector<vector<int>>&res,vector<int>&output,int first,int len)
{
if(first==len){
res.push_back(output);
return;
}
for(int i=first;i<len;i++)
{
swap(output[i],output[first]);
backtrace(res,output,first+1,len);
swap(output[i],output[first]);
}
}
};
让我们一步步分析 nums = [1, 2, 3]
的运行过程。
-
初始调用:
backtrack(res, nums, 0, 3)
,此时nums = [1, 2, 3]
。 -
第一层递归
-
first = 0循环 i 从 0 到 2。
i = 0
时,不改变nums
,继续递归:backtrack(res, nums, 1, 3)
。i = 1
时,交换nums[0]
和nums[1]
,变成[2, 1, 3]
,继续递归:backtrack(res, nums, 1, 3)
。i = 2
时,交换nums[0]
和nums[2]
,变成[3, 2, 1]
,继续递归:backtrack(res, nums, 1, 3)
。
-
-
第二层递归
-
针对每个第一层递归生成的数组进行操作。例如,对于 [1, 2, 3]
-
first = 1,循环 i从 1 到 2。
i = 1
时,不改变nums
,继续递归:backtrack(res, nums, 2, 3)
。i = 2
时,交换nums[1]
和nums[2]
,变成[1, 3, 2]
,继续递归:backtrack(res, nums, 2, 3)
。
-
-
对于
[2, 1, 3]
和[3, 2, 1]
,类似地操作。
-
-
第三层递归
- 当
first = 3
时,表示一个排列完成,将其加入res
中。 - 例如,对于
[1, 2, 3]
,添加[1, 2, 3]
;对于[1, 3, 2]
,添加[1, 3, 2]
。
- 当
-
回溯
- 每次递归完成后,交换回去以保持数组的原始状态。
最终,res
包含所有的排列结果:
css
复制代码
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]]
2.子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的
子集
(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
递归
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>>res;
vector<int>output;
backtrace(res,nums,output,0);
return res;
}
void backtrace(vector<vector<int>>&res,vector<int>&nums,vector<int>&output,int first)
{
res.push_back(output);
for(int i=first;i<nums.size();i++)
{
output.push_back(nums[i]);
backtrace(res,nums,output,i+1);
output.pop_back();
}
}
};
3.电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
代码
class Solution {
public:
// 主函数:输入是数字字符串,输出是所有可能的字母组合
vector<string> letterCombinations(string digits) {
vector<string> ans; // 存储结果的向量
if(digits.empty()) return ans; // 如果输入为空,则直接返回空结果
string combination; // 用于存储当前组合的字符串
backTrace(ans, digits, combination, 0); // 开始回溯搜索
return ans; // 返回结果
}
private:
// 数字到字母的映射表
unordered_map<char, string> phoneMap = {
{'2', "abc"}, {'3', "def"}, {'4', "ghi"},
{'5', "jkl"}, {'6', "mno"}, {'7', "pqrs"},
{'8', "tuv"}, {'9', "wxyz"}
};
// 回溯函数
void backTrace(vector<string>& ans, string& digits, string& combination, int index) {
// 如果已经处理完所有的数字,将当前组合加入结果中
if(index == digits.size()) {
ans.push_back(combination);
return;
}
// 获取当前数字对应的字母串
string letters = phoneMap[digits[index]];
// 遍历该字母串中的每个字母
for(char letter : letters) {
combination.push_back(letter); // 将当前字母加入组合
backTrace(ans, digits, combination, index + 1); // 递归处理下一个数字
combination.pop_back(); // 回溯:移除最后一个添加的字母
}
}
};
4.组合总和
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
重复子集剪枝
class Solution {
public:
// 主函数:找到所有和为 target 的组合
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans; // 用于存储所有符合条件的组合
vector<int> combination; // 当前组合
sort(candidates.begin(), candidates.end()); // 排序候选数字,方便剪枝
backTrace(ans, combination, candidates, target, 0); // 开始回溯搜索
return ans; // 返回结果
}
// 回溯函数:搜索所有可能的组合
void backTrace(vector<vector<int>>& ans, vector<int>& combination, const vector<int>& candidates, int target, int index) {
// 如果目标值为0,表示找到一个符合条件的组合
if (target == 0) {
ans.push_back(combination); // 保存当前组合
return;
}
// 遍历所有候选数字,从index开始以避免重复组合
for (int i = index; i < candidates.size(); i++) {
// 如果当前数字大于目标值,后面的数字也都不符合条件,可以剪枝
if (target - candidates[i] < 0) break;
combination.push_back(candidates[i]); // 选择当前数字
// 递归地搜索剩余的数字,允许重复使用当前数字
backTrace(ans, combination, candidates, target - candidates[i], i);
combination.pop_back(); // 撤销选择,回溯
}
}
};
5.括号生成
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
剪枝
右括号的数量已经等于左括号的数量,则不能再添加右括号
class Solution {
public:
// 主函数:生成包含n对括号的所有合法括号组合
vector<string> generateParenthesis(int n) {
vector<string> ans; // 存储结果
string bracket; // 当前括号组合
backtrace(ans, n, bracket, 0, 0); // 开始回溯搜索
return ans; // 返回所有合法的括号组合
}
// 回溯函数:生成括号组合
void backtrace(vector<string>& ans, int n, string& bracket, int left, int right) {
// 如果左右括号的数量都达到n,表示找到一个合法的组合
if (left == n && right == n) {
ans.push_back(bracket); // 将当前组合加入结果
return;
}
// 如果左括号的数量小于n,可以继续添加左括号
if (left < n) {
bracket.push_back('('); // 添加左括号
backtrace(ans, n, bracket, left + 1, right); // 递归地生成后续组合
bracket.pop_back(); // 回溯:撤销添加的左括号
}
// 如果右括号的数量小于左括号的数量,可以继续添加右括号
if (right < n && right < left) {
bracket.push_back(')'); // 添加右括号
backtrace(ans, n, bracket, left, right + 1); // 递归地生成后续组合
bracket.pop_back(); // 回溯:撤销添加的右括号
}
}
};
6.单词搜索
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true
示例 3:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false
DFS
- 剪枝: 在搜索中,遇到“这条路不可能和目标字符串匹配成功”的情况,例如当前矩阵元素和目标字符不匹配、或此元素已被访问,则应立即返回,从而避免不必要的搜索分支。
- 访问过的字母改成非字母即可,不必开辟visited数组空间来标记是否被访问
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
int rows = board.size();
int cols = board[0].size();
// 遍历每个起始点
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (dfs(board, word, 0, i, j)) {
return true;
}
}
}
return false;
}
private:
bool dfs(vector<vector<char>>& board, const string& word, int index, int row, int col) {
// 全部匹配完成
if (index == word.size()) {
return true;
}
// 越界检查和字符匹配检查
if (row < 0 || row >= board.size() || col < 0 || col >= board[0].size() || board[row][col] != word[index]) {
return false;
}
// 标记访问过的格子
char temp = board[row][col];
board[row][col] = '0';
// DFS 向四个方向搜索
for (const auto& [dr, dc] : directions) {
if (dfs(board, word, index + 1, row + dr, col + dc)) {
return true;
}
}
// 回溯,恢复格子原来的字符
board[row][col] = temp;
return false;
}
vector<pair<int, int>> directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
};
7.分割回文串
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是
回文串
。返回 s
所有可能的分割方案。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
代码
isPalindrome
检查:
- 每次生成前缀
bg
后,首先检查它是否为回文。只有在isPalindrome(bg)
返回true
时才会继续递归。这避免了在bg
不是回文的情况下,进行不必要的递归调用。
空字符串的终止条件:
if(s.size() == 0)
这一行代码确保当所有字符都已被处理后(即s
已经为空),将当前路径st
添加到ans
中。这确保了只在必要的情况下继续递归。
class Solution {
public:
// 主函数,返回所有可能的回文分割方案
vector<vector<string>> partition(string &s) {
backtrace(s, st);
return ans;
}
private:
// 判断字符串是否是回文
bool isPalindrome(string &s) {
if(s.size() == 0) return true;
int start = 0, end = s.size() - 1;
while(start <= end) {
if(s[start] != s[end]) {
return false; // 不是回文
}
start++;
end--;
}
return true; // 是回文
}
// 回溯函数,寻找所有可能的回文分割方案
void backtrace(string s, vector<string> &st) {
// 如果当前字符串为空,表示已经处理完所有字符
if(s.size() == 0) {
ans.push_back(st); // 将当前路径 st 添加到结果 ans 中
return;
}
// 遍历字符串的所有可能前缀
for(int i = 0; i < s.size(); i++) {
string bg = s.substr(0, i + 1); // 取前缀字符串 bg
// 检查前缀是否为回文
if(isPalindrome(bg)) {
st.push_back(bg); // 将回文前缀添加到当前路径 st
backtrace(s.substr(i + 1), st); // 递归处理剩余部分
st.pop_back(); // 回溯,移除最后一个添加的回文前缀
}
}
}
vector<vector<string>> ans; // 存储所有回文分割的结果
vector<string> st; // 存储当前的分割路径
};
8.N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[["Q"]]
代码
按行访问,col,dg,udg分别记录是否被皇后攻击
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
// 初始化棋盘,填充为 '.',表示空位
vector<string> design(n, string(n, '.'));
// 用于记录每一列是否有皇后
vector<bool> col(n, false);
// 用于记录主对角线(从左上到右下)的状态
vector<bool> dg(2 * n, false);
// 用于记录副对角线(从右上到左下)的状态
vector<bool> udg(2 * n, false);
// 开始递归回溯
backTrace(design, col, dg, udg, 0, n);
// 返回所有可能的解法
return ans;
}
private:
vector<vector<string>> ans; // 存储所有解法的结果
// 回溯函数
void backTrace(vector<string>& design, vector<bool>& col, vector<bool>& dg, vector<bool>& udg, int count, int n) {
// 如果所有行都已填充皇后,保存结果
if (count == n) {
ans.push_back(design);
return;
}
// 遍历当前行的每一列,尝试放置皇后
for (int i = 0; i < n; i++) {
// 检查是否可以放置皇后,确保同列、主对角线、副对角线没有皇后
if (!col[i] && !dg[count - i + n] && !udg[i + count]) {
design[count][i] = 'Q'; // 放置皇后
col[i] = true; // 标记当前列被占用
dg[count - i + n] = true; // 标记主对角线被占用
udg[count + i] = true; // 标记副对角线被占用
// 递归处理下一行
backTrace(design, col, dg, udg, count + 1, n);
// 回溯:移除皇后并重置状态
design[count][i] = '.';
col[i] = false;
dg[count - i + n] = false;
udg[count + i] = false;
}
}
}
};
11.二分查找
1.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
代码
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
// 二分查找
while (left <= right) {
int mid = (left + right) / 2; // 计算中间位置
if (nums[mid] < target) {
left = mid + 1; // 目标值在右半部分
} else {
right = mid - 1; // 目标值在左半部分或等于当前元素
}
}
// 返回插入位置:如果找到目标值,left 正好指向目标值的位置;否则,left 会指向第一个比目标值大的位置
return left;
}
};
2.搜索二维矩阵
给你一个满足下述两条属性的 m x n
整数矩阵:
- 每行中的整数从左到右按非严格递增顺序排列。
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target
,如果 target
在矩阵中,返回 true
;否则,返回 false
。
示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false
Z型查找
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int row = 0, col = n - 1;
while (row < m && col >= 0) {
if (matrix[row][col] == target)
return true;
else if (matrix[row][col] > target) {
col--;
} else {
row++;
}
}
return false;
}
};
3.在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
代码
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
// 如果数组为空,直接返回 [-1, -1]
if (nums.size() == 0)
return {-1, -1};
int left = 0, right = nums.size() - 1;
int mid;
// 二分查找目标值
while (left <= right) {
mid = (left + right) / 2; // 计算中间位置
if (target == nums[mid]) {
break; // 找到目标值,跳出循环
} else if (target < nums[mid]) {
right = mid - 1; // 目标值在左侧
} else {
left = mid + 1; // 目标值在右侧
}
}
// 检查是否找到目标值
if (target == nums[mid]) {
int first = mid;
// 向左扩展找到目标值的第一个出现位置
while (first - 1 >= 0 && nums[first - 1] == nums[mid]) {
first--;
}
int last = mid;
// 向右扩展找到目标值的最后一个出现位置
while (last + 1 < nums.size() && nums[last + 1] == nums[mid]) {
last++;
}
// 返回第一个和最后一个出现的位置
return {first, last};
}
// 如果目标值不在数组中,返回 [-1, -1]
return {-1, -1};
}
};
4.搜索旋转排序数组
整数数组 nums
按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums
在预先未知的某个下标 k
(0 <= k < nums.length
)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]
。
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
二分加判断
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
// 开始二分查找
while (left <= right) {
int mid = (left + right) / 2;
// 如果找到目标值,直接返回下标
if (nums[mid] == target)
return mid;
// 判断左半部分是否有序
else if (nums[left] <= nums[mid]) {
// 如果目标值在有序的左半部分内,调整右边界
if (nums[left] <= target && nums[mid] > target) {
right = mid - 1;
} else {
// 否则目标值在右半部分,调整左边界
left = mid + 1;
}
}
// 否则右半部分有序
else {
// 如果目标值在有序的右半部分内,调整左边界
if (nums[mid] < target && nums[right] >= target) {
left = mid + 1;
} else {
// 否则目标值在左半部分,调整右边界
right = mid - 1;
}
}
}
// 如果没有找到目标值,返回 -1
return -1;
}
};
5.寻找旋转排序数组中的最小值
已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
- 若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 3 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
代码
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]) {
right = mid;
}
// 否则,最小值在右半部分
else {
left = mid + 1;
}
}
return nums[left];
}
};
6.寻找两个正序数组的中位数
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
代码
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// 确保 nums1 是较短的数组,这样二分查找会更高效
if (nums1.size() > nums2.size()) {
return findMedianSortedArrays(nums2, nums1);
}
int m = nums1.size(); // nums1 的长度
int n = nums2.size(); // nums2 的长度
int lmin = 0, lmax = m; // 二分查找的范围
int half = (m + n + 1) / 2; // 合并数组的中点
double maxl, minr; // 存储左半部分的最大值和右半部分的最小值
// 二分查找
while (lmin <= lmax) {
int i = (lmin + lmax) / 2; // nums1 中的二分查找索引
int j = half - i; // 对应的 nums2 中的索引
// 调整查找范围
if (i < m && nums1[i] < nums2[j - 1]) {
// i 太小,需要右移
lmin = i + 1;
} else if (i > 0 && nums2[j] < nums1[i - 1]) {
// i 太大,需要左移
lmax = i - 1;
} else {
// i 是完美的
// 确定左半部分的最大值
if (i == 0) {
// nums1 的所有元素都在右半部分
maxl = nums2[j - 1];
} else if (j == 0) {
// nums2 的所有元素都在右半部分
maxl = nums1[i - 1];
} else {
// 比较左半部分的最大值
maxl = max(nums1[i - 1], nums2[j - 1]);
}
// 如果总元素个数是奇数,返回左半部分的最大值
if ((m + n) % 2 == 1) {
return maxl;
}
// 确定右半部分的最小值
if (i == m) {
// nums1 的所有元素都在左半部分
minr = nums2[j];
} else if (j == n) {
// nums2 的所有元素都在左半部分
minr = nums1[i];
} else {
// 比较右半部分的最小值
minr = min(nums1[i], nums2[j]);
}
// 中位数是左半部分最大值和右半部分最小值的平均值
return (maxl + minr) / 2.0;
}
}
// 理论上不应该到达这里
return 0.0;
}
};
12.栈
1.有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
代码1
class Solution {
public:
bool isValid(string s) {
map<char,char>mp={{'(',')'},{'{','}'},{'[',']'}};
stack<char>st;
int n=s.size();
for(int i=0;i<n;i++)
{
if(!st.empty()){
char ch=st.top();
if(mp.find(ch)!=mp.end()&&mp[ch]==s[i]){
st.pop();
continue;
}
}
st.push(s[i]);
}
if(st.empty())return true;
return false;
}
};
代码2
class Solution {
public:
bool isValid(string s) {
stack<char> st;
int n = s.size();
for (int i = 0; i < n; i++) {
if (s[i] == '(' || s[i] == '{' || s[i] == '[')
st.push(s[i]);
else if (!st.empty()) {
char ch = st.top();
if ((s[i] == ')' && ch == '(') || (s[i] == '}' && ch == '{') ||
(s[i] == ']' && ch == '[')) {
st.pop();
} else
return false;
} else {
return false;
}
}
if (st.empty())
return true;
return false;
}
};
2.最小栈
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
示例 1:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
代码
class MinStack {
public:
MinStack() {
minst.push(minval);
}
void push(int val) {
if(val<minval)
{
minval=val;
}
minst.push(minval);
st.push(val);
}
void pop() {
minst.pop();
minval=minst.top();
st.pop();
}
int top() {
return st.top();
}
int getMin() {
return minval;;
}
private:
int minval=INT_MAX;
stack<int>minst;
stack<int>st;
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(val);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
3.字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
示例 1:
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
示例 2:
输入:s = "3[a2[c]]"
输出:"accaccacc"
示例 3:
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
示例 4:
输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"
代码1
stack st
class Solution {
public:
string decodeString(string s) {
string ans; // 用于存储解码后的最终结果
stack<char> st; // 栈,用于处理嵌套的字符串
int n = s.size(); // 字符串的长度
// 遍历输入字符串
for (int i = 0; i < n; i++) {
// 如果遇到 ']',需要处理栈中的内容
if (s[i] == ']') {
string tmp; // 存储方括号内的字符串
// 将栈中的字符弹出,直到遇到 '['
if (!st.empty()) {
char ch = st.top();
while (!st.empty() && ch != '[') {
tmp.push_back(ch);
st.pop();
if (!st.empty())
ch = st.top();
}
}
// 弹出 '['
if (!st.empty() && st.top() == '[') {
st.pop();
// 处理数字部分
if (!st.empty()) {
int num = 0;
int j = 0;
// 将数字从栈中弹出,并计算实际的数值
do {
num += (st.top() - '0') * pow(10, j++);
st.pop();
} while (!st.empty() && st.top() >= '0' && st.top() <= '9');
// 将重复的字符串再压回栈中
while (num--) {
for (int j = tmp.size() - 1; j >= 0; j--) {
st.push(tmp[j]);
}
}
}
}
}
// 如果不是 ']',直接将字符压入栈
else st.push(s[i]);
}
// 将栈中的字符弹出并存储到结果字符串中
while (!st.empty()) {
ans.insert(ans.begin(), st.top());
st.pop();
}
return ans; // 返回解码后的字符串
}
};
代码2
stack strStack
class Solution {
public:
string decodeString(string s) {
stack<string> strStack; // 存储部分解码字符串
stack<int> numStack; // 存储重复次数
string currentStr; // 当前正在构建的字符串
int currentNum = 0; // 当前数字
for (char ch : s) {
if (isdigit(ch)) {
currentNum = currentNum * 10 + (ch - '0');
} else if (ch == '[') {
strStack.push(currentStr); // 将当前字符串压入栈
numStack.push(currentNum); // 将当前数字压入栈
currentStr = ""; // 重置当前字符串
currentNum = 0; // 重置当前数字
} else if (ch == ']') {
string decodedStr = strStack.top(); // 取出栈顶字符串
strStack.pop();
int repeatTimes = numStack.top(); // 取出栈顶数字
numStack.pop();
while (repeatTimes--) {
decodedStr += currentStr; // 将 currentStr 重复附加到 decodedStr
}
currentStr = decodedStr; // 更新 currentStr 为解码后的字符串
} else {
currentStr += ch; // 将字符附加到当前字符串
}
}
return currentStr;
}
};
4.每日温度
给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
示例 2:
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]
示例 3:
输入: temperatures = [30,60,90]
输出: [1,1,0]
单调栈
栈中只存索引
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st; // 栈存储温度的索引
int n = temperatures.size(); // 温度数组的大小
vector<int> ans(n,0); // 用于存储结果的数组,初始大小为n
// 遍历温度数组
for (int i = 0; i < n; i++) {
// 当栈不为空且栈顶索引对应的温度小于当前温度时
while (!st.empty() && temperatures[st.top()] < temperatures[i]) {
ans[st.top()] = i - st.top(); // 计算当前温度与栈顶温度的索引差
st.pop(); // 弹出栈顶索引
}
// 将当前索引压入栈
st.push(i);
}
return ans; // 返回结果数组
}
};
5.柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
示例 2:
输入: heights = [2,4]
输出: 4
单调栈
- 进栈前弹出的都是左边比自己大的→确定左边界;
- 出栈时必定是右边第一次遇到比自己小的→确定右边界
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int maxarea = 0; // 变量用于存储最大矩形面积
stack<int> st; // 栈用于存储柱子的索引
int index; // 当前栈顶柱子的索引
int n = heights.size(); // 柱子的数量
st.push(-1); // 初始化栈,放入一个哨兵值-1,便于计算宽度
// 遍历所有的柱子
for (int i = 0; i < n; i++) {
// 如果栈顶是哨兵值-1,直接压入当前柱子的索引
if (st.top() == -1)
st.push(i);
// 如果当前柱子高度大于等于栈顶柱子的高度,压入当前柱子的索引
else if (heights[i] >= heights[st.top()])
st.push(i);
else {
// 当前柱子高度小于栈顶柱子的高度,处理栈顶柱子
index = st.top();
while (index != -1 && heights[index] > heights[i]) {
st.pop(); // 弹出栈顶柱子
// 计算以弹出柱子高度为高的矩形面积
maxarea = max(maxarea, (i - 1 - st.top()) * heights[index]);
index = st.top(); // 更新栈顶柱子的索引
}
st.push(i); // 压入当前柱子的索引
}
}
// 处理栈中剩余的柱子
index = st.top();
while (st.top() != -1) {
int h = heights[st.top()]; // 获取栈顶柱子的高度
st.pop(); // 弹出栈顶柱子
// 计算以弹出柱子高度为高的矩形面积
maxarea = max(maxarea, (index - st.top()) * h);
}
return maxarea; // 返回最大矩形面积
}
};
13.堆
1.数组中第K个最大元素
给定整数数组 nums
和整数 k
,请返回数组中第 **k**
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
快速选择排序
代码1
超时了,平均时间复杂度O(N),最坏情况O(N^2)
class Solution {
public:
// 主函数,查找数组中第 k 大的元素
int findKthLargest(vector<int>& nums, int k) {
// 由于我们要找的是第 k 大的元素,因此实际要找的是排序后数组的第 nums.size() - k 小的元素
return quickselect(nums, 0, nums.size() - 1, nums.size() - k);
}
private:
// Quickselect 算法的递归函数,用于在数组的指定范围内查找第 k 小的元素
int quickselect(vector<int>& nums, int left, int right, int k) {
// 如果左边界等于右边界,说明数组中只有一个元素,直接返回
if (left == right)
return nums[left];
// 分区操作,返回枢轴的位置
int pivotIndex = partition(nums, left, right);
// 如果枢轴的位置恰好是第 k 个元素的位置,返回该元素
if (k == pivotIndex)
return nums[k];
// 如果第 k 个元素在枢轴的左边,则递归在左边部分查找
else if (k < pivotIndex)
return quickselect(nums, left, pivotIndex - 1, k);
// 如果第 k 个元素在枢轴的右边,则递归在右边部分查找
else
return quickselect(nums, pivotIndex + 1, right, k);
}
// 分区函数,将数组分为两个部分,小于或等于枢轴的在左边,大于枢轴的在右边
int partition(vector<int>& nums, int left, int right) {
// 选择最右边的元素作为枢轴
int pivot = nums[right];
// i 是小于或等于枢轴的部分的边界索引,初始为 left - 1
int i = left - 1;
// 遍历数组,将小于或等于枢轴的元素移到左边
for (int j = left; j < right; j++) {
if (nums[j] <= pivot) {
i++; // 增加边界索引
swap(nums[i], nums[j]); // 交换当前元素和边界索引元素
}
}
// 将枢轴放到它的正确位置,即边界索引的下一个位置
swap(nums[i + 1], nums[right]);
// 返回枢轴的位置
return i + 1;
}
};
代码2
看了题解后,复杂度不变,但是过了测试
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
// 查找数组中第 k 大的元素
int findKthLargest(vector<int>& nums, int k) {
// 将问题转化为查找第 (n - k) 小的元素
// nums.size() - k 是因为我们在找第 k 大的元素
return quickselect(nums, 0, nums.size() - 1, nums.size() - k);
}
private:
// 快速选择算法的实现
int quickselect(vector<int>& nums, int left, int right, int k) {
// 如果左边界和右边界相同,说明数组中只有一个元素
if (left == right)
return nums[left];
// 根据分区函数获取分区后的枢轴索引
int pivotIndex = partition(nums, left, right);
// 如果枢轴索引正好是目标索引 k,返回该位置的元素
if (k == pivotIndex)
return nums[k];
// 如果目标索引 k 小于枢轴索引,递归查找左侧子数组
else if (k < pivotIndex)
return quickselect(nums, left, pivotIndex - 1, k);
// 如果目标索引 k 大于枢轴索引,递归查找右侧子数组
else
return quickselect(nums, pivotIndex + 1, right, k);
}
// 分区函数,使用 Hoare 分区方案
int partition(vector<int>& nums, int left, int right) {
int pivot = nums[left]; // 选择最左边的元素作为枢轴
int i = left - 1; // 左边界的指针
int j = right + 1; // 右边界的指针
// 当左右指针没有交错时,继续循环
while (i < j) {
// 移动左指针,直到找到大于等于枢轴的元素
do {
i++;
} while (nums[i] < pivot);
// 移动右指针,直到找到小于等于枢轴的元素
do {
j--;
} while (nums[j] > pivot);
// 如果左指针小于右指针,交换两个元素
if (i < j)
swap(nums[i], nums[j]);
}
// 返回分区后的枢轴位置
return j;
}
};
最小堆
priority_queue实现最小堆
时间复杂度O(Nlogk)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int,vector<int>,greater<int>>minHeap;
//建立最小堆
for(int i=0;i<k;i++){
minHeap.push(nums[i]);
}
//维护最小堆
int n=nums.size();
for(int i=k;i<n;i++){
if(nums[i]>minHeap.top()){
minHeap.pop();
minHeap.push(nums[i]);
}
}
return minHeap.top();
}
};
计数排序
O(n+k)但是假如最大值和最小值相差过大会造成空间消耗过大,不推荐
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 找到数组中的最大值和最小值
int maxValue = *max_element(nums.begin(), nums.end());
int minValue = *min_element(nums.begin(), nums.end());
// 计算计数数组的范围
int range = maxValue - minValue + 1;
// 创建并初始化计数数组
vector<int> count(range, 0);
// 统计每个元素的出现次数
for (int num : nums) {
count[num - minValue]++;
}
// 从最大值开始累加计数,找到第 k 大的元素
for (int i = range - 1; i >= 0; --i) {
k -= count[i];
if (k <= 0) {
return i + minValue;
}
}
// 如果找不到(按理说不应该发生),返回 -1
return -1;
}
};
2.前K个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
哈希表+优先队列(最小堆)
// 比较函数,用于优先队列的排序
struct Compare {
bool operator()(pair<int, int> const& p1, pair<int, int> const& p2) {
return p1.second > p2.second;
}
};
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
vector<int> ans;
unordered_map<int, int> mp;
// 统计每个数字出现的次数
for (int num : nums) {
mp[num]++;
}
// 使用优先队列实现最小堆
priority_queue<pair<int, int>, vector<pair<int, int>>, Compare> minHeap;
// 遍历map,将元素及其出现次数压入最小堆
for (auto& x : mp) {
if (minHeap.size() < k) {
minHeap.push(x);
} else {
if (x.second > minHeap.top().second) {
minHeap.pop();
minHeap.push(x);
}
}
}
// 从最小堆中取出结果
while (!minHeap.empty()) {
ans.push_back(minHeap.top().first);
minHeap.pop();
}
return ans;
}
};
3.数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]
的中位数是3
。 - 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类:
MedianFinder()
初始化MedianFinder
对象。void addNum(int num)
将数据流中的整数num
添加到数据结构中。double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5
以内的答案将被接受。
示例 1:
输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]
解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1); // arr = [1]
medianFinder.addNum(2); // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3); // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0
multiset
代码一
addNum
平均复杂度: O(logn)
findMedian
平均复杂度: O(n)
插入和查找的操作交替进行,O(n^2)
#include <set>
#include <iterator>
class MedianFinder {
public:
MedianFinder() {}
void addNum(int num) {
s.insert(num);
}
double findMedian() {
int len = s.size();
auto it = s.begin();
std::advance(it, len / 2);
if (len % 2 == 1) {
return *it;
} else {
auto it1 = it;
--it1;
return (*it + *it1) / 2.0;
}
}
private:
std::multiset<int> s;
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
代码二 双指针+multiset
addNum
平均复杂度: O(logn)
findMedian
平均复杂度: O(1)
#include <iterator>
#include <set>
class MedianFinder {
private:
std::multiset<int> s;
multiset<int>::iterator left, right;
public:
MedianFinder() : left(s.end()), right(s.end()) {}
void addNum(int num) {
s.insert(num); // 将新数字插入集合中
int n = s.size(); // 获取当前集合的大小
if (n == 1) {
left = right = s.begin(); // left 和 right 都指向新插入的唯一元素
} else if (n & 1) { // 如果集合的大小是奇数
if (num < *left) {
right--; // 如果新数字小于左边的中位数,更新 right
} else if (num >= *right) {
left++; // 更新 left
} else {
left++;
right--;
}
} else { // 如果集合的大小是偶数
if (num < *left) {
left--; // 如果新数字小于左边的中位数,更新 left
} else {
right++;
}
}
}
double findMedian() { return (*left + *right) / 2.0; }
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
双堆
addNum(int num)
方法的时间复杂度是 O(log n)
。
findMedian()
方法的时间复杂度是 O(1)
。
总的空间复杂度是 O(n)
。
#include <iterator>
#include <set>
#include <queue>
#include <vector>
class MedianFinder {
public:
// 构造函数,初始化 MedianFinder 对象
MedianFinder() {}
// 添加一个新的数字到数据结构中
void addNum(int num) {
// 如果最大堆为空或新数字小于等于最大堆的堆顶(最大值),则将数字添加到最大堆
if (maxHeap.empty() || num <= maxHeap.top())
maxHeap.push(num);
else
// 否则,将数字添加到最小堆
minHeap.push(num);
// 如果最大堆的大小超过最小堆的大小超过 1,重新平衡堆
if (maxHeap.size() > minHeap.size() + 1) {
minHeap.push(maxHeap.top());
maxHeap.pop();
}
// 如果最小堆的大小超过最大堆的大小,重新平衡堆
else if (minHeap.size() > maxHeap.size()) {
maxHeap.push(minHeap.top());
minHeap.pop();
}
}
// 返回当前数据流的中位数
double findMedian() {
// 如果两个堆的大小相等,中位数是两个堆顶元素的平均值
if (maxHeap.size() == minHeap.size()) {
return (maxHeap.top() + minHeap.top()) / 2.0;
}
// 否则,中位数是最大堆的堆顶元素
return maxHeap.top();
}
private:
// 最大堆,存储数据流中较小的一半元素
std::priority_queue<int, std::vector<int>, std::less<int>> maxHeap;
// 最小堆,存储数据流中较大的一半元素
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
14.贪心算法
1.1买股票的最佳时机
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
代码
假设是今天卖出,顺带求今天之前的历史最低点
(不像贪心算法,而是动态规划吧hhh)
class Solution {
public:
int maxProfit(vector<int>& prices) {
// 初始化最大利润为0
int maxprofit = 0;
// 初始化最小价格为第一个价格
int minprice = prices[0];
// 遍历所有价格
for (int &price : prices) {
// 低买高卖
if(price<minprice)minprice=price;
else maxprofit=max(maxprofit,price-minprice);
}
// 返回最大利润
return maxprofit;
}
};
1.2买股票的最佳时机||
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
最大总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。
贪心
class Solution {
public:
int maxProfit(vector<int>& prices) {
// 初始化最大利润为0
int ans = 0;
// 获取价格数组的大小
int n = prices.size();
// 从第二天开始遍历价格数组
for (int i = 1; i < n; ++i) {
// 计算当前一天和前一天的价格差,如果价格差为正则将其加到最大利润中
ans += max(0, prices[i] - prices[i - 1]);
}
// 返回最大利润
return ans;
}
};
动态规划
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size(); // 获取天数
vector<vector<int>> dp(n, vector<int>(2, 0)); // 创建一个大小为 n x 2 的 dp 表,初始化为 0
dp[0][0] = -prices[0]; // dp[i][0] 表示第 i 天持有股票的最大利润
dp[0][1] = 0; // dp[i][1] 表示第 i 天不持有股票的最大利润
for (int i = 1; i < n; i++) {
// 第 i 天持有股票的情况:前一天就持有股票或者今天买入股票
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
// 第 i 天不持有股票的情况:前一天就不持有股票或者今天卖出股票
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
}
return dp[n-1][1]; // 最后一天不持有股票的最大利润
}
};
优化
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size(); // 获取天数
int dp1 = -prices[0]; // dp1 表示当前持有股票的最大利润,初始化为买入第0天的股票
int dp0 = 0; // dp0 表示当前不持有股票的最大利润,初始化为0
for (int i = 1; i < n; i++) {
// newdp0 表示在第 i 天不持有股票的最大利润
// 可以是前一天也不持有股票的利润,或者是今天卖出股票的利润
int newdp0 = max(dp0, dp1 + prices[i]);
// newdp1 表示在第 i 天持有股票的最大利润
// 可以是前一天也持有股票的利润,或者是今天买入股票的利润
int newdp1 = max(dp1, dp0 - prices[i]);
// 更新 dp0 和 dp1 到新计算的值
dp0 = newdp0;
dp1 = newdp1;
}
return dp0; // 最后一天不持有股票的最大利润
}
};
2.跳跃游戏
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
递归(超时)
class Solution {
public:
bool canJump(vector<int>& nums) {
if (nums.size() == 1)
return true;
return jump(nums, 0);
}
private:
bool jump(vector<int>& nums, int start) {
// if(start>=nums.size())return false;
for (int i = nums[start]; i > 0; i--) {
if (start + i >= nums.size() - 1)
return true;
else if (jump(nums, start + i))
return true;
}
return false;
}
};
动态规划
代码1
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n);
// 初始化第一个位置能跳到的最远距离
dp[0] = nums[0];
// 从第一个位置开始遍历
for (int i = 1; i < n; i++) {
// 如果当前索引大于上一个位置能跳到的最远距离,说明无法到达当前位置,返回 false
if (i > dp[i - 1]) return false;
// 更新当前索引能跳到的最远距离
dp[i] = max(dp[i - 1], i + nums[i]);
// 如果当前能跳到的最远距离已经达到或超过最后一个位置,返回 true
if (dp[i] >= n - 1) return true;
}
// 如果遍历完整个数组,没有提前返回,说明可以到达最后一个位置,返回 true
return true;
}
};
代码2
dp[]可以优化为maxroad(贪心)
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size(); // 获取数组的大小
int maxroad = 0; // 初始化可以到达的最远位置为0
// 遍历数组中的每一个位置
for (int i = 0; i < n; i++) {
// 更新可以到达的最远位置
if (i <= maxroad)
maxroad = max(maxroad, i + nums[i]);
// 如果可以到达或超过最后一个位置,返回 true
if (maxroad >= n - 1)
return true;
}
// 如果遍历结束后仍未能到达最后一个位置,返回 false
return false;
}
};
3.跳跃游戏||
给定一个长度为 n
的 0 索引整数数组 nums
。初始位置为 nums[0]
。
每个元素 nums[i]
表示从索引 i
向前跳转的最大长度。换句话说,如果你在 nums[i]
处,你可以跳转到任意 nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
代码
复制过来的,这个情景应用很好理解
class Solution {
public int jump(int[] nums) {
int ans = 0; //跳槽次数
int curUnlock = 0; //当前你的水平能入职的最高公司级别
int maxUnlock = 0; //当前可选公司最多能帮你提到几级
for (int i = 0; i < nums.length - 1; i++) { //从前向后遍历公司,最高级公司(nums.length-1)是目标,入职后不再跳槽,所以不用看,故遍历范围是左闭右开区间[0,nums.length-1)
maxUnlock = Math.max(maxUnlock, i + nums[i]); //计算该公司最多能帮你提到几级(公司级别i+成长空间nums[i]),与之前的提级最高记录比较,打破记录则更新记录
if (i == curUnlock) { // 把你当前水平级别能选的公司都看完了,你选择跳槽到记录中给你提级最多的公司,以解锁更高级公司的入职权限
curUnlock = maxUnlock; // 你跳槽到了该公司,你的水平级别被提升了
ans++; //这里记录你跳槽了一次
}
}
return ans; //返回跳槽总次数
}
}
动态规划
class Solution {
public:
int jump(vector<int>& nums) {
// 获取数组的长度
int n = nums.size();
// 创建一个长度为n的dp数组,并初始化为无穷大
vector<int> dp(n, INT_MAX);
// 起点的位置跳跃次数为0
dp[0] = 0;
// 遍历数组中的每一个位置
for(int i = 0; i < n; i++) {
// 从当前位置尝试所有可能的跳跃步数
for(int j = 1; j <= nums[i]; j++) {
// 确保不跳出数组边界
if(i + j < n) {
// 更新dp数组中的值,选择最小的跳跃次数
dp[i + j] = min(dp[i + j], dp[i] + 1);
}
}
}
// 返回到达终点位置的最小跳跃次数
return dp[n - 1];
}
};
4.划分字母区间
给你一个字符串 s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s
。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = "eccbbbbdec"
输出:[10]
15.动态规划
1.爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
斐波那契
class Solution {
public:
int climbStairs(int n) {
// 如果楼梯只有一级台阶,那么只有一种爬法
if (n == 1) return 1;
// 定义两个变量来保存前两个状态,初始状态都为1
int a0 = 1, a1 = 1;
// 定义一个变量来保存当前的计算结果
int ans = 0;
// 从第二级台阶开始,一直到第n级台阶
while (n-- > 1) {
// 当前台阶的方法数等于前两个台阶方法数之和
ans = a0 + a1;
// 更新前两个状态
a0 = a1;
a1 = ans;
}
// 返回计算结果
return ans;
}
};
dp
class Solution {
public:
int climbStairs(int n) {
// 如果楼梯只有一级台阶,那么只有一种爬法
if(n == 1) return 1;
// 创建一个长度为 n+1 的数组 dp,用于存储每一级台阶的爬法数
vector<int> dp(n + 1);
// 初始化 dp[0] 和 dp[1],即爬到第0级和第1级台阶的方法数都为1
dp[0] = dp[1] = 1;
// 从第2级台阶开始计算,每一级台阶的方法数等于前两级台阶方法数之和
for(int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// 返回爬到第n级台阶的方法数
return dp[n];
}
};
2.杨辉三角
给定一个非负整数 *numRows
,*生成「杨辉三角」的前 numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入: numRows = 1
输出: [[1]]
代码
class Solution {
public:
vector<vector<int>> generate(int numRows) {
// 定义一个二维向量,用于存储帕斯卡三角形的所有行
vector<vector<int>> ans;
// 循环生成每一行
for (int i = 1; i <= numRows; i++) {
// 定义一个向量 level,长度为 i,用于存储当前行的元素
vector<int> level(i);
// 设置当前行的第一个和最后一个元素为1
level[0] = level[i - 1] = 1;
// 如果当前行的长度大于2,计算中间的元素
if (i > 2) {
// 从第二个元素到倒数第二个元素
for (int j = 1; j < i - 1; j++) {
// 当前元素等于上一行相邻两个元素之和
level[j] = ans[i - 2][j - 1] + ans[i - 2][j];
}
}
// 将当前行添加到结果中
ans.push_back(level);
}
// 返回生成的帕斯卡三角形
return ans;
}
};
3.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
dp
class Solution {
public:
int rob(vector<int>& nums) {
int n=nums.size();
vector<int>dp(nums.size()+1);
dp[0] = 0; // dp[0] 表示没有房屋时偷窃的最大金额为0
dp[1] = nums[0]; // dp[1] 表示只有一间房屋时偷窃的最大金额为该房屋的金额
// 从第二间房屋开始,计算偷窃的最大金额
for (int i = 1; i < n; i++) {
// 状态转移方程:
// dp[i+1] 表示偷窃前 i+1 间房屋的最大金额
// 可以选择不偷第 i 间房屋(此时最大金额为 dp[i]),
// 或者偷第 i 间房屋(此时最大金额为 dp[i-1] + nums[i])
dp[i+1] = max(dp[i], dp[i-1] + nums[i]);
}
return dp[n];
}
};
空间优化
class Solution {
public:
int rob(vector<int>& nums) {
int n=nums.size();
int dp0 = 0;
int dp1 = nums[0];
// 从第二间房屋开始,计算偷窃的最大金额
for (int i = 1; i < n; i++) {
int dp2 = max(dp1, dp0 + nums[i]);
dp0=dp1;
dp1=dp2;
}
return dp1;
}
};
4.完全平方数
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
dp
遍历方式1
class Solution {
public:
int numSquares(int n) {
vector<int>dp(n+1);
dp[0]=0;
// 遍历每个数 i 从 1 到 n,计算组成 i 所需的最小完全平方数的数量。
for (int i = 1; i <= n; ++i) {
// 对于每个 i,遍历所有的完全平方数 j*j(j 从 1 开始),
// 检查是否可以用这些完全平方数来优化 dp[i]。
for (int j = 1; j * j <= i; ++j) {
// 更新 dp[i] 为当前值和 dp[i - j * j] + 1 的最小值。
// dp[i - j * j] 表示去掉一个 j*j 后所需的最小完全平方数的数量,
// 所以 dp[i - j * j] + 1 就是当前情况的数量。
dp[i] = min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
};
遍历方式2
class Solution {
public:
int numSquares(int n) {
vector<int>dp(n+1,n+1);
dp[0]=0;
for(int i=1;i*i<=n;i++){
for(int j=i*i;j<=n;j++){
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
return dp[n];
}
};
背包问题
0/1 背包问题
问题描述
给定 n 个物品,每个物品有一个重量 wi 和价值 vi,一个容量为 W 的背包。每个物品只能选择一次,问如何选择物品使得装入背包的物品总重量不超过 W 且总价值最大。
动态规划解法
定义 dp[i][j]
表示前 i个物品在容量不超过 j 时的最大价值。
对于每个物品 i
,我们有两种选择:
- 不选第 i 个物品:
- 在这种情况下,背包的最大价值就是前
i−1
个物品在容量 j 时的最大价值。 - 因此有:
dp[i][j]=dp[i−1][j]
- 在这种情况下,背包的最大价值就是前
- 选第 i 个物品:
- 如果选择第 i 个物品,那么背包剩余的容量将减少 wi(物品 i 的重量)。
- 这意味着我们需要考虑在容量为
j−wi
时,前i−1
个物品的最大价值,再加上第i
个物品的价值vi
。 - 因此有:
dp[i][j]=dp[i−1][j−wi]+vi
综合考虑
为了确保我们在这两种选择中取得最大值,状态转移方程写作:
dp[i][j]=max(dp[i−1][j],dp[i−1][j−wi]+vi)
public:
int knapsack(vector<int>& weights, vector<int>& values, int W) {
int n = weights.size();
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= W; ++j) {
if (j >= weights[i - 1]) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][W];
}
};
优化空间复杂度
可以将二维数组优化为一维数组,从右向左更新。
// 初始化一个大小为 W + 1 的 dp 数组,初始值均为 0
// dp[j] 表示背包容量为 j 时所能获得的最大价值
vector<int> dp(W + 1, 0);
// 遍历每一个物品
for (int i = 0; i < n; i++) {
// 对于当前物品,从容量 W 开始,向容量 w[i] 递减
// 这样确保每个物品只被选择一次(0/1 背包的特性)
for (int j = W; j >= w[i]; j--) {
// 更新 dp[j],选择当前物品 i 或不选择物品 i
// 选择物品 i 时的价值为 dp[j - w[i]] + v[i]
// 不选择物品 i 时的价值为 dp[j]
// 取两者中的较大值作为 dp[j] 的值
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
完全背包问题
问题描述
与 0/1 背包问题类似,但每个物品可以选择无限次。
动态规划解法
定义 dp[j]
表示容量不超过 j 时的最大价值。 状态转移方程: dp[j]=max(dp[j],dp[j−wi]+vi)
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
for (int j = w[i]; j <= W; j++) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
总结
外层循环遍历物品,内层循环遍历容量(从大到小):
- 用于 0/1 背包问题。
- 确保每个物品只被考虑一次,因为内层循环从大到小。
外层循环遍历物品,内层循环遍历容量(从小到大):
- 用于完全背包问题。
- 允许每个物品被多次考虑,因为内层循环从小到大。
0/1 背包问题的代码(从大到小遍历容量)
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
for (int j = W; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
完全背包问题的代码(从小到大遍历容量)
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
for (int j = w[i]; j <= W; j++) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
5.零钱兑换
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
dp
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, amount + 1); // 初始化 dp 数组,长度为 amount + 1,初始值为 amount + 1
dp[0] = 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); // 更新 dp[j] 为当前值和使用当前硬币后的最小值
}
}
return dp[amount] < amount + 1 ? dp[amount] : -1; // 如果 dp[amount] 仍为初始值,返回 -1,否则返回 dp[amount]
}
};
6.单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
dp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int n = s.length(); // 获取字符串 s 的长度
vector<bool> dp(n + 1, false); // 初始化 dp 数组,长度为 n + 1,初始值为 false
dp[0] = true; // 空字符串可以被视为有效分割
// 外层循环遍历字符串的每个位置
for (int i = 1; i <= n; ++i) {
// 遍历词典中的每个单词
for (auto word : wordDict) {
int j = word.size(); // 获取当前单词的长度
// 如果 i-j >= 0 并且 dp[i-j] 为 true,并且 s 的子字符串 s[i-j, i] 等于当前单词
if (i - j >= 0 && dp[i - j] && s.substr(i - j, j) == word) {
dp[i] = true; // 设置 dp[i] 为 true,表示前 i 个字符可以被分割
break; // 找到一个匹配的单词后,可以跳出当前循环
}
}
}
return dp[n]; // 返回 dp[n],即整个字符串是否可以被词典中的单词组成
}
};
7.最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的
子序列
。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
dp
时间复杂度:O(N^2)
dp好难啊啊啊~
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int>dp(n,1);
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
dp[i]=max(dp[i],dp[j]+1);
}
}
}
return *max_element(dp.begin(),dp.end());
}
};
Patience Sorting
或者叫 二分查找优化的动态规划算法
Patience Sorting 是一种排序技术,源自一种纸牌游戏,其核心思想如下:
- 构建牌堆:按照一定规则将牌放入多个堆中,每个堆的牌是递增的。
- 找到合适的堆:对于每一张新牌,放置在第一个能接纳它的堆顶,若没有合适的堆,则新建一个堆。
- 使用二分查找:可以通过二分查找快速找到适合当前牌的位置。
这种方法与求解 LIS 的过程类似:
- 堆顶元素:每个堆顶元素表示一个可能的递增子序列的末尾。
- 二分查找:用于快速找到要替换的位置,使得递增子序列尽可能长。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// v 用于存放当前找到的最小递增子序列
vector<int> v;
// 遍历输入数组中的每个数字
for (int num : nums) {
// 使用 lower_bound 在 v 中找到第一个不小于 num 的位置
auto it = lower_bound(v.begin(), v.end(), num);
// 如果没有找到这样的元素,说明 num 比 v 中所有元素都大
if (it == v.end()) {
// 将 num 添加到 v 的末尾
v.push_back(num);
} else {
// 如果找到了这样的元素,则替换它,保持 v 的元素尽可能小
*it = num;
}
}
// v 的大小就是最长递增子序列的长度
return v.size();
}
};
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// 定义一个指针 end,初始化指向 nums 的起始位置
auto end = nums.begin();
// 遍历输入数组中的每一个元素 num
for (int num : nums) {
// 使用 lower_bound 在 nums 的范围 [nums.begin(), end) 中找到第一个不小于 num 的位置
auto it = lower_bound(nums.begin(), end, num);
// 替换找到的位置 it 处的值为 num
*it = num;
// 如果 it 等于 end,说明 num 比范围中的所有元素都大
// 将 end 向右移动一个位置,以包含新的最大的元素
if (it == end) {
end++;
}
}
// 返回从 nums 的起始位置到 end 的距离,即最长递增子序列的长度
return end - nums.begin();
}
};
8.乘积最大子数组
给你一个整数数组 nums
,请你找出数组中乘积最大的非空连续
子数组
(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
示例 1:
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
代码
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size();
// 初始化 imax 和 imin 为数组的第一个元素,
// 使用 long long 以避免乘积过程中可能的溢出
long long imax = nums[0], imin = nums[0];
// 初始化结果为数组的第一个元素,表示全局最大乘积
long long result = nums[0];
// 从第二个元素开始遍历数组
for (int i = 1; i < n; i++) {
// 如果当前元素是负数,则交换 imax 和 imin,
// 因为负数乘积会使得最大变最小,最小变最大
if (nums[i] < 0) {
swap(imax, imin);
}
// 更新 imax 为当前元素或当前元素乘以之前的最大乘积中的较大值
imax = max((long long)nums[i], imax * nums[i]);
// 更新 imin 为当前元素或当前元素乘以之前的最小乘积中的较小值
imin = min((long long)nums[i], imin * nums[i]);
// 如果 imin 小于 INT_MIN,意味着可能出现了溢出风险,
// 在这种情况下将 imin 重置为当前元素值
if(imin < INT_MIN) {
imin = nums[i];
}
// 更新结果,确保 result 始终保存全局最大乘积
result = max(result, imax);
}
// 返回结果,由于 result 最终应该在 int 范围内,
// 可以安全地转换为 int 类型返回
return result;
}
};
9.分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
dp
背包
class Solution {
public:
bool canPartition(vector<int>& nums) {
// 计算数组中所有元素的总和
int sum = 0;
for (int num : nums) {
sum += num;
}
// 如果总和是奇数,则不可能分割成两个和相等的子集
if (sum % 2 == 1) return false;
// 目标是找到一个子集,使其和为总和的一半
sum /= 2;
int n = nums.size();
// dp[j] 表示是否可以找到一个子集,使其和为 j
vector<int> dp(sum + 1, 0);
dp[0] = 1; // 和为 0 的子集总是可以找到,即空集
// 遍历每一个数字
for (int i = 0; i < n; i++) {
// s 记录当前考虑的最大和,避免不必要的遍历
int s = min(s + nums[i], sum);
// 从后向前更新 dp 数组
// 这样做是为了确保每个数字只被考虑一次
for (int j = s; j >= nums[i]; j--) {
dp[j] |= dp[j - nums[i]];
}
}
// 如果 dp[sum] 为 1,说明存在和为 sum 的子集
return dp[sum];
}
};
10.最长有效括号
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号
子串
的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:
输入:s = ""
输出:0
dp
class Solution {
public:
int longestValidParentheses(string s) {
int n = s.size();
if (n == 0) return 0; // 如果字符串为空,直接返回 0
// dp[i] 表示以第 i 个字符结尾的最长有效括号长度
vector<int> dp(n, 0);
// 从字符串的第二个字符开始遍历
for (int i = 1; i < n; i++) {
// 只考虑当前字符是 ')' 的情况
if (s[i] == ')') {
// 如果前一个字符是 '(',我们发现了一对完整的括号
if (s[i - 1] == '(') {
// 这对括号加上之前的有效长度(如果有的话)
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
}
// 如果前一个字符是 ')',并且存在与当前字符匹配的 '('
else if (i - dp[i - 1] - 1 >= 0 && s[i - dp[i - 1] - 1] == '(') {
// 当前的有效长度等于前面已经匹配的有效长度加上新找到的这对括号的长度
// 以及在这对括号之前的有效长度(如果有的话)
dp[i] = dp[i - 1] + 2 + (i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] : 0);
}
}
}
// 返回 dp 数组中的最大值,即最长有效括号的长度
return *max_element(dp.begin(), dp.end());
}
};
栈
class Solution {
public:
int longestValidParentheses(string s) {
int n = s.size();
stack<int> st;
int ans = 0;
// 初始化栈,压入 -1 作为基准
st.push(-1);
for (int i = 0; i < n; 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;
}
};
16.多维动态规划
1.不同路径
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
二维dp
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>>dp(m,vector<int>(n,0));
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0)dp[i][j]=1;
else if(j==0)dp[i][j]=1;
else dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
滚动数组优化
O(N)
class Solution {
public:
int uniquePaths(int m, int n) {
vector<int> dp(n, 1); // 初始化 dp 数组,每个元素初始值为 1
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] += dp[j - 1]; // 当前 dp[j] 是上一行的 dp[j] 和左边 dp[j - 1] 的和
}
}
return dp[n - 1]; // 返回最后一个元素,这就是路径总数
}
};
2.不同路径||
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
二维dp
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
dp[0][0] = obstacleGrid[0][0] == 1 ? 0 : 1; // 初始位置
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
dp[i][j] = 0; // 当前是障碍物,无法通过
} else {
if (i > 0) dp[i][j] += dp[i - 1][j]; // 来自上方的路径数
if (j > 0) dp[i][j] += dp[i][j - 1]; // 来自左方的路径数
}
}
}
return dp[m - 1][n - 1];
}
};
优化空间复杂度
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<int>dp(n, 0);
dp[0] = 1; // 初始位置
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
dp[j] = 0; // 当前是障碍物,无法通过
} else {
if (j > 0)
dp[j] += dp[j - 1]; // 来自上方和左方的路径数
}
}
}
return dp[n - 1];
}
};
3.最小路径和
给定一个包含非负整数的 *m* x *n*
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
**说明:**每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
二维dp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m=grid.size();
int n=grid[0].size();
//vector<vector<int>>dp(m,vector<int>(n,0));
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0&&j>0)grid[i][j]+=grid[i][j-1];
else if(j==0&&i>0)grid[i][j]+=grid[i-1][j];
else if(i>0&&j>0)grid[i][j]+=min(grid[i-1][j],grid[i][j-1]);
}
}
return grid[m-1][n-1];
}
};
滚动数组优化
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m=grid.size();
int n=grid[0].size();
vector<int>dp(n,INT_MAX);
//初始化
dp[0]=0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(j==0)dp[j]+=grid[i][j];
else dp[j]=min(dp[j],dp[j-1])+grid[i][j];
}
}
return dp[n-1];
}
};
4.最长回文子串
给你一个字符串 s
,找到 s
中最长的
回文
子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
二维dp
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n == 1) return s;
int start = 0;
int maxlen = 1;
vector<vector<bool>> dp(n, vector<bool>(n, false));
// 所有长度为1的子串都是回文
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 检查长度为2的子串
for (int i = 0; i < n - 1; i++) {
if (s[i] == s[i + 1]) {
dp[i][i + 1] = true;
maxlen = 2;
start = i;
}
}
// 检查长度大于2的子串
for (int len = 3; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
if (s[i] == s[j] && dp[i + 1][j - 1]) {
dp[i][j] = true; // 修正了这个地方
maxlen = len;
start = i;
}
}
}
return s.substr(start, maxlen);
}
};
中心扩散法
5.最长公共子序列
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
dp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
// 获取两个字符串的长度
int m = text1.size(), n = text2.size();
// 创建一个二维动态规划表 dp,大小为 (m+1) x (n+1)
// 初始化时,将所有元素设置为0。dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的最长公共子序列的长度
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
// 遍历 text1 和 text2 的所有字符
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 如果两个字符相等,dp[i+1][j+1] 就等于 dp[i][j] + 1
// 表示当前最长公共子序列在前一个基础上加上当前匹配的字符
if (text1[i] == text2[j]) {
dp[i + 1][j + 1] = dp[i][j] + 1;
} else {
// 如果两个字符不相等,则取 dp[i+1][j] 和 dp[i][j+1] 中的最大值
// 表示当前字符不匹配时,最长公共子序列不增加,需要继续比较其他字符
dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
// 返回 dp[m][n],即两个字符串的最长公共子序列的长度
return dp[m][n];
}
};
滚动数组dp
public:
int longestCommonSubsequence(string text1, string text2) {
// 获取两个字符串的长度
int m = text1.size(), n = text2.size();
// 初始化一个大小为 n+1 的一维动态规划数组,初始值为 0
vector<int> dp(n + 1, 0);
// 遍历 text1 的每个字符
for (int i = 0; i < m; i++) {
int pre = 0; // pre 用来存储 dp[j+1] 在更新前的值
// 遍历 text2 的每个字符
for (int j = 0; j < n; j++) {
int tmp = dp[j + 1]; // 记录 dp[j+1] 的当前值,供下一轮使用
// 如果 text1 和 text2 当前字符相等,则更新 dp[j+1] 为 pre + 1
// pre 对应的是上一行、上一列的 dp 值,代表不包括当前字符的 LCS 长度
if (text1[i] == text2[j])
dp[j + 1] = pre + 1;
else
// 如果不相等,取 dp[j] 和 dp[j+1] 的最大值
// dp[j] 对应左边的 dp 值(不包括 text1 当前字符)
// dp[j+1] 是 dp 数组中该位置的原始值
dp[j + 1] = max(dp[j], dp[j + 1]);
pre = tmp; // 更新 pre 为当前的 dp[j+1] 值,用于下一列计算
}
}
// 最后 dp[n] 中存储的是最长公共子序列的长度
return dp[n];
}
};
6.编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
二维dp
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
// 如果其中一个字符串为空,返回另一个字符串的长度
if (m == 0) return n;
else if (n == 0) return m;
// 创建一个二维dp数组,大小为(m+1) x (n+1)
// dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最小操作数
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
// 初始化 dp 数组的第一列,表示将 word1 的前 i 个字符转换为空字符串的代价
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
// 初始化 dp 数组的第一行,表示将空字符串转换为 word2 的前 j 个字符的代价
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 填充 dp 数组
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 如果字符 word1[i-1] 和 word2[j-1] 不相等
// dp[i][j] = 1 + min(删除操作dp[i-1][j], 插入操作dp[i][j-1], 替换操作dp[i-1][j-1])
dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
// 如果字符 word1[i-1] 和 word2[j-1] 相等,则可以直接继承 dp[i-1][j-1] 的值
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = min(dp[i][j], dp[i - 1][j - 1]);
}
}
}
// 返回最终的结果,即将 word1 转换为 word2 的最小操作数
return dp[m][n];
}
};
17.技巧
1.只出现1次的数字
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
代码
按位与:&
按位或:|
异或:^
0^a=a
a^a=0
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans=0;
for(int &num:nums){
ans^=num;
}
return ans;
}
};
2.多数元素
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
Boyer-Moore 投票算法
在线性时间和常数空间内找到多数元素(即出现次数超过一半的元素)。
每次不同的数字抵消候选多数元素的投票数,最终能够在一次遍历后找到多数元素。
class Solution {
public:
// 定义一个函数,接收一个整数向量并返回多数元素
int majorityElement(vector<int>& nums) {
// 初始化候选多数元素和投票数
int major = 0;
int vote = 0;
// 遍历每个数字
for(int num : nums) {
// 如果当前投票数为0,更新候选多数元素并将投票数设为1
if(vote == 0) {
major = num;
vote++;
} else {
// 如果当前数字与候选多数元素相同,投票数加1
if(num == major) {
vote++;
} else {
// 否则,投票数减1
vote--;
}
}
}
// 返回最终确定的候选多数元素
return major;
}
};
3.颜色分类
给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0
、 1
和 2
分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
双指针
荷兰国旗问题
class Solution {
public:
void sortColors(std::vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
int pos = 0;
while (pos <= right) {
if (nums[pos] == 0) {
// 将0交换到左边,并移动左指针和当前指针
std::swap(nums[pos], nums[left]);
left++;
pos++;
} else if (nums[pos] == 2) {
// 将2交换到右边,并移动右指针
std::swap(nums[pos], nums[right]);
right--;
} else {
// 如果是1,直接移动当前指针
pos++;
}
}
}
};
4.下一个排列
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,
arr = [1,2,3]
,以下这些都可以视作arr
的排列:[1,2,3]
、[1,3,2]
、[3,1,2]
、[2,3,1]
。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
- 例如,
arr = [1,2,3]
的下一个排列是[1,3,2]
。 - 类似地,
arr = [2,3,1]
的下一个排列是[3,1,2]
。 - 而
arr = [3,2,1]
的下一个排列是[1,2,3]
,因为[3,2,1]
不存在一个字典序更大的排列。
给你一个整数数组 nums
,找出 nums
的下一个排列。
必须** 原地 **修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
字典序算法
class Solution {
public:
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)reverse(nums.begin(),nums.end());
else{
int j=n-1;
// 找到第一个比 nums[i] 大的元素
while(j>=0&&nums[j]<=nums[i]){
j--;
}
// 交换元素 nums[i] 和 nums[j]
swap(nums[i],nums[j]);
// 反转 i 之后的元素
reverse(nums.begin()+i+1,nums.end());
}
}
};
5.寻找重复数
给定一个包含 n + 1
个整数的数组 nums
,其数字都在 [1, n]
范围内(包括 1
和 n
),可知至少存在一个重复的整数。
假设 nums
只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums
且只用常量级 O(1)
的额外空间。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
示例 3 :
输入:nums = [3,3,3,3,3]
输出:3
交换排序
改变了数组所以不符合题意
class Solution {
public:
int findDuplicate(std::vector<int>& nums) {
// 获取数组的长度
int n = nums.size();
// 遍历数组的每个元素
for (int i = 0; i < n; ++i) {
// 当当前元素不在它应该在的位置上(即 nums[i] != i + 1)
while (nums[i] != i + 1) {
// 检查当前元素是否已经在它正确的位置上
// 如果 nums[i] == nums[nums[i] - 1],说明找到了重复的数字
if (nums[i] == nums[nums[i] - 1]) {
return nums[i]; // 返回重复的数字
}
// 否则,将 nums[i] 放到它正确的位置上(即交换 nums[i] 和 nums[nums[i] - 1])
std::swap(nums[i], nums[nums[i] - 1]);
}
}
// 如果没有找到重复的数字,返回 -1(理论上不应该到这里,因为题目保证有一个重复的整数)
return -1;
}
};
Floyd’s Tortoise and Hare
class Solution {
public:
int findDuplicate(std::vector<int>& nums) {
int slow = nums[0];
int fast = nums[0];
// 阶段1:找到相遇点
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
// 阶段2:找到环的入口点
slow = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
};
return s.substr(start, maxlen);
}
};
#### 中心扩散法
### 5.最长公共子序列
给定两个字符串 `text1` 和 `text2`,返回这两个字符串的最长 **公共子序列** 的长度。如果不存在 **公共子序列** ,返回 `0` 。
一个字符串的 **子序列** 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,`"ace"` 是 `"abcde"` 的子序列,但 `"aec"` 不是 `"abcde"` 的子序列。
两个字符串的 **公共子序列** 是这两个字符串所共同拥有的子序列。
**示例 1:**
```
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
```
**示例 2:**
```
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
```
**示例 3:**
```
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
```
#### dp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
// 获取两个字符串的长度
int m = text1.size(), n = text2.size();
// 创建一个二维动态规划表 dp,大小为 (m+1) x (n+1)
// 初始化时,将所有元素设置为0。dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的最长公共子序列的长度
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
// 遍历 text1 和 text2 的所有字符
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 如果两个字符相等,dp[i+1][j+1] 就等于 dp[i][j] + 1
// 表示当前最长公共子序列在前一个基础上加上当前匹配的字符
if (text1[i] == text2[j]) {
dp[i + 1][j + 1] = dp[i][j] + 1;
} else {
// 如果两个字符不相等,则取 dp[i+1][j] 和 dp[i][j+1] 中的最大值
// 表示当前字符不匹配时,最长公共子序列不增加,需要继续比较其他字符
dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
// 返回 dp[m][n],即两个字符串的最长公共子序列的长度
return dp[m][n];
}
};
#### 滚动数组dp
~~~class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
// 获取两个字符串的长度
int m = text1.size(), n = text2.size();
// 初始化一个大小为 n+1 的一维动态规划数组,初始值为 0
vector<int> dp(n + 1, 0);
// 遍历 text1 的每个字符
for (int i = 0; i < m; i++) {
int pre = 0; // pre 用来存储 dp[j+1] 在更新前的值
// 遍历 text2 的每个字符
for (int j = 0; j < n; j++) {
int tmp = dp[j + 1]; // 记录 dp[j+1] 的当前值,供下一轮使用
// 如果 text1 和 text2 当前字符相等,则更新 dp[j+1] 为 pre + 1
// pre 对应的是上一行、上一列的 dp 值,代表不包括当前字符的 LCS 长度
if (text1[i] == text2[j])
dp[j + 1] = pre + 1;
else
// 如果不相等,取 dp[j] 和 dp[j+1] 的最大值
// dp[j] 对应左边的 dp 值(不包括 text1 当前字符)
// dp[j+1] 是 dp 数组中该位置的原始值
dp[j + 1] = max(dp[j], dp[j + 1]);
pre = tmp; // 更新 pre 为当前的 dp[j+1] 值,用于下一列计算
}
}
// 最后 dp[n] 中存储的是最长公共子序列的长度
return dp[n];
}
};
6.编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
二维dp
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
// 如果其中一个字符串为空,返回另一个字符串的长度
if (m == 0) return n;
else if (n == 0) return m;
// 创建一个二维dp数组,大小为(m+1) x (n+1)
// dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最小操作数
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
// 初始化 dp 数组的第一列,表示将 word1 的前 i 个字符转换为空字符串的代价
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
// 初始化 dp 数组的第一行,表示将空字符串转换为 word2 的前 j 个字符的代价
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 填充 dp 数组
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 如果字符 word1[i-1] 和 word2[j-1] 不相等
// dp[i][j] = 1 + min(删除操作dp[i-1][j], 插入操作dp[i][j-1], 替换操作dp[i-1][j-1])
dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
// 如果字符 word1[i-1] 和 word2[j-1] 相等,则可以直接继承 dp[i-1][j-1] 的值
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = min(dp[i][j], dp[i - 1][j - 1]);
}
}
}
// 返回最终的结果,即将 word1 转换为 word2 的最小操作数
return dp[m][n];
}
};
17.技巧
1.只出现1次的数字
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
代码
按位与:&
按位或:|
异或:^
0^a=a
a^a=0
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans=0;
for(int &num:nums){
ans^=num;
}
return ans;
}
};
2.多数元素
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
Boyer-Moore 投票算法
在线性时间和常数空间内找到多数元素(即出现次数超过一半的元素)。
每次不同的数字抵消候选多数元素的投票数,最终能够在一次遍历后找到多数元素。
class Solution {
public:
// 定义一个函数,接收一个整数向量并返回多数元素
int majorityElement(vector<int>& nums) {
// 初始化候选多数元素和投票数
int major = 0;
int vote = 0;
// 遍历每个数字
for(int num : nums) {
// 如果当前投票数为0,更新候选多数元素并将投票数设为1
if(vote == 0) {
major = num;
vote++;
} else {
// 如果当前数字与候选多数元素相同,投票数加1
if(num == major) {
vote++;
} else {
// 否则,投票数减1
vote--;
}
}
}
// 返回最终确定的候选多数元素
return major;
}
};
3.颜色分类
给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0
、 1
和 2
分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
双指针
荷兰国旗问题
class Solution {
public:
void sortColors(std::vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
int pos = 0;
while (pos <= right) {
if (nums[pos] == 0) {
// 将0交换到左边,并移动左指针和当前指针
std::swap(nums[pos], nums[left]);
left++;
pos++;
} else if (nums[pos] == 2) {
// 将2交换到右边,并移动右指针
std::swap(nums[pos], nums[right]);
right--;
} else {
// 如果是1,直接移动当前指针
pos++;
}
}
}
};
4.下一个排列
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,
arr = [1,2,3]
,以下这些都可以视作arr
的排列:[1,2,3]
、[1,3,2]
、[3,1,2]
、[2,3,1]
。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
- 例如,
arr = [1,2,3]
的下一个排列是[1,3,2]
。 - 类似地,
arr = [2,3,1]
的下一个排列是[3,1,2]
。 - 而
arr = [3,2,1]
的下一个排列是[1,2,3]
,因为[3,2,1]
不存在一个字典序更大的排列。
给你一个整数数组 nums
,找出 nums
的下一个排列。
必须** 原地 **修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
字典序算法
class Solution {
public:
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)reverse(nums.begin(),nums.end());
else{
int j=n-1;
// 找到第一个比 nums[i] 大的元素
while(j>=0&&nums[j]<=nums[i]){
j--;
}
// 交换元素 nums[i] 和 nums[j]
swap(nums[i],nums[j]);
// 反转 i 之后的元素
reverse(nums.begin()+i+1,nums.end());
}
}
};
5.寻找重复数
给定一个包含 n + 1
个整数的数组 nums
,其数字都在 [1, n]
范围内(包括 1
和 n
),可知至少存在一个重复的整数。
假设 nums
只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums
且只用常量级 O(1)
的额外空间。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
示例 3 :
输入:nums = [3,3,3,3,3]
输出:3
交换排序
改变了数组所以不符合题意
class Solution {
public:
int findDuplicate(std::vector<int>& nums) {
// 获取数组的长度
int n = nums.size();
// 遍历数组的每个元素
for (int i = 0; i < n; ++i) {
// 当当前元素不在它应该在的位置上(即 nums[i] != i + 1)
while (nums[i] != i + 1) {
// 检查当前元素是否已经在它正确的位置上
// 如果 nums[i] == nums[nums[i] - 1],说明找到了重复的数字
if (nums[i] == nums[nums[i] - 1]) {
return nums[i]; // 返回重复的数字
}
// 否则,将 nums[i] 放到它正确的位置上(即交换 nums[i] 和 nums[nums[i] - 1])
std::swap(nums[i], nums[nums[i] - 1]);
}
}
// 如果没有找到重复的数字,返回 -1(理论上不应该到这里,因为题目保证有一个重复的整数)
return -1;
}
};
Floyd’s Tortoise and Hare
class Solution {
public:
int findDuplicate(std::vector<int>& nums) {
int slow = nums[0];
int fast = nums[0];
// 阶段1:找到相遇点
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
// 阶段2:找到环的入口点
slow = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
};