写在前面
- 主要是分类一下刷题遇到的一些题型。
- 有很多思路的图都来源于力扣的题解,如侵权会及时删除。
- 不过代码都是个人实现的,所以有一些值得记录的理解。
一、哈希表
1. 数组中重复的数字
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
int record[100000];
for(int i=0;i<100000;i++)
{
record[i] = 0;
}
for(int i=0;i<nums.size();i++)
{
if(record[nums[i]])
{
return nums[i];
}
else
{
record[nums[i]] = 1;
}
}
return -1;
}
};
2. 第一个只出现一次的字符
- 思路:
- 用哈希表来记录每个字母出现的次数;
- 注意:这里要求返回第一个出现次数为1的字母,而不是任意一个,因此还要用一个数组(或者队列)来记录
key 进入哈希表的顺序,即所谓的有序哈希; - 当然不记录也可以,直接再次访问字符串即可,但由于哈希表的大小(仅26)可能远小于字符串顺序,因此会更加耗时(虽说两种方式时间复杂度都是O(N));
- 代码:
class Solution {
public:
char firstUniqChar(string s) {
unordered_map<char, int> hash_map;
vector<char> order_arr;
for(char c: s) {
if(hash_map.count(c) != 0)
{
hash_map[c] += 1;
}
else
{
order_arr.push_back(c);
hash_map[c] = 1;
}
}
for(int i=0;i<order_arr.size();++i) {
if(hash_map[order_arr[i]] == 1) {
return order_arr[i];
}
}
return ' ';
}
};
二、二维矩阵
1. 二维数组中的查找
- 思路:
- 利用其排序的特点,从矩阵的右上角,仿照二叉搜索树的方式查找
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
int i = matrix.size()-1;
int j = 0;
while(i>=0 && j<matrix[0].size())
{
if(matrix[i][j]==target)
{
return true;
}
else
{
if(matrix[i][j] < target)
{
j++;
}
else
{
i--;
}
}
}
return false;
}
};
2. 矩阵中的路径
class Solution {
private:
bool trace(vector<vector<char>>& board, string &word, vector<vector<int>>& mark, int i, int j, int k)
{
if(k>=word.length())
{
return true;
}
if(i<0 || j<0 || i>=board.size() || j>=board[0].size())
{
return false;
}
if(mark[i][j] == 1 || board[i][j] != word[k])
{
return false;
}
else
{
mark[i][j] = 1;
if(trace(board, word, mark, i+1, j, k+1))
{
return true;
}
if(trace(board, word, mark, i, j+1, k+1))
{
return true;
}
if(trace(board, word, mark, i-1, j, k+1))
{
return true;
}
if(trace(board, word, mark, i, j-1, k+1))
{
return true;
}
mark[i][j] = 0;
return false;
}
}
public:
bool exist(vector<vector<char>>& board, string word) {
vector<vector<int>> mark(board.size(), vector<int>(board[0].size(), 0));
for(int i=0;i<board.size();i++)
{
for(int j=0;j<board[0].size();j++)
{
if(trace(board, word, mark, i, j, 0))
{
return true;
}
}
}
return false;
}
};
3. 机器人的运动范围
- 思路:
- 方法一:广度优先遍历,使用队列queue来辅助。
- 方法二:深度优先遍历,使用递归搜索。
- 但无论是广度优先遍历和深度优先遍历,都需要用一个visited矩阵记录走过的地方以便回溯;
- 而且只用往右走和往下走即可,无需回头。
- 方法三:直接遍历数组(相当于从地图的角度看),但要判断当前点是否可达。
- 代码:
class Solution {
private:
bool isReachable(int x, int y, const int &k)
{
int sum = 0;
while(x!=0)
{
sum += (x % 10);
x = x / 10;
}
while(y!=0)
{
sum += (y % 10);
y = y / 10;
}
if(sum<=k)
{
return true;
}
else
{
return false;
}
}
void trace_breadth(const int &m, const int &n, const int &k, int &count, vector<vector<int>> &visited)
{
queue<int> x_queue, y_queue;
x_queue.push(0);
y_queue.push(0);
int x, y;
while(!x_queue.empty())
{
x = x_queue.front();
x_queue.pop();
y = y_queue.front();
y_queue.pop();
if(x<m && y<n && !visited[x][y] && isReachable(x, y, k))
{
++count;
visited[x][y] = 1;
x_queue.push(x+1);
y_queue.push(y);
x_queue.push(x);
y_queue.push(y+1);
}
}
}
void trace_depth(int x, int y, const int &m, const int &n, const int &k, int &count, vector<vector<int>> &visited)
{
if(x<m && y<n && !visited[x][y] && isReachable(x, y, k))
{
++count;
visited[x][y] = 1;
trace_depth(x+1, y, m, n, k, count, visited);
trace_depth(x, y+1, m, n, k, count, visited);
}
return;
}
public:
int movingCount(int m, int n, int k) {
int count = 0;
vector<vector<int>> visited(m, vector<int>(n, 0));
for(int i=0;i<m;++i)
{
for(int j=0;j<n;++j)
{
bool mark = false;
if(i-1>=0 && visited[i-1][j])
{
mark = true;
}
if(j-1>=0 && visited[i][j-1])
{
mark = true;
}
if(i==0 && j==0)
{
mark = true;
}
if(!isReachable(i, j, k))
{
mark = false;
}
if(mark)
{
++count;
visited[i][j] = 1;
}
}
}
return count;
}
};
4. 顺时针打印矩阵
-
思路: -
用一个大循环和四个次循环,同时维护四个边界变量,模拟顺时针走势; -
四个边界变量均为紧确界(闭区间),等号可以取到; -
用所有的元素数量作为退出循环的标志,每个次循环做完之后都要判断一次; -
做完一行/一列之后,该边界值就要加1; -
返回空vector的写法为return vector<int>() (推荐)或者return {}; (C++11); -
代码:
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> re;
int ru = 0, cl = 0;
int rb = matrix.size() - 1;
if(rb >= 0) {
int cr = matrix[0].size() - 1;
int n = matrix.size() * matrix[0].size();
int i, j;
while(true) {
for(i=ru, j=cl;j<=cr;++j) {
re.push_back(matrix[i][j]);
n--;
}
++ru;
if (n <= 0) {
break;
}
for(i=ru, j=cr;i<=rb;++i) {
re.push_back(matrix[i][j]);
n--;
}
--cr;
if (n <= 0) {
break;
}
for(i=rb, j=cr;j>=cl;--j) {
re.push_back(matrix[i][j]);
n--;
}
--rb;
if (n <= 0) {
break;
}
for(i=rb, j=cl;i>=ru;--i) {
re.push_back(matrix[i][j]);
n--;
}
++cl;
if (n <= 0) {
break;
}
}
}
return re;
}
};
三、字符串
1. 替换空格
class Solution {
public:
string replaceSpace(string s) {
string re_s;
re_s.resize(30000);
int i, j=0;
for(i=0;i<s.size();i++)
{
if(s[i] == ' ')
{
re_s[j++]='%';
re_s[j++]='2';
re_s[j++]='0';
}
else
{
re_s[j++]=s[i];
}
}
return re_s;
}
};
2. 字符串的排列
- 思路:
- 主要的难点是字符串中有重复的字符,因此需要从全排列中剔除掉这些重复的排列;
- 思路是使用深度优先搜索+剪枝;
- 每一次深搜前都判断当前字符是否被处理过;
- 通过交换字符可以达到遍历所有组合的效果(个人觉得这里设计得十分巧妙);
- 用
unordered_set 可以判断重复元素,当然也可以用unordered_map ; - 代码:
class Solution {
private:
vector<string> re;
void dfs(string &s, int x) {
if(x == s.length()) {
re.push_back(s);
return;
}
unordered_set<char> set;
for(int i=x;i<s.length();++i) {
if(set.find(s[i]) == set.end()) {
set.insert(s[i]);
swap(s[i], s[x]);
dfs(s, x + 1);
swap(s[i], s[x]);
}
}
}
public:
vector<string> permutation(string s) {
dfs(s, 0);
return re;
}
};
3. 数字序列中某一位的数字
-
思路: -
其实就是找规律,既要找到当前n表示的数字,也要记录它前面的数字和字符数量; -
注意计算的中间结果有可能超过int最大范围,一个简单粗暴的解决方法是将所有中间变量定义为long类型; -
提取数字的余数时用到了一个小技巧,就是将数字转成字符串再输出,这样就不用逆序求余输出了; -
代码:
class Solution {
public:
int findNthDigit(int n) {
if(n < 10) {
return n;
}
long digit_carry = 2;
long digit_real_num = 9;
long digit_char_num = 9;
long digit_tmp = 9;
while(digit_char_num + digit_carry*digit_tmp*10 < n) {
digit_char_num += digit_carry*digit_tmp*10;
digit_real_num += digit_tmp*10;
digit_tmp = digit_tmp*10;
++digit_carry;
}
int quotient = (n - digit_char_num) / digit_carry;
int reminder = (n - digit_char_num) % digit_carry;
int real_num = digit_real_num + quotient;
if(reminder > 0) {
real_num += 1;
}
string s = to_string(real_num);
return s[(reminder+digit_carry-1)%digit_carry] - '0';
}
};
4. 把数组排成最小的数
-
思路: -
就是将数组转成字符串按照字典序进行排序,然后依次把排序后的序列拼接起来; -
所以核心是完成一个自定义规则的排序; -
自己实现的话当然是快排最好(立刻给我写个快排出来!叉腰 ); -
如果是不自己实现而用sort 函数的话也不错,时间上要比自己的快排要更快; -
注意sort 函数是cmp 函数的逻辑是:返回true 则第一个参数排在前面; -
这应该算是一道经典的排序题; -
代码:
class Solution {
private:
void quick_sort(vector<string> &str_arr, int low, int high) {
if(low > high) {
return;
}
string pivot = str_arr[low];
int i = low;
int j = high;
while(i < j) {
while(i<j && pivot + str_arr[j] <= str_arr[j] + pivot) {
--j;
}
if(i < j) {
str_arr[i] = str_arr[j];
++i;
}
while(i<j && str_arr[i] + pivot <= pivot + str_arr[i]) {
++i;
}
if(i < j) {
str_arr[j] = str_arr[i];
--j;
}
}
str_arr[i] = pivot;
quick_sort(str_arr, low, i-1);
quick_sort(str_arr, i+1, high);
}
public:
string minNumber(vector<int>& nums) {
vector<string> str_arr;
for(int num: nums) {
str_arr.push_back(to_string(num));
}
sort(str_arr.begin(), str_arr.end(), [](string &a, string &b) {
if(a+b < b+a) {
return true;
}
else {
return false;
}
});
string re;
for(string str: str_arr) {
re.append(str);
}
return re;
}
};
5. 把字符串转换成整数
- 思路:
- 非常繁琐但又不太繁琐的字符串处理;
- 确实是比其他的算法来得“繁琐”,但在字符串处理里面又不算太繁琐(=0=~常规操作);
class Solution {
public:
int strToInt(string str) {
int i = 0;
while(str[i] == ' ') {
++i;
}
bool is_negative = false;
if(str[i] == '-') {
is_negative = true;
++i;
}
else {
if(str[i] == '+') {
++i;
}
}
if(str[i]<'0' || str[i]>'9') {
return 0;
}
int res = 0;
int max_bound = 214748364;
while(str[i]>='0' && str[i]<='9') {
int x = str[i] - '0';
if(res > max_bound) {
if(is_negative) { return -2147483648; }
else { return 2147483647; }
}
if(res == max_bound) {
if(is_negative && x >= 8) { return -2147483648; }
else { if(!is_negative && x >=7) { return 2147483647; } }
}
res = res * 10 + x;
++i;
}
if(is_negative) {
res = -res;
}
return res;
}
};
6. 翻转单词顺序
-
思路: -
很麻烦的字符串处理,但符合字符串处理的一贯特点(也就是麻烦{{{(>_<)}}}); -
C++的做法利用类似双指针会比较好,别的方法也蛮复杂的,而且可以用的库函数不多; -
两个指针一个记录单词的起点-1 ,一个记录单词的终点 ,然后用substr 函数截取字串; -
难点在于去除连续的空格,也要注意指针移动时字符串数组越界的问题; -
两个数组越界分别是写作了
while(i >= 0 && s[i] == ' ') ;- 和
if(re.length() > 0 && re[re.length() - 1] == ' ') ; - 先判断是否越界再作进一步的判断;
-
代码:
class Solution {
public:
string reverseWords(string s) {
if(s.length() == 0) {
return "";
}
string re;
int i = s.length() - 1;
int j;
while(i >= 0 && s[i] == ' ') {
--i;
}
j = i;
while(i >= 0) {
if(s[i] == ' ') {
re.append(s.substr(i+1, j-i));
re.append(" ");
while(i >= 0 && s[i] == ' ') {
--i;
}
j = i;
}
else {
--i;
}
}
if(i != j)
{
re.append(s.substr(i+1, j-i));
}
if(re.length() > 0 && re[re.length() - 1] == ' ') {
re = re.substr(0, re.length() - 1);
}
return re;
}
};
7. 左旋转字符串
- 思路:
- 不那么复杂的字符串处理;
- 两种处理的思路:
- 一种是通过切片来拼接字符串(这种比较API优雅);
- 一种是通过循环求余来重组字符串(这种比较底层优雅);
- 总之都可以很优雅
(不是) ; - 代码:
- 切片拼接:
class Solution {
public:
string reverseLeftWords(string s, int n) {
string re;
re.append(s.substr(n, s.length() - n));
re.append(s.substr(0, n));
return re;
}
};
class Solution {
public:
string reverseLeftWords(string s, int n) {
string re(s);
for(int i=n;i<n+s.length();++i) {
re[i-n] = s[i%s.length()];
}
return re;
}
};
四、链表
1. 从尾到头打印链表
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
ListNode* cur = head;
vector<int> arr;
if(!cur)
{
return arr;
}
arr.push_back(cur->val);
while(cur->next)
{
cur = cur->next;
arr.push_back(cur->val);
}
vector<int> final_arr(arr);
for(int i=0;i<arr.size();i++)
{
final_arr[i] = arr[arr.size() - 1 - i];
}
return final_arr;
}
};
2. 删除链表的节点
- 思路:
- 就是简单的链表删除节点操作。
- 注意需要额外考虑头节点的删除。
- 代码:
class Solution {
public:
ListNode* deleteNode(ListNode* head, int val) {
ListNode *cur = head;
ListNode *pre = head;
while(cur)
{
if(cur->val==val)
{
if(cur==head)
{
head = cur->next;
}
else
{
pre->next = cur->next;
}
cur = nullptr;
pre = nullptr;
break;
}
else
{
pre = cur;
cur = cur->next;
}
}
return head;
}
};
3. 反转链表
- 思路:
- 直接遍历一次整个链表,然后用一前一中一后三个指针直接修改节点之间的指向
- 注意初始头节点->next最后要置空
- 注意
head==nullptr 的情况 - 代码:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *first, *second, *temp;
first = head;
if(first)
{
second = head->next;
while(second)
{
temp = second->next;
second->next = first;
first = second;
second = temp;
}
head->next = NULL;
}
return first;
}
};
4. 合并两个排序的链表
- 思路:
- 用一个新的头节点,然后逐个比较两个原有列表的头节点,逐个插入到新的链表中;
- 如果不用伪头节点,则要单独处理头节点;
- 下图是采用了伪头节点的,也就是
prehead 指向的节点,最终返回的是prehead->next ; - 别的链表的题目也可以尝试使用伪头节点来避免单独处理头节点;
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(!l1)
{
return l2;
}
if(!l2)
{
return l1;
}
ListNode* head = new ListNode(-1);
ListNode* pre = head;
while(l1 && l2)
{
if(l1->val <= l2->val)
{
pre->next = l1;
l1 = l1->next;
}
else
{
pre->next = l2;
l2 = l2->next;
}
pre = pre->next;
}
if(l1)
{
pre->next = l1;
}
else
{
pre->next = l2;
}
return head->next;
}
};
5. 复杂链表的复制
-
总的思路: -
其实和普通的链表复制相比,这里的难点是要处理random指针指向的节点未新建情况; -
而且新节点random指向的是新节点值,且值要和原节点random指向的节点值相同; -
思路1: -
递归处理random和next; -
如果指针指向的节点还没有新建,则继续递归先新建该复制节点,再返回来用指针指向; -
由于是随机新建复制节点,而不是顺序新建复制节点,所以还要用哈希表记录当前已经新建的复制节点; -
这样的时间复杂度是O(N),空间复杂度也是O(N),因为用了空间为N的哈希表; -
代码1:
class Solution {
private:
unordered_map<Node*, Node*> cachedMap;
public:
Node* copyRandomList(Node* head) {
if(head == nullptr) {
return nullptr;
}
else {
if(cachedMap.count(head) == 0) {
cachedMap[head] = new Node(head->val);
cachedMap[head]->next = copyRandomList(head->next);
cachedMap[head]->random = copyRandomList(head->random);
}
return cachedMap[head];
}
}
};
- 思路2:
- 分三次遍历原链表;
- 第一次遍历只新建复制节点,同时将复制节点放在原节点之后;
- 第二次遍历按照原链表的random指向,修改复制节点的random指向;
- 由于此时所有的复制节点都已经新建了,所以不会有random指向的节点仍没有新建的情况;
- 第三次遍历分离两个链表;
- 这样的时间复杂度是O(N),空间复杂度降为O(1),避免了哈希表的使用;
- 但时间复杂度上应该要高一点,这里是O(3N),上面思路1是O(N),虽然它们的数量级相同;
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == nullptr) {
return nullptr;
}
Node* cur = head;
while(cur != nullptr) {
Node* tmp = new Node(cur->val);
tmp->next = cur->next;
cur->next = tmp;
cur = tmp->next;
}
cur = head;
while(cur != nullptr) {
if(cur->next->random == nullptr) {
if(cur->random == nullptr) {
cur->next->random = nullptr;
}
else {
cur->next->random = cur->random->next;
}
}
cur = cur->next->next;
}
Node* new_head = head->next;
cur = head;
Node* new_cur = new_head;
while(cur != nullptr) {
cur->next = new_cur->next;
cur = cur->next;
if(cur != nullptr) {
new_cur->next = cur->next;
new_cur = new_cur->next;
}
}
return new_head;
}
};
五、二叉树
1. 重建二叉树
class Solution {
private:
unordered_map<int, int> inorder_map;
TreeNode* myBuildTree(int preorder_left, int preorder_right, int inorder_left, int inorder_right, vector<int>& preorder, vector<int>& inorder){
if(preorder_left>preorder_right)
{
return nullptr;
}
TreeNode* root = new TreeNode(preorder[preorder_left]);
int left_length = inorder_map[preorder[preorder_left]] - inorder_left;
root->left = myBuildTree(preorder_left+1, preorder_left+left_length, inorder_left, inorder_left+left_length-1, preorder, inorder);
int right_length = inorder_right - inorder_map[preorder[preorder_left]];
root->right = myBuildTree(preorder_left+1+left_length, preorder_right, inorder_left+left_length+1, inorder_right, preorder, inorder);
return root;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
for(int i=0;i<inorder.size();i++)
{
inorder_map[inorder[i]] = i;
}
return myBuildTree(0, preorder.size()-1, 0, inorder.size()-1, preorder, inorder);
}
};
2. 树的子结构
class Solution {
private:
bool recur(TreeNode* A, TreeNode* B) {
if(B == nullptr) {
return true;
}
else {
if(A == nullptr) {
return false;
}
if(A->val != B->val) {
return false;
}
return recur(A->left, B->left) && recur(A->right, B->right);
}
}
public:
bool isSubStructure(TreeNode* A, TreeNode* B) {
if(B == nullptr) {
return false;
}
else {
if(A == nullptr) {
return false;
}
return recur(A, B) || isSubStructure(A->left, B) || isSubStructure(A->right, B);
}
}
};
3. 二叉树的镜像
-
思路: -
就是用递归的思想; -
先递归处理左右子树,让左右子树镜像; -
然后再交换左右子树; -
代码:
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
if(root == nullptr)
{
return nullptr;
}
TreeNode* temp_left = mirrorTree(root->left);
TreeNode* temp_right = mirrorTree(root->right);
root->left = temp_right;
root->right = temp_left;
return root;
}
};
4. 对称的二叉树
- 思路:
- 还是递归;
- 转换为判断两棵子树是否为镜像;
- 左树的左子树和右树的右子树镜像,左树的右子树和右树的左子树镜像;
class Solution {
private:
bool check(TreeNode* left, TreeNode* right) {
if(left == nullptr && right == nullptr)
{
return true;
}
if(left == nullptr || right == nullptr) {
return false;
}
if(left->val != right->val) {
return false;
}
if(check(left->left, right->right) && check(left->right, right->left))
{
return true;
}
else {
return false;
}
}
public:
bool isSymmetric(TreeNode* root) {
if(root == nullptr) {
return true;
}
else {
return check(root->left, root->right);
}
}
};
5. 从上到下打印二叉树
class Solution {
public:
vector<int> levelOrder(TreeNode* root) {
vector<int> re;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()) {
TreeNode* tmp = q.front();
if(tmp != nullptr) {
re.push_back(tmp->val);
q.push(tmp->left);
q.push(tmp->right);
}
q.pop();
}
return re;
}
};
变体1. 按层输出的从上到下打印二叉树
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> re;
queue<TreeNode*> q1;
queue<TreeNode*> q2;
q1.push(root);
while(!q1.empty() || !q2.empty()) {
vector<int> cur;
while(!q1.empty()) {
TreeNode* tmp = q1.front();
if(tmp != nullptr) {
cur.push_back(tmp->val);
q2.push(tmp->left);
q2.push(tmp->right);
}
q1.pop();
}
if(!cur.empty()) {
re.push_back(cur);
}
cur.clear();
while(!q2.empty())
{
TreeNode* tmp = q2.front();
if(tmp != nullptr) {
cur.push_back(tmp->val);
q1.push(tmp->left);
q1.push(tmp->right);
}
q2.pop();
}
if(!cur.empty()) {
re.push_back(cur);
}
}
return re;
}
};
- 思路2:
- 每次开始处理队列的时候,先记录队列的长度;
- 因为这个时候队列中的所有元素都是同一层的;
- 然后集中处理完这些元素,再进行下一次的队列处理;
- 这样只需要用一个队列即可完成;
- 代码2:
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> re;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()) {
vector<int> cur;
int q_size = q.size();
for(int i=0;i<q_size;i++) {
TreeNode* tmp = q.front();
if(tmp != nullptr) {
cur.push_back(tmp->val);
q.push(tmp->left);
q.push(tmp->right);
}
q.pop();
}
if(!cur.empty()) {
re.push_back(cur);
}
}
return re;
}
};
变体2. 按层之字形输出的从上到下打印二叉树
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> re;
queue<TreeNode*> q;
bool is_left = true;
q.push(root);
while(!q.empty()) {
int q_size = q.size();
deque<int> dq;
for(int i=0;i<q_size;++i) {
TreeNode* tmp = q.front();
if(tmp != nullptr) {
if(is_left) {
dq.push_back(tmp->val);
}
else {
dq.push_front(tmp->val);
}
q.push(tmp->left);
q.push(tmp->right);
}
q.pop();
}
if(!dq.empty()) {
re.emplace_back(vector<int>{dq.begin(), dq.end()});
}
is_left = !is_left;
}
return re;
}
};
6. 二叉搜索树的后序遍历序列
class Solution {
private:
bool backOrderTraverse(vector<int>& postorder, int begin, int end) {
if(begin == end) {
return true;
}
if(postorder[begin] < postorder[end]) {
int left = begin;
while(left+1<end && postorder[left + 1]<postorder[end]) {
left++;
}
for(int i=left+1;i<end;i++) {
if(postorder[i]<postorder[end]) {
return false;
}
}
return backOrderTraverse(postorder, begin, left) && backOrderTraverse(postorder, left+1, end);
}
else {
for(int i=begin;i<end;i++) {
if(postorder[i]<postorder[end]) {
return false;
}
}
return backOrderTraverse(postorder, begin, end-1);
}
}
public:
bool verifyPostorder(vector<int>& postorder) {
if(postorder.size() == 0) {
return true;
}
else {
return backOrderTraverse(postorder, 0, postorder.size()-1);
}
}
};
7. 二叉树中和为某一值的路径
class Solution {
private:
void traverse(TreeNode* cur, int rest, vector<int> &cur_path, vector<vector<int>> &re) {
if(cur == nullptr) {
return;
}
cur_path.push_back(cur->val);
if(rest - cur->val == 0 && cur->left == nullptr && cur->right == nullptr) {
re.push_back(cur_path);
}
traverse(cur->left, rest-cur->val, cur_path, re);
traverse(cur->right, rest-cur->val, cur_path, re);
cur_path.pop_back();
}
public:
vector<vector<int>> pathSum(TreeNode* root, int target) {
vector<vector<int>> re;
vector<int> cur_path;
traverse(root, target, cur_path, re);
return re;
}
};
8. 二叉搜索树与双向链表
- 思路:
- 二叉搜索树能和双向链表相互转换的关键是:树有左右两个指针,双向链表也有前后两个指针;
- 核心是用中序遍历,因为中序遍历二叉搜索树之后能够得到有序序列;
- 可以简化成按照中序遍历打印节点的顺序处理节点,如下:
void dfs(Node* root) {
if(root == nullptr) {
return;
}
dfs(root->left);
printf("%d\n", root->val);
dfs(root->right);
}
class Solution {
private:
Node *pre = nullptr, *head = nullptr;
void dfs(Node* root) {
if(root == nullptr) {
return;
}
dfs(root->left);
if(pre == nullptr) {
pre = root;
head = root;
}
else {
pre->right = root;
root->left = pre;
pre = root;
}
dfs(root->right);
}
public:
Node* treeToDoublyList(Node* root) {
if(root == nullptr) {
return nullptr;
}
dfs(root);
head->left = pre;
pre->right = head;
return head;
}
};
9. 二叉搜索树的第k大节点
-
思路: -
因为二叉搜索树的中序遍历是有序的,所以仿照中序遍历即可; -
但注意这里找的是第k大的节点,而中序遍历是从小到大的,因此需要按照中序遍历的逆序,也就是“右->根->左”的顺序来遍历即可; -
要用两个全局变量保存当前计数和第k大节点的值; -
可以进行剪枝,因为一旦搜索到目标值,就不需要再往下遍历了; -
代码:
class Solution {
private:
int re;
int count;
void traverse(TreeNode* root, int k) {
if(root == nullptr) {
return;
}
else {
traverse(root->right, k);
if(count == k) {
return;
}
++count;
if(count == k) {
re = root->val;
return;
}
traverse(root->left, k);
}
}
public:
int kthLargest(TreeNode* root, int k) {
count = 0;
traverse(root, k);
return re;
}
};
10. 二叉树的深度
class Solution {
private:
int traverse(TreeNode* root) {
if(root == nullptr) {
return 0;
}
return max(traverse(root->left), traverse(root->right)) + 1;
}
public:
int maxDepth(TreeNode* root) {
return traverse(root);
}
};
11. 平衡二叉树
- 思路:
- 和10. 二叉树的深度很像;
- 可以用从底往上的方法来遍历二叉树,也就是后序遍历;
- 同时用一个全局变量记录结果,并可用于提前剪枝;
- 剪枝是在每一个递归调用(除最后一个)后进行的,作用是避免进入之后的递归调用;
- 当然提前剪枝后最终的返回值就不是树的高度了,虽然使用了
int 返回类型; - 代码:
class Solution {
private:
bool re;
int traverse(TreeNode* root) {
if(root == nullptr) {
return 0;
}
int left = traverse(root->left);
if(re == false) {
return -1;
}
int right = traverse(root->right);
if(abs(left - right) > 1) {
re = false;
}
return max(left, right) + 1;
}
public:
bool isBalanced(TreeNode* root) {
re = true;
traverse(root);
return re;
}
};
12. 二叉搜索树的最近公共祖先
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(p->val < root->val && q->val < root->val) {
return lowestCommonAncestor(root->left, p, q);
}
if(p->val > root->val && q->val > root->val) {
return lowestCommonAncestor(root->right, p, q);
}
return root;
}
};
变体. 二叉树的最近公共祖先
-
思路: -
其实思路和上面的是很像的,只不过去掉了二叉搜索树这个前提,改为一般的二叉树; -
也就是说,现在无法通过单纯的值比较来确定p 和q 节点是在左子树上还是右子树上; -
只能通过深度搜索来确定,所以这样最坏情况下整棵树的所有节点都会被遍历到; -
值得注意的是,需要单独处理root是p 或者q 节点而另一个节点在它的左右子树中的情况,在这种情况下,root就是要找的最近父节点; -
代码:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr) {
return nullptr;
}
if(root == p || root == q) {
return root;
}
TreeNode *left = lowestCommonAncestor(root->left, p, q);
TreeNode *right = lowestCommonAncestor(root->right, p, q);
if(left == nullptr) {
return right;
}
if(right == nullptr) {
return left;
}
return root;
}
};
六、队列和栈
1. 用两个栈实现队列
-
思路: -
用两个栈,一个存输入数据,一个反序压栈,做输出。 -
代码:
class CQueue {
stack<int> input_stack, output_stack;
public:
CQueue() {
}
void appendTail(int value) {
input_stack.push(value);
}
int deleteHead() {
if(!output_stack.empty())
{
int re = output_stack.top();
output_stack.pop();
return re;
}
else
{
if(input_stack.empty())
{
return -1;
}
else
{
while(!input_stack.empty())
{
output_stack.push(input_stack.top());
input_stack.pop();
}
int re = output_stack.top();
output_stack.pop();
return re;
}
}
}
};
2. 栈的压入、弹出序列
-
思路: -
直接用栈来模拟,验证能不能得到popped序列; -
分别用两个指针指向两序列当前正在验证的元素; -
栈每压入一个pushed的元素,就尝试弹出所有能和popped序列对应的元素; -
直到压入全部元素,此时验证popped序列对应元素是否都能被栈弹出(也就是指针是否到尽头); -
另外,两个序列长度不等时立刻就能判断它们一定不对应; -
使用s.top() 前一定要先判断s.empty() ; -
代码:
class Solution {
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
stack<int> s;
if(pushed.size() != popped.size()) {
return false;
}
int index1 = 0;
int index2 = 0;
while(index1 < pushed.size()) {
s.push(pushed[index1]);
++index1;
while(!s.empty() && s.top() == popped[index2]) {
s.pop();
++index2;
}
}
if(index2!=popped.size()) {
return false;
}
else {
return true;
}
}
};
3. 包含min函数的栈 [单调栈]
class MinStack {
private:
std::stack<int> stack;
std::stack<int> min_stack;
public:
MinStack() {
}
void push(int x) {
stack.push(x);
if(!min_stack.empty()) {
min_stack.push(std::min(min_stack.top(), x));
}
else {
min_stack.push(x);
}
}
void pop() {
stack.pop();
min_stack.pop();
}
int top() {
return stack.top();
}
int min() {
return min_stack.top();
}
};
4. 滑动窗口的最大值 [单调栈]
- 思路:
- 如果是暴力解的话,也就是用嵌套循环遍历,时间复杂度是O(k*N);
- 其实这一题和上面的4. 滑动窗口的最大值很像,也是类似滑动窗口,也是要保存一系列最值(而非单个最值),因此也可以用单调栈解决,时间复杂度降为O(2N);
- 这里虽然说是用了双端队列
deque 实现单调队列,但核心还是维护一个单调栈,栈内单调非增; - 那为什么不直接用栈来实现呢?主要是因为滑动窗口的左指针也移动,所以栈底(也就是队列首)的元素可能不在窗口内了,需要从栈底弹出元素,这是栈无法做到的;
- 仅当栈底元素恰好等于滑动窗口左外第一个元素时,也就是刚好移出了滑动窗口时,才从栈底弹出元素,其他情况仍是维护一个单调栈;
- 原因是入栈的顺序的按照遍历的顺序的,如果栈底的元素不是刚好移出窗口的那个元素,那么它和栈内的其他元素一定都是在窗口里面的元素(因为入栈时间晚);
- 注意等于的元素也是要保留在栈内的,也就是说一定是要单调非增而不是单调减,这是为了应对当前元素是最大值(或者以后会成为最大值)的情况,因为所有的最大值或者潜在可能的最大值都应该保留在栈内;
- 代码:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> queue;
vector<int> re;
int i;
for(i=0;i<k;++i) {
while(!queue.empty() && queue.back() < nums[i]) {
queue.pop_back();
}
queue.push_back(nums[i]);
}
re.push_back(queue.front());
for(i=k;i<nums.size();++i) {
if(queue.front() == nums[i-k]) {
queue.pop_front();
}
while(!queue.empty() && queue.back() < nums[i]) {
queue.pop_back();
}
queue.push_back(nums[i]);
re.push_back(queue.front());
}
return re;
}
};
5. 队列的最大值 [单调栈]
-
思路: -
还是单调栈的思路; -
其实是4. 滑动窗口的最大值的变体,相当于是将定长的滑动窗口改成变长的滑动窗口; -
在形式上和3. 包含min函数的栈是一样的,只是栈是先入后出,相当于滑动窗口的左指针不移动,队列是先入先出,左指针会一直移动; -
只要左指针移动,就需要使用双向队列来实现栈; -
注意是均摊的时间复杂度为O(1),因为就某次入队操作而言,max_queue 可能不是1次操作就能完成的(也可能不需要操作),但所有元素入栈后,max_queue 的总操作次数是O(N),所以均摊下来是O(1); -
代码:
class MaxQueue {
private:
deque<int> max_queue;
queue<int> queue;
public:
MaxQueue() {
}
int max_value() {
if(queue.empty()) {
return -1;
}
return max_queue.front();
}
void push_back(int value) {
queue.push(value);
while(!max_queue.empty() && max_queue.back() < value) {
max_queue.pop_back();
}
max_queue.push_back(value);
}
int pop_front() {
if(queue.empty()) {
return -1;
}
int re = queue.front();
if(max_queue.front() == queue.front()) {
max_queue.pop_front();
}
queue.pop();
return re;
}
};
|