两天速通力扣HOT100[DAY1] (1~54)
本题解旨在以最简单的语言总结hot100各题思路,为每一题提供一个思考入口,但想要手撕出来,需要自己认真推理细节。
目录
哈希 1~3
双指针 4~7
滑动窗口 8~9
子串 10~12
普通数组 13~17
矩阵 18~21
链表 22~35
二叉树 36~50
图论 51~54
1、两数之和
思路
遍历数组,同时hashtable
记录已存在的数,查找target - nums[i]
。
code
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> un_map;
for(int i = 0; i < nums.size(); ++i) {
if(un_map.find(target - nums[i]) != un_map.end()) {
return {un_map[target-nums[i]], i};
} else {
un_map[nums[i]] = i;
}
}
return {};
}
};
2、字母异位词分组
思路
将出现的每个单词排序,作为键存入hashtable
,每个键对应的值维护为一个vecter<string>
,遍历即可。
code
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string> > hashtable;
for(int i = 0; i < strs.size(); ++i) {
string word_s = strs[i];
sort(word_s.begin(), word_s.end());
hashtable[word_s].emplace_back(strs[i]);
}
vector<vector<string> > res;
for(auto it = hashtable.begin(); it != hashtable.end(); ++it) {
vector<string> tmp = it->second;
res.push_back(tmp);
}
return res;
}
};
3、最长连续序列
思路
使用set<int>
对数组去重,同时将查找复杂度降为log(n)
。遍历数组,以当前元素为连续序列的最小值,开始向上依次查找。
关键:if(s.find(num - 1) != s.end()) continue;
查找 num - 1
,保证当前元素为连续序列的最小值,排除重复查找的情况。
code
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
set<int> s;
for(int i = 0; i < nums.size(); ++i) {
s.insert(nums[i]);
}
int res = 0;
for(int i = 0; i < nums.size(); ++i) {
int num = nums[i];
if(s.find(num - 1) != s.end()) continue;
int curL = 0;
while(s.find(num) != s.end()) {
++curL;
++num;
}
res = max(res, curL);
}
return res;
}
};
4、移动零
思路
双指针,一个指向下一个非零元素应该在的位置,一个指向下一个非零元素,交换即可。
不需要管这两个指针是否重叠。
code
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int j = 0; // 下一个 非零元素应该在的位置
for(int i = 0; i < nums.size(); ++i) {
// 非零元素,交换到j位置, j++;
// i和j可以指向同一个元素,原地交换,相当于不做处理
if(nums[i] != 0) {
swap(nums[i], nums[j++]);
}
}
}
};
5、盛最多水的容器
思路
双指针指向两头,维护当前的存储水量和全局的最大存储水量。每次将height
较小的指针向内移动。
为什么可行?
移动较大值的指针,结果一定变小;移动较小值的指针, 结果可能变大。一次排除一半的可能性。
code
class Solution {
public:
int maxArea(vector<int>& height) {
int i = 0, j = height.size() - 1;
int res = 0;
while(i < j) {
int area = min(height[j], height[i]) * (j - i);
res = max(area, res);
if(height[j] < height[i]) j--;
else i++;
}
return res;
}
};
6、 三数之和
思路
将数组排序,三个指针k, i, j
分别指向 0,k + 1, size - 1
,对每个k
,将i, j
不断向内移动,三数之和大于0
移动j
,小于0
移动i
。
去重:
k
指向的元素大于0
,退出nums[k] == nums[k - 1]
无需再算nums[i] == nums[i - 1]
跳过nums[j] == nums[j + 1]
跳过
code
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); ++i) {
if(i > 0 && nums[i] == nums[i - 1]) continue;
if(nums[i] > 0) break;
int j = i + 1, k = nums.size() - 1;
while(j < k) {
int s = nums[i] + nums[j] + nums[k];
if(s < 0){
++j;
while(j < k && nums[j] == nums[j - 1]) ++j;
}
else if(s > 0){
--k;
while(j < k && nums[k] == nums[k + 1]) --k;
}
else {
res.push_back({nums[i], nums[j], nums[k]});
++j;
while(j < k && nums[j] == nums[j - 1]) ++j;
--k;
while(j < k && nums[k] == nums[k + 1]) --k;
}
}
}
return res;
}
};
7、接雨水
思路
对每个格子i
,能接的水为左右两边最大值的较小值,减去i
处格子的高度。
动态规划:维护两个数组,分别记录i
的左右两边的最大值
双指针:优化空间。
左右指针从两端向中间移动,想要正确计算储水量需要知道本端的最高点高度,以及确保对方有一个不低于本端最高点的柱子。左右两端轮流占领本端最高点,当一方占据着全局最高点时,另一方前进,直到遇到一个更高点,换另一方前进。
对于前进的一端来说,由于对方占据着更高点,己方最高点<对方更高点,所以储水量为己方最高点-当前柱子高度
,可以保证计算正确。
code
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size(), res = 0;
int left = 0, right = n - 1;
int left_max = height[left], right_max = height[right];
while(left < right) {
int level = min(left_max, right_max);
if(height[left] <= level) {
res += level - height[left];
++left;
continue;
}
if(height[right] <= level) {
res += level - height[right];
--right;
continue;
}
left_max = max(left_max, height[left]);
right_max = max(right_max, height[right]);
}
return res;
}
};
8、无重复字符的最长子串
思路
维护一个滑动窗口。当前窗口内满足条件时,更新长度,窗口右扩。窗口不满足条件时,窗口左缩。
判断是否满足条件:unordered_set<char>
code
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> unset;
int res = 0, left = 0;
for(int i = 0; i < s.size(); ++i) {
while(unset.find(s[i]) != unset.end()) { // 不满足条件
unset.erase(s[left]); // 左边界收缩
left++;
}
res = max(res, i - left + 1); // 满足条件,更新结果
unset.insert(s[i]); // 右边界扩张
}
return res;
}
};
9、找到字符串中所有字母异位词
思路
维护一个定长滑动窗口,每次移动判断是否异位词,更新结果。
判断的方法:int count[26]
code
class Solution {
private:
int count[26] = {0}, count_p[26] = {0};
public:
bool judge() {
for(int i = 0; i < 26; ++i) {
if(count[i] != count_p[i]) return false;
}
return true;
}
vector<int> findAnagrams(string s, string p) {
int len_s = s.size(), len_p = p.size();
int left = 0, right = len_p - 1;
if(len_s < len_p) return {};
for(int i = 0; i < len_p; ++i) { // 模式串的字符数量
count_p[p[i] - 'a']++;
count[s[i] - 'a']++;
}
vector<int> res;
while(right < len_s) {
if(judge()) {
res.push_back(left);
}
if(right == len_s - 1) break;
count[s[left++] - 'a'] --; // 左边界退出
count[s[++right] - 'a'] ++; // 右边界加入
}
return res;
}
};
10、和为k的子数组
思路
前缀和+哈希表
对当前数nums[i]
,判断哈希表中有几个curSum - k
,累加即可。
code
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n = nums.size();
int res = 0;
unordered_map<int, int> m;
m[0] = 1;
int sum = 0;
for(int i = 0; i < n; i++){
sum += nums[i];
if(m.find(sum - k) != m.end()) {
res += m[sum - k];
}
m[sum]++;
}
return res;
}
};
11、滑动窗口最大值
思路
单调队列,保存当前窗口从前到后单调递减的值。
新进元素,比队尾大,那么队尾的元素这辈子都不可能是最大值,可以直接删除。
队头是当前窗口的最大值,为了记录每个元素退出窗口的时间,单调队列中保存的是数组下标,当i - q.front() >= k
时,说明队头对应的值该退出历史舞台了。
code
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
deque<int> q;
vector<int> res;
for(int i = 0; i < k; ++i) {
while(!q.empty() && nums[i] >= nums[q.back()]) {
q.pop_back();
}
q.push_back(i);
}
res.push_back(nums[q.front()]);
for(int i = k; i < n; ++i) {
while(!q.empty() && nums[i] >= nums[q.back()]) {
q.pop_back();
}
q.push_back(i);
while(!q.empty() && i - q.front() >= k) {
q.pop_front();
}
res.push_back(nums[q.front()]);
}
return res;
}
};
12、最小覆盖子串
思路
维护一个滑动窗口,满足条件时,记录结果,左缩。不满足条件时,右扩。
判断是否条件:unordered_map<char, int>
code
class Solution {
public:
unordered_map <char, int> ori, cnt;
bool check() {
for (const auto &p: ori) {
if (cnt[p.first] < p.second) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
for (const auto &c: t) {
++ori[c];
}
int l = 0, r = -1;
int len = INT_MAX, ansL = -1, ansR = -1;
while (r < int(s.size())) {
if (ori.find(s[++r]) != ori.end()) {
++cnt[s[r]];
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
}
if (ori.find(s[l]) != ori.end()) {
--cnt[s[l]];
}
++l;
}
}
return ansL == -1 ? string() : s.substr(ansL, len);
}
};
13、最大子数组和
思路
动态规划 f ( i ) = m a x { f ( i − 1 ) + n u m s [ i ] , n u m s [ i ] } f(i) = max\{f(i - 1) + nums[i], nums[i]\} f(i)=max{f(i−1)+nums[i],nums[i]}
code
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if(nums.size() == 0) return 0;
int sum = nums[0], res = sum;
for(int i = 1; i < nums.size(); ++i) {
sum = max(sum + nums[i], nums[i]);
res = max(res, sum);
}
return res;
}
};
14、合并区间
思路
按区间起点从小到大排序,维护当前区间的起终点,起点在终点前的,更新终点,起点在终点后的,开新区间。
code
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> res;
sort(intervals.begin(), intervals.end(), [](vector<int> a, vector<int>b) {
return a[0] < b[0];
});
int l = intervals[0][0], r = intervals[0][1];
for(int i = 1; i < intervals.size(); ++i) {
if(intervals[i][0] <= r) {
r = max(r, intervals[i][1]);
}
else {
res.push_back({l, r});
l = intervals[i][0];
r = intervals[i][1];
}
}
res.push_back({l, r});
return res;
}
};
15、轮转数组
思路
整体反转,前k个反转, 后面的反转
code
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());
}
};
16、除自身以外数组的乘积
思路
一个前缀乘积数组一个后缀乘积数组,对每个数分别取两个数组对应的数相乘即可
code
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> pre(n, 1), end(n, 1);
int k = 1;
for(int i = 1; i < nums.size(); ++i) {
k *= nums[i - 1];
pre[i] = k;
}
k = 1;
for(int i = nums.size() - 2; i >= 0; --i) {
k *= nums[i + 1];
end[i] = k;
}
vector<int> res;
for(int i = 0; i < n; ++i) {
res.push_back(pre[i] * end[i]);
}
return res;
}
};
17、缺失的第一个正数
思路
长度为n
的不缺正数的数组,只有[1, 2, 3, ..., n]
,即:数字i
位于下标i - 1
处。
遍历数组,将数字nums[i]
不断与 nums[nums[i] - 1]
交换,直到nums[i] == nums[nums[i] - 1]
再遍历数组找到不符合nums[i] == i + 1
的数即可。
code
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 + 1 && nums[i] != nums[nums[i] - 1]) {
swap(nums[i], nums[nums[i] - 1]);
}
}
for(int i = 0; i < n; ++i) {
if(nums[i] != i + 1) return i + 1;
}
return n + 1;
}
};
18、矩阵置零
思路
vector<int>
记录 i
行或 i
列是否存在 0
,遍历数组并对矩阵赋值。
code
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
unordered_set<int> line, col;
for(int i = 0, m = matrix.size(); i < m; ++i) {
for(int j = 0, n = matrix[0].size(); j < n; ++j) {
if(matrix[i][j] == 0) {
line.insert(i);
col.insert(j);
}
}
}
for(auto it = line.begin(); it != line.end(); ++it) {
for(int j = 0, n = matrix[0].size(); j < n; ++j) {
matrix[*it][j] = 0;
}
}
for(auto it = col.begin(); it != col.end(); ++it) {
for(int i = 0, m = matrix.size(); i < m; ++i) {
matrix[i][*it] = 0;
}
}
}
};
19、螺旋矩阵
思路
维护上下左右四个边界,按照左->右,上->下,右->左,下->上的顺序输出并更新边界。
code
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector <int> ans;
if(matrix.empty()) return ans; //若数组为空,直接返回答案
int u = 0; //赋值上下左右边界
int d = matrix.size() - 1;
int l = 0;
int r = matrix[0].size() - 1;
while(true)
{
for(int i = l; i <= r; ++i) ans.push_back(matrix[u][i]); //向右移动直到最右
if(++ u > d) break; //重新设定上边界,若上边界大于下边界,则遍历遍历完成,下同
for(int i = u; i <= d; ++i) ans.push_back(matrix[i][r]); //向下
if(-- r < l) break; //重新设定有边界
for(int i = r; i >= l; --i) ans.push_back(matrix[d][i]); //向左
if(-- d < u) break; //重新设定下边界
for(int i = d; i >= u; --i) ans.push_back(matrix[i][l]); //向上
if(++ l > r) break; //重新设定左边界
}
return ans;
}
};
20、旋转图像
思路
转置,左右交换
code
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for(int i = 0; i < n; ++i) {
for(int j = 0; j < i; ++j) {
swap(matrix[i][j], matrix[j][i]);
}
}
for(int i = 0; i < n; ++i) {
for(int j = 0; j < n / 2; ++j) {
swap(matrix[i][j], matrix[i][n - 1 - j]);
}
}
}
};
21、搜索二维矩阵II
思路
从左下角开始,向右上角移动搜索,一步排除一行或一列
code
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int x = m - 1, y = 0;
while(x >= 0 && y < n) {
if(matrix[x][y] == target) return true;
else if(matrix[x][y] < target) ++y;
else --x;
}
return false;
}
};
22、相交链表
思路
-
哈希保存节点
-
双指针分别从两条链表开始,走到头换另一条链表重走,两个相遇时即为第一个交叉节点。
code
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(!headA || !headB) return nullptr;
ListNode *pA = headA, *pB = headB;
while(pA != pB) {
if(!pA) pA = headB;
else pA = pA->next;
if(!pB) pB = headA;
else pB = pB->next;
}
return pA;
}
};
23、反转链表
思路
新建一个头节点,将原链表的节点依次以头插法插入新链表即可。
code
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *vhead = new ListNode();
ListNode *p = head;
while(p) {
ListNode *node = p->next;
p->next = vhead->next;
vhead->next = p;
p = node;
}
return vhead->next;
}
};
24、回文链表
思路
翻转一半链表,然后当作两条链表依次判断是否相等。
code
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (head == nullptr) {
return true;
}
// 找到前半部分链表的尾节点并反转后半部分链表
ListNode* firstHalfEnd = endOfFirstHalf(head);
ListNode* secondHalfStart = reverseList(firstHalfEnd->next);
// 判断是否回文
ListNode* p1 = head;
ListNode* p2 = secondHalfStart;
bool result = true;
while (result && p2 != nullptr) {
if (p1->val != p2->val) {
result = false;
}
p1 = p1->next;
p2 = p2->next;
}
// 还原链表并返回结果
firstHalfEnd->next = reverseList(secondHalfStart);
return result;
}
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
ListNode* endOfFirstHalf(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
};
25、环形链表
思路
快慢指针,相遇即是有环,快指针为null说明无环。
code
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (head == nullptr) {
return true;
}
// 找到前半部分链表的尾节点并反转后半部分链表
ListNode* firstHalfEnd = endOfFirstHalf(head);
ListNode* secondHalfStart = reverseList(firstHalfEnd->next);
// 判断是否回文
ListNode* p1 = head;
ListNode* p2 = secondHalfStart;
bool result = true;
while (result && p2 != nullptr) {
if (p1->val != p2->val) {
result = false;
}
p1 = p1->next;
p2 = p2->next;
}
// 还原链表并返回结果
firstHalfEnd->next = reverseList(secondHalfStart);
return result;
}
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
ListNode* endOfFirstHalf(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
};
26、环形链表II
思路
快指针一次两步,慢指针一次一步。相遇后,快指针指回头节点,一次一步,再相遇时即为入环的第一个节点。
code
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(!head || !head->next || !head->next->next) return nullptr;
ListNode *fast = head->next->next;
ListNode *slow = head->next;
while(fast != slow) {
if(!fast->next || !fast->next->next) return nullptr;
fast = fast->next->next;
slow = slow->next;
}
fast = head;
while(fast != slow) {
fast = fast->next;
slow = slow->next;
}
return fast;
}
};
27、合并两个有序链表
思路
两个指针交替进行
code
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode *vhead = new ListNode();
ListNode *p1 = list1, *p2 = list2, *p = vhead;
while(p1 && p2) {
if(p1->val < p2->val) {
p->next = p1;
p1 = p1->next;
} else {
p->next = p2;
p2 = p2->next;
}
p = p->next;
}
p->next = p1 == nullptr ? p2 : p1;
return vhead->next;
}
};
28、两数相加
思路
模拟
code
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *res = new ListNode(-1, nullptr);
ListNode *p = l1, *q = l2, *nxt = res;
int c = 0, v = 0;
while(p && q) {
v = (p->val + q->val + c) % 10;
c = (p->val + q->val + c) / 10;
ListNode *node = new ListNode(v, nullptr);
nxt->next = node;
nxt = node;
p = p->next;
q = q->next;
}
while(p || q) {
v = ((p ? p->val : q->val) + c) % 10;
c = ((p ? p->val : q->val) + c) / 10;
ListNode *node = new ListNode(v, nullptr);
nxt->next = node;
nxt = node;
p ? p = p->next : q = q->next;
}
if(c) {
ListNode *node = new ListNode(1, nullptr);
nxt->next = node;
nxt = node;
}
return res->next;
}
};
// 精简版本
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode();
ListNode* curr = dummy;
int carry = 0;
while(l1 || l2 || carry) {
int a = l1 ? l1->val : 0;
int b = l2 ? l2->val : 0;
int sum = a + b + carry;
carry = sum >= 10 ? 1 : 0;
curr->next = new ListNode(sum % 10);
curr = curr->next;
if(l1) l1 = l1->next;
if(l2) l2 = l2->next;
}
return dummy->next;
}
};
29、删除链表的倒数第N个节点
思路
快指针先走N步,随后一起走,当快指针指向空时,删除慢指针节点
code
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *p1 = head, *p2 = head;
for(int i = 0; i < n; ++i) {
p1 = p1->next;
}
if(!p1) return head->next;
while(p1->next) {
p1 = p1->next;
p2 = p2->next;
}
ListNode *delnode = p2->next;
p2->next = delnode->next;
delete delnode;
return head;
}
};
30、两两交换链表中的节点
思路
模拟
code
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; // 待交换节点1
ListNode* node2 = temp->next->next; // 待交换节点2
temp->next = node2; // 交换
node1->next = node2->next;
node2->next = node1;
temp = node1; // 更新temp
}
ListNode* ans = dummyHead->next;
delete dummyHead;
return ans;
}
};
31、K个一组翻转链表
思路
模拟、递归
code
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* p = head;
int cnt = 0;
while (p){
cnt += 1;
p = p->next;
}
if (cnt < k) {
return head;
}
ListNode *pre = head, *nex = head->next;
for (int ii = 0; ii < k-1; ii++) {
ListNode* temp = nex->next;
nex->next = pre;
pre = nex;
nex = temp;
}
head->next = reverseKGroup(nex, k);
return pre;
}
};
32、随机链表的赋值
思路
哈希表保存原节点和新节点的映射,遍历哈希表构建next和random关系
优化空间占用:拼接 + 拆分,妙不可言 138. 随机链表的复制 - Krahets题解)
code
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == nullptr) return nullptr;
Node* cur = head;
unordered_map<Node*, Node*> map;
// 1. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
while(cur != nullptr) {
map[cur] = new Node(cur->val);
cur = cur->next;
}
cur = head;
// 2. 构建新链表的 next 和 random 指向
while(cur != nullptr) {
map[cur]->next = map[cur->next];
map[cur]->random = map[cur->random];
cur = cur->next;
}
// 3. 返回新链表的头节点
return map[head];
}
};
33、排序链表
思路
归并排序,从底至顶
code
34、合并K个升序链表
思路
- 两两合并,类似数组求和,不过把求和计算换成合并两个有序链表的计算
- 归并合并,分治,[0, k]个链表合并 --> [0, k/2] 合并 [k/2, k] 合并再两个一起合并
- 使用优先队列合并,将每个链表的头存入优先队列,取出最小的加入结果链表,将新的节点加入优先队列。妙不可言。
code
class Solution {
public:
struct Status {
int val;
ListNode *ptr;
bool operator < (const Status &rhs) const {
return val > rhs.val;
}
};
priority_queue <Status> q;
ListNode* mergeKLists(vector<ListNode*>& lists) {
for (auto node: lists) {
if (node) q.push({node->val, node});
}
ListNode head, *tail = &head;
while (!q.empty()) {
auto f = q.top(); q.pop();
tail->next = f.ptr;
tail = tail->next;
if (f.ptr->next) q.push({f.ptr->next->val, f.ptr->next});
}
return head.next;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/merge-k-sorted-lists/solutions/219756/he-bing-kge-pai-xu-lian-biao-by-leetcode-solutio-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
35、LRU缓存
思路
双向链表+哈希表模拟
get: 如果在哈希表中,取出节点移动到头部;如果不在表中,return -1;
put: 如果在哈希表中,修改其值,并移动到头部;如果不在表中,new一个节点,存入哈希表和链表头部,如果容量超限,从尾部删除多余节点。
code
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int size;
int capacity;
public:
LRUCache(int _capacity): capacity(_capacity), size(0) {
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (!cache.count(key)) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
DLinkedNode* node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
if (!cache.count(key)) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode* node = new DLinkedNode(key, value);
// 添加进哈希表
cache[key] = node;
// 添加至双向链表的头部
addToHead(node);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode* removed = removeTail();
// 删除哈希表中对应的项
cache.erase(removed->key);
// 防止内存泄漏
delete removed;
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
DLinkedNode* node = cache[key];
node->value = value;
moveToHead(node);
}
}
void addToHead(DLinkedNode* node) {
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(DLinkedNode* node) {
removeNode(node);
addToHead(node);
}
DLinkedNode* removeTail() {
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
};
36、二叉树的中序遍历
思路
左根右,递归
code
class Solution {
public:
vector<int> res;
void inorder(TreeNode* root) {
if(!root) return ;
inorder(root->left);
res.push_back(root->val);
inorder(root->right);
return ;
}
vector<int> inorderTraversal(TreeNode* root) {
if(!root) return {};
inorder(root);
return res;
}
};
37、二叉树最大深度
思路
二叉树最大深度 = max(左子树最大深度, 右子树最大深度) + 1;
code
class Solution {
public:
int maxDepth(TreeNode* root) {
if(!root) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};
38、翻转二叉树
思路
翻转二叉树 =
翻转左子树,翻转右子树,交换左右子节点
code
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(!root) return nullptr;
invertTree(root->left);
invertTree(root->right);
swap(root->left, root->right);
return root;
}
};
39、对称二叉树
思路
对称二叉树 == 左右子树镜像
两颗树镜像 =
根节点值相等 && 左树的右子树和右树的左子树镜像 && 左树的左子树和右树的右子树镜像
code
class Solution {
public:
bool judge(TreeNode* tree1, TreeNode* tree2) {
if(!tree1 && !tree2) return true;
if(tree1 && tree2)
return tree1->val == tree2->val &&
judge(tree1->right, tree2->left) &&
judge(tree1->left, tree2->right);
return false;
}
bool isSymmetric(TreeNode* root) {
if(!root) return true;
return judge(root->left, root->right);
}
};
40、二叉树的直径
思路
二叉树的直径 = max(左子树的深度 + 右子树的深度 + 1,左子树的直径,右子树的直径)
code
class Solution {
int ans;
int depth(TreeNode* rt){
if (rt == NULL) {
return 0; // 访问到空节点了,返回0
}
int L = depth(rt->left); // 左儿子为根的子树的深度
int R = depth(rt->right); // 右儿子为根的子树的深度
ans = max(ans, L + R + 1); // 计算d_node即L+R+1 并更新ans
return max(L, R) + 1; // 返回该节点为根的子树的深度
}
public:
int diameterOfBinaryTree(TreeNode* root) {
ans = 1;
depth(root);
return ans - 1;
}
};
41、二叉树的层序遍历
思路
BFS
code
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
queue<TreeNode*> queue;
if (root != nullptr) queue.push(root);
while (!queue.empty()) {
int n = queue.size();
vector<int> level;
for (int i = 0; i < n; ++i) {
TreeNode* node = queue.front();
queue.pop();
level.push_back(node->val);
if (node->left != nullptr) queue.push(node->left);
if (node->right != nullptr) queue.push(node->right);
}
res.push_back(level);
}
return res;
}
};
42、将有序数组转换为二叉搜索树
思路
根据数组构建二叉树 =
-
取中位数作为根节点
-
根据左半部分构建左子树
-
根据右半部分构建右子树
code
class Solution {
public:
TreeNode* build(vector<int>& nums, int left, int right) {
if(right < left) return nullptr;
if(right == left)
return new TreeNode(nums[left], nullptr, nullptr);
int mid = (right + left) >> 1; // 中位数
TreeNode *root = new TreeNode(nums[mid], // 中位数构建根节点
build(nums, left, mid - 1), // 左子树
build(nums, mid + 1, right)); // 右子树
return root;
}
TreeNode* sortedArrayToBST(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
return build(nums, left, right);
}
};
43、验证二叉搜索树
思路
根节点值在[lower_bound, upper_bound]之间
左子树是二叉搜索树 && 左子树的值在[lower_bound, 根节点val]之间 &&
右子树是二叉搜索树 && 右子树的值在[根节点val, upper_bound]之间
code
class Solution {
public:
bool helper(TreeNode* root, long long lower, long long upper) {
if (root == nullptr) {
return true;
}
if (root -> val <= lower || root -> val >= upper) {
return false;
}
return helper(root -> left, lower, root -> val) && helper(root -> right, root -> val, upper);
}
bool isValidBST(TreeNode* root) {
return helper(root, LONG_MIN, LONG_MAX);
}
};
44、二叉搜索树中第k小的元素
思路
中序遍历,到k个的时候退出
code
class Solution {
public:
vector<int> inorder;
void getInorder(TreeNode* root) {
if(!root) return ;
getInorder(root->left);
inorder.push_back(root->val);
getInorder(root->right);
}
int kthSmallest(TreeNode* root, int k) {
getInorder(root);
return inorder[k - 1];
}
};
45、二叉树的右视图
思路
层序遍历,每层取最后一个
code
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
vector<int> res;
if(!root) return res;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()) {
int count = q.size();
TreeNode* node;
while(count--) {
node = q.front(); q.pop();
if(node->left) q.push(node->left);
if(node->right) q.push(node->right);
}
res.push_back(node->val);
}
return res;
}
};
46、二叉树展开为链表
思路
- 将左子树插入到右子树的位置
- 将原来的右子树接到左子树的最右边节点
- 考虑新的右子树的根节点
code
class Solution {
public:
void flatten(TreeNode* root) {
while(root) {
if(!root->left) {
root = root->right;
} else {
TreeNode *pre = root->left;
while(pre->right) {
pre = pre->right;
}
pre->right = root->right;
root->right = root->left;
root->left = nullptr;
root = root->right;
}
}
}
};
47、从前序和中序遍历序列构造二叉树
思路
前序遍历第一个为根节点,该节点将中序遍历分为左右两个中序遍历,根据左右中序遍历的数量将前序遍历再分为左右两部分,变成两个从前序和中序遍历序列构造二叉树的小规模问题,递归。
code
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
unordered_map<int, int> index;
public:
TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
if (preorder_left > preorder_right) {
return nullptr;
}
// 前序遍历中的第一个节点就是根节点
int preorder_root = preorder_left;
// 在中序遍历中定位根节点
int inorder_root = index[preorder[preorder_root]];
// 先把根节点建立出来
TreeNode* root = new TreeNode(preorder[preorder_root]);
// 得到左子树中的节点数目
int size_left_subtree = inorder_root - inorder_left;
// 递归地构造左子树,并连接到根节点
// 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
// 递归地构造右子树,并连接到根节点
// 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int n = preorder.size();
// 构造哈希映射,帮助我们快速定位根节点
for (int i = 0; i < n; ++i) {
index[inorder[i]] = i;
}
return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
}
};
48、路径总和III
思路
根节点的路径总和 = 以当前根节点为起点向下有多少条路径符合条件 + 左子树的路径总和 + 右子树的路径总和
code
/**
* 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 rootSum(TreeNode* root, long targetSum) {
if (!root) {
return 0;
}
int ret = 0;
if (root->val == targetSum) {
ret++;
}
ret += rootSum(root->left, targetSum - root->val);
ret += rootSum(root->right, targetSum - root->val);
return ret;
}
int pathSum(TreeNode* root, int targetSum) {
if (!root) {
return 0;
}
int ret = rootSum(root, targetSum);
ret += pathSum(root->left, targetSum);
ret += pathSum(root->right, targetSum);
return ret;
}
};
49、二叉树的最近公共祖先
思路
在左子树和右子树中分别找 p
和 q
的最近公共祖先,返回值为left, right
- 如果left 和 right 同时为空,说明左右子树都不包含 p, q,返回null
- 如果left 和 right 同时不为空,说明p q 分列root两侧,返回root
- 如果一个为空一个不为空,说明p q有继承关系,返回不为空的。
code
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(!root || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if(!left) return right;
if(!right) return left;
return root;
}
};
50、二叉树中的最大路径和
思路
参考40. 二叉树的直径
code
class Solution {
private:
int maxSum = INT_MIN;
public:
int maxGain(TreeNode* node) {
if (node == nullptr) {
return 0;
}
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点
int leftGain = max(maxGain(node->left), 0);
int rightGain = max(maxGain(node->right), 0);
// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
int priceNewpath = node->val + leftGain + rightGain;
// 更新答案
maxSum = max(maxSum, priceNewpath);
// 返回节点的最大贡献值
return node->val + max(leftGain, rightGain);
}
int maxPathSum(TreeNode* root) {
maxGain(root);
return maxSum;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/binary-tree-maximum-path-sum/solutions/297005/er-cha-shu-zhong-de-zui-da-lu-jing-he-by-leetcode-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
51、岛屿数量
思路
DFS/BFS
可以直接修改原矩阵,0 表示海,1 表示陆地,2 表示访问过
code
class Solution {
public:
int m, n;
bool inGrid(int i, int j) {
return i >= 0 && i < m && j >= 0 && j < n;
}
void dfs(vector<vector<char>>& grid, int i, int j) {
if(!inGrid(i, j) || grid[i][j] != '1') return ;
grid[i][j] = '2';
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
}
int numIslands(vector<vector<char>>& grid) {
m = grid.size(), n = grid[0].size();
int res = 0;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
if(grid[i][j] == '1') {
res++;
dfs(grid, i, j);
}
}
}
return res;
}
};
52、腐烂的橘子
思路
BFS,每轮将所有挨着腐烂橘子的新鲜橘子烂掉并加入队列
code
class Solution {
public:
int m, n;
bool inGrid(int x, int y) {
return x >= 0 && x < m && y >= 0 && y < n;
}
int orangesRotting(vector<vector<int>>& grid) {
deque<pair<int, int>> rotten;
m = grid.size();
n = grid[0].size();
int total = 0;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
if(grid[i][j] == 2) {
rotten.push_back({i, j});
}
if(grid[i][j] != 0) total++;
}
}
if(total == 0) return 0;
int res = 0, count = 0;
while(!rotten.empty()) {
int cnt = rotten.size();
cout << cnt << endl;
while(cnt--) {
// cout << cnt << endl;
pair<int, int> tmp = rotten.front();
rotten.pop_front();
count++;
int x = tmp.first, y = tmp.second;
if(inGrid(x-1, y) && grid[x-1][y] == 1) {
grid[x-1][y] = 2;
rotten.push_back({x-1, y});
}
if(inGrid(x+1, y) && grid[x+1][y] == 1) {
grid[x+1][y] = 2;
rotten.push_back({x+1, y});
}
if(inGrid(x, y-1) && grid[x][y-1] == 1) {
grid[x][y-1] = 2;
rotten.push_back({x, y-1});
}
if(inGrid(x, y+1) && grid[x][y+1] == 1) {
grid[x][y+1] = 2;
rotten.push_back({x, y+1});
}
}
res++;
}
if(count == total) return res - 1;
return -1;
}
};
53、课程表
思路
哈希表记录每门课的完成会减小哪些课程的入度,当入度减为0
时,加入待完成队列,BFS。
code
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> ins(numCourses);
unordered_map<int, vector<int>> mp;
queue<int> q;
for(auto pre : prerequisites) {
ins[pre[0]] ++;
mp[pre[1]].emplace_back(pre[0]);
}
for(int i = 0; i < numCourses; ++i) {
if(ins[i] == 0) {
q.push(i);
}
}
while(!q.empty()) {
int course = q.front(); q.pop();
for(auto c : mp[course]) {
ins[c]--;
if(ins[c] == 0) {
q.push(c);
//mp[course].erase(remove(mp[course].begin(), mp[course].end(), c));
}
}
}
for(int i = 0; i < numCourses; ++i) {
if(ins[i]) return false;
}
return true;
}
};
54、前缀树
思路
模拟,26叉树,加一个标记位表示该节点是不是某个单词的结尾。
code
class Trie {
public:
Trie() {
subTrie.resize(26, nullptr); // 26个子节点代表26个字母
end = false; // 表示是否有单词在该字母结束
}
void insert(string word) {
Trie *t = this; // 根节点
for(int i = 0; i < word.size(); ++i) {
if(!t->subTrie[word[i] - 'a'])
t->subTrie[word[i] - 'a'] = new Trie(); // 依次在对应节点创建子树
t = t->subTrie[word[i] - 'a'];
}
t->end = true; // 有单词在此结束
}
bool search(string word) {
Trie *t = this; // 根节点
for(int i = 0; i < word.size(); ++i) {
if(!t->subTrie[word[i] - 'a']) return false; // 字母对应节点未创建
t = t->subTrie[word[i] - 'a'];
}
return t && t->end; // 最后一个字母节点未创建或没在此结束
}
bool startsWith(string prefix) { // 和search方法一样,不需要判断end
Trie *t = this;
for(int i = 0; i < prefix.size(); ++i) {
if(!t->subTrie[prefix[i] - 'a']) return false;
t = t->subTrie[prefix[i] - 'a'];
}
return t;
}
private:
vector<Trie *> subTrie;
bool end;
};