递归的思想
- 递归函数
- 递归的终止条件
- 递归逻辑
哈希表
STL中常用哈希表容器
STL容器 | 解释 | 力扣题号 |
---|---|---|
unordered_set | 无序集合,其中的元素是唯一的,不允许重复。它基于哈希表实现,支持 O(1) 平均时间复杂度的插入、删除和查找操作。 | 128 |
unordered_map | 无序映射,包含一对键值对,其中的键是唯一的,不允许重复。它基于哈希表实现,支持 O(1) 平均时间复杂度的插入、删除和查找操作。 | 1、49 |
unordered_multiset | 无序多重集合,其中的元素允许重复。它基于哈希表实现,支持 O(1) 平均时间复杂度的插入、删除和查找操作。 | |
unordered_multimap | 无序多重映射,包含一对键值对,其中的键允许重复。它基于哈希表实现,支持 O(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]
分析
也可以直接用双指针,但是时间复杂度N*N,用哈希表优化`
由假设同一个元素不会在数组中重复出现,可以通过遍历一次数值nums,current指向当前值,通过std::unordered_map.find()函数查找target-current在不在哈希表中。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//使用哈希表,提高时间复杂度
unordered_map<int,int> hash_map;
int n=nums.size();
for(int i=0;i<n;++i){
auto it = hash_map.find(target-nums[i]);
if(it != hash_map.end()){
//在哈希表中搜索成功
return {it->second,i};
}
hash_map[nums[i]] = i;
}
return {};
};
};
时间复杂度:O(N),哈希查找时间复杂度1,但是要遍历一遍nums数组。
空间复杂度:O(N),哈希表存储
49、字母异位词分组(中等)(已二刷)
分析:该题疑惑的点在于怎么区分字母异位词,注意到string排序后如果是字母异位词结果应该一样。用哈希表建立键值对(键—排序后的单词,值—排序前的单词),返回哈希表的结果即可。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector <string>> ans;
//制作哈希表
unordered_map<string,vector<string>> hash_map;
for(string prob:strs){
string temp=prob;
sort(temp.begin(),temp.end()); //排序后得到键
hash_map[temp].push_back(prob);
}
//利用哈希表返回结果
for(auto it = hash_map.begin();it != hash_map.end();++it){
//遍历哈希表
ans.push_back(it->second);
}
return ans;
}
};
时间复杂度:O(N),制作哈希表需要遍历一次strs
空间复杂度:O(N),哈希表的开销
注意:不熟悉哈希表的遍历,和sort函数的调用。
128、最长连续序列(中等)(已二刷)
分析:
方法一
遍历数组,查找当前数字下一个连续的最长序列。为优化时间复杂度,使用哈希表查找。
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
if (nums.size()==0) return 0; //特判
unordered_set<int> hash_set(nums.begin(),nums.end()); //只用来查找,提高时间效率
int maxLenth = 1;
for(int num:nums){
//遍历nums,找出最长的连续序列
if(hash_set.find(num-1) == hash_set.end()){
//即num为连续序列的开头
int currentLenth = 1;
while(hash_set.find(num+1)!=hash_set.end()){
//即存在num之后的连续序列
currentLenth+=1;
num++;
}
maxLenth=max(maxLenth,currentLenth);
}
}
return maxLenth;
};
};
class Solution
{
public:
int longestConsecutive(vector<int>& nums)
{
unordered_set<int> numSet(nums.begin(),nums.end()); //将nums传值给unordered_set,建立无重复元素的numSet
int maxLen = 0; //最长连续序列的长度
for(int num:numSet)
{
if(numSet.find(num-1)==numSet.end()) //查找连续序列最小的
{
int currentNum = num;
int currentLen = 1;
while(numSet.find(currentNum+1)!=numSet.end())
{
currentNum++;
currentLen++;
}
maxLen = max(maxLen,currentLen);
}
}
return maxLen;
};
};
时间复杂度:O(N),遍历一次,哈希查找为O(1)
空间复杂度:O(N),额外哈希表
注意:只是少用了一个currentNum变量,运行时间和空间多了很多,为什么?
双指针
283.移动零(简单)(已二刷)
分析
从左往右,left和right指针,
- left指向第一个0.right指向left后第一个非0。
- 交换left和right值
- 交换之后left的下一个一定是0,让left指向right。再循环交换过程即可(关键在于交换之后left的下一个一定是0)
利用left和right两个指针指向nums数组中第一个零元素,如果left指向零,而right指向非零则交换其值。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
//双指针求解
if(nums.empty()) return;
int n = nums.size();
if(n==1) return;
int left=0,right=0;
while(nums[left]!=0){
//left指向第一个0
left++;
if(left==n) return;
}
//right指向下一个非0,交换left和right之后,left的下一个一定是0
for(right = left;right<n;++right){
if(nums[left]==0 && nums[right]!=0){
//交换
int mid = nums[left];
nums[left] = nums[right];
nums[right] = mid;
left++;
}
}
return;
};
};
时间复杂度O(n)
空间复杂度O(1)。
注意:关键在于理解交换后left的下一个元素一定为0
11.盛最多水的容器(中等)(已二刷)
分析
left指向数组头,right指向数组尾。记录当前最大面积。在left和right中选择一个较小值进行移动。left指向下一个较大数或者right指向下一个较大数,计算面积,记录历史出现的最大面积。直到left和right相遇。
class Solution {
public:
int maxArea(vector<int>& height) {
int maxArea = 0;
int n = height.size();
int left = 0;
int right = n-1;
while(left<right){
//当left在right左边时,一直循环判断
int currentArea = (right-left)*min(height[left],height[right]);
maxArea = max(maxArea,currentArea);
int temp;
if(height[left]<height[right]){
//找到left右边第一个比left值大的数
temp = height[left];
while(temp>=height[left] && left<n-1) {left++;}
}else{
//找到right左边第一个比right值大的数
temp = height[right];
while(temp>=height[right] && right>0) {right--;}
}
}
return maxArea;
};
};
时间复杂度:O(N)
空间复杂度:O(1)
15.三数之和(中等)(已二刷,值得再次刷)
分析
暴力遍历的话时间复杂度为O(n3),
1)使用排序加双指针,左指针从向右移动,右指针从右向左移动。并行,时间复杂度为O(n2)。
2)难点:不包含重复的三元组;每次只移动一个指针,移动之前和移动之后元素一样的话,肯定就是重复的。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int n = nums.size();
sort(nums.begin(), nums.end()); //排序
vector<vector<int>> ans;
for (int first = 0; first < n - 2; ++first) {
// 跳过重复的数字
if (first > 0 && nums[first] == nums[first - 1]) continue;
int third = n - 1;
int target = -nums[first];
//使用双指针查找目标数字
for (int second = first + 1; second < n; ++second) {
// 跳过重复的数字
if (second > first + 1 && nums[second] == nums[second - 1]) continue;
// 移动第三个指针,找到合适的组合
while (second < third && nums[second] + nums[third] > target) {
--third;
}
// 检查是否越界
if (second == third) break;
if (nums[second] + nums[third] == target) {
ans.push_back({nums[first], nums[second], nums[third]});
}
}
}
return ans;
};
};
时间复杂度:O(N*N),sort排序时间复杂度O(NlogN)
空间复杂度:O(1)
注意:使用双指针,一前一后对有序数组进行查找,时间复杂度为O(N)
42.接雨水(困难)(已二刷)
给定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
分析: 将柱子看作一个一个单独的水桶,水桶包含能接的水和柱子。水桶的容积等于左边的最大值和右边的最大值中取较小的一个,水桶容积减去柱子的高度就是水的高度。
class Solution {
public:
int trap(vector<int>& height) {
/* 将柱子每一列看作一个水桶,水桶的容积等于该列左边的最大值和右边的最大值中较小者,水桶容积减去当前柱子高度,就是水的体积
优化算法思路,遍历一次,用数组存储各个位置的最大前缀和最大后缀
*/
int n = height.size();
int sum = 0;
vector<int> pre_Max(n);
int pre_iMax=0,sub_iMax=0;
vector<int> sub_Max(n);
//制作最大前缀和最大后缀
for(int i=0;i<n;i++){
pre_iMax = max(pre_iMax,height[i]);
pre_Max[i] = pre_iMax;
}
for(int i=n-1;i>=0;i--){
sub_iMax=max(sub_iMax,height[i]);
sub_Max[i]=sub_iMax;
}
//遍历计算容积
for(int i=0;i<n;i++){
int capacity = min(pre_Max[i],sub_Max[i])-height[i];
sum+=capacity;
}
return sum;
}
};
时间复杂度:O(N),需要遍历三次数组
空间复杂度:O(N),需要两个额外的大小为N的数组来存储前缀最大值和后缀最大值。
滑动窗口
窗口的维护:满足条件收缩,不满足条件拓张
记录下满足条件下的最优解
滑动窗口题目:3、30、76、159、209、239、567、632、727
分析
这道题主要用到思路是:滑动窗口
什么是滑动窗口?
其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列!
如何移动?
我们只要把队列的左边的元素移出就行了,直到满足题目要求!一直维持这样的队列,找出队列出现最长的长度时候,求出解!
时间复杂度:O(n)
3.无重复字符的最长子串(中等)(已二刷)
分析
设定一个滑动窗口,用两个指针left和right表示左右边界。遍历字符串s,
1)如果当前字符插入满足无重复子串条件,则插入。
2)如果不满足,即出现重复元,依次在删除重复元左侧的所有元素。
同时记录下滑动窗口出现最大长度的数值输出即可。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
/*1. 判断滑动窗口内无重复字符
2.出现重复字符时,删除出现的重复字符左侧字符,记录历史最大值*/
int n=s.size();
if(n==0) return 0;
if(n==1) return 1;
int maxlenth=0;
unordered_map<char,int> hash_map; //用来记录窗口内出现的字符及脚标
hash_map[s[0]]=0;
for(int left=0,right=1;right<n;right++){
//遍历字符串s
//如果right指向的字符在窗口内,left移动删除到重复的字符处
if(hash_map.find(s[right])!=hash_map.end()) {
//left移动
for(left;left<hash_map[s[right]]+1;left++){
hash_map.erase(s[left]);
}
}
hash_map[s[right]]=right;
maxlenth = max(maxlenth,right-left);
}
return maxlenth+1;
};
};
时间复杂度:O(N),遍历一次字符串S,删除一次字符串S
空间复杂度:O(N),哈希表的开销
438.找到字符串中所有字母异位词(中等)(已二刷)
分析
p为s的子串,则滑动窗口大小设置为p的大小,判断滑动窗口内的走,字母出现的次数是否和p的一致。
class Solution
{
public:
vector<int> findAnagrams(string s, string p)
{
int sLen = s.size();
int pLen = p.size();
vector<int> ans;
if(sLen<pLen) return ans; //特判
int l = 0;
int r = 0;
vector<int> libary1(26);
vector<int> libary2(26);
for(char i : p) //libary1统计p中各个字符的个数
{
int num = i-'a';
libary1[num]+=1;
}
for (l,r; r<sLen; ++r) //固定窗口l和r。
{
int num = s[r]-'a';
libary2[num]+=1;
if(libary1 == libary2)
{
ans.push_back(l);
}
if(r==(l+pLen-1)) //左指针右移并且要删除左指针的计数
{
int mid = s[l]-'a';
libary2[mid]-=1;
l++;
}
}
return ans;
}
};
时间复杂度:O(N)
空间复杂度:O(p_size),存储p的字典出现次数。
567.字符串的排列(中等)
分析
解题思路与438一致
1)建立两个26个元素的数组,用来统计字符串中字母出现的频率。
2)用一个固定s1大小的窗口滑动指向s2,如果满足条件返回true,否则右指针右移同时添加右指针指向的字母计数,左指针右移,同时删除左指针指向字母的计数。
比较两数组是否一致即可。
class Solution
{
public:
bool checkInclusion(string s1, string s2)
{
int s1Len = s1.size();
int s2Len = s2.size();
if (s1Len>s2Len) return false; //特判
vector<int> s1_count(26);
vector<int> s2_count(26);
for (char s:s1) //统计s1的字母计数
{
int num = s-'a';
s1_count[num]+=1;
}
int r = 0, l = 0;
for (l,r; r<s2Len; ++r) //注意添加和删除的顺序
{
int num = s2[r]-'a';
s2_count[num]+=1;
if(s1_count==s2_count) return true;
if(l==r-s1Len+1) //窗口大小达到s1的大小时,之后的移动要删除左边元素
{
int mid = s2[l]-'a';
s2_count[mid]-=1;
l++;
}
}
return false;
}
};
时间复杂度O(n),空间复杂度O(1)
子串
560.和为K的子数组(中等)(二刷)
相似题目974、560、523
分析
思路一:暴力求解,列出数组所有的子串,如果有子串和等于k的连续子数组则计数加一。时间复杂度O(n2)
思路二:前缀和(前N项和,级数)+哈希表,时间复杂度O(n),空间复杂度O(n)
0<pre[j]<pre[i]<nums.size();
要统计连续子数组和为k的个数,即统计pre[i]-k=pre[j],出现的次数。所以想到使用哈希表的键值对来映射计量关系。
pre[i]
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
/*使用滑动窗口,当窗口内和为k则记录一次,思路一:左指针和右指针依次遍历,时间O(N*N),思路二:用空间换时间,用前缀和数组,如果前缀和等于k,则算一个,否则,任意两个前缀和差为k也算一个,用哈希表来存储前缀和,查找时间复杂度较低。*/
int count = 0;
int currentSum = 0;
unordered_map<int, int> prefixSumCount;
prefixSumCount[0] = 1; // 初始化,前缀和为0的情况有1次
for (int num : nums) {
currentSum += num;
// 检查是否存在前缀和等于 currentSum - k
if (prefixSumCount.find(currentSum - k) != prefixSumCount.end()) {
count += prefixSumCount[currentSum - k];
}
// 更新当前前缀和的计数
prefixSumCount[currentSum]++;
}
return count;
};
};
时间复杂度:O(N)
空间复杂度:O(N)
239.滑动窗口最大值(困难)(二刷)
给你一个整数数组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
示例 2:
输入:nums = [1], k = 1
输出:[1]
分析:利用双指针指向滑动窗口的大小,存储当前窗口的最大值。将窗口向右移动一格。
- 如果前一个最大值在左指针处,需要重新遍历当前窗口找到当前最大值
- 如果前一个最大值不在左指针处,只需比较新增加的数是否比前一个最大值大即可,更新当前最大值。
- 直到移动到末尾。
class Solution {
public:
void findCurrentMax(vector<int> nums,int ¤t_Max,int &max_Index,int left,int right){
//用函数用于辅助找到当前窗口的最大值
if(left>right) {
//用于修正窗口异常情况
current_Max = INT_MIN;
max_Index = 0;
}
for(int i=left;i<=right;i++){
if(nums[i]> current_Max) {
current_Max = nums[i];
max_Index = i;
}
}
return;
}
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
//1. 判断窗口内的最大值,并返回
//2. 移动窗口
/*暴力算法:每移动一次判断一次窗口内最大值,
优化:记录上一次最大值出现的位置,如果移动后还在窗口内,则比较right的值谁大
如果移动后不在窗口内,则需要重新找当前窗口的最大值*/
int left=0;
int right = left+k-1;
vector<int> ans;
int n=nums.size();
int current_Max = INT_MIN;
int max_Index = 0;
//初始化
findCurrentMax(nums,current_Max,max_Index,left,right);
for(right;right<n;right++){
//窗口右移
if(nums[right]>current_Max) {
//处理右边界
current_Max = nums[right];
max_Index = right;
}
ans.push_back(current_Max);
if(max_Index == left) {
//处理左边界
current_Max = INT_MIN;
max_Index = left+1;
findCurrentMax(nums,current_Max,max_Index,left+1,right);
}
left++;
}
return ans;
}
};
时间复杂度:O(N*K),会产生很多不必要的重复计算。超时
空间复杂度:O(N)。
优化:使用双端队列,来保证每次在O(1)的时间复杂度内获取最大值。向前移除不在窗口的元素索引,向后移除小于当前元素值的索引。因此,队列中存储了当前窗口的最大值索引。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> ans;
deque<int> dq; // 双端队列,存储可能成为窗口最大值的元素的索引
for (int i = 0; i < nums.size(); ++i) {
// 移除不在窗口内的元素的索引
while (!dq.empty() && dq.front() < i - k + 1) {
dq.pop_front();
}
// 移除所有小于当前元素的值的索引
while (!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
dq.push_back(i);
// 当窗口大小达到 k 时,记录当前窗口的最大值
if (i >= k - 1) {
ans.push_back(nums[dq.front()]);
}
}
return ans;
}
};
时间复杂度:O(N),遍历所有元素,并且查找当前最大元素的时间为O(1)
空间复杂度:O(N)。
76.最小覆盖子串(困难)
给你一个字符串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 的子串中,
因此没有符合条件的子字符串,返回空字符串。
思路:利用双指针,开始时两指针都指向S的开头。right指针向右移动至两指针包含T中所有字符串。再移动left指针,将区间压缩得尽可能小。记录此时的子串。
重复上述操作,直到右指针到达S的末尾,返回满足条件的最小子串。
如何表示子串是否满足t中字符的数量,维护一个字典,当滑动窗口中满足一个字符在t中时,计数减一。当滑动窗口收缩减少一个字符时,字典计数加一。字典计数为0则表示找到了子串。
class Solution {
public:
string minWindow(string s, string t) {
//创建字典索引计数
unordered_map<char, int> hash_map;
for (char c : t) {
hash_map[c]++;
}
//查找子串
int min_length = INT_MAX, min_start = 0;
int left = 0, right = 0;
int required = t.size();
while (right < s.size()) {
//当right指向的数在t中
if (hash_map[s[right]] > 0) {
required--;
}
//对所有的right字符都进行操作
hash_map[s[right]]--;
right++;
while (required == 0) {
//当left和right包含t所有字符时,收缩left
if (right - left < min_length) {
min_length = right - left;
min_start = left;
}
hash_map[s[left]]++;
if (hash_map[s[left]] > 0) {
required++;
}
left++;
}
}
if (min_length == INT_MAX) return "";
return s.substr(min_start, min_length);
}
};
时间复杂度:O(N)
空间复杂度:O(1),M为s中出现的字符种类个数。最多为26个。
普通数组
53.最大子数组和(中等)(二刷)
给你一个整数数组Nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
分析
用一个prenums[]数组记录前n项和,当为负数时,累积和肯定更小,所以可以舍弃。记录出现的最大值,输出即可。
class Solution
{
public:
int maxSubArray(vector<int>& nums)
{
/*采用前N项和,最大子数组一定是开头和结尾都比价大的数*/
vector<int> prenums = nums;
int n = nums.size();
int Max = prenums[0];
for (int i = 0 ; i < n-1 ; i++)
{
if (prenums[i] > 0)
{
prenums[i+1] = prenums[i]+nums[i+1];
}
else
{
prenums[i+1] = nums[i+1];
}
if (prenums[i+1] > Max)
{
Max = prenums[i+1];
}
// return max prenums;
}
return Max;
}
};
前n项和常用思路。
时间复杂度:O(N)
空间复杂度:O(N)。
56.合并区间(中等)(二刷)
以数组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] 可被视为重叠区间。
思路:
1、排序intervals,排序后合并的区间一定为一个连续的范围
2、遍历intervals,如果需要合并则合并,不需要合并则推入答案数组。需要特判当答案数组为空时,推入intervals。
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
/*判断如果,endi大于startj,则合并*/
int N = intervals.size();
if (N==0 || N==1) return intervals;
sort(intervals.begin(),intervals.end());
vector<vector<int>> res;
for (int i=0;i<N;i++){
//遍历数组
int L =intervals[i][0];
int R = intervals[i][1];
if(!res.size()||res.back()[1]<L){
res.push_back({L,R});
}
else{
res.back()[1] = max(res.back()[1],R);
}
}
return res;
}
};
时间复杂度:O(N)
空间复杂度:O(N)
189.轮转数组(中等)(二刷)
给定一个数组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]
思路:将数组划分为两部分,拼接
我们可以使用额外的数组来将每个元素放至正确的位置。用 n表示数组的长度,我们遍历原数组,将原数组下标为 i的元素放至新数组下标为== (i+k) mod n==的位置,最后将新数组拷贝至原数组即可。
class Solution {
public:
void rotate(vector<int>& nums, int k) {
/*把数组划分为两部分,先推入后面部分,再推入前面部分,翻译为将后k个数据挪到前面来*/
int N = nums.size();
if(N==0||N==1||k==0||k==N) return;
if(k>N) k=k%N;
vector<int> temp;
for(int i=N-k;i<N;++i){
temp.push_back(nums[i]);
}
for(int i=N-1;i>k-1;--i){
nums[i] = nums[i-k];
}
for(int i=0;i<k;i++){
nums[i] = temp[i];
}
return;
}
};
//用mod
class Solution {
public:
void rotate(vector<int>& nums, int k) {
/*用mod*/
int n = nums.size();
if(n==0 || n==1 || k==n ||k==0) return;
vector<int> ans =nums;
for (int i=0;i<n;i++){
ans[(i+k)%n] = nums[i];
}
nums=ans;
return;
}
};
时间复杂度:O(N)
空间复杂度:O(N)
238.除自身以外数组的乘积(中等)(二刷)
给你一个整数数组 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();
vector<int> left(N,1),right(N,1);
for(int i=N-2;i>=0;i--){
right[i] = right[i+1]*nums[i+1];
}
for (int i=1;i<N;i++){
left[i] = left[i-1]*nums[i-1];
}
for(int i=0;i<N;i++){
left[i] = left[i]*right[i];
}
return left;
}
};
矩阵
73.矩阵置零(中等)(二刷)
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
第一版:
思路
- 定义一个2*k的数组temp用以存储0元素所在的行号和列号
- 遍历matrix,找出所有0元素的脚标
- 遍历temp,将matrix中所有0元素脚标的行和列设为0
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
/*用一个二维数组存储为零元素的坐标值*/
int m = matrix.size();
int n = matrix[0].size();
vector< vector<int>> temp;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
//遍历矩阵找到为0的元素
if(matrix[i][j]==0) temp.push_back({i,j});
}
}
//将对应位置置零
for(vector<int>res:temp){
matrix[res[0]] = vector<int> (n,0); //对应行置零
for(int i=0;i<m;i++){
//对应列置零
matrix[i][res[1]] = 0;
}
}
return;
}
};
时间复杂度:O(m*n)
空间复杂度:O(m+n)
时间:24ms ,击败5.48%使用C++用户
内存:13.20MB ,击败5.09%使用C++用户
第二版优化思路:
- 可以充分利用好矩阵的第0行和第0列。(用以作为是否要将该行和该列置0的依据)
- 首先需要检测第0行和第0列是否有0,及第0行和第0列是否需要被置0。
- 在检测余子式是否有0,并用第0行,和第0列标记。
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
bool firstRowHasZero = false;
bool firstColHasZero = false;
bool rowHasZero = false;
int i,j;
//检测第0行和第0列是否有0,是否需要置0
for(i=0; i<n; i++){
if(matrix[0][i]==0){
firstRowHasZero = true;
break;
}
}
for(i=0; i<m; i++){
if(matrix[i][0]==0){
firstColHasZero = true;
break;
}
}
//检测余子式是否有0,并用第0行和第0列标记,比较反复赋值和多次判断哪个效率更低
for(i=1; i<m; i++){
rowHasZero = false;
for(j=1; j<n; j++){
if(matrix[i][j]==0 ){
matrix[0][j]=0;
rowHasZero = true; //会多次赋值
}
}
if(rowHasZero){
matrix[i][0] = 0;
}
}
//根据第0行和第0列的标记,将对应行列置0
for(j=1;j<n;j++){
if(matrix[0][j]==0){
for(i=1;i<m;i++){
matrix[i][j] = 0;
}
}
}
for(i=1;i<m;i++){
if(matrix[i][0]==0){
for(j=1;j<n;j++){
matrix[i][j]=0;
}
}
}
//根据第0行和第0列是否有0决定是否置零第0行和第0列
if(firstRowHasZero){
for(j=0;j<n;j++){
matrix[0][j]=0;
}
}
if(firstColHasZero){
for(i=0;i<m;i++){
matrix[i][0]=0;
}
}
return;
}
};
通过设置rowHasZero变量,来避免重复给第0行赋0值。
时间复杂度:O(m*n),8ms 击败96.92%
空间复杂度:O(1),13.23MB,击败5.09%
需要注意的是,根据矩阵在内存中的存储特性。遍历行比遍历列效率更高
54.螺旋矩阵(中等)(二刷)
给你一个m行n列的矩阵matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
第一版:
思路:
自己第一遍没想出来,参考力扣官方解答
按层模拟
剥洋葱,由外圈向内圈一圈一圈的剥开。变量较多而已。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.size() == 0) {
return {};
}
int rows = matrix.size(), columns = matrix[0].size();
vector<int> order;
int left = 0, right = columns - 1, top = 0, bottom = rows - 1;
while (left <= right && top <= bottom) {
//输出上和右
for (int column = left; column <= right; column++) {
order.push_back(matrix[top][column]);
}
for (int row = top + 1; row <= bottom; row++) {
order.push_back(matrix[row][right]);
}
//严格小时,输出下和左
if (left < right && top < bottom) {
for (int column = right - 1; column > left; column--) {
order.push_back(matrix[bottom][column]);
}
for (int row = bottom; row > top; row--) {
order.push_back(matrix[row][left]);
}
}
left++;
right--;
top++;
bottom--;
}
return order;
}
};
时间复杂度:O(m*n) 0ms,击败100%
空间复杂度:O(1) 6.92MB,击败5.12%
48.旋转图像(中等)(二刷)
给定一个nxn的二维矩阵matrix表示一个图像。请你将图像顺时针旋转90°。
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
第一版解答:
思路:
力扣官网解答
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// C++ 这里的 = 拷贝是值拷贝,会得到一个新的数组
auto matrix_new = matrix;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
matrix_new[j][n - i - 1] = matrix[i][j];
}
}
// 这里也是值拷贝
matrix = matrix_new;
}
};
时间复杂度: O(n^2), 4ms,击败46.59%
空间复杂度: O(n^2),7.28MB击败5.01%
方法二:原地旋转
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < (n + 1) / 2; ++j) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
}
};
时间复杂度: O(n^2), 4ms,击败46.59%
空间复杂度: O(1),7.22MB击败5.01%
240 搜索二维矩阵(中等)(二刷)
编写一个高效的算法来搜索mxn矩阵matrix中的一个目标值target。该矩阵具有以下特性:
- 每行的元素从左到右升序排列
- 每列的元素从上到下升序排列
示例:
输入: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
第一版:参考力扣官方解答
初始思路:顺序主子式的右下角元素一定是该矩阵中最大的一个元素。
- 遍历(i,i),找到第一个比target大的元素
- 则target只能出现在,该元素所在的行和列,否则,将不存在target
- 问题是矩阵为mxn,而不是nxn,对于nxn的很好用
思路:参考红黑树的概念,将矩阵逆时针旋转45°
- 从右上角元素开始搜索
- 左叶子节点比根节点小,右叶子节点比根节点大。(左值比根节点小,下值比根节点大)
代码:
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
/*类似红黑树,将矩阵逆时针旋转45°,
即从右上角开始查找,如果target大于节点值,向下找,否则向左找,直到遇到矩阵边界*/
int m = matrix.size();
int n = matrix[0].size();
int i=0,j=n-1;
while(i<m && j>=0){
//从右上角开始搜索
if(matrix[i][j]==target) return true;
else if(matrix[i][j]>target) j--;
else i++;
}
return false;
}
};
时间复杂度:O(N+M) ,96ms,击败67.56%
空间复杂度:O(1) ,14.55MB,击败5.07%
链表
unodered_set :容器只存储一个值,相当于用哈希表实现的数组
unodered_map:存储的是键值对,不止存储数据,还要存储关键词
160.相交链表(简单)(二刷)
给你两个单链表的头节点head A和 head B,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回NULL。
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at ‘8’
第一版:
思路
用一个集合保存a链表,再遍历b链表。查看b链表中有无a链表中的同一个结点。
/**
* 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) {
unordered_set<ListNode *> visited;
ListNode *temp = headA;
//将链表A插入到visited哈希集合中
while (temp != nullptr) {
visited.insert(temp);
temp = temp->next;
}
temp = headB;
while (temp != nullptr) {//遍历链表B
if (visited.count(temp)) {
return temp;
}
temp = temp->next;
}
return nullptr;
}
};
206. 反转链表(简单)(二刷)
给你单链表的头结点head,请你反转链表,并返回反转后的链表。
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
第一版:
思路
- 先遍历一遍给的链表,并用vector依次存储链表的值
- 用创建的vector来动态创建新的链表
- 返回新链表
class Solution {
public:
ListNode* reverseList(ListNode* head) {
/*使用vector容器来存储节点,遍历两次链表,第一次读取所有的值,并用vector存储,第二次依次幅值*/
vector<int> value;
ListNode * temp = head;
while(temp!=nullptr){
//遍历链表
value.push_back(temp->val);
temp = temp->next;
}
temp = head;
while(temp != nullptr){
temp->val = value.back();
value.pop_back();
temp = temp->next;
}
return head;
}
};
时间复杂度:O(n)4ms,击败93.3%
空间复杂度:O(n) 8.6MB,击败5.01%
利用了额外的空间,其中指针的使用是重点
作为函数,用new创建了新的结点,但是没有用delete删除,会导致内存泄漏。
第二版:原地反转
操作指针
- 创建一个curr指针指向当前节点和一个prev指针,指向前一个节点
- curr->next 赋值为prev
- 两指针同时后移
class Solution {
public:
ListNode* reverseList(ListNode* head) {
/*原地翻转
通过前后两个指针,同时移动,并将原有的next反转即可*/
if(head == nullptr) return head;
ListNode* current =head->next;
if(current== nullptr) return head; //单节点不用反转
ListNode* pre = head;
pre->next = nullptr;
while (current!=nullptr){
//遍历整个链表
ListNode* temp = current->next;
current->next = pre;
pre = current;
current = temp;
}
return pre;
}
};
时间复杂度: O(n), 4ms 击败93.31%
空间复杂度:O(1), 8.47MB击败5.01%
234.回文链表(简单)(二刷)
给你一个单链表的头结点head,请你判断该链表是否为回文链表。如果是,返回true,否则,返回false。
示例:
输入:head = [1,2,2,1]
输出:true
第一版
思路:
- 遍历链表,依次记录下链表节点的值
- 如果第i位和第n-1-i位值不等,则为非回文链表
- 否则则为回文链表
/**
* 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) {
vector<int> temp;
while(head != nullptr){
//当链表指针非空指针时,存储值并且指针后移
temp.push_back(head->val);
head = head->next;
}
int n = temp.size();
for(int i=0; i<(n/2);i++){
if(temp[i] != temp[n-1-i]) return false;
}
return true;
}
};
时间复杂度: O(N),240ms,16.10%
空间复杂度:O(N),122.9MB,5.87%
优化思路:从优化空间复杂度的角度出发,第一版使用了额外的N空间来存储vector值,可以使用两个指针,从链表的中间出发,用O(1)的额外空间来对比左右两指针所指的元素值是否相同。
实现复杂且优化效果不显著。此优化思路不好,用大量的时间换一点空间。
141.环形链表(简单)(二刷)
给你一个链表的头节点head,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。为了表示给定链表中的环,评测系统内部使用整数pos来表示链表尾连接到链表中的位置(索引从0开始)。注意:pos不作为参数进行传递。仅仅是为了表示链表的实际情况。
如果链表中存在环,则返回true。否则,返回false
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
第一版思路:
快慢指针,判断有无环
class Solution {
public:
bool hasCycle(ListNode *head) {
/*快慢指针,
快指针一次移动两节点,慢指针一次移动一个节点,
如果快慢指针指向同一个节点了就说明有环,如果快指针先指向nullptr则无*/
if(head==nullptr) return false;
ListNode* fast_ptr = head;
ListNode* slow_ptr = head;
do{
slow_ptr = slow_ptr->next;
fast_ptr = fast_ptr->next;
if(slow_ptr==nullptr || fast_ptr==nullptr) return false;
fast_ptr = fast_ptr->next;
if(fast_ptr==nullptr) return false; //有可能链表长为奇数
}while(fast_ptr != slow_ptr);
return true;
}
};
时间复杂度:O(2N),8ms,93.21%
空间复杂度: O(1),8.06MB,27.82%
142.环形链表二(中等)(二刷)
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
第一版思路:
用一个vector来存储链表节点
- 如果节点在vector中,则说明产生了环。vector中的那个节点则为要返回的索引节点
- 如果节点不在vector且下一个节点为空,则不存在环。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
/*使用哈希表容器来存储每个节点,
如果遍历的过程中发现表内已经有了该节点则说明存在环,并且该节点就是环的入口节点*/
if(head==nullptr || head->next == nullptr) return nullptr;
unordered_set<ListNode *> hash_set;
ListNode* temp = head;
while(temp!=nullptr){
//如果temp为nullptr则无环
if(hash_set.count(temp)) return temp;
hash_set.insert(temp);
temp = temp->next;
}
return nullptr;
}
};
之所以采用哈希表是因为哈希表的查找时间复杂度为O(1),高效
时间复杂度: O(N^2),324ms,5.05%
空间复杂度: O(N),8.13MB,21.22%
优化:从优化时间复杂度的角度出发
第二版:
快慢指针的数学关系:
- 先使用快慢指针判断链表中是否存在环。
- 若存在,再将其中一个指针定位到链表的头部,然后让两个指针以相同的速度移动,当它们再次相遇的时候,相遇的点就睡循环的开始。重点理解规律
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
/*快慢指针,得出结论a=c+(n-1)(b+c),则相遇之后再创建一个链表头的指针与慢指针同时走,一定会相遇在入环第一个节点处*/
if(head==nullptr || head->next==nullptr) return nullptr;
ListNode* fast_ptr = head;
ListNode* slow_ptr = head;
do{
//直到两指针相遇
fast_ptr = fast_ptr->next;
slow_ptr = slow_ptr->next;
if (fast_ptr==nullptr) return nullptr;
fast_ptr = fast_ptr->next;
if(fast_ptr==nullptr) return nullptr;
}while(fast_ptr!=slow_ptr);
ListNode* temp = head;
while(temp!=slow_ptr){
temp=temp->next;
slow_ptr = slow_ptr->next;
}
return temp;
}
};
时间复杂度O(N),8ms,76.64%
空间复杂度O(1),7.63MB,29.26%
21.合并两个有序链表(简单)(二刷)
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
第一版思路:(迭代)
- 创建一个新节点
- 比较l1和l2,将较小者插入到新节点尾部
- 直到某一个链表全部插完,将剩余的链表直接插入到新链表结尾
- 返回新链表(此时新链表多了一个新建的头节点)
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
/*通过两个指向链表的指针实现,优先推入较小者,如果有一方为nullptr则直接将另一方剩余部分推入*/
ListNode* L1 = list1, *L2 = list2;
ListNode* new_head = new ListNode; //哑结点
ListNode* temp = new_head;
while(L1!=nullptr && L2 != nullptr){
//两链表都没到末尾时
if(L1->val <= L2->val) {
temp->next = L1;
L1 = L1->next;
temp = temp->next;
}else{
temp->next = L2;
L2 = L2->next;
temp = temp->next;
}
}
if(L1==nullptr) temp->next = L2;
if(L2==nullptr) temp->next = L1;
return new_head->next;
}
};
思路二:
递归
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
//递归终止条件
if (l1 == nullptr) {
return l2;
} else if (l2 == nullptr) {
return l1;
} else if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
};
2.两数相加(中等)(二刷)
给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字0之外,这两个数都不会以0开头
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
第一版思路:
- 动态创建一个链表,以相同的格式存储结果
- 依次按位运算,如果大于9,创建临时变量加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) {
int flags = 0;
ListNode * result = new ListNode(0);
ListNode * ans = result;
while(l1 != nullptr && l2 != nullptr){
//l1和l2都不为空,则相加运算
int tempVal = l1->val + l2->val;
if(flags==1) {
tempVal+=1;
flags = 0;
}
if((tempVal+flags) > 9){
tempVal = tempVal%10;
flags = 1;
}
result->next = new ListNode(tempVal);
l1=l1->next;
l2=l2->next;
result = result->next;
}
//l1或者l2有一个为空指针,则将另一个链接到链表尾部
if(l1 == nullptr && flags ==0 ) result->next = l2;
else if (l2 == nullptr && flags ==0) result->next = l1;
else if (l1 == nullptr && flags ==1){
//将l2剩余部分值+1,再链接到末尾
ListNode * tempptr = l2;
while(tempptr != nullptr){
if(flags ==1){
tempptr->val+=1;
flags = 0;
if(tempptr->val > 9){
tempptr->val = tempptr->val%10;
flags = 1;
}
tempptr=tempptr->next;
}
else {
tempptr=tempptr->next;
}
}
result->next = l2;
}
else{
//将l1剩余部分值+1,再链接到末尾
ListNode * tempptr = l1;
while(tempptr != nullptr){
if(flags ==1){
tempptr->val+=1;
flags = 0;
if(tempptr->val > 9){
tempptr->val = tempptr->val%10;
flags = 1;
}
tempptr=tempptr->next;
}
else {
tempptr=tempptr->next;
}
}
result->next = l1;
}
if(flags==1){
//如果最后一位需要进位,则再创建一个值为1的节点,链接到链表末尾
ListNode * tempptr1 = ans;
while(tempptr1->next != nullptr){
tempptr1 = tempptr1->next;
}
tempptr1->next = new ListNode(1);
}
return ans->next;
}
};
时间复杂度:O(N+M),44ms,6.6%
空间复杂度:O(1),68.63MB,5.1%
代码逻辑过于繁琐,可以优化,指针操作不到位
第二版思路:
- 创建一个哑结点,作为输出结果链表的头部
- 输出结果链表的每一个节点的值为两非空链表对应值之和加flags(进位)
-
- 如果其中一个链表节点为空,则令其值为0即可
-
- 否则其值为对应的val值
- 判断最后一位的进位
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
//创建一个哑结点,作为输出结果链表的开头
ListNode * ans = new ListNode(0);
ListNode * current = ans;
//动态创建结果链表的节点,其值为两输入链表对应位置的和加上进位
int flags = 0;
while(l1 != nullptr || l2 != nullptr){
int x = l1==nullptr?0:l1->val;
int y = l2==nullptr?0:l2->val;
int sum = x+y+flags;
if(sum>9) flags = 1;
else flags =0;
current->next = new ListNode(sum%10);
current = current->next;
if(l1 != nullptr) l1 = l1->next;
if(l2 != nullptr) l2 = l2->next;
}
//处理最后一位的进位
if(flags ==1){
current->next = new ListNode(1);
}
return ans->next;
}
};
时间复杂度:O(N),20ms,90.22%
空间复杂度:O(N),68.54MB,8.11%
19.删除链表的倒数第N个结点(中等)(二刷)
给你一个链表,删除链表的倒数第n个结点,并且返回链表的头结点
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
第一版思路:
- 遍历两次
- 删除、链接
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
/*遍历两次,第一次找到链表的总长度,第二次定位需要删除的节点位置,删除即可*/
if(head==nullptr) return head;
if(head->next==nullptr && n==1) return nullptr;
ListNode* temp = head;
int N=0;
while(temp!=nullptr){
//遍历一次,确定总长度
N++;
temp = temp->next;
}
if(N==n) return head->next;
temp = head;
for(int i=0;i<N-n-1;i++){
//遍历到需要删除节点的前一个节点
temp = temp->next;
}
temp->next = temp->next->next;
return head;
}
};
时间复杂度:O(2N),8ms,27.97%
空间复杂度:O(1),10.41MB,26.66%
第二版优化:
- 通过一次遍历来实现。利用两个相距为n的指针,当右指针到达链表末尾的nullptr时,左指针指向要删除的结点的前一个结点
- 引入哑结点,来处理边界条件
- 内存管理,释放删除结点的内存
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *dummyHead = new ListNode(0); //创建哑结点
dummyHead->next = head;
ListNode *first = dummyHead;
ListNode *second = dummyHead;
//第一个指针移动n+1次
for(int i=0;i<=n;i++){
first = first->next;
}
//两个指针同时移动,直到第一个指针到nullptr
while(first != nullptr){
first = first->next;
second = second->next;
}
//删除second指针的下一个结点
first = second->next;
second->next = first->next;
delete first; //删除要删除结点的内存空间
ListNode *newhead = dummyHead->next;
delete dummyHead; //删除哑结点
return newhead;
}
};
/*二刷代码*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
/*利用两个相距为n的指针,当后一个指针到链表末尾时,前一个指针正好指向需要删除的节点
则只需要遍历一次就够了*/
if(head==nullptr) return nullptr;
if(head->next==nullptr && n==1) return nullptr;
ListNode *pre = head, *currt = head;
for(int i=0;i<n;i++){
//后一个指针后移
currt = currt->next;
}
if(currt==nullptr) return head->next; //特判,当要删除的是第一个节点时
while(currt->next!=nullptr){
pre=pre->next;
currt=currt->next;
}
pre->next = pre->next->next; //删除指定节点
return head;
}
};
时间复杂度: O(n),0ms,100.00%
空间复杂度:O(1),10.46MB,21.73%
总结:对链表进行插入删除等操作,使用哑结点可以简化流程
24.两两交换链表中的节点(中等)(二刷,注意理解递归)
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部值的情况下完成本题(即,只能进行节点交换)
输入:head = [1,2,3,4]
输出:[2,1,4,3]
第一版思路:
- 创建哑结点,对边界节点统一操作
- 用三个指针,current,first,second来操作
- current指示当前一对节点的开始,first和second分别指向两个节点,进行交换操作
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
//处理0和1个节点的情况
if(head->next == nullptr || head == nullptr) return head;
//创建哑结点,统一边界操作
ListNode * dummyHead = new ListNode(0);
ListNode * first;
ListNode * second;
dummyHead->next = head;
//创建指针进行交换操作
ListNode * current = dummyHead;
while(current->next != nullptr && current->next->next != nullptr){
second = current->next;
first = current->next->next;
//交换两节点
current->next = first;
current->next->next = second;
second->next = first->next;
//移动到下一对节点
current = current->next->next;
}
ListNode * newHead = dummyHead->next;
delete dummyHead; //删除哑结点
return newHead;
}
};
代码没问题,但是letcode超出时间限制。
时间复杂度:O(N),N为链表的长度,遍历了一次
空间复杂度:O(1)
第二版:
思路:(递归)
要有递归终止条件。
为从整体到局部,先从形式上解决大问题,较小问题给出形式解。
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* newHead = head->next;
head->next = swapPairs(newHead->next);
newHead->next = head;
return newHead;
}
};
第三版
思路:(迭代)力扣官方题解
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* temp = dummyHead;
while (temp->next != nullptr && temp->next->next != nullptr) {
ListNode* node1 = temp->next;
ListNode* node2 = temp->next->next;
temp->next = node2;
node1->next = node2->next;
node2->next = node1;
temp = node1;
}
ListNode* ans = dummyHead->next;
delete dummyHead;
return ans;
}
};
25. K个一组翻转链表(困难)
给你链表的头结点head,每k个节点一组进行翻转,请你返回修改后的链表。
k是一个正整数,它的值小于或等于链表的长度,如果节点总数不是k的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
class Solution {
public:
// 翻转一个子链表,并且返回新的头与尾
pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {
ListNode* prev = tail->next;
ListNode* p = head;
while (prev != tail) {
ListNode* nex = p->next;
p->next = prev;
prev = p;
p = nex;
}
return {tail, head};
}
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* hair = new ListNode(0);
hair->next = head;
ListNode* pre = hair;
while (head) {
ListNode* tail = pre;
// 查看剩余部分长度是否大于等于 k
for (int i = 0; i < k; ++i) {
tail = tail->next;
if (!tail) {
return hair->next;
}
}
ListNode* nex = tail->next;
// 这里是 C++17 的写法,也可以写成
// pair<ListNode*, ListNode*> result = myReverse(head, tail);
// head = result.first;
// tail = result.second;
tie(head, tail) = myReverse(head, tail);
// 把子链表重新接回原链表
pre->next = head;
tail->next = nex;
pre = tail;
head = tail->next;
}
return hair->next;
}
};
138.随机链表的复制(中等)
浅拷贝和深拷贝的区别
浅拷贝:会复制一个指向拷贝对象的指针,两个指针指向同一个对象,在物理上只有一个对象,任意指针修改都会导致对象的修改。
深拷贝:会复制一个拷贝对象及其子对象,建立一个完整的副本,两个对象彼此操作独立。对一个对象的操作不会影响另一个对象。
给你一个长度为 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 作为传入参数。
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
第一版:思路
用一个数组来存储random的值,先建立next的连接,第二次遍历再建立randm的连接。
class Solution {
public:
Node* copyRandomList(Node* head) {
Node* oldHead = head;
Node* yummyNode = new Node(0); //创建哑结点
Node* newHead = yummyNode;
vector<Node* > tempRandom; //存储新节点指针
while(oldHead->next != nullptr){
//head遍历
newHead->next = new Node(oldHead->val);//创建新节点
tempRandom.push_back(newHead->next);//按顺序将新创建节点的地址保存
//后移
newHead = newHead->next;
oldHead = oldHead->next;
}
//第二次遍历head,建立random连接
oldHead = head;
newHead = yummyNode->next;
while(oldHead != nullptr){
newHead->random = tempRandom(oldHead->random); //这一步有待商榷
newHead = newHead->next;
oldHead = oldHead->next;
}
newHead = yummyNode->next;
delete yummyNode;
return newHead;
}
};
错误分析:想利用数组来建立random连接的前后关系,但是类型无法转换。如果用哈希表,每个哈希项存储一个节点的两个指针,就可以避免类型转换的麻烦。
第二版:
递归
思路方法一:递归+ 哈希表
思路及算法
本题要求我们对一个特殊的链表进行深拷贝。如果是普通链表,我们可以直接按照遍历的顺序创建链表节点。而本题中因为随机指针的存在,当我们拷贝节点时,「当前节点的随机指针指向的节点」可能还没创建,因此我们需要变换思路。一个可行方案是,我们利用回溯的方式,让每个节点的拷贝操作相互独立。对于当前节点,我们首先要进行拷贝,然后我们进行「当前节点的后继节点」和「当前节点的随机指针指向的节点」拷贝,拷贝完成后将创建的新节点的指针返回,即可完成当前节点的两指针的赋值。
具体地,我们用哈希表记录每一个节点对应新节点的创建情况。遍历该链表的过程中,我们检查「当前节点的后继节点」和「当前节点的随机指针指向的节点」的创建情况。如果这两个节点中的任何一个节点的新节点没有被创建,我们都立刻递归地进行创建。当我们拷贝完成,回溯到当前层时,我们即可完成当前节点的指针赋值。注意一个节点可能被多个其他节点指向,因此我们可能递归地多次尝试拷贝某个节点,为了防止重复拷贝,我们需要首先检查当前节点是否被拷贝过,如果已经拷贝过,我们可以直接从哈希表中取出拷贝后的节点的指针并返回即可。
class Solution {
public:
unordered_map<Node*, Node*> cachedNode;
Node* copyRandomList(Node* head) {
if (head == nullptr) {
return nullptr;
}
if (!cachedNode.count(head)) {
Node* headNew = new Node(head->val);
cachedNode[head] = headNew;
headNew->next = copyRandomList(head->next);
headNew->random = copyRandomList(head->random);
}
return cachedNode[head];
}
};
148.排序链表(中等)(二刷)(归并排序没怎么看懂)
思路:遍历两次,用vector存储值,排序vector,将排序后的结果赋值
class Solution {
public:
ListNode* sortList(ListNode* head) {
/*遍历一次链表,用vector记录下所有的值,然后再遍历第二次,修改值*/
if(head==nullptr) return head;
ListNode* temp_ptr = head;
vector<int> ans;
while(temp_ptr!=nullptr){
ans.push_back(temp_ptr->val);
temp_ptr = temp_ptr->next;
}
sort(ans.begin(),ans.end());
temp_ptr = head;
int i = 0;
while(temp_ptr!=nullptr){
temp_ptr->val = ans[i];
i++;
temp_ptr = temp_ptr->next;
}
return head;
}
};
时间复杂度:O(2N)
空间复杂度:O(N);
给你链表的头结点head,请将其按升序排列,并返回排序后的链表。
总结排序算法
时间复杂度:
- 冒泡排序:O(N^2)
不断比较两元素的大小进行交换。并且设置标志位,如果列表已经是有序的,则某次迭代不会产生交换,应该提前结束排序。最差情况下,时间复杂度为N^2,(n(n-1))/2
void BubbleSort(Sqlist *L){
int i,j;
Status flag = true;
for(i=1; i<L->length && flag; i++){
flag = false;
for (j=L->length-1; j>=i; j--){
if(L->r[j] > L->r[j+1]){
swap(L,j,j+1);
flag = true;
}
}
}
}
-
插入排序:O(N^2)
思想:理牌 -
希尔排序:O(n^(3/2))
思想:对基本有序序列分别进行,分块插入排序和总体插入排序。 -
选择排序:O(N^2)
思想:通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换。依次选出最小/最大的 -
堆排序:O(n log n)
思想:利用完成二叉树,创建大堆树,或者小堆树进行排序。 -
归并排序:O(Nlog N )
堆排序的平替。利用完全二叉树来排序,又不创建管理这样的数据结构。
思想:假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到【n/2】(向下取整)个长度为2或1的有序子序列;再两两归并,···,如此重复,直到得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
分为递归实现和非递归实现(迭代)。迭代效率更高。 -
快读排序:平均情况O(N logN),最坏情况O(N^2)
思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。 -
桶排序:O(n+k),其中k为桶的数量
-
基数排序:O(nk),其中k是最大数字的位数
-
计数排序:O(n+k),其中k是输入的范围
第一版思路:
冒泡排序
思想:不断比较交换两个元素,直到全部遍历一遍
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(head == nullptr) return nullptr;
ListNode* yummyHead = new ListNode(0); //创建哑结点
yummyHead->next = head;
ListNode* first = head;
ListNode* second = head->next;
while(second != nullptr && first != second){
//比较值,判断是否交换
if(first->val > second->val){
//交换
int temp = first->val;
first->val = second->val;
second->val = temp;
}
second = second->next;
if(second == nullptr) {
//进入下一次迭代
first = first->next;
second = first->next;
}
}
ListNode* ans = yummyHead->next;
delete yummyHead;
return ans;
}
};
时间复杂度:O(NN),超出时间限制。
如果列表已经是有序的,没有提前终止的处理。也就是,无论列表如何,时间复杂度都是NN。进一步优化,可以引入标志位,如果某次遍历没有进行交换说明列表已经是有序的了。可以提前终止排序。
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(head == nullptr || head->next == nullptr) return head; // 如果链表为空或只有一个节点,直接返回
ListNode* yummyHead = new ListNode(0); //创建哑结点
yummyHead->next = head;
bool swapped; // 用于检查是否发生了交换
ListNode* first;
ListNode* second;
do {
swapped = false; // 在每次遍历开始时,设置swapped为false
first = yummyHead;
second = yummyHead->next;
while(second != nullptr && second->next != nullptr) {
if(second->val > second->next->val) {
// 交换
int temp = second->val;
second->val = second->next->val;
second->next->val = temp;
swapped = true; // 发生了交换,设置swapped为true
}
first = second;
second = second->next;
}
} while(swapped); // 如果在某次遍历中没有发生交换,提前终止
ListNode* ans = yummyHead->next;
delete yummyHead;
return ans;
}
};
时间复杂度还是N*N,超过时间限制
归并排序
自顶向下的归并排序
对链表自顶向下归并排序的过程如下。
1、找到链表的中点,以中点为分界,将链表拆分成两个子链表。寻找链表的中点可以使用快慢指针的做法,快指针每次移动 2 步,慢指针每次移动 1 步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。
2、对两个子链表分别排序。
3、将两个排序后的子链表合并,得到完整的排序后的链表。可以使用「21. 合并两个有序链表」的做法,将两个有序的子链表进行合并。
上述过程可以通过递归实现。递归的终止条件是链表的节点个数小于或等于 111,即当链表为空或者链表只包含 111 个节点时,不需要对链表进行拆分和排序。
class Solution {
public:
ListNode* sortList(ListNode* head) {
//需要头尾两个指针
return sortList(head, nullptr);
}
ListNode* sortList(ListNode* head, ListNode* tail) {
/*递归的终止条件*/
if (head == nullptr) {
return head;
}
if (head->next == tail) { //如果满足头节点的下一个节点是尾节点,则说明已经达到最小的两组
head->next = nullptr; //将连接断开
return head;
}
/*快慢指针找链表中点*/
ListNode* slow = head, *fast = head;
while (fast != tail) {
slow = slow->next;
fast = fast->next;
if (fast != tail) {
fast = fast->next;
}
}
ListNode* mid = slow;
//返回融合后的前后两段有序链表
return merge(sortList(head, mid), sortList(mid, tail)); //递归
}
ListNode* merge(ListNode* head1, ListNode* head2) {
//融合两段有序链表
ListNode* dummyHead = new ListNode(0); //创建哑结点
ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
while (temp1 != nullptr && temp2 != nullptr) {
//依大小将两段链表插入到哑结点之后
if (temp1->val <= temp2->val) {
temp->next = temp1;
temp1 = temp1->next;
} else {
temp->next = temp2;
temp2 = temp2->next;
}
temp = temp->next;
}
//将剩余部分链接到temp末尾
if (temp1 != nullptr) {
temp->next = temp1;
} else if (temp2 != nullptr) {
temp->next = temp2;
}
return dummyHead->next;
}
};
时间复杂度:O(NlogN),172ms,62.31%
空间复杂度:O(1),70.86MB,18.29%
23.合并K个升序链表(困难)
给你一个链表数组,每个链表都是升序排列的,请你将所有链表合并到一个升序链表中
思路:合并两个链表简单,那么多次调用合并两个链表的函数,将前一次的结果作为后一次的其中一个输入即可。
class Solution {
public:
ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
if ((!a) || (!b)) return a ? a : b;
ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
while (aPtr && bPtr) {
if (aPtr->val < bPtr->val) {
tail->next = aPtr; aPtr = aPtr->next;
} else {
tail->next = bPtr; bPtr = bPtr->next;
}
tail = tail->next;
}
tail->next = (aPtr ? aPtr : bPtr);
return head.next;
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode *ans = nullptr;
for (size_t i = 0; i < lists.size(); ++i) {
ans = mergeTwoLists(ans, lists[i]);
}
return ans;
}
};
146.LRU缓存(中等)(有综合性,重写)
请你设计并实现一个满足LRU(最近最少使用)缓存约束的数据结构实现LRUCache类:
- LURCache(int capacity) 以正整数作为容量capacity初始化LRU缓存
- int get(int key) 如果关键字key存在于缓存中,则返回关键字的值,否则返回-1。
- void put(int key, int value)如果关键字key已经存在,则变更其数据值value;如果不存在,则向缓存中插入该组key-value。如果插入操作导致关键字数量超过capacity,则应该逐出最久未使用的关键字。
函数get和put必须以0(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
第一版:
思路
- LRUCache(int capacity) 函数:
创建capacity个节点,依次链接起来 - int get(int key)
-
- 遍历链表,如果key存在于缓存中,返回key值,并将该节点链接到链表尾部
-
- 否则返回-1
- void put (int key,int value)
-
- 遍历链表,如果key存在,改变其数据值。并将节点链接到链表尾部
-
- 如果不存在,修改第一个节点的数值,并链接到链表尾部
因为要频繁的插入和删除操作,使用双链表get和put函数的时间复杂度才为O(1)
class myListNode{
public:
int key;
int val;
myListNode* next;
myListNode(int x): key(x),val(x),next(nullptr) {};
};
class LRUCache {
public:
int size = 0;
int capacity;
myListNode* yummyHead;//哑结点指针
void moveNodetoTail(myListNode* current){
//移动当前节点到尾部
myListNode* first = yummyHead;
myListNode* second = yummyHead->next;
while(second != current){
first = first->next;
second = second->next;
}
myListNode* tail = second;
while(tail->next != nullptr){
tail = tail->next;
}
//移动
first->next = second->next;
second->next = nullptr;
tail->next = second;
return ;
}
LRUCache(int capacity) {
this->capacity = capacity;
//创建capacity个节点的链表
this->yummyHead = new myListNode(0); //哑结点
myListNode* current = yummyHead;
for(int i=0; i<capacity; i++){
current->next = new myListNode(0);
current = current->next;
}
}
int get(int key) {
//遍历链表,如果key在缓存中,返回关键字的值,并将节点链表到末尾
myListNode* current = yummyHead->next;
for(int i=0; i< this->size;i++){
if(current->key == key){
//将节点链接到末尾,并返回关键字的值
moveNodetoTail(current);
return current->val;
}
current = current->next;
}
//否则返回-1
return -1;
}
void put(int key, int value) {
//遍历链表,如果key存在,变更其数据值为value,并将结点移动到末尾
myListNode* current = yummyHead->next;
for(int i=0;i<this->size;i++){
if(current->key == key){
current->key = key;
current->val = value;
moveNodetoTail(current);
return ;
}
current = current->next;
}
//如果不存在,向缓存中插入该组,插入到链表尾部
if(this->size < this->capacity){
current = yummyHead->next;
for(int i =0; i<this->size-1;i++){
current = current->next;
}
current->key = key;
current->val = value;
this->size++;
return;
}
//如果超过容量,修改第一个节点值,并移动到链表末尾
else if(this->size == this->capacity){
current = yummyHead->next;
current->key = key;
current->val = value;
moveNodetoTail(current);
return;
}
}
};
该代码在某些特殊输入的情况下有bug,暂时没能改对
官方思路:
使用哈希表+双链表(key做成双链表)
哈希表的查找时间复杂度为O(1),即不用遍历链表。
class DoubleLinkNode {
public:
int key, val;
DoubleLinkNode* prev;
DoubleLinkNode* next;
DoubleLinkNode(int k, int v): key(k), val(v), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DoubleLinkNode*> cache;//键值对
//用head和tail两个指针简化双链表的插入和删除,head和tail为两个哑结点
DoubleLinkNode* head;
DoubleLinkNode* tail;
//size代表当前缓存的大小,capacity代表缓存的最大容量
int size;
int capacity;
void removeNode(DoubleLinkNode* node) {
//该函数用于删除node节点
node->prev->next = node->next;
node->next->prev = node->prev;
}
void addToHead(DoubleLinkNode* node) {
//该函数用于在链表头部添加node节点
node->next = head->next;
node->prev = head;
head->next->prev = node;
head->next = node;
}
public:
LRUCache(int capacity): size(0), capacity(capacity) {
//该函数用于初始化缓存
//head和tail为哑结点
head = new DoubleLinkNode(-1, -1);
tail = new DoubleLinkNode(-1, -1);
head->next = tail;
tail->prev = head;
}
int get(int key) {
//该函数用于获取链表中是否有key关键字
if (cache.find(key) == cache.end()) return -1;
DoubleLinkNode* node = cache[key];
removeNode(node);
addToHead(node);
return node->val;
}
void put(int key, int value) {
//该函数用于向缓存中插入键值对
if (cache.find(key) != cache.end()) {
DoubleLinkNode* node = cache[key];
node->val = value;
removeNode(node);
addToHead(node);
} else {
DoubleLinkNode* newNode = new DoubleLinkNode(key, value);
cache[key] = newNode;
addToHead(newNode);
size++;
if (size > capacity) {
DoubleLinkNode* lastNode = tail->prev;
removeNode(lastNode);
cache.erase(lastNode->key);
delete lastNode; //内存释放
size--;
}
}
}
};
时间复杂度:O(1),468ms,27.05%
空间复杂度:O(capacity),157.73MB,42.49%
二叉树
二叉树的算法经常用到递归
94.二叉树的中序遍历(简单)(二刷)
给定一个二叉树的根节点root,返回它的中序遍历。
输入:root = [1,null,2,3]
输出:[1,3,2]
第一版思路:
中序遍历顺序:左根右,递归调用(递归函数,终止条件)。
class Solution {
public:
void inorder (TreeNode* root,vector<int> &ans){
//该函数用于中序遍历root树
//递归终止条件
if(root == nullptr) return;
//递归遍历
inorder(root->left,ans);
ans.push_back(root->val);
inorder(root->right,ans);
return;
};
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
if(root == nullptr) return ans;
inorder(root,ans); //调用中序遍历函数
return ans;
};
};
时间复杂度:O(n),0ms,100.00%
空间复杂度:O(1),8.3MB,25.36%
104.二叉树的最大深度(简单)(二刷)
给定一个二叉树root,返回其最大深度。
二叉树的最大深度是指从根节点到最远叶子节点的最长路径上的节点数。
输入:root = [3,9,20,null,null,15,7]
输出:3
第一版思路:
深度优先搜索:
如果我们知道了左子树和右子树的最大深度l和r,那么该二叉树的最大深度即为
max(l,r)+1
而左子树和右子树的最大深度又可以以同样的方式进行计算。因此,我们可以用【深度优先搜索】的方法来计算二叉树的最大深度。具体而言,在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在O(1)时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。
class Solution {
public:
int maxDepth(TreeNode* root) {
//递归终止条件
if(root == nullptr) return 0;
//递归函数
return max(maxDepth(root->left),maxDepth(root->right))+1;
}
};
时间复杂度:O(n),4ms,93.77%。其中 n为二叉树节点的个数。每个节点在递归中只被遍历一次。
空间复杂度:O(height),18.43MB,6.43%。其中 height表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
第二版思路:
广度优先搜索:
我们也可以用「广度优先搜索」的方法来解决这道题目,但我们需要对其进行一些修改,此时我们广度优先搜索的队列里存放的是「当前层的所有节点」。每次拓展下一层的时候,不同于广度优先搜索的每次只从队列里拿出一个节点,我们需要将队列里的所有节点都拿出来进行拓展,这样能保证每次拓展完的时候队列里存放的是当前层的所有节点,即我们是一层一层地进行拓展,最后我们用一个变量 ans来维护拓展的次数,该二叉树的最大深度即为 ans。
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr) return 0;
queue<TreeNode*> Q; //队列存储每一层的节点
Q.push(root);
int ans = 0;
while (!Q.empty()) {
int sz = Q.size(); //上一层节点的计数
while (sz > 0) {
TreeNode* node = Q.front();//指向队列的头结点
Q.pop();//头结点出队列
if (node->left) Q.push(node->left);
if (node->right) Q.push(node->right);
sz -= 1; //上一层剩余节点的计数
}
ans += 1; //遍历完一层,ans+1
}
return ans;
}
};
226.翻转二叉树(简单)(二刷)
给你一棵二叉树的根节点root,翻转这棵二叉树,并返回其根节点。
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
思路:
递归调用,交换左右两子树。
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
//递归终止条件
if(root == nullptr) return nullptr;
//递归函数
TreeNode* temp = root->left;
root->left = root->right;
root->right = temp;
invertTree(root->left);
invertTree(root->right);
//
return root;
}
};
时间复杂度:O(N),0ms,100.00%
空间复杂度:O(N),9.56MB,19.26%。递归实现需要栈
101.对称二叉树(简单)(二刷,看思路)
给你一个二叉树的根节点root,检查它是否轴对称。
输入:root = [1,2,2,3,4,4,3]
输出:true
思路:递归
- 如果一个树的左子树和右子树镜像对称,那么这个树是对称的。
- 因此,问题转化为:两个树在什么情况下互为镜像。如果同时满足下面的条件,两个树互为镜像:
-
- 它们的两个根节点具有相同的值
-
- 每个树的右子树都与另一个树的左子树镜像对称
实现这样的递归函数,通过【同步移动】两个指针的方法来遍历这棵树,p指针和q指针一开始都指向这棵树的根,随后p右移时,q左移,p左移时,q右移。每次检查当前p和q节点的值是否相等,如果相等再判断左右子树是否对称
class Solution {
public:
bool check(TreeNode* p, TreeNode* q){
//递归终止条件
if(!p && !q) return true;
if(!p || !q) return false;
//递归函数
return p->val == q->val && check(p->left,q->right) && check(p->right,q->left);
}
bool isSymmetric(TreeNode* root) {
if (root == nullptr) return true;
return check(root,root);
}
};
时间复杂度:O(N),12ms,8.49%
空间复杂度:O(N),15.85MB,38.59%