0 前言
1 数组
1.1 在排序数组中查找元素的第一个和最后一个位置
- 思路
二分查找分多种情况,主要为查找区间的不同(左闭右闭
,左闭右开
等),会影响到循环判断的结束条件、左值右值的更新、mid的取值。
本题可以用两次二分查找解决。其中两次的二分查找的查找区间分别为左闭右开
和左开右闭
,需要注意的是,在左开右闭
的区间查找,mid的取值为(left + right + 1) / 2
,即为向上取整。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int n = nums.size();
if(n == 0) return {-1, -1};
// 找最左边和最右边出现的地方
int ansl = -1, ansr = -1, left = 0, right = n - 1;
// 第一次左闭右开
while(left < right) {
int mid = (left + right) / 2;
if(nums[mid] >= target) {
if(nums[mid] == target) {
ansl = mid;
}
right = mid;
} else {
left = mid + 1;
}
}
if(ansl == -1) {
if(nums[n-1] == target) {
return {n - 1, n - 1};
} else {
return {-1, -1};
}
}
left = 0, right = n - 1;
第二次左开右闭
while(left < right) {
int mid = (left + right + 1) / 2;
if(nums[mid] > target) {
right = mid - 1;
} else {
if(nums[mid] == target) {
ansr = mid;
}
left = mid;
}
}
if(ansr == -1) return {0, 0};
else return {ansl, ansr};
}
};
1.2 移除元素
- 思路
在要求数组进行原地删除元素时(在数组中添加和删除元素的时间复杂度均为O(n)
),可以考虑使用双指针
,用快指针遍历整个数组,用慢指针维护结果数据。(类似题目:移动零)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int fast = 0, slow = 0;
int len = nums.size();
while(fast < len){
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
};
1.3 长度最小的子数组
- 思路
滑动窗口
。这里直接引用 代码随想录 中对滑动窗口的解释:所谓滑动窗口
,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。在使用滑动窗口,有三点需要注意:1、窗口内是什么?2、如何移动窗口的起始位置?3、如何移动窗口的结束位置?
本题中,窗口内是什么:连续的子数组,并且其总和大于等于target ;窗口的起始位置如何移动:如果当前窗口的值大于等于target 了,起始位置就要向前移动了;窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。(类似题目:水果成篮)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int ans = n + 1, sum = 0, count = 0;
for(int i = 0, j = 0; j < n; ++ j) {
sum += nums[j];
++ count;
while(sum >= target) {
ans = min(ans, count);
sum -= nums[i];
++ i;
-- count;
}
}
return ans == n + 1 ? 0 : ans;
}
};
1.4 区间和
题目链接 (题目来自卡码网KamaCoder, 使用的是ACM模式)
- 思路
使用前缀和
可以避免重复的加法计算。博主说:C++ 代码面对大量数据的读取和输出操作,最好用scanf
和printf
,耗时会小很多。(类似题目:开发商购买土地)
#include <iostream>
#include <vector>
int main() {
int n = 0, a, b, p = 0;
scanf("%d", &n);
vector<int> preSum(n);
for(int i = 0; i < n; ++ i) {
int t;
scanf("%d", &t);
p += t;
preSum[i] = p;
}
while(~scanf("%d%d", &a, &b)) {
if(a == 0) {
printf("%d\n", preSum[b]);
} else {
printf("%d\n", preSum[b]- preSum[a - 1]);
}
}
return 0;
}
1.5 找出分区值(2024.7.26每日一题)
- 思路
对于第一眼没有思路的数组题目可以考虑先排序
, 排完可能就有了
class Solution {
public:
int findValueOfPartition(vector<int>& nums) {
sort(nums.begin(), nums.end());
int ans = nums[1] - nums[0];
for(int i = 1; i < nums.size() - 1; ++ i) {
int sub = nums[i + 1] - nums[i];
ans = ans < sub ? ans : sub;
}
return ans;
}
};
2 链表
2.0 定义链表
// 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) {}
};
2.1 移除链表元素
- 思路
主要说明一个思想:对于链表题目都可以考虑添加一个虚拟节点dummyHead
来简化操作。
本题中,在移除某个节点时(头节点除外),都是通过前一个结点来移除当前节点的,而头节点没有前一个节点,因此这里引入一个虚拟节点dummyHead
,使得移除头节点的操作更加方便。
在链表题目中,通常会有很多节点的移动操作,这个过程容易混乱,指针指着指着指哪里去了都不知道,可以手动画一个链表示意图方便理解(这很好用)。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode *dummyHead = new ListNode(0, head);
ListNode *p = dummyHead;
while(p->next != nullptr) {
if(p->next->val == val) {
p->next = p->next->next;
} else {
p = p->next;
}
}
return dummyHead->next;
}
};
2.2 反转链表
- 思路
本题有两种方法:
头插法
,简单来说就是定义一个虚拟头节点dummyHead
,然后在遍历链表的同时,把遍历到的节点插入到dummyHead
后面,其中的难点在于节点断开和节点接入容易产生逻辑混乱,建议画图理解。
双指针法 (很妙的想法,可以学习) - 头插法代码
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* dummyHead = new ListNode(0);
ListNode* p = head;
while(p != nullptr) {
ListNode* tmp = dummyHead->next;
dummyHead->next = p;
p = p->next;
dummyHead->next->next = tmp;
}
return dummyHead->next;
}
};
- 双指针法代码
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *pre = nullptr, *curr = head;
while(curr != nullptr) {
ListNode *tmp = curr->next;
curr->next = pre;
pre = curr;
curr = tmp;
}
return pre;
}
};
2.3 两两交换链表中的节点
- 思路
主要是在断开和接入节点时容易发生混乱,画图是个好方法。
还有一个好方法,来自龙之笔记 ,就是多定义变量,可以简化很多操作,以下代码就是这样的思路。
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode *dummy_head = new ListNode(-1, head);
ListNode *p = dummy_head;
while (p->next != NULL && p->next->next != NULL) {
// a和b为要进行交换的节点,c为后面的节点
ListNode *a = p->next;
ListNode *b = a->next;
ListNode *c = b->next;
// 将a和b从原链表中断开(为了方便理解,可省略)
p->next = NULL;
a->next = NULL;
b->next = NULL;
// 重新拼接
p->next = b;
b->next = a;
a->next = c;
p = a;
}
return dummy_head->next;
}
};
2.4 删除链表的倒数第N个节点
- 思路
使用快慢指针
,即让快指针先跑n步,然后慢指针和快指针一起往前跑,当快指针跑到最后一个节点时,此时慢指针所在节点即为要删除的节点的前一个节点。注意,当要删除某个节点时,要从在前一个节点操作。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// if(head->next == nullptr) return nullptr;
ListNode* dummy = new ListNode(0, head);
ListNode* fast = dummy;
ListNode* slow = dummy;
for(int i = 0; i <= n; i++){
fast = fast->next;
}
while(fast != nullptr) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return dummy->next;
}
};
2.5 链表相交
- 思路
首先判断两个链表是否有交点,如果相交,则两个链表的后N (N >= 1)
个节点均为同一个节点,判断两个链表的尾节点是否为同一个节点即可,同时并记录两个链表的长度numA
和numB
。此时需要让两个链表的长度“相等”,让较长链表往前走|numA - numB|
步,再同时遍历两个链表,当遍历到的节点为同一个时,即为所求。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *pa = headA, *pb= headB;
if(pa == NULL || pb == NULL) return NULL;
int numA = 0, numB = 0;
while(pa->next != NULL) {
pa = pa->next;
++ numA;
}
while(pb->next != NULL) {
pb = pb->next;
++ numB;
}
if(pa != pb) return NULL; // 尾节点不是同一个,不相交
pa = headA, pb= headB;
if(numA > numB) {
for(int i = 0; i < numA - numB; ++ i) {
pa = pa->next;
}
} else {
for(int i = 0; i < numB - numA; ++ i) {
pb = pb->next;
}
}
while(pa != pb) {
pa = pa->next;
pb = pb->next;
}
return pa;
}
};
2.6 环形链表II
- 思路
遍历链表,并用一个set
存储节点,目的是为了判断是否出现重复节点,当节点重复,则说明找到了环,当遍历到NULL
时,则说明无环。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *p = head;
unordered_set<ListNode*> s;
while(p != NULL) {
if(s.find(p) == s.end()) {
s.insert(p);
} else {
break;
}
p = p->next;
}
return p;
}
};
3 哈希表
3.1 有效的字母异位词
- 思路
用一个长度为26的数组记录每个字母出现的字数,出现在s
中则加1,出现在t
中则减1,最后判断数组元素是否全为0即可。(数组本质上也是一种哈希表)
class Solution {
public:
bool isAnagram(string s, string t) {
if(s.size() != t.size()){
return false;
}
vector<int> letter(26, 0);
for(int i = 0; i < s.size(); i++){
letter[s[i] - 'a']++;
letter[t[i] - 'a']--;
}
for(int i = 0 ; i < 26; i++){
if(letter[i] != 0){
return false;
}
}
return true;
}
};
3.2 快乐数
- 思路
可以用一个map
把每次变换的数字进行保存,只需判断下一次变换的数字是否在map
中即可。
这里也可以用快慢指针
的思路:如果要判断某个“链表”是否有环,可以使用快慢指针
进行判断,快指针
和慢指针
一起出发,快指针
一次走两步,慢指针
一次走一步,如果链表有环,那么快慢指针
终将相遇,如果无环,则快指针
会先抵达终点。本题中,可以把所给数字比作链表,用快慢指针
进行遍历,如果快慢指针
的值相等,则说明有环;如果快指针
的值为1,则说明无环,即为快乐数。
class Solution {
public:
bool isHappy(int n) {
int slow = int2happyNumber(n);
int fast = int2happyNumber(slow);
do {
slow = int2happyNumber(slow);
fast = int2happyNumber(int2happyNumber(fast));
if(fast == 1) return true;
} while(slow != fast);
return false;
}
int int2happyNumber(int num) {
int ret = 0;
while(num != 0) {
ret += (num % 10) * (num % 10);
num /= 10;
}
return ret;
}
};
3.3 有人相爱,有人夜里开车看海,有人LeetCode第一题都做不出来
- 思路
定义一个map
用于存储数值和下标,边遍历边找是否有符合条件的值,有则返回,无则把数值和下标写入map
中。
本题使用数据结构类型为unordered_map
, 原因为哈希表中的key
不要求有序,并且查询操作较多,因此选择unordered_map
可以节省时间。map
和unordered_map
具体区别详见代码随想录
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hashmap;
for(int i = 0; i < nums.size(); i++){
if(hashmap.count((target - nums[i]))){
return {i, hashmap[target - nums[i]]};
}
hashmap[nums[i]] = i;
}
return {};
}
};
3.4 四数相加 II
- 思路
第一想到的是直接暴力求解,四层循环,时间复杂度为O(n^4)
,果断超时。优化一层,可以把num1
用map
保存,剩下三个数组使用三层循环遍历,时间复杂度为O(n^3)(实际为 O(n^3) + O(n))
,又超时。那继续优化,把num1
和num2
中的数值两两相加,并用map
保存,剩下两个数组使用二重循环遍历,时间复杂度为O(n^2)(实际为 O(n^2) + O(n^2))
,通过!(本题依旧使用unordered_map
,原因是key
可以无序,并且查询操作多。)
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> hashmap; // 存 nums1 + nums2 的出现的情况,key为数值, value为出现的次数
int ans = 0, n = nums1.size();
for(int i = 0; i < n; ++ i) {
for(int j = 0; j < n; ++ j) {
int add = nums1[i] + nums2[j];
if(hashmap.count(add) == 0) {
hashmap[add] = 1;
} else {
++ hashmap[add];
}
}
}
for(int k = 0; k < n; ++ k) {
for(int l = 0; l < n; ++ l) {
int sub = 0 - nums3[k] - nums4[l];
if(hashmap.count(sub) != 0) {
ans += hashmap[sub];
}
}
}
return ans;
}
};
4 字符串
4.1 反转字符串
- 思路
用双指针
,一个往前遍历,一个往后遍历,遍历的同时交换数值,即可达到空间复杂读为O(1)
的效果。
class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0, right = s.size() - 1;
while(left < right) {
auto tmp = s[left];
s[left] = s[right];
s[right] = tmp;
left ++;
right --;
}
}
};
4.2 反转字符串II
- 思路
大致思路和上题一样,只需要关注left
和right
的位置即可。
class Solution {
public:
string reverseStr(string s, int k) {
for (int i = 0; i < s.size(); i += 2*k) {
int left = i;
int right = i + k - 1;
if (right > s.size() - 1) {
right = s.size() - 1;
}
while (left < right) {
auto tmp = s[left];
s[left] = s[right];
s[right] = tmp;
left ++;
right --;
}
}
return s;
}
};
4.3 替换数字
- 思路
遍历字符串,逐个判断字符。
#include <iostream>
using namespace std;
int main() {
string s, ans;
cin >> s;
for(auto i : s) {
if(i >= '0' && i <= '9') {
ans += "number";
} else {
ans += i;
}
}
cout << ans;
return 0;
}
4.4 反转字符串中的单词
- 思路
可以用vector<string>
把字符串中的单词存起来,最后再拼接成一个题目所要求的字符串。
(进阶)使用O(1)
的空间,那就要在原地反转。原地反转的操作如下:先反转整个字符串,然后再逐个反转单词(这个过程可以举个例子手写模拟一下)。题目中会出现多余的空格,因此在反转操作前,要先移除多余的空格,如果使用erase()
操作进行移除,时间复杂度会过高(erase()
操作本身就是O(n)
),因此这里使用双指针
移除多余空格,用快指针
遍历整个字符串,用慢指针
维护结果数据。
class Solution {
public:
string reverseWords(string s) {
// 双指针法移除空格
int fast = 0, slow = 0;
char pre = ' ';
while(fast < s.size()) {
if(s[fast] != ' ' || pre != ' ') {
s[slow] = s[fast];
pre = s[slow];
++ slow;
}
++ fast;
}
// 移除多余字符
if(s[slow - 1] == ' ') {
s = s.substr(0, slow - 1); // substr() 操作可以用 resize() 替代, 即 s.resize(slow - 1)
} else {
s = s.substr(0, slow);
}
// 先反转整个字符串
reverse(s, 0, s.size() - 1);
// 再逐个反转单词
int left = 0, right = 0;
for(; right < s.size(); ++ right) {
if(s[right] == ' ') {
reverse(s, left, right - 1);
left = right + 1;
}
}
reverse(s, left, s.size() - 1);
return s;
}
void reverse(string& s, int left, int right) {
while(left < right) {
auto tmp = s[left];
s[left] = s[right];
s[right] = tmp;
left ++;
right --;
}
}
};
4.5 右旋字符串
- 思路
可以通过字符串反转的方式实现。先反转前len-k
个,再反转后k
个,最后反转整个字符串。
#include <iostream>
using namespace std;
void reverse(string &s, int left, int right) {
while(left < right) {
auto tmp = s[left];
s[left] = s[right];
s[right] = tmp;
++ left;
-- right;
}
}
int main() {
int k, len;
string s;
cin >> k >> s;
len = s.size();
reverse(s, 0, len - 1 - k);
reverse(s, len - k, len - 1);
reverse(s, 0, len - 1);
cout << s << endl;;
return 0;
}
5 栈与队列
5.1 用栈实现队列
- 思路
维护两个栈in
和out
,一个用于接收数据,一个用于输出数据。输入的数据直接存进in
中,当需要输出数据时,如果out
为空,则把输入栈in
的数据“倒入”out
中,然后输出out
中的top
元素,以达到一个先进后出
的效果。
class MyQueue {
public:
MyQueue() {
}
void push(int x) {
in.push(x);
}
int pop() {
if(out.empty()) {
while(!in.empty()) {
out.push(in.top());
in.pop();
}
}
int ret = out.top();
out.pop();
return ret;
}
int peek() {
if(out.empty()) {
while(!in.empty()) {
out.push(in.top());
in.pop();
}
}
return out.top();
}
bool empty() {
return in.empty() && out.empty();
}
private:
stack<int> in, out;
};
5.2 用队列实现栈
- 思路
其实使用单队列就可以解决。需要维护一个队列和队列长度len
。当进行pop
操作时,需要len
来定位pop
的元素的位置,这里的做法是,把队列中前len - 1
个元素先出队再入队,那么此时队列的第一个元素就是需要pop
的元素。实现top
同理。
class MyStack {
public:
MyStack() {
}
void push(int x) {
que.push(x);
++ len;
}
int pop() {
for(int i = 0; i < len - 1; ++ i) {
que.push(que.front());
que.pop();
}
-- len;
int ret = que.front();
que.pop();
return ret;
}
int top() {
for(int i = 0; i < len - 1; ++ i) {
que.push(que.front());
que.pop();
}
que.push(que.front());
int ret = que.front();
que.pop();
return ret;
}
bool empty() {
return len == 0;
}
private:
queue<int> que;
int len = 0;
};
5.3 有效的括号
- 思路
维护一个栈用于保存没有消掉的左括号,遇到匹配的右括号则消除,不匹配则返回false
。
class Solution {
public:
bool isValid(string s) {
if(s.size() % 2 == 1) return false;
stack<char> stk;
for(auto i : s){
if(i == '(' || i == '{' || i == '[') {
stk.push(i);
} else {
if(stk.size() == 0) {
return false;
} else {
if(abs(stk.top() - i) > 2) return false;
// 字符运算 '(' - ')' = 1, '[' - ']' = 2, '{' - '}' = 2
else stk.pop();
}
}
}
return stk.empty();
}
};
5.4 删除字符串中的所有相邻重复项
- 思路
和上题类似。
class Solution {
public:
string removeDuplicates(string s) {
stack<char> st;
for(auto i : s) {
if (st.empty() || i != st.top()) {
st.push(i);
} else {
st.pop();
}
}
string ans;
while(!st.empty()) {
ans += st.top();
st.pop();
}
reverse(ans.begin(), ans.end());
return ans;
}
};
5.5 逆波兰表达式求值
- 思路
用stack
维护数值,遍历tokens
,遇到数值就入栈,如果遇到运算符,则把栈顶两个元素取出运算,再把运算结果送回栈中。结果返回栈顶元素。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> stk;
for(auto i : tokens) {
if (i == "+" || i == "-" || i == "*" || i == "/") {
int num2 = stk.top();
stk.pop();
int num1 = stk.top();
stk.pop();
int result;
if (i == "+") result = num1 + num2;
else if (i == "-") result = num1 - num2;
else if (i == "*") result = num1 * num2;
else if (i == "/") result = num1 / num2;
stk.push((result));
} else {
stk.push(stoi(i));
}
}
return stk.top();
}
};
5.6 滑动窗口最大值
- 思路
有点难,可以结合 代码随想录者本人的B站视频 边学边写。学习到了一个新数据结构和用法:单调队列
。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> ans;
deque<int> dq(1, nums[0]);
int n = nums.size(), m = -10000001;
k = k < n ? k : n;
for(int i = 1; i < k; ++ i) {
while (!dq.empty() && nums[i] > dq.back()) {
dq.pop_back();
}
dq.push_back(nums[i]);
}
ans.push_back(dq.front());
for(int left = 0, right = k; right < n; ++ left, ++ right) {
if (dq.front() == nums[left]) {
dq.pop_front();
}
while (!dq.empty() && nums[right] > dq.back()) {
dq.pop_back();
}
dq.push_back(nums[right]);
ans.push_back(dq.front());
}
return ans;
}
};
5.7 前K个高频元素
- 思路
代码随想录 把这题归到栈于队列中了,但是我也妹用到栈和队列啊。先说说我的想法,用一个map
存数值和出现的频率,然后对频率进行排序,最后返回前的k
个元素。代码如下。其中,使用sort()
函数排序时,使用的是自己定义的比较函数cmp
,可以通过传递cmp
作为第三个参数来实现。比较函数应该接受两个参数并返回一个布尔值,指示第一个参数是否应该排在第二个参数之前。
class Solution {
public:
static bool cmp(pair<int,int> a, pair<int,int> b) {
return a.second > b.second;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
map<int, int> hashmap;
for (auto i : nums) {
hashmap[i] ++;
}
vector<pair<int, int>> vec;
for (auto it = hashmap.begin(); it != hashmap.end(); ++ it) {
vec.push_back(pair<int, int>(it->first,it->second));
}
sort(vec.begin(), vec.end(), cmp);
vector<int> ans;
for(int i = 0; i < k; ++ i) {
ans.push_back(vec[i].first);
}
return ans;
}
};
- 法2 :优先队列
本质上是通过优先队列实现堆
,堆
对于输出最大(最小)k 个元素有奇效。这里的思路和上面的思路有相似之处,但是在最后找到 k 个元素的地方有很大优化。
class Solution {
public:
// 小顶堆
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
// 要统计元素出现频率
unordered_map<int, int> map; // map<nums[i],对应出现的次数>
for (int i = 0; i < nums.size(); i++) {
map[nums[i]]++;
}
// 对频率排序
// 定义一个小顶堆,大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆,扫面所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
pri_que.push(*it);
if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
pri_que.pop();
}
}
// 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
vector<int> result(k);
for (int i = k - 1; i >= 0; i--) {
result[i] = pri_que.top().first;
pri_que.pop();
}
return result;
}
};
6 二叉树
6.0 定义二叉树
// 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) {}
};
6.1 二叉树的递归遍历
- 写递归都要确定三要素:1、确定递归函数的参数和返回值; 2、确定终止条件; 3、确定单层递归的逻辑。二叉树的递归遍历主要有三种:
前序遍历
,中序遍历
,后序遍历
,遍历的顺序分别为:中左右
,左中右
,左右中
。以下是一段用递归前序遍历
的代码实现。(题目链接)
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> tree;
preOrder(root, tree);
return tree;
}
void preOrder(TreeNode* root, vector<int>& tree) {
if(root == nullptr) return;
tree.push_back(root->val); // 中序遍历就是把这行代码放在下一行,后序遍历是把这行代码放在下两行,
preOrder(root->left, tree);
preOrder(root->right, tree);
}
};
6.2 二叉树的迭代遍历
- 思路
二叉树的迭代遍历本质上是模拟递归栈的过程。前序遍历
是通过维护一个栈,每次从栈中获取节点并访问,同时将该节点的右孩子和左孩子(注意顺序)压入栈中,当栈为空时结束。而后序遍历
有类似的过程,不同的在与将孩子节点压入栈中时,是先左孩子再右孩子,最后还需要将结果反转(reverse()
)。
二叉树的中序迭代遍历就相对麻烦一些,不止要维护一个栈用于模拟递归的过程,还需要维护一个指针当作信号一样,控制入栈和遍历操作。 - 前序遍历
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
if(!root) return {};
vector<int> ans;
stack<TreeNode*> stk;
stk.push(root);
while (!stk.empty()) {
TreeNode* cur = stk.top();
ans.push_back(cur->val);
stk.pop();
if(cur->right != nullptr) stk.push(cur->right);
if(cur->left != nullptr) stk.push(cur->left);
}
return ans;
}
};
- 后序遍历
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
if(!root) return {};
vector<int> ans;
stack<TreeNode*> stk;
stk.push(root);
while (!stk.empty()) {
TreeNode* cur = stk.top();
ans.push_back(cur->val);
stk.pop();
if(cur->left != nullptr) stk.push(cur->left);
if(cur->right != nullptr) stk.push(cur->right);
}
reverse(ans.begin(), ans.end());
return ans;
}
};
- 中序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
if(!root) return {};
vector<int> ans;
stack<TreeNode*> stk;
TreeNode* cur = root;
while(cur != nullptr || !stk.empty()) {
if(cur != nullptr) {
stk.push(cur);
cur = cur->left;
} else {
cur = stk.top();
stk.pop();
ans.push_back(cur->val);
cur = cur->right;
}
}
return ans;
}
};
6.3 二叉树的层序遍历
- 思路
配合队列的先进先出
的特性实现层序遍历。先将根节点入队, 每次从队列中获取节点并访问,同时将该节点的左右节点放进队列中。
插一嘴(), 二叉树的层序遍历是一种广度优先(BFS)
的遍历方式,而上问提到的前中后序遍历,都是深度优先(DFS)
的遍历方式。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
deque<TreeNode*> dq;
if(root != nullptr) dq.push_back(root);
vector<vector<int>> ans;
while(!dq.empty()) {
int len = dq.size();
vector<int> level;
for(int i = 0; i < len; ++ i) {
TreeNode* node = dq.front();
dq.pop_front();
level.push_back(node->val);
if(node->left != nullptr) dq.push_back(node->left);
if(node->right != nullptr) dq.push_back(node->right);
}
ans.push_back(level);
}
return ans;
}
};
6.4 对称二叉树
题目链接
-思路
硬判断,用 if
遍历所有情况。写递归思路要清晰。
class Solution {
public:
bool check(TreeNode* left, TreeNode* right) {
if(left == nullptr && right == nullptr) return true;
else if(left == nullptr && right != nullptr) return false;
else if(left != nullptr && right == nullptr) return false;
else if(left->val != right->val) return false;
else return check(left->left, right->right) && check(left->right, right->left);
}
bool isSymmetric(TreeNode* root) {
return root == nullptr ? true : check(root->left, root->right);
}
};
6.5 平衡二叉树
- 思路
有几个点需要注意,二叉树的高度和深度是不一样的概念,高度是从下往上数,深度是从上往下数。本题使用了一个小技巧,就是把一些特殊情况进行标记。函数getHeigh(TreeNode* node)
本意是获取树的高度,但是这里使用了-1
当作不平衡的标记,当树平衡时会返回树的高度,当不平衡时树的高度就无所谓了,此时返回-1
。
class Solution {
public:
int getHeigh(TreeNode* node) {
if(node == nullptr) return 0;
int leftH = getHeigh(node->left);
int rightH = getHeigh(node->right);
if(leftH != -1 && rightH != -1 && abs(leftH - rightH) <= 1) return leftH > rightH ? leftH +1 : rightH + 1;
else return -1;
}
bool isBalanced(TreeNode* root) {
if(getHeigh(root) != -1) return true;
else return false;
}
};
6.6 二叉树的所有路径
- 思路
从上遍历到下,当到叶子节点时,就遍历完了一条路径。把要返回的字符串数组设置为全局变量(也可以不使用全局变量,把该字符串数组当作参数传入函数也行),以便直接记录路径。
class Solution {
public:
void travel(TreeNode* node, string s) {
s += to_string(node->val);
if(node->left == nullptr && node->right == nullptr) {
ret.push_back(s);
return ;
}
s += "->";
if(node->left) travel(node->left, s);
if(node->right) travel(node->right, s);
}
vector<string> binaryTreePaths(TreeNode* root) {
string s;
travel(root, s);
return ret;
}
private:
vector<string> ret;
};
6.7 从中序与后序遍历序列构造二叉树
- 思路
看看视频挺好的《代码随想录》算法视频公开课。但是问题又来了,为什么我写出来的代码效率比这么差,耗时又长内存占用又大(摊手,What can I say)。
(内存占用大是因为每层遍历都用了新的vector
。优化:使用题目所给的数组初始化新的数组)
class Solution {
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
int len = postorder.size();
if(len == 0) return nullptr;
int rootVal = postorder[len - 1];
TreeNode* root = new TreeNode(rootVal);
vector<int> inLeft, postLeft, inRight, postRight;
int index = 0;
while(index < len) {
if(inorder[index] != rootVal) {
inLeft.push_back(inorder[index]);
postLeft.push_back(postorder[index]);
++ index;
} else {
break;
}
}
while(index + 1< len) {
inRight.push_back(inorder[index + 1]);
postRight.push_back(postorder[index]);
++ index;
}
root->left = buildTree(inLeft, postLeft);
root->right = buildTree(inRight, postRight);
return root;
}
};
6.8 合并二叉树
- 思路
递归就要多写,多写了就会。
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
if(root1 == nullptr && root2 == nullptr) return nullptr;
if(root1 == nullptr) return root2;
else if(root2 == nullptr) return root1;
root1->val += root2->val;
root1->left = mergeTrees(root1->left, root2->left);
root1->right = mergeTrees(root1->right, root2->right);
return root1;
}
};
6.9 验证二叉搜索树
- 思路
有一个妙手,中序遍历
的遍历顺序为左中右
,正好符合二叉搜索树的特性。因此用中序遍历二叉树,对遍历的结果进行判断,如果严格递增,则为二叉搜索树。(By the way,记得复习中序遍历
的迭代写法 -> 代码随想录)
class Solution {
public:
void preOrder(TreeNode* root, vector<int>& vec) {
if(root->left != nullptr) preOrder(root->left, vec);
vec.push_back(root->val);
if(root->right != nullptr) preOrder(root->right, vec);
}
bool isValidBST(TreeNode* root) {
vector<int> vec;
preOrder(root, vec);
int pre = vec[0];
for(int i = 1; i < vec.size(); ++ i) {
if(vec[i] > pre) {
pre = vec[i];
} else {
return false;
}
}
return true;
}
};
6.10 删除二叉搜索树中的节点
- 思路
涉及链式节点的删除,通常要使用到该节点的前一个节点。但是用什么值进行替换呢,两种值有效:一是该节点的左子树中的最大值,也就是该节点左子树中最右边的值,二是该节点右子树中的最小值。
(请欣赏💩山)(太辣眼了,还是看看 代码随想录的代码 吧)
class Solution {
public:
vector<TreeNode*> findNode(vector<TreeNode*> nodes, int key) {
if(nodes[1] == nullptr) return nodes;
if(nodes[1]->val == key) return nodes;
else if(nodes[1]->val > key) return findNode({nodes[1], nodes[1]->left}, key);
else if(nodes[1]->val < key) return findNode({nodes[1], nodes[1]->right}, key);
return nodes;
}
TreeNode* deleteNode(TreeNode* root, int key) {
TreeNode* dummpNode = new TreeNode(0, root, nullptr);
vector<TreeNode*> nodes = findNode({dummpNode, root}, key);
// node[1]为删除的节点,node[0]为要删除节点的前一个节点
if(nodes[1] == nullptr) return root;
else {
if(nodes[1]->left == nullptr && nodes[1]->right == nullptr) {
if(nodes[0]->left == nodes[1]) nodes[0]->left = nullptr;
else nodes[0]->right = nullptr;
} else if(nodes[1]->left != nullptr) {
TreeNode *tmp = nodes[1]->left, *pre = nodes[1];
if(tmp->right == nullptr) {
nodes[1]->val = tmp->val;
pre->left = tmp->left;
}
else {
while(tmp->right != nullptr) {
pre = tmp;
tmp = tmp->right;
}
nodes[1]->val = tmp->val;
pre->right = tmp->left;
}
} else if(nodes[1]->right != nullptr) {
TreeNode *tmp = nodes[1]->right, *pre = nodes[1];
if(tmp->left == nullptr) {
nodes[1]->val = tmp->val;
pre->right = tmp->right;
} else {
while(tmp->left != nullptr) {
pre = tmp;
tmp = tmp->left;
}
nodes[1]->val = tmp->val;
pre->left = tmp->right;
}
}
}
return dummpNode->left;
}
};
6.11 把二叉搜索树转化为搜索树
- 思路
想到二叉搜索树,首先想到中序遍历
,因为中序遍历
的二叉搜索树结果是一个单调的数组。本题使用一次倒过来的中序遍历
(右中左),边遍历边修改节点的值。(中序遍历的题都使用迭代法
写,熟悉一下迭代法
的写法)
class Solution {
public:
TreeNode* convertBST(TreeNode* root) {
int sum = 0;
// 用一个倒过来的中序遍历
stack<TreeNode*> st;
if(root) st.push(root);
while(!st.empty()) {
TreeNode* node = st.top();
st.pop();
if(node != nullptr) {
if(node->left) st.push(node->left);
st.push(node);
st.push(nullptr);
if(node->right) st.push(node->right);
} else {
node = st.top();
st.pop();
sum += node->val;
node->val = sum;
}
}
return root;
}
};
7 回溯(sù)
7.0 回溯模板
来自代码随想录
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
7.1 组合
- 思路
主要参考7.0
中提到的回溯模板,根据模板一点一点往里面填东西即可。其实还有可以优化的地方,就是在回溯中的for
循环中,可以将不必要的循环移除,称为剪枝
。本体中,可以将i <= n
优化成i <= n - k + v.size() + 1
(优化的过程可以好好想一想,n - k + v.size() + 1
这个值是怎么来的)。
class Solution {
public:
void backtracking(vector<vector<int>>& ans, vector<int>& v, int n, int k, int next) {
if(v.size() == k) {
ans.push_back(v);
return;
}
for(int i = next; i <= n; ++i) {
v.push_back(i);
backtracking(ans, v, n, k, i + 1);
v.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> ans;
vector<int> v;
backtracking(ans, v, n, k, 1);
return ans;
}
};
7.2 组合总和III
- 思路
套模板。本题的剪枝
主要在两个方面,一是总和n
, 如果已经累加的数已经超过n
了,则直接return
,二是个数k
,和上题一样,在循环中,把i <= 9
优化成i <= 9 - k + path.size() + 1
。
class Solution {
public:
int sum = 0;
vector<int> path;
vector<vector<int>> result;
void backTracking(int k, int n, int startNum) {
if(sum > n) return;
else if(sum == n) {
if(path.size() == k) result.push_back(path);
return;
} else {
if(path.size() >= k) return;
else {
for(int i = startNum; i <= 9 - (k - path.size()) + 1; ++ i) {
sum += i;
path.push_back(i);
backTracking(k, n, i + 1);
sum -= i;
path.pop_back();
}
}
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backTracking(k, n, 1);
return result;
}
};
7.3 电话号码的字母组合
- 思路
同样套模板,但是又不完全和上两题一样。总之多做
,思考
,总结
。
class Solution {
public:
unordered_map<char, string> dict = {
{'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5', "jkl"},
{'6', "mno"}, {'7', "pqrs"}, {'8', "tuv"}, {'9', "wxyz"}
};
string path;
vector<string> result;
void backTracking(string digits, int startNum) {
if(startNum == digits.size()) {
result.push_back(path);
return;
}
for(auto c : dict[digits[startNum]]) {
path.push_back(c);
backTracking(digits, startNum + 1);
path.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if(digits == "") return {};
backTracking(digits, 0);
return result;
}
};
7.4 组合总和
- 思路
什么时候适合用回溯?个人感觉要符合一下几点要求:1、要遍历所有情况 ; 2、遍历时循环的层数是根据输入的情况定的,不是限定好的(比如只有双层循环、三层循环等)。
写回溯背模板,再根据模板填信息,就能过。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
int sum = 0;
void backTracking(vector<int>& candidates, int target, int startIndex) {
if(sum == target) {
result.push_back(path);
return ;
} else if(sum > target) return;
for(int i = startIndex; i < candidates.size(); ++ i) {
sum += candidates[i];
path.push_back(candidates[i]);
backTracking(candidates, target, i);
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backTracking(candidates, target, 0);
return result;
}
};
7.5 复原IP地址
- 思路
对于要求所给的字符串(或者数组)都用上的题,不需要写最外面一层的循环(即for(int i = startIndex; i < s.size(); ++ i)
)。本题和上面的电话组合就是如此。
class Solution {
public:
vector<string> result;
vector<string> path;
void backTracking(string s, int startIndex) {
if(path.size() == 4) {
if(startIndex != s.size()) return ;
else {
result.push_back(path[0] + "." + path[1] + "." + path[2] + "." + path[3]);
return;
}
}
for(int j = 1; j <= min(3, int(s.size() - startIndex)); ++ j) {
string cur = s.substr(startIndex, j);
if(cur[0] == '0' && cur != "0") continue;
if(stoi(cur) > 255) continue;
int rest = s.size() - startIndex - j;
if(rest < 3 - path.size() || rest > (3 - path.size()) * 3) continue;
path.push_back(cur);
backTracking(s, startIndex + j);
path.pop_back();
}
}
vector<string> restoreIpAddresses(string s) {
if(s.size() < 4 || s.size() > 12) return result;
backTracking(s, 0);
return result;
}
};