STL中常用哈希表容器
STL容器 | 解释 | 力扣题号 |
---|---|---|
unordered_set | 无序集合,其中的元素是唯一的,不允许重复。它基于哈希表实现,支持 O(1) 平均时间复杂度的插入、删除和查找操作。 | 128 |
unordered_map | 无序映射,包含一对键值对,其中的键是唯一的,不允许重复。它基于哈希表实现,支持 O(1) 平均时间复杂度的插入、删除和查找操作。 | 1、49 |
unordered_multiset | 无序多重集合,其中的元素允许重复。它基于哈希表实现,支持 O(1) 平均时间复杂度的插入、删除和查找操作。 | |
unordered_multimap | 无序多重映射,包含一对键值对,其中的键允许重复。它基于哈希表实现,支持 O(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]
分析
因为要返回数组的下标,所以采用unordered_map,用键值对来存储值和下标
由假设同一个元素不会在数组中重复出现,可以通过遍历一次数值nums,current指向当前值,通过std::unordered_map.find()函数查找target-current在不在哈希表中。
#include
using namespace std ;
#include
#include
class Solution
{
public:
vector<int> twoSum(vector<int>& nums, int target)
{
unordered_map <int,int> hashmap;
for (int i=0;i<nums.size();i++)
{
auto it = hashmap.find(target - nums[i]);
if (it != hashmap.end())
{
return {it->second,i};
}
hashmap[nums[i]] = i; //推入哈希表中
}
return {};
};
};
更新:在学习程序优化之后尝试对上述代码进行一定的改进。
#include
using namespace std ;
#include
#include
class Solution
{
public:
vector<int> twoSum(vector<int>& nums, int target)
{
unordered_map <int,int> hashmap;
//for (int i=0;i
/* 每次函数循环都调用了.size()函数,造成了不必要的函数调用开销*/
int n = nums.size();
for (int i=0;i<n;i++)
{
auto it = hashmap.find(target - nums[i]);
if (it != hashmap.end())
{
return {it->second,i};
}
hashmap[nums[i]] = i; //推入哈希表中
}
return {};
};
};
代码性能:
增加了0.2MB的内存消耗,换得了更快的运行速度。
分析:
class Solution
{
public:
vector<vector<string>> groupAnagrams(vector<string>& strs)
{
unordered_map<string,vector<string>> mp;
for (string str : strs) //制作哈希表
{
string T = str;
sort(T.begin(),T.end());
mp[T].emplace_back(str);
}
vector<vector<string>> ans;
for(auto it=mp.begin();it!=mp.end();++it) //哈希表的遍历
{
ans.emplace_back(it->second);
}
return ans;
}
};
分析:
方法一
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+1在numSet中,循环
{
currentNum++;
currentLen++;
}
maxLen = max(maxLen,currentLen);
}
}
return maxLen;
}
};
另外还有一种不使用unorded_set()容器的方法。
方法二
分析:
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
int n = nums.size();
if (!n) return 0;
sort(nums.begin(), nums.end());
int maxLen = 1, len_ = 1;
for (int i=1; i<n; ++i) {
if (nums[i] == nums[i-1]+1) maxLen = max(maxLen, ++len_);
else if (nums[i] != nums[i-1]) len_ = 1;
}
return maxLen;
}
};
sort()时间复杂度nlogn
分析
利用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;
int right = 0;
while(nums[left]!=0) //让left指向第一个为零的元素
{
left++;
if(left == n) return;
}
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指针从数组表头遍历,right指针从数组表位遍历。要求(x2-x1)*min(y1,y2)取最大
class Solution
{
public:
int maxArea(vector<int>& height)
{
int n= height.size();
int left = 0;
int right = n-1;
int max = 0;
for (left; left < n-1; ++left)
{
int mid = 0;
for (right=n-1; right>left; --right)
{
mid = (right-left)*min(height[left],height[right]);
if(mid>max) max = mid;
}
}
return max;
}
};
超出时间限制,时间复杂度O(n2),注意到右边比更右小的值没必要计算。
力扣官网给的解答
每次只移动左指针或者右指针,意识到只遍历一次就够了。
时间复杂度O(n)
class Solution
{
public:
int maxArea(vector<int>& height)
{
int l = 0, r = height.size() - 1;
int ans = 0;
while (l < r)
{
int area = min(height[l], height[r]) * (r - l);
ans = max(ans, area);
if (height[l] <= height[r])
{
++l;
}
else
{
--r;
}
}
return ans;
}
};
注意到,每移动一次指针都进行了一次四则运算。但是,当移动一次指针,其值反而减少时,面积必然减少,四则运算是不必须的。可进一步优化。
通过增加一个premaxmin,来存储上一个最大的最小值减少四则运算的效率
class Solution
{
public:
int maxArea(vector<int>& height)
{
int l = 0, r = height.size() - 1;
int ans = 0;
int area = min(height[l], height[r]) * (r - l);
int premaxmin = min(height[r],height[l]);
while (l < r)
{
if(premaxmin<min(height[l],height[r]))
{
area = min(height[l], height[r]) * (r - l);
premaxmin = min(height[l], height[r]);
}
ans = max(ans, area);
if (height[l] <= height[r])
{
++l;
}
else
{
--r;
}
}
return ans;
}
};
提交结果显示,执行用时得到了一定的优化。
分析
暴力遍历的话时间复杂度为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;
// 枚举 a
for (int first = 0; first < n; ++first) {
// 需要和上一次枚举的数不相同
if (first > 0 && nums[first] == nums[first - 1]) {
continue;
}
// c 对应的指针初始指向数组的最右端
int third = n - 1;
int target = -nums[first];
// 枚举 b
for (int second = first + 1; second < n; ++second) {
// 需要和上一次枚举的数不相同
if (second > first + 1 && nums[second] == nums[second - 1]) {
continue;
}
// 需要保证 b 的指针在 c 的指针的左侧
while (second < third && nums[second] + nums[third] > target) {
--third;
}
// 如果指针重合,随着 b 后续的增加
// 就不会有满足 a+b+c=0 并且 b
if (second == third) {
break;
}
if (nums[second] + nums[third] == target) {
ans.push_back({nums[first], nums[second], nums[third]});
}
}
}
return ans;
}
};
给定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();
//构建前缀最大值
vector<int> pre_max(n);
int current_max = 0;
for(int i=0;i<n;++i){
current_max = max(current_max,height[i]);
pre_max[i] = current_max;
}
//构建后缀最大值
vector<int> sub_max(n);
current_max =0;
for(int i=n-1;i>=0;--i){
current_max = max(current_max,height[i]);
sub_max[i] = current_max;
}
//每个通能接的水等于前缀最大值和后缀最大值中较小者-柱子的高度
int sum =0;
for(int i=0;i<n;++i){
sum = sum + (min(pre_max[i],sub_max[i])-height[i]);
}
return sum;
}
};
时间复杂度:O(N),需要遍历三次数组
空间复杂度:O(N),需要两个额外的大小为N的数组来存储前缀最大值和后缀最大值。
思路二:利用双指针,将空间复杂度降为O(1)。用左右两个指针分别指向开头和结尾,注意到,如果右指针的后缀最大值大于左指针的前缀最大值,则两者取较小一定是左指针,并且左指针所指向的木桶所能接的水就是左指针的前缀最大值减去柱子高度。
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
int left=0,right=n-1;
int pre_max = 0,sub_max = 0;
int sum = 0;
while(left<=right){
//当左右指针不相遇时
pre_max = max(pre_max,height[left]);
sub_max = max(sub_max,height[right]);
if(pre_max<=sub_max){
//当前木桶的高度为前缀最大值,左指针右移
sum = sum + (pre_max-height[left]);
left++;
}else{
//当前木桶的高度为后缀最大值
sum = sum + (sub_max-height[right]);
right--;
}
}
return sum;
}
};
时间复杂度:O(N),双指针仍需要遍历一遍数组。
空间复杂度:O(1)。
窗口的维护:满足条件收缩,不满足条件拓张
记录下满足条件下的最优解
滑动窗口题目:3、30、76、159、209、239、567、632、727
分析
这道题主要用到思路是:滑动窗口
什么是滑动窗口?
其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列!
如何移动?
我们只要把队列的左边的元素移出就行了,直到满足题目要求!一直维持这样的队列,找出队列出现最长的长度时候,求出解!
时间复杂度:O(n)
分析
设定一个滑动窗口,用两个指针left和right表示左右边界。遍历字符串s,
1)如果当前字符插入满足无重复子串条件,则插入。
2)如果不满足,即出现重复元,依次在删除重复元左侧的所有元素。
同时记录下滑动窗口出现最大长度的数值输出即可。
class Solution
{
public:
int lengthOfLongestSubstring(string s)
{
if (s.empty()) return 0;
int n = s.size();
if (n==1) return 1;
unordered_set<char> uset;
int left = 0;
int maxLen = 1;
for (int i = 0; i<n; ++i) //i遍历s,先删除待插入字符重复的左边子串,再插入
{
while(uset.find(s[i])!=uset.end()) //当i指向的char在哈希表中重复时,删除哈希表左侧char,直到不重复
{
uset.erase(s[left]);
left++;
}
uset.insert(s[i]); //将当前char插入哈希表中
maxLen = max(maxLen,i-left+1); //i和left在同一个维度下,精髓
}
return maxLen;
}
};
算法遍历字符串s一次,时间复杂度O(n)。但是每次都使用了uset.find()查找函数。时间复杂度应该更高。
分析
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;
}
};
分析
解题思路与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)
相似题目974、560、523
分析
思路一:暴力求解,列出数组所有的子串,如果有子串和等于k的连续子数组则计数加一。时间复杂度O(n2)
思路二:前缀和(前N项和,级数)+哈希表,时间复杂度O(n),空间复杂度O(n)
0
要统计连续子数组和为k的个数,即统计pre[i]-k=pre[j],出现的次数。所以想到使用哈希表的键值对来映射计量关系。
pre[i]class Solution { public: int subarraySum(vector<int>& nums, int k) { unordered_map<int, int> mp; mp[0] = 1; //因为存储的是前缀和,初始化需要弄为【0,1】 int count = 0, pre = 0; for (auto& x:nums) //遍历一次 { pre += x; //前缀和 if (mp.find(pre - k) != mp.end()) //如果查找到pre-k在当前哈希表中,说明有子串和为K { count += mp[pre - k]; //mp的值,表示为前缀和重复的次数 } mp[pre]++; } return count; } };
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:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> ans;
int n = nums.size();
int left =0,right = -1;
int current_max = INT_MIN;
int current_max_index = 0;
//创建初始窗口
for(int i=0;i<k;i++){
if(current_max <= nums[i]){
current_max = nums[i];
current_max_index = i;
}
right++;
}
ans.push_back(current_max);
//移动窗口
while(right<n-1){
//当增加到n-1可能会越界
if(current_max_index != left ){
//比较新增加的数和当前最大的大小
left++;
right++;
if(nums[right] >= current_max){
current_max = nums[right];
current_max_index = right;
}
ans.push_back(current_max);
}
else {
//需要重新遍历新窗口找到当前最大
left++;
right++;
current_max = INT_MIN;
for(int i=left;i<k+left;i++){
if(current_max <= nums[i]){
current_max = nums[i];
current_max_index = i;
}
}
ans.push_back(current_max);
}
}
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)。
给你一个字符串s,一个字符串t。返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串“”。
注意:
示例 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个。
分析
用一个prenums[]数组记录前n项和,当为负数时,累积和肯定更小,所以可以舍弃。记录出现的最大值,输出即可。
看官方解析,理解动态规划和分治的概念
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
第一版:
思路
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
//定义二维数组,存储0元素的脚标
vector<vector<int>> temp;
int k = 0;
//遍历matrix,找出所有0元素的脚标
int i,j;
for(i = 0; i < m;i++){
for (j = 0; j<n; j++){
if(matrix[i][j]==0){
temp.push_back({i,j});
k++;
}
}
}
//遍历temp数组,将所有0元素脚标的行和列设为0
for(i = 0; i < k;i++){
//matrix[temp[i][0]][]行全设为0
for(j = 0;j<n;j++){
matrix[temp[i][0]][j] = 0;
}
//matrix[][temp[i][1]] 全设为0
for (j=0; j<m; j++){
matrix[j][temp[i][1]] = 0;
}
}
return ;
}
};
时间复杂度:O(m*n)
空间复杂度:O(m+n)
时间:24ms ,击败5.48%使用C++用户
内存:13.20MB ,击败5.09%使用C++用户
第二版优化思路:
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%
需要注意的是,根据矩阵在内存中的存储特性。遍历行比遍历列效率更高
给你一个m行n列的矩阵matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
第一版:
思路:
自己第一遍没想出来,参考力扣官方解答
按层模拟
剥洋葱,由外圈向内圈一圈一圈的剥开。变量较多而已。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.size() == 0 || matrix[0].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%
给定一个nxn的二维矩阵matrix表示一个图像。请你将图像顺时针旋转90°。
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
第一版解答:
思路:
力扣官网解答
class Solution {
public:
void rotate(vector>& 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>& 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%
编写一个高效的算法来搜索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
第一版:参考力扣官方解答
初始思路:顺序主子式的右下角元素一定是该矩阵中最大的一个元素。
思路:参考红黑树的概念,将矩阵逆时针旋转45°
代码:
class Solution {
public:
bool searchMatrix(vector>& matrix, int target) {
//从右上角开始搜索,左边小,下面大。如果超过边界则不存在
int m = matrix.size();
int n = matrix[0].size();
int i=0, j=n-1; //右上角元素的脚标
while(i<=m-1 && j>=0){ //没超过矩阵边界则一直搜索
if (matrix[i][j]==target) return true;
else if (matrix[i][j]
时间复杂度:O(N+M) ,96ms,击败67.56%
空间复杂度:O(1) ,14.55MB,击败5.07%
给你两个单链表的头节点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 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;
}
};
给你单链表的头结点head,请你反转链表,并返回反转后的链表。
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
第一版:
思路
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *temp = head;
//遍历一遍给的单链表head,并用vector依次存储数值
vector oldVal;
while(temp!=nullptr){
oldVal.push_back(temp->val);
temp = temp->next;
}
if(oldVal.empty()) return nullptr;
//创建一个新的单链表new,并反转赋值
ListNode * newHead = new ListNode(oldVal.back());
oldVal.pop_back();
ListNode* current = newHead;
while(!oldVal.empty()){
current->next = new ListNode(oldVal.back());
current = current->next;
oldVal.pop_back();
}
//返回新链表
return newHead;
}
};
时间复杂度:O(n)4ms,击败93.3%
空间复杂度:O(n) 8.6MB,击败5.01%
利用了额外的空间,其中指针的使用是重点
作为函数,用new创建了新的结点,但是没有用delete删除,会导致内存泄漏。
第二版:原地反转
操作指针
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
//创建两个指针用来指向前后两个节点
ListNode* prev = nullptr;
ListNode* curr = head;
while(curr != nullptr){
//反转
ListNode* temp = curr->next; //临时保存prev的下一个节点
curr->next = prev; //反转
//两指针后移
prev = curr;
curr = temp;
}
return prev;
}
};
时间复杂度: O(n), 4ms 击败93.31%
空间复杂度:O(1), 8.47MB击败5.01%
给你一个单链表的头结点head,请你判断该链表是否为回文链表。如果是,返回true,否则,返回false。
示例:
输入:head = [1,2,2,1]
输出:true
第一版
思路:
/**
* 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 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)的额外空间来对比左右两指针所指的元素值是否相同。
实现复杂且优化效果不显著。
给你一个链表的头节点head,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。为了表示给定链表中的环,评测系统内部使用整数pos来表示链表尾连接到链表中的位置(索引从0开始)。注意:pos不作为参数进行传递。仅仅是为了表示链表的实际情况。
如果链表中存在环,则返回true。否则,返回false
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
第一版思路:
快慢指针,判断有无环
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head == nullptr) return false; //特判
//快慢指针
ListNode * fast=head;
ListNode * slow=head;
do {
slow = slow->next;
fast = fast->next;
if(fast!=nullptr) fast = fast->next;
}while(fast != slow && fast!=nullptr);
if(fast == nullptr) return false;
else return true;
}
};
时间复杂度:O(2N),8ms,93.21%
空间复杂度: O(1),8.06MB,27.82%
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
第一版思路:
用一个vector来存储链表节点
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(head == nullptr) return nullptr;
vector temp;
//用vector来存储节点
while(head != nullptr){
//判断head所指的节点是否在vector内
int n = temp.size();
for(int i=0; inext;
}
return nullptr;
}
};
时间复杂度: O(N^2),324ms,5.05%
空间复杂度: O(N),8.13MB,21.22%
优化:从优化时间复杂度的角度出发
第二版:
当fast == slow时, 两指针在环中第一次相遇。下面分析此时 fast 与 slow 走过的步数关系:
设链表共有 a+ba+ba+b 个节点,其中 链表头部到链表入口 有 aaa 个节点(不计链表入口节点), 链表环 有 bbb 个节点(这里需要注意,aaa 和 bbb 是未知数,例如图解上链表 a=4a=4a=4 , b=5b=5b=5);设两指针分别走了 fff,sss 步,则有:
fast 走的步数是 slow 步数的 222 倍,即 f=2sf = 2sf=2s;(解析: fast 每轮走 222 步)
fast 比 slow 多走了 nnn 个环的长度,即 f=s+nbf = s + nbf=s+nb;( 解析: 双指针都走过 aaa 步,然后在环内绕圈直到重合,重合时 fast 比 slow 多走 环的长度整数倍 )。
将以上两式相减得到 f=2nbf = 2nbf=2nb,s=nbs = nbs=nb,即 fast 和 slow 指针分别走了 2n2n2n,nnn 个环的周长。
接下来该怎么做呢?
如果让指针从链表头部一直向前走并统计步数k,那么所有 走到链表入口节点时的步数 是:k=a+nbk=a+nbk=a+nb ,即先走 aaa 步到入口节点,之后每绕 111 圈环( bbb 步)都会再次到入口节点。而目前 slow 指针走了 nbnbnb 步。因此,我们只要想办法让 slow 再走 aaa 步停下来,就可以到环的入口。
但是我们不知道 aaa 的值,该怎么办?依然是使用双指针法。考虑构建一个指针,此指针需要有以下性质:此指针和 slow 一起向前走 a 步后,两者在入口节点重合。那么从哪里走到入口节点需要 aaa 步?答案是链表头节点head。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
//快慢指针来检测环
ListNode *slow = head, *fast = head;
bool hasCycle = false;
while(fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if(slow == fast) {
hasCycle = true;
break;
}
}
//没有环,返回nullptr
if(!hasCycle) return nullptr;
//找到环的起始点
slow = head;
while(slow != fast) {
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
时间复杂度O(N),8ms,76.64%
空间复杂度O(1),7.63MB,29.26%
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
第一版思路:(迭代)
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* preHead = new ListNode(-1);
ListNode* prev = preHead;
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
prev->next = l1;
l1 = l1->next;
} else {
prev->next = l2;
l2 = l2->next;
}
prev = prev->next;
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev->next = l1 == nullptr ? l2 : l1;
return preHead->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;
}
}
};
给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字0之外,这两个数都不会以0开头
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
第一版思路:
/**
* 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%
代码逻辑过于繁琐,可以优化,指针操作不到位
第二版思路:
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%
给你一个链表,删除链表的倒数第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 * prev = head;
ListNode * current = head;
int m = 0;
while(current != nullptr){
current = current->next;
m+=1;
}
current = head;
//将prev和current指针后移
for(int i=0;i<(m-n);i++){
current = current->next;
}
for(int i=0;i<(m-n-1);i++){
prev = prev->next;
}
prev->next = current->next;
if(current == prev){
head = head->next;
}
return head;
}
};
时间复杂度:O(2N),8ms,27.97%
空间复杂度:O(1),10.41MB,26.66%
第二版优化:
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;
}
};
时间复杂度: O(n),0ms,100.00%
空间复杂度:O(1),10.46MB,21.73%
总结:对链表进行插入删除等操作,使用哑结点可以简化流程
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部值的情况下完成本题(即,只能进行节点交换)
输入:head = [1,2,3,4]
输出:[2,1,4,3]
第一版思路:
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;
}
};
浅拷贝和深拷贝的区别
浅拷贝:会复制一个指向拷贝对象的指针,两个指针指向同一个对象,在物理上只有一个对象,任意指针修改都会导致对象的修改。
深拷贝:会复制一个拷贝对象及其子对象,建立一个完整的副本,两个对象彼此操作独立。对一个对象的操作不会影响另一个对象。
给你一个长度为 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];
}
};
给你链表的头结点head,请将其按升序排列,并返回排序后的链表。
总结排序算法
时间复杂度:
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%
请你设计并实现一个满足LRU(最近最少使用)缓存约束的数据结构实现LRUCache类:
函数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
第一版:
思路
因为要频繁的插入和删除操作,使用双链表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%
给定一个二叉树的根节点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%
给定一个二叉树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%。其中 nnn 为二叉树节点的个数。每个节点在递归中只被遍历一次。
空间复杂度: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;
}
};
给你一棵二叉树的根节点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%。递归实现需要栈
给你一个二叉树的根节点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%