class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map;
for(int i=0;i<nums.size();++i) {
if(map.find(target - nums[i]) != map.end()) {
return {i, map[target - nums[i]]};
}
map[nums[i]] = i;
}
return {};
}
};
思路:
可以用排序后的字符串作为key
,这样同字母构成的字符串的key
就相同了;
其实难点是在于STL的使用,涉及的STL包括string
,unordered_map
和二维数组,以及sort
函数;
代码:
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>> re;
unordered_map<string, int> map;
for(int i=0;i<strs.size();++i) {
// 复制字符串做排序
string sorted_str = strs[i];
sort(sorted_str.begin(), sorted_str.end());
// 检查map中是否已经存在key
if(map.find(sorted_str) == map.end()) {
re.push_back(vector<string>());
map[sorted_str] = re.size() - 1;
}
// 根据key值将strs[i]分类
re[map[sorted_str]].push_back(strs[i]);
}
return re;
}
};
x->x+1->x+2...
的生长,直到找到右边界;x-1
的x
执行生长,因为这样的元素必定不会被别的生长遍历到;class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> set;
// 用hash set保存
for(int i=0;i<nums.size();++i) {
set.insert(nums[i]);
}
int max_len = 0;
for(int i=0;i<nums.size();++i) {
int cur = nums[i];
int right = cur;
if(set.find(cur-1) == set.end()) {
// 不存在前一个元素,表明cur不可能由别的元素经过while遍历到
while(set.find(right+1) != set.end()) {
// 通过逐一生长右边界,统计以cur开始的最长连续序列
++right;
}
}
int len = right - cur + 1;
if(max_len < len) {
max_len = len;
}
}
return max_len;
}
};
cur
的最长连续子序列长度为:m a p [ l e f t , . . . c u r , . . . r i g h t ] = m a p [ c u r − 1 ] + 1 + m a p [ c u r + 1 ] map[left,...cur,...right] = map[cur-1] + 1 + map[cur+1] map[left,...cur,...right]=map[cur−1]+1+map[cur+1]
class Solution {
public:
/*
动态规划:map[left-cur-right] = map[cur-1] + 1 + map[cur+1]
*/
int longestConsecutive(vector<int>& nums) {
unordered_map<int, int> map;
int max_len = 0;
for(int i=0;i<nums.size();++i) {
int cur = nums[i];
if(map.find(cur) != map.end()) {
// 需要判断cur是否已经被访问过了
continue;
}
int left = 0;
if(map.find(cur-1) != map.end()) {
left = map[cur - 1];
}
int right = 0;
if(map.find(cur + 1) != map.end()) {
right = map[cur + 1];
}
int len = left + 1 + right;
if(max_len < len) {
max_len = len;
}
// cur值要记录是否已被访问,且要在边界更新前,以免被重写
map[cur] = -1;
// 只更新左右边界的最大长度,因为边界内的元素不会被访问了,所以不需要更新
map[cur - left] = len;
map[cur + right] = len;
}
return max_len;
}
};
x
并入x+1
所在连通分量,也就是说x+1
是x
的根节点,方向不能改变;find
函数直接找到最右边界,然后得到子序列长度;find
函数中使用路径压缩,这会极大减少运行的时间,但两种路径压缩方式之间相差不大;class Solution {
private:
unordered_map<int, int> map;
void set_init(vector<int>& nums) {
for(int i=0;i<nums.size();++i) {
map[nums[i]] = nums[i];
}
}
int set_find(int x) {
// 返回的是最右边界
if(map[x] == x) {
return x;
}
map[x] = set_find(map[x]);
return map[x];
}
void set_union(int x, int y) {
// x是y的根节点
int parent_x = set_find(x);
int parent_y = set_find(y);
if(parent_x != parent_y) {
map[y] = parent_x;
}
}
public:
int longestConsecutive(vector<int>& nums) {
// 初始化map
set_init(nums);
// 合并相邻的nums[i]
for(int i=0;i<nums.size();++i) {
if(map.find(nums[i]+1) != map.end()) {
// 只按照nums[i] -> nums[i] + 1合并,方向不能改变
set_union(nums[i]+1, nums[i]);
}
}
int max_len = 0;
for(int i=0;i<nums.size();++i) {
int cur = nums[i];
int right = set_find(cur);
int len = right - cur + 1;
if(max_len < len) {
max_len = len;
}
}
return max_len;
}
};
vector
而不用unordered_set
,因为key
范围已知);class Solution {
/*
原地哈希:
index: 0 1 2 3 4 [0, n-1] = value - 1
value: 1 2 3 4 5 [1, n]
+ n : 6 7 8 9 10[1, n] + n > n
*/
public:
vector<int> findDisappearedNumbers(vector<int>& nums) {
int n = nums.size();
for(int i=0;i<nums.size();++i) {
nums[(nums[i]-1)%n] += n;
}
vector<int> re;
for(int i=0;i<nums.size();++i) {
if(nums[i] <= n) {
// 小于等于n的数均是没有+n的,也就是缺失的数
re.push_back(i+1);
}
}
return re;
}
};
nums[i]
都是正数,确实是可以用双指针来做的,参看:剑指offer算法题02的十、5. 和为s的连续正数序列 [滑动窗口];i
,向后枚举每个[i:j]
区间之和是否是k
,时间复杂度最低能到O(N^2);i
个数的前缀和(含i
)是prefix_num[i]
;sum[i:j] = prefix_sum[j] - prefix_sum[i-1]
;j
结尾的区间是否能凑成k
,只需看之前的前缀和是否存在prefix_sum[i-1] = prefix_sum[j] - k
;prefix_sum[i-1]
会取到prefix_sum[-1]
的,因此需要增加一个prefix_sum[-1] = 0 -> 1
的映射,这是前缀和解法均需要考虑的点;class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
// map含义:
unordered_map<int, int> map;
int prefix_sum = 0;
// prefix_sum[-1]
map[prefix_sum] = 1;
int re = 0;
for(int i=0;i<nums.size();++i) {
// 计算前缀和
prefix_sum += nums[i];
// sum[i:j] = prefix_sum[j]-prefix_sum[i-1] = k
// prefix_sum[j] - k = prefix_sum[i-1]
if(map.find(prefix_sum - k) != map.end()) {
re += map[prefix_sum - k];
}
++map[prefix_sum];
}
return re;
}
};
思路:
其实是可以转换为5. 和为 K 的子数组 [前缀和] 来做的;
令所有的0
替换为-1
,则问题转换为寻找和为0的最长子数组;
还是用前缀和来做,只是哈希表中不是存放前缀和的个数,而是存放第一次出现该前缀和的下标;
代码:
class Solution {
/*
* 如果将所有的0换成-1,则等价于找和为0的最长子数组
* prefix_sum[j+1] - prefix_sum[i] = sum[i,j]
* prefix_sum[i] = prefix_sum[j+1] - sum[i,j]
*/
public:
int findMaxLength(vector<int>& nums) {
// map含义:
unordered_map<int, int> map;
int re_max = 0;
int prefix_sum = 0;
map[prefix_sum] = 0; // index=0的前缀和是prefix_sum
for(int i=1;i<=nums.size();++i) {
if(nums[i-1] == 0) {
// 将所有的0换成-1
prefix_sum += -1;
}
else {
prefix_sum += nums[i-1];
}
// 因为这里的map[0]=0是要用作下标的,所以下面的判断不能用0判断有无记录
if(map.find(prefix_sum) != map.end()) {
re_max = max(re_max, i - map[prefix_sum]);
}
else {
// 首次记录
map[prefix_sum] = i;
}
}
return re_max;
}
};
class Solution {
public:
int pivotIndex(vector<int>& nums) {
// 求和
int sum = 0;
for(int i=0;i<nums.size();++i) {
sum += nums[i];
}
// 找第一个中心下标
int re_index = -1;
int prefix_sum = 0;
for(int i=0;i<nums.size();++i) {
int rest_sum = sum - nums[i];
// 注意下面的取余必须是非负数才行
if(abs(rest_sum) % 2 == 1) {
prefix_sum += nums[i];
continue;
}
if(prefix_sum == rest_sum/2) {
re_index = i;
// 取第一次满足的下标即可
break;
}
prefix_sum += nums[i];
}
return re_index;
}
};
class Solution {
public:
/*
(0,0) -> (0, n-1) -> (n-1, n-1) -> (n-1,0)
(0,1) -> (1, n-1) -> (n-1, n-2) -> (n-2,0)
(0,2) -> (2, n-1) -> (n-1, n-3) -> (n-3,0)
直到(0,n-2), 即完成最外圈的旋转
(1,1) -> (1, n-2) -> (n-2, n-2) -> (n-2,1)
(1,2) -> (2, n-2) -> (n-2, n-3) -> (n-3,1)
直到(1,n-3), 即完成倒数第二圈的旋转
每次从(i,i)开始走,直到奇数i => n/2, 偶数i => n/2 - 1 (含)
*/
void rotate(vector<vector<int>>& matrix) {
int n = matrix[0].size();
for(int i=0;i<=(n-1)/2;++i) {
for(int j=i;j<=n-2-i;++j) {
// 依次旋转四个元素
int tmp = matrix[i][j];
matrix[i][j] = matrix[n-1-j][i];
matrix[n-1-j][i] = matrix[n-1-i][n-1-j];
matrix[n-1-i][n-1-j] = matrix[j][n-1-i];
matrix[j][n-1-i] = tmp;
}
}
}
};
思路:
深度搜索遍历+剪枝;
注意的点如下:
代码:
class Solution {
private:
bool re; // 用于辅助剪枝
void dfs(vector<vector<int>>& visited, const vector<vector<char>>& board, const string& word, int i, int j, int k) {
int m = board.size();
int n = board[0].size();
if(board[i][j] == word[k]) {
if(k == word.size() - 1) {
// 单词匹配上了
re = true;
return;
}
visited[i][j] = 1;
if(i+1 < m && !visited[i+1][j] && !re) {
dfs(visited, board, word, i+1, j, k+1);
}
if(j+1 < n && !visited[i][j+1] && !re) {
dfs(visited, board, word, i, j+1, k+1);
}
if(i-1 >= 0 && !visited[i-1][j] && !re) {
dfs(visited, board, word, i-1, j, k+1);
}
if(j-1 >= 0 && !visited[i][j-1] && !re) {
dfs(visited, board, word, i, j-1, k+1);
}
visited[i][j] = 0;
}
else {
return;
}
}
public:
bool exist(vector<vector<char>>& board, string word) {
int m = board.size();
int n = board[0].size();
vector<vector<int>> visited(m, vector<int>(n, 0));
re = false;
for(int i=0;i<m;++i) {
for(int j=0;j<n;++j) {
dfs(visited, board, word, i, j, 0);
if(re) {
break;
}
}
}
return re;
}
};
'1'
的点,则为一个岛屿;visited
矩阵记录是否已经经过当前的'1'
;visited
矩阵,直接将grid
中经过的'1'
置为'0'
即可;print_info
函数,调试可以更加直观;class Solution {
private:
void dfs(vector<vector<int>>& visited, vector<vector<char>>& grid, int i, int j) {
if(visited[i][j] || grid[i][j]!='1') {
return;
}
visited[i][j] = 1;
if(i+1 < grid.size()) {
dfs(visited, grid, i+1, j);
}
if(j+1 < grid[0].size()) {
dfs(visited, grid, i, j+1);
}
if(i-1 >= 0) {
dfs(visited, grid, i-1, j);
}
if(j-1 >= 0) {
dfs(visited, grid, i, j-1);
}
}
void print_info(vector<vector<int>>& m) {
for(int i=0;i<m.size();++i) {
for(int j=0;j<m[0].size();++j) {
printf("%d ", m[i][j]);
}
printf("\n");
}
printf("\n");
}
public:
int numIslands(vector<vector<char>>& grid) {
vector<vector<int>> visited(grid.size(), vector<int>(grid[0].size(), 0));
int re_count = 0;
for(int i=0;i<grid.size();++i) {
for(int j=0;j<grid[0].size();++j) {
if(!visited[i][j] && grid[i][j]=='1') {
++re_count;
dfs(visited, grid, i, j);
//print_info(visited);
}
}
}
return re_count;
}
};
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int cur_x = 0, cur_y = n-1;
while(cur_x<m && cur_y>=0) {
if(target == matrix[cur_x][cur_y]) {
return true;
}
if(target > matrix[cur_x][cur_y]) {
// 往下走
++cur_x;
}
else {
// 往左走
--cur_y;
}
}
return false;
}
};
思路:
是全排列的类型,而且不用考虑数字本身的全排列,比如“23”,则第一位的字母只能是“2”对应的字母,第二位只能是“3”对应的字母;
全排列的类型用递归来实现即可,类似于剑指offer算法题01的三、2. 字符串的排列的解法;
但这题相当于是纵向全排列,也就是每一位有多种选择;剑指offer算法题01中的相当于是横向全排列,也就是每一位只有一种选择,但是彼此之间可以调换位置;相对来说横向全排列会更难一些;
数字和字母之间的对应关系可以用map来构建(这种写法比较优雅);
另外注意,如果数字串本身为空,则返回的vector也是为空,也就是说长度为0的cur不用压入结果;
代码:
class Solution {
private:
void dfs(string &digits, string cur, int x, vector<string> &re, unordered_map<char, string> &map) {
if(x!=0 && x==digits.length()) {
re.push_back(cur);
return;
}
string letters = map[digits[x]];
for(int j=0;j<letters.length();++j) {
cur[x] = letters[j];
dfs(digits, cur, x+1, re, map);
}
return;
}
public:
vector<string> letterCombinations(string digits) {
vector<string> re;
// 存放对应关系
unordered_map<char, string> map{
{'2', "abc"},
{'3', "def"},
{'4', "ghi"},
{'5', "jkl"},
{'6', "mno"},
{'7', "pqrs"},
{'8', "tuv"},
{'9', "wxyz"}
};
// 用于保存当前排列
string cur(digits.length(), ' ');
// 递归
dfs(digits, cur, 0, re, map);
return re;
}
};
class Solution {
private:
void dfs(string cur, int x, int numLeft, int numRight, vector<string> &re) {
// printf("%s\n", cur.c_str());
if(numRight==0 && numRight==0) {
re.push_back(cur);
return;
}
else {
if(numLeft > 0) {
// 左括号有剩余
cur[x] = '(';
dfs(cur, x+1, numLeft-1, numRight, re);
}
if(numRight > 0 && numLeft < numRight) {
// 右括号有剩余且数量多于左括号
cur[x] = ')';
dfs(cur, x+1, numLeft, numRight-1, re);
}
}
}
public:
vector<string> generateParenthesis(int n) {
vector<string> re;
string cur(2*n, ' ');
dfs(cur, 0, n, n, re);
return re;
}
};
核心的点大概是如下:
希望修改后的字典序更大,但修改前后的字典序相差要尽可能小;
(1) 要找从后往前数第一个升序序列中的倒数第二个元素,记为nums[i]
;
(2) 再找从后往前数第一个大于nums[i]
的数,记为nums[j]
;
i
后面找一个比nums[i]
大但又相差最近的数;i
之后均为倒序,因此从后往前找必定是一个递增序列,一旦大于nums[i]
肯定是最小的大于nums[i]
的数;(3) 交换两个数,然后为i
之后到结尾的序列排成升序,其实是翻转即可;
i
之后的序列是倒序,即使交换之后也是倒序,交换前有nums[j-1]>nums[j]>nums[i]>nums[j+1]
,所以交换后有nums[j-1]>nums[i]>nums[j+1]
;i
之后的序列从倒序(字典序最大)换成升序(字典序最小),然后nums[i]
又刚好换成最逼近的大于它的数,因此在字典序上变换前后的两个序列刚好是相邻的;(4) 最后,如果整个序列是倒序的话,也就是说这时候字典序最大,就把整个序列翻转即可,变成字典序最小,即又回到了循环的起点;
不得不说,这个思路真的把握住了字典序的本质特点,十分巧妙,很amazing!
而且我一开始都不知道把这道题归到哪个类别中,后来想到字典序,还是放在字符串里面比较合适;
值得补充的是:reverse()
函数可以直接使用C++的库,它的实现本质是双指针;
代码:
class Solution {
public:
void nextPermutation(vector<int>& nums) {
bool isLast = true;
int i = nums.size() - 1 - 1;
// 找从后往前数第一个升序序列中的倒数第二个元素
// 也就是第一个小于后一个数的数
// 这是因为所有的倒序序列均已经是字典序最大了,没法通过调整数字的顺序获得更大的字典序
while(i >= 0) {
if(nums[i] < nums[i + 1]) {
isLast = false;
break;
}
--i;
}
if(isLast) {
// The range used is [first,last)
reverse(nums.begin(), nums.end());
}
else {
int j = nums.size() - 1;
// 找从后往前数第一个大于nums[i]的元素
// 这是因为num[i]后的序列是倒序序列,从后往前遍历的话数是递增的
// 所以第一个找到的大于nums[i]的元素就是最贴近nums[i]的数
while(j > i) {
if(nums[j] > nums[i]) {
break;
}
--j;
}
swap(nums[i], nums[j]);
reverse(nums.begin()+i+1, nums.end());
}
}
};
class Solution {
private:
vector<vector<int>> re;
void dfs(vector<int>& candidates, int target, vector<int> cur, int k) {
if(target == 0) {
re.push_back(cur);
return;
}
// 当前层元素必须是要么和上层元素相同,要么是在上层元素之后
for(int i=k;i<candidates.size();++i) {
if(target >= candidates[i]) {
// candidates[i]是当前层元素
cur.push_back(candidates[i]);
// 因此传到下一层的k是i
dfs(candidates, target - candidates[i], cur, i);
cur.pop_back();
}
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
dfs(candidates, target, vector<int>(), 0);
return re;
}
};
i
要从k
开始遍历,k
和自己交换就是这一位保持不变的意思;class Solution {
private:
vector<vector<int>> re;
void dfs(vector<int>& nums, int k) {
if(k == nums.size() - 1) {
re.push_back(nums);
return;
}
// i从k开始遍历
for(int i=k;i<nums.size();++i) {
swap(nums[k], nums[i]);
dfs(nums, k+1);
swap(nums[k], nums[i]);
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
dfs(nums, 0);
return re;
}
};
class Solution {
private:
vector<vector<int>> re;
void dfs(vector<int>& nums, vector<int> cur, int k) {
if(k == nums.size()) {
re.push_back(cur);
return;
}
// 不放nums[k]
dfs(nums, cur, k+1);
// 放nums[k]
cur.push_back(nums[k]);
dfs(nums, cur, k+1);
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<int> cur;
dfs(nums, cur, 0);
return re;
}
};
class Solution {
public:
/*
类似动态规划思路:dp[i]表示前i个数的解集,dp[i] = dp[i - 1] + collections(i)
其中,collections(i)表示把dp[i-1]的所有子集都加上第i个数形成的子集
时间复杂度是O(2^n):
1. n个元素共有2^n个排列组合,每个元素可以选或者不选;
2. 构建每个排列组合的时间复杂度是O(1),因为仅需在末尾增加一个元素;
相当于是利用了前面构建的元素的排列组合来构建本次的排列组合,相当巧妙
例如[1,2,3],一开始解集为[[]],表示只有一个空集。
遍历到1时,依次拷贝解集中所有子集,只有[],把1加入拷贝的子集中得到[1],然后加回解集中。
此时解集为[[], [1]]。
遍历到2时,依次拷贝解集中所有子集,有[], [1],把2加入拷贝的子集得到[2], [1, 2],然后加回解集中。
此时解集为[[], [1], [2], [1, 2]]。
遍历到3时,依次拷贝解集中所有子集,有[], [1], [2], [1, 2],把3加入拷贝的子集得到[3], [1, 3], [2, 3], [1, 2, 3],然后加回解集中。
此时解集为[[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]。
*/
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> re;
re.push_back(vector<int>());
for(int i=0;i<nums.size();++i) {
int size = re.size();
for(int j=0;j<size;++j) {
vector<int> tmp = re[j];
tmp.push_back(nums[i]);
re.push_back(tmp);
}
}
return re;
}
};
数字[重复字符串]
,因为一旦出现数字,后面必须有[重复字符串]
的结构,这样定义就比较清晰一点;tmp_len
,然后处理重复字符串部分tmp_s
;]
则表示子结构已结束,完成了本层的递归,返回数字*重复字符串
的字串给上一层;i
仅遍历一遍数字即可,因此传参用的是引用传递;class Solution {
private:
/*
decodeStringAux:处理s[i]是数字的情况
*/
string decodeStringAux(string& s, int& i) {
string re;
// 处理数字到'['前的部分
int tmp_len = 0;
while(i<s.length() && s[i]!='[') {
tmp_len = tmp_len*10 + (s[i]-'0');
++i;
}
// 跳过'['
++i;
// 处理'['后到']'的字符部分
string tmp_s;
while(i<s.length()) {
if(s[i]>='a' && s[i]<='z') {
// 是字符则直接追加
tmp_s += s[i]; // char用+=追加
++i;
}
else {
if(s[i] == ']') {
// 遇到']'表明需要用tmp_len构建re并返回
for(int j=0;j<tmp_len;++j) {
re = re.append(tmp_s); // string用append或者+=追加
}
// 跳过']'
++i;
return re;
}
else {
// 遇数字则继续递归,递归结果作为tmp_s的一部分
tmp_s.append(decodeStringAux(s, i));
}
}
}
return re;
}
public:
string decodeString(string s) {
string re;
int i = 0;
while(i < s.length()) {
if(s[i]>='0' && s[i]<='9') {
re.append(decodeStringAux(s, i));
}
else {
re += s[i];
++i;
}
}
return re;
}
};
char
类型,则只能使用+
运算符重载;string
类型,则既可以用+
运算符重载,也可以用append(string&)
函数;/**
* 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 rest = 0; // 进位
int cur = 0; // 当前值
ListNode* re_head = nullptr;
ListNode* re_cur = nullptr;
while(l1 && l2) {
cur = (l1->val + l2->val + rest) % 10;
rest = (l1->val + l2->val + rest) / 10;
if(re_head == nullptr) {
re_cur = new ListNode(cur);
re_head = re_cur;
}
else {
re_cur->next = new ListNode(cur);
re_cur = re_cur->next;
}
l1 = l1->next;
l2 = l2->next;
}
// l1还没完
while(l1) {
cur = (l1->val + rest) % 10;
rest = (l1->val + rest) / 10;
if(re_head == nullptr) {
re_cur = new ListNode(cur);
re_head = re_cur;
}
else {
re_cur->next = new ListNode(cur);
re_cur = re_cur->next;
}
l1 = l1->next;
}
// l2还没完
while(l2) {
cur = (l2->val + rest) % 10;
rest = (l2->val + rest) / 10;
if(re_head == nullptr) {
re_cur = new ListNode(cur);
re_head = re_cur;
}
else {
re_cur->next = new ListNode(cur);
re_cur = re_cur->next;
}
l2 = l2->next;
}
// 处理最后的rest进位
if(rest != 0) {
re_cur->next = new ListNode(rest);
re_cur = re_cur->next;
}
return re_head;
}
};
x->next = y->next
的格式,一般是有错误的,正确的一般是x = y->next
的形式;/**
* 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* mergeTwoLists(ListNode* list1, ListNode* list2) {
// 伪头指针
ListNode *fakeHead = new ListNode();
ListNode *cur = fakeHead;
while(list1!=nullptr && list2!=nullptr) {
if(list1->val > list2->val) {
cur->next = list2;
list2 = list2->next;
}
else {
cur->next = list1;
list1 = list1->next;
}
cur = cur->next;
}
while(list1!=nullptr) {
cur->next = list1;
list1 = list1->next;
cur = cur->next;
}
while(list2!=nullptr) {
cur->next = list2;
list2 = list2->next;
cur = cur->next;
}
cur->next = nullptr;
return fakeHead->next;
}
};
思路:
还是同2. 合并两个有序链表的思路,找各个链表的当前头指针下最小的插入到新链表即可;
但由于有多个链表,每次用循环比较各个链表的头指针会相当耗时,故使用小顶堆维护最小节点;
直接将所有的链表的所有节点放入堆中,然后依次从堆中取节点构建链表即可;
需要考虑的点有两个:
<
运算符来间接实现;代码:
/**
* 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 {
private:
// 因为C++只有大顶堆,所以需要重载<函数实现小顶堆
class ListHelp {
public:
ListNode *p;
ListHelp(ListNode *newP) {
p = newP;
}
// 注意重载的参数均为引用而不是指针
bool operator < (const ListHelp &lh) const{
if(p && lh.p && p->val > lh.p->val) {
return true;
}
else {
return false;
}
}
};
// 用辅助类实现小顶堆
priority_queue<ListHelp> q;
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode* fakeHead = new ListNode();
// 直接把所有链表的所有节点都放入堆
for(int i=0;i<lists.size();++i) {
ListNode* tmp = lists[i];
while(tmp) {
//q.push(ListHelp(tmp)); // 用构造函数构建新对象,不用加new
q.push({tmp}); // 直接用所有的成员变量构建新对象
tmp = tmp->next;
}
}
// 从堆里拿元素构建链表
ListNode *cur = fakeHead;
while(!q.empty()) {
ListHelp tmp = q.top();
cur->next = tmp.p;
cur = cur->next;
q.pop();
}
cur->next = nullptr;
return fakeHead->next;
}
};
思路:
正常的缓存直接用hash map即可,如redis之类的内存数据库就是这样;
但这里增加了最近最少使用的限制,需要增加双向链表来辅助实现时间相关的队列;
队列的头部是最新使用的节点(新插入或者新访问),尾部是最少使用的节点(即将删除);
增加节点的规则:
key
,则修改其value
,并将其移动到队列的头部;访问节点的规则:
为什么使用双向链表而不是单向链表?
因为移动、增加或者删除节点时,需要获得当前节点的前一个节点和后一个节点的指针,如果是单项链表需要从head
开始遍历一遍,时间复杂度是O(N),使用双向链表时间复杂度是O(1);
代码:
直接使用STL库来实现会简洁很多:
std::list
来实现;std::unordered_map
来实现;但要注意:
list::iterator
作为链表节点的指向;*iterator
获取链表节点的值;list::iterator
,置空和判空时很巧妙地使用了list.end()
;
list.end()
是一个伪尾节点,存放的数据是不含该节点的;push_back()
后,数据是放在list.end()
的前一个节点;std::list
的迭代器不会失效,因此置空可以将map[key] = list.end()
,判空的时候也可以直接用map[key] == list.end()
;class LRUCache {
private:
class Node {
public:
int key;
int value;
Node(int a, int b): key(a), value(b) {};
};
int _size;
list<Node> cache_list;
unordered_map<int, list<Node>::iterator> map;
public:
LRUCache(int capacity) {
_size = capacity;
}
int get(int key) {
if(map.find(key) != map.end() && map[key] != cache_list.end()) {
// 已经在cache_list中,移动到链表末尾
Node tmp = *map[key];
cache_list.erase(map[key]);
map[key] = cache_list.end();
cache_list.emplace_back(tmp);
map[key] = prev(cache_list.end());
return tmp.value;
}
return -1;
}
void put(int key, int value) {
if(map.find(key) != map.end() && map[key] != cache_list.end()) {
// 已经在cache_list中,移动到链表末尾
Node tmp = *map[key];
cache_list.erase(map[key]);
map[key] = cache_list.end();
tmp.value = value;
cache_list.emplace_back(tmp);
map[key] = prev(cache_list.end());
}
else {
if(cache_list.size() < _size) {
// cache_list中仍有空间,直接存入
cache_list.emplace_back(key, value);
map[key] = prev(cache_list.end());
}
else {
// cache_list已满,先逐出再存入
Node tmp = cache_list.front();
cache_list.pop_front();
map[tmp.key] = cache_list.end();
cache_list.emplace_back(key, value);
map[key] = prev(cache_list.end());
}
}
}
};
std::list
快一点,空间开销也小;class MyListNode {
public:
int key, value;
MyListNode *pre, *next;
MyListNode(): key(0), value(0), pre(nullptr), next(nullptr) {}
MyListNode(int k, int v): key(k), value(v), pre(nullptr), next(nullptr) {}
};
/*
1. hash map:保证get函数是O(1)
2. 双向列表:保证在任意位置增加和删除节点是O(1)
*/
class LRUCache {
private:
int _capacity, _size;
unordered_map<int, MyListNode*> _map; // hash map
MyListNode *_list_head, *_list_tail; // 伪头、尾节点
void print_info() {
MyListNode *cur = _list_head;
while(cur != nullptr) {
printf("{%d, %d} ", cur->key, cur->value);
cur = cur->next;
}
printf("\n");
}
public:
LRUCache(int capacity) {
_capacity = capacity;
_size = 0;
_list_head = new MyListNode();
_list_tail = new MyListNode();
_list_head->next = _list_tail;
_list_tail->pre = _list_head;
}
int get(int key) {
if(_map.find(key) != _map.end()) {
// 从双向列表中断开节点
MyListNode *pre = _map[key]->pre;
pre->next = _map[key]->next;
_map[key]->next->pre = pre;
// 移动节点到头部
_map[key]->next = _list_head->next;
_list_head->next = _map[key];
_map[key]->next->pre = _map[key];
_map[key]->pre = _list_head;
//print_info();
return _map[key]->value;
}
else {
return -1;
}
}
void put(int key, int value) {
if(_map.find(key) != _map.end()) {
// key已经存在,直接修改值
_map[key]->value = value;
// 从双向列表中断开节点
MyListNode *pre = _map[key]->pre;
pre->next = _map[key]->next;
_map[key]->next->pre = pre;
// 移动节点到头部
_map[key]->next = _list_head->next;
_list_head->next = _map[key];
_map[key]->next->pre = _map[key];
_map[key]->pre = _list_head;
}
else {
if(_size < _capacity) {
++_size;
}
else {
// 删除双向列表的尾部节点
MyListNode *tail = _list_tail->pre;
_map.erase(tail->key);
tail->pre->next = tail->next;
tail->next->pre = tail->pre;
delete tail;
}
// 在双向列表头部插入
MyListNode *cur = new MyListNode(key, value);
cur->next = _list_head->next;
cur->next->pre = cur;
_list_head->next = cur;
cur->pre = _list_head;
_map[key] = cur;
}
//print_info();
}
};
class Node {
int value;
};
list<Node> test_list;
list<Node>::iterator iter = test_list.begin(); // 指向开头元素
// 注意:
//begin()和end()取的是迭代器
//front()和back()取的是节点对象
Node node = *iter; // 取节点值
int value = iter->value; // 取节点内元素
iter = test_list.end(); // 迭代器置空
if(iter == test_list.end()) ... // 迭代器判空
test_list.emplace_back(node);
iter = std::prev(test_list.end()); // 取尾元素的迭代器
// 等价于
iter = test_list.end();
--iter;
思路一:sort函数
先用vector
存下每个节点的指针,然后用sort
函数排序;
需要重载cmp
函数;
实现上最为容易,但空间复杂度不是常数空间;
代码:
class Solution {
private:
static bool cmp(ListNode* &a, ListNode* &b) {
// 必须是严格小于,否则会出现空的a或者b
if(a->val < b->val) {
return true;
}
else {
return false;
}
}
public:
ListNode* sortList(ListNode* head) {
if(head == nullptr) {
return head;
}
vector<ListNode*> list;
ListNode* cur = head;
while(cur != nullptr) {
list.push_back(cur);
cur = cur->next;
}
sort(list.begin(), list.end(), cmp);
head = list[0];
cur = head;
for(int i=1;i<list.size();++i) {
cur->next = list[i];
cur = cur->next;
}
cur->next = nullptr;
return head;
}
};
思路二:归并排序
这个思路是究极麻烦,因为要用链表实现;
一定要注意的两个点是:
next
赋值nullptr
;归并排序的实现思路有两种:
sort
方法和自底向上方法;step
到大的step
,直到step>=len
,step
每次增长一倍(乘2);sub1
不满step
,sub1
刚好为step
,sub2
不满step
和sub2
也满step
;sort
方法相差不大;但不管是哪一种实现,它们的mergeList
都是一样,且实现思路就是合并两个有序链表的思路,需要返回合并后的链表头节点;
(与思路无关的)补充:
print_info
函数来打印链表能在调试的时候用来救命(≧﹏ ≦);cur = cur->next;
忘记写了;head
相连,而且在结束的时候注意delete
掉内存,防止内存泄漏;自顶向下的代码:
class Solution {
private:
ListNode* mergeList(ListNode *head1, ListNode *head2) {
ListNode *fake_head = new ListNode(); // 伪头节点
ListNode *cur = fake_head;
while(head1!=nullptr && head2!=nullptr) {
if(head1->val <= head2->val) {
cur->next = head1;
head1 = head1->next;
}
else {
cur->next = head2;
head2 = head2->next;
}
cur = cur->next;
}
if(head1 != nullptr) {
cur->next = head1;
}
if(head2 != nullptr) {
cur->next = head2;
}
cur = fake_head->next;
delete fake_head;
return cur; // 返回头节点
}
void print_info(ListNode *head) {
ListNode *cur = head;
while(cur != nullptr) {
printf("%d, ", cur->val);
cur = cur->next;
}
printf("\n");
}
public:
ListNode* sortList(ListNode* head) {
if(head==nullptr || head->next==nullptr) {
return head;
}
ListNode *fake_head = new ListNode(); // 伪头节点
fake_head->next = head;
ListNode *slow = fake_head, *fast = fake_head;
ListNode *sub1 = head, *sub2;
while(fast->next != nullptr) {
slow = slow->next;
fast = fast->next;
if(fast->next != nullptr) {
fast = fast->next;
}
}
if(slow != nullptr) {
sub2 = slow->next;
slow->next = nullptr; // 断开sub1
}
// 记得重新赋值sub,否则sub不能代表sub的head
sub1 = sortList(sub1);
sub2 = sortList(sub2);
// 记得连接上fake_head
fake_head->next = mergeList(sub1, sub2);
return fake_head->next;
}
};
class Solution {
private:
ListNode* mergeList(ListNode *head1, ListNode *head2) {
ListNode *fake_head = new ListNode(); // 伪头节点
ListNode *cur = fake_head;
while(head1!=nullptr && head2!=nullptr) {
if(head1->val <= head2->val) {
cur->next = head1;
head1 = head1->next;
}
else {
cur->next = head2;
head2 = head2->next;
}
cur = cur->next;
}
if(head1 != nullptr) {
cur->next = head1;
}
if(head2 != nullptr) {
cur->next = head2;
}
cur = fake_head->next;
delete fake_head;
return cur; // 返回头节点
}
void print_info(ListNode *head) {
ListNode *cur = head;
while(cur != nullptr) {
printf("%d, ", cur->val);
cur = cur->next;
}
printf("\n");
}
public:
ListNode* sortList(ListNode* head) {
ListNode *fake_head = new ListNode(); // 伪头节点
fake_head->next = head;
int len = 0;
ListNode *cur = head;
while(cur != nullptr) {
++len;
cur = cur->next;
}
int step = 1; // 合并的子list的长度
ListNode *sub1, *sub2;
ListNode *pre = fake_head;
cur = head;
while(step < len) {
while(cur != nullptr) {
sub1 = cur; // 第一个sub头
for(int i=0;i<step-1 && cur!=nullptr;++i) {
cur = cur->next;
}
if(cur == nullptr) {
// sub1就不足step
break;
}
sub2 = cur->next; // 第二个sub头
if(sub2 == nullptr) {
// sub1刚好是step,即sub2为空
break;
}
// sub1和sub2均不为空
cur->next = nullptr; // 断开sub1
cur = sub2;
for(int i=0;i<step-1 && cur!=nullptr;++i) {
cur = cur->next;
}
if(cur == nullptr) {
// sub2不足step,即sub2后为nullptr
ListNode *temp = mergeList(sub1, sub2);
pre->next = temp; // 接起来pre和合并的sub
}
else {
// sub1和sub2都满step
ListNode *temp = cur;
cur = cur->next;
temp->next = nullptr; // 断开sub2
temp = mergeList(sub1, sub2); // temp指向sub的head
pre->next = temp; // 接起来pre和合并的sub
while(temp->next != nullptr) {
// 令temp指向sub的最后一个节点
temp = temp->next;
}
pre = temp;
temp->next = cur; // 接起来合并的sub和cur
}
}
step *= 2;
// 记得重置pre和cur以开启下一次循环
pre = fake_head;
cur = fake_head->next;
}
return fake_head->next;
}
};
pre
,当前节点cur
,后一个节点aft
;cur
指向aft
,然后三个指针均后移一个节点;aft
是为了能让cur
能够按照原链表后移;cur
为空;head->next = nullptr
;class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr) {
return head;
}
ListNode *pre = head, *cur = head->next, *aft = cur->next;
while(cur != nullptr) {
aft = cur->next;
cur->next = pre;
pre = cur;
cur = aft;
}
head->next = nullptr;
return pre;
}
};
class Solution {
public:
bool isPalindrome(ListNode* head) {
stack<int> s;
// 一次遍历,入栈
ListNode *cur = head;
while(cur != nullptr) {
s.push(cur->val);
cur = cur->next;
}
// 二次遍历,和栈顶元素比较
cur = head;
while(cur != nullptr) {
if(cur->val != s.top()) {
return false;
}
else {
s.pop();
cur = cur->next;
}
}
return true;
}
};
slow
和fast
的起始节点;fast
移动的条件是fast->next != nullptr
;pre
指针;
pre
的初始值为nullptr
,cur
的初始值是要反转的最后一个节点,这样cur->next = pre
就直接将最后一个节点的下一节点置空了;aft
的赋值要在cur->next = pre
之前完成,然后用cur = aft
移动cur
节点;class Solution {
public:
bool isPalindrome(ListNode* head) {
// 伪头节点,注意一定要连上head
ListNode *fake_head = new ListNode();
fake_head->next = head;
// 用快慢指针断开链表
ListNode *slow = fake_head, *fast = fake_head;
while(fast->next != nullptr) {
slow = slow->next;
fast = fast->next;
if(fast->next != nullptr) {
fast = fast->next;
}
}
// 反转链表
ListNode *pre, *cur, *aft;
pre = nullptr;
cur = slow->next;
while(cur != nullptr) {
aft = cur->next;
cur->next = pre;
pre = cur;
cur = aft;
}
ListNode *head2 = pre;
// 判断两个链表的前缀是否相等
ListNode *cur1 = head, *cur2 = head2;
while(cur1!=nullptr && cur2!=nullptr) {
if(cur1->val != cur2->val) {
return false;
}
cur1 = cur1->next;
cur2 = cur2->next;
}
// 反转第二个链表并重新连接复原
pre = nullptr;
cur = head2;
while(cur != nullptr) {
aft = cur->next;
cur->next = pre;
pre = cur;
cur = aft;
}
slow->next = pre;
return true;
}
};
class Solution {
private:
void dfs(TreeNode* root, vector<int>& re) {
if(root == nullptr) {
return;
}
dfs(root->left, re);
re.push_back(root->val);
dfs(root->right, re);
}
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> re;
dfs(root, re);
return re;
}
};
思路:
(1)第一种方法:自顶向下遍历二叉树(推荐);
深度遍历函数用参数维护当前子树的理论上界和理论下界;
如果当前子树根节点不在理论上界和理论下界之中,则可以判断不符合二叉搜索树;
这种方法时间和空间的开销最佳,但理论上界和理论下界的处理要妥当;
这里需要用long long
类型或者double
类型(推荐)来存放理论上界和理论下界;
int
类型转double
类型的速度比转long long
类型的快;
(2)第二种方法:中序遍历并用栈保持遍历结果;
如果遍历结果是单调增,即可判断符合二叉搜索树;
需要用一个额外的空间来存放遍历结果,时间上也略长,因为整个二叉搜索树均需要完整遍历,无法提前剪枝;
但实现起来最简单;
(3)第三种方法:自底向上遍历二叉树;
深度遍历函数返回当前子树的实际下界和实际上界;
如果当前根节点小于等于左子树的上界,或者大于等于右子树的下界,则可以判断不符合二叉搜索树;
实际的时间和空间消耗都是最大的,因为要用vector
做返回类型,需要频繁进行值复制;
代码:
方法一:自顶向下遍历
class Solution {
private:
bool dfs(TreeNode* root, double lower, double upper) {
if(root->val <= lower || root->val >= upper) {
return false;
}
bool left_val = true, right_val = true;
if(root->left != nullptr) {
left_val = dfs(root->left, lower, root->val);
}
if(root->right != nullptr) {
right_val = dfs(root->right, root->val, upper);
}
return left_val && right_val;
}
public:
bool isValidBST(TreeNode* root) {
// 注意题目所给范围已经是完整的int范围,所以上下界需要进一步拓展
return dfs(root, LONG_MIN, LONG_MAX);
}
};
class Solution {
private:
stack<int> s;
void dfs(TreeNode* root) {
if(root == nullptr) {
return;
}
dfs(root->left);
s.push(root->val);
dfs(root->right);
}
public:
bool isValidBST(TreeNode* root) {
dfs(root);
// 检验站内元素是否单调增
int tmp = s.top();
s.pop();
while(!s.empty()) {
if(tmp <= s.top()) {
return false;
}
tmp = s.top();
s.pop();
}
return true;
}
};
class Solution {
private:
bool re;
vector<int> dfs(TreeNode* root) {
vector<int> left_vec = {root->val, root->val}, right_vec = {root->val, root->val};
if(re && root->left != nullptr) {
left_vec = dfs(root->left);
if(left_vec[1] >= root->val) {
re = false;
}
}
if(re && root->right != nullptr) {
right_vec = dfs(root->right);
if(right_vec[0] <= root->val) {
re = false;
}
}
return {left_vec[0], right_vec[1]};
}
public:
bool isValidBST(TreeNode* root) {
re = true;
dfs(root);
return re;
}
};
int
最小值和最大值:min = int(~((unsigned)(-1) >> 1))
,max = int((unsigned)(-1) >> 1)
;
int(-1)
的补码是1111111111...111
(32个1,含1个符号位);(unsigned)(-1)
就是取1111111111...111
为原码, 是unsigned_int
的最大值,为2^32-1
;011111111...111
(31个1,符号位为0),是int
的最大值,为2^31-1
;10000000...000
,是-0
作-2^31
的补码(为了充分利用空间),也是int
的最小值;int
类型在存储时用的是补码来存储;int
最小值和最大值也可以直接写:INT_MIN
和INT_MAX
;long
最小值和最大值也可以直接写:LONG_MIN
和LONG_MAX
;float
最小值和最大值也可以直接写:FLT_MIN
和FLT_MAX
;double
最小值和最大值也可以直接写:DBL_MIN
和DBL_MAX
;class Solution {
private:
bool isSymmetricHelp(TreeNode* leftTree, TreeNode* rightTree) {
if(leftTree == nullptr && rightTree == nullptr) {
return true;
}
else {
if(leftTree == nullptr || rightTree == nullptr) {
return false;
}
}
if(leftTree->val != rightTree->val) {
return false;
}
return isSymmetricHelp(leftTree->left, rightTree->right) && isSymmetricHelp(leftTree->right, rightTree->left);
}
public:
bool isSymmetric(TreeNode* root) {
return isSymmetricHelp(root->left, root->right);
}
};
思路:
方法1:用两个队列交换记录每一层的广度优先遍历;
方法2:只用一个队列记录,但每层遍历前先用一个变量q_size
记录当前队列(也就是当前层)元素的数量;
方法1容易想到,但是实现起来比较繁琐;方法2实现较为整洁;两者的时间复杂度和空间复杂度均相同;
和剑指offer算法题01中的五、5. 变体1. 按层输出的从上到下打印二叉树同题;
代码:
这里只写方法2的实现;
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> re;
if(root == nullptr) {
return 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();
cur.push_back(tmp->val);
q.pop();
if(tmp->left != nullptr) {
q.push(tmp->left);
}
if(tmp->right != nullptr) {
q.push(tmp->right);
}
}
re.push_back(cur);
}
return re;
}
};
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root == nullptr) {
return 0;
}
int left = maxDepth(root->left);
int right = maxDepth(root->right);
return max(left, right) + 1;
}
};
思路2:
也可以用广度优先遍历,逐层增加高度即可;
代码2:
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root == nullptr) {
return 0;
}
queue<TreeNode*> q;
q.push(root);
int depth = 0;
while(!q.empty()) {
++depth;
int q_size = q.size();
for(int i=0;i<q_size;++i) {
TreeNode* cur = q.front();
q.pop();
if(cur->left != nullptr) {
q.push(cur->left);
}
if(cur->right != nullptr) {
q.push(cur->right);
}
}
}
return depth;
}
};
class Solution {
private:
/*
preorder[p1, p2]和inorder[p3, p4]共同表示一棵子树
*/
TreeNode* buildTreeHelp(vector<int>& preorder, vector<int>& inorder, int p1, int p2, int p3, int p4) {
int root_p1 = p1;
int root_val = preorder[root_p1];
int root_p2 = -1;
// inorder中找根节点
for(int i=p3;i<=p4;++i) {
if(root_val == inorder[i]) {
root_p2 = i;
break;
}
}
// preorder中找左右子树分界点
int left_nums = root_p2 - p3; // 左子树元素个数
int left_pr1 = root_p1 + left_nums;
int right_nums = p4 - root_p2; // 右子树元素个数
// 建root节点
TreeNode *root = new TreeNode(root_val);
if(left_nums > 0) {
root->left = buildTreeHelp(preorder, inorder, root_p1+1, left_pr1, p3, root_p2-1);
}
else {
root->left = nullptr;
}
if(right_nums > 0) {
root->right = buildTreeHelp(preorder, inorder, left_pr1+1, p2, root_p2+1, p4);
}
else {
root->right = nullptr;
}
return root;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
return buildTreeHelp(preorder, inorder, 0, preorder.size()-1, 0, inorder.size()-1);
}
};
class Solution {
public:
/*
先序遍历是根左右,也就是说左子树需要全部位于右子树之前
1. 将右子树移动到左子树的最右下节点(左子树先序遍历的最后一个节点)的right指针下
2. 将左子树(连同刚刚移动的右子树)移动到root->right,并置root->left = nullptr
3. 移动root指针到root->right(也就是刚刚左子树的根节点),继续处理
*/
void flatten(TreeNode* root) {
TreeNode *cur = root;
while(cur != nullptr) {
TreeNode *tmp = cur->left;
if(tmp != nullptr) {
// 左节点不为空才移动右子树和左子树
while(tmp->right != nullptr) {
tmp = tmp->right;
}
// 第一步
tmp->right = cur->right;
// 第二步
cur->right = cur->left;
cur->left = nullptr;
}
// 第三步
cur = cur->right;
}
}
};
思路:
由于字母的类型有26
种,所以前缀树每个节点都有26
个子节点;
因此实现上,就不是仅有两个指针指向子节点,而是用一个指针数组存26
个指针分别指向26
个子节点;
是二叉树的扩展,为26
-叉树;
从扩展节点结构中可以看出:
如果是判断存在前缀,则无需判断当前节点是否is_end == true
;
但如果是判断是否存在单词,则需要判断当前节点是否is_end == true
;
另外,一定不会出现通过别的路径到达当前is_end == true
节点的,有且仅有一种可以到达当前节点的路径;
代码:
class Trie {
private:
vector<Trie*> children; // 指向子节点的指针数组
bool is_end; // 标记当前节点是否为某个单词的结尾
public:
Trie() {
// 相当于先构建一个对象,再用vector的=重载函数复制过去
children = vector<Trie*>(26, nullptr);
is_end = false;
}
void insert(string word) {
Trie* cur_node = this;
for(int i=0;i<word.length();++i) {
int cur_letter = word[i] - 'a';
if(cur_node->children[cur_letter] == nullptr) {
cur_node->children[cur_letter] = new Trie();
}
cur_node = cur_node->children[cur_letter];
}
// 最后一个字母的节点标记is_end
cur_node->is_end = true;
}
bool search(string word) {
Trie* cur_node = this;
for(int i=0;i<word.length();++i) {
int cur_letter = word[i] - 'a';
if(cur_node->children[cur_letter] == nullptr) {
return false;
}
cur_node = cur_node->children[cur_letter];
}
// 检查是仅前缀还是完整的单词
if(cur_node->is_end) {
return true;
}
else {
return false;
}
}
bool startsWith(string prefix) {
Trie* cur_node = this;
for(int i=0;i<prefix.length();++i) {
int cur_letter = prefix[i] - 'a';
if(cur_node->children[cur_letter] == nullptr) {
return false;
}
cur_node = cur_node->children[cur_letter];
}
// 不需要进一步检查是否为完整单词
return true;
}
};
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr) {
return root;
}
TreeNode *left = invertTree(root->left);
TreeNode *right = invertTree(root->right);
root->left = right;
root->right = left;
return root;
}
};
题目:https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/
思路:
和剑指offer算法题01中的五、12. 变体. 二叉树的最近公共祖先同题;
注意这样的一个点:如果root==p
或者root==q
,则root
就是所求的公共祖先,没有必要继续向下遍历了;
代码:
这个实现是额外增加了dfs函数的写法,也可以直接用lowestCommonAncestor
函数来递归,参看:剑指offer算法题01中的五、12. 变体. 二叉树的最近公共祖先;
class Solution {
private:
TreeNode *re;
// 返回true表明该子树至少含有p或者q中的一个
bool dfs(TreeNode *root, TreeNode *p, TreeNode *q) {
if(root == nullptr) {
return false;
}
bool is_left = dfs(root->left, p, q);
bool is_right = dfs(root->right, p, q);
if(root == p || root == q) {
// root是其中一个节点,则需要检查它的孩子是否有另一个true
// 但实际上其实是不需要检查了,root就是所求节点
// 因为最先遇到的是root,另一个节点必定在它的子树里面
if(is_left || is_right) {
re = root;
}
return true;
}
else {
// 否则,就要检查两个孩子是否均有一个true
if(is_left && is_right) {
re = root;
}
if(is_left || is_right) {
return true;
}
else {
return false;
}
}
}
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
dfs(root, p, q);
return re;
}
};
root
出发到当前节点(含当前节点)经过的所有节点的值之和;a
和b
节点之间的路径和(含a
和b
)为:prefix(x)
返回节点x
的前缀和;b
时,如果要判断是否有某个以b
为结尾的路径满足路径和为targetSum
,则只需判断之前是否遍历过前缀和为prefix(b) - targetSum
的父节点即可;prefix(b) - targetSum
的父节点,若有,统计有多少个这样的父节点,个数就是符合条件的路径个数;unordered_map
实现,这里只存前缀和是key
的节点有value
个;0 -> 1
的映射,也就是前缀和为0的节点有一个,这是因为prefix(a.parent)
中的a
如果是root
节点则无法计算(也就是当某个节点的前缀和恰好等于targetSum
时),因为深度优先遍历时root
之前并无记录,故可视作在root
前增加了一个前缀和为0的节点;/**
一个例子如下,括号中是当前节点的前缀和:
5(5)
4(9) 8(13)
11(20) 13(26) 4(17)
7(27) 2(22) 5(22) 1(18)
*/
class Solution {
private:
// 前缀和映射表,前缀和为[root to cur],含当前节点
// [first, second]表示:当前节点的前缀和为first的父节点的数量是second
// 前缀和有可能超过int的范围,故用long long类型
unordered_map<long long, int> prefix_map;
// 目标是找prefix_cur - prefix_parent == targetSum的数量
// 等价于first的目标值 = prefix_cur - targetSum
int dfs(TreeNode* root, const int targetSum, long long prefix) {
if(root == nullptr) {
return 0;
}
else {
long long prefix_cur = prefix + root->val; // 当前节点的前缀和
int re = 0;
if(prefix_map.find(prefix_cur-targetSum) != prefix_map.end()) {
re = prefix_map[prefix_cur-targetSum];
}
if(prefix_map.find(prefix_cur) == prefix_map.end()) {
prefix_map[prefix_cur] = 0;
}
++prefix_map[prefix_cur]; // 把当前节点放到父节点路径里面
re += dfs(root->left, targetSum, prefix_cur);
re += dfs(root->right, targetSum, prefix_cur);
--prefix_map[prefix_cur]; // 当前节点从父节点路径里面移除
return re;
}
}
public:
int pathSum(TreeNode* root, int targetSum) {
// first = 0表示当前路径恰好是从root出发的
// 也就是对某个节点而言,它的前缀和恰好等于targetSum
// 如果不加这个值,则所有从root出发的符合条件的路径都不计数
prefix_map[0] = 1;
return dfs(root, targetSum, 0);
}
};
int
类型肯定是32位的;
long (int)
类型由编译平台决定,可能是32位或者64位;
long long (int)
类型肯定是64位的;
因此,如果需要突破int
的范围,则推荐使用long long
;
long
一般情况下不使用,因为无法确定它可表示数的范围;
参考博文:c++中int, long, long long都占几个字节和编译平台定义有关;
一些编译器内部定义的别名如下:
char => int8_t
short int => int16_t
int => int32_t
long int => int32_t / int64_t
long long int => int64_t
float => float32_t
double => float64_t
sum
记录累加即可;class Solution {
private:
int sum;
// 按照右根左遍历即是从大到小遍历,可以计算累加和
void dfs(TreeNode *root) {
if(root == nullptr) {
return;
}
dfs(root->right);
sum += root->val;
root->val = sum;
dfs(root->left);
}
public:
TreeNode* convertBST(TreeNode* root) {
sum = 0;
dfs(root);
return root;
}
};
sum_prefix
记录当前的已有的前缀和;
class Solution {
private:
// 返回以root为子树的所有节点值之和
int dfs(TreeNode *root, int sum_plefix) {
if(root->right != nullptr) {
// 计算右子树之和
int sum_right = dfs(root->right, sum_plefix);
// 累加和 = root->val + 右子树之和
root->val += sum_right;
}
else {
// 累加和 = root->val + 前缀和
root->val += sum_plefix;
}
if(root->left != nullptr) {
int sum_left = dfs(root->left, root->val);
return sum_left;
}
else {
return root->val;
}
}
public:
TreeNode* convertBST(TreeNode* root) {
if(root == nullptr) {
return root;
}
int sum = dfs(root, 0);
return root;
}
};
class Solution {
private:
int re;
// 返回当前子树的最大高度
int dfs(TreeNode *root) {
if(root == nullptr) {
return 0;
}
int left_height = dfs(root->left);
int right_height = dfs(root->right);
// 计算当前子树的直径
int diameter = left_height + right_height;
if(re < diameter) {
re = diameter;
}
return max(left_height, right_height) + 1;
}
public:
int diameterOfBinaryTree(TreeNode* root) {
re = 0;
int depth = dfs(root);
return re;
}
};
思路:
就是同时深度优先搜索遍历两棵树;
可以生成一棵新的树,也可以直接在原树上修改;
代码:
生成一棵新的树的代码如下:
class Solution {
TreeNode* dfs(TreeNode *root1, TreeNode *root2) {
if(root1 == nullptr) {
return root2;
}
if(root2 == nullptr) {
return root1;
}
TreeNode *new_node = new TreeNode(root1->val + root2->val);
new_node->left = dfs(root1->left, root2->left);
new_node->right = dfs(root1->right, root2->right);
return new_node;
}
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
return dfs(root1, root2);
}
};
class Solution {
// root1是被重叠的树,root2是另一颗树,且均不为空
void dfs(TreeNode *root1, TreeNode *root2) {
root1->val += root2->val;
if(root1->left == nullptr) {
if(root2->left != nullptr) {
root1->left = root2->left;
}
}
else {
if(root2->left != nullptr) {
dfs(root1->left, root2->left);
}
}
if(root1->right == nullptr) {
if(root2->right != nullptr) {
root1->right = root2->right;
}
}
else {
if(root2->right != nullptr) {
dfs(root1->right, root2->right);
}
}
}
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
if(root1 == nullptr) {
return root2;
}
if(root2 == nullptr) {
return root1;
}
dfs(root1, root2);
return root1;
}
};
class Solution {
/*
后序遍历:左右根
1. 在当前root计算经过当前root的最大路径和
2. 向上返回以root为起始点的最大路径和
需要考虑负数的影响
*/
private:
int re_max;
int dfs(TreeNode* root) {
if(root == nullptr) {
return 0;
}
// 1. 在当前root计算经过当前root的最大路径和
int left = dfs(root->left);
int right = dfs(root->right);
int route_sum = left + root->val + right;
if(route_sum > re_max) {
re_max = route_sum;
}
// 2. 向上返回以root为起始点的最大路径和
// 注意,若以root为起始点的最大路径和仍小于0,则舍弃
// 也就是说向上返回的最小值是0
return max(max(left, right) + root->val, 0);
}
public:
int maxPathSum(TreeNode* root) {
re_max = INT_MIN; // 初始值是整数最小值
int tmp = dfs(root);
return re_max;
}
};
思路:
(1) 序列化:
string
类型的字符串中;,
分隔;NULL
字符串标识;(2) 反序列化:
string
类型字符串,将每个字符串分开并保存到vector
数组中;遍历的方式有两种:
node1,node2,...noden
;
根 -> 左 -> 右
;root
节点都要记录到字符串中,即使节点为空;根 -> 左 -> 右
;root
,并递归处理它的左右子树;node1,node2,...noden,
;
特别注意:
new
申请空间,则传参数的时候只能用引用来传递指针;代码1:深度优先遍历实现
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Codec {
private:
void DFS1(TreeNode* root, string& s) {
if(root == nullptr) {
s += "NULL";
return;
}
s += to_string(root->val);
s += ',';
DFS1(root->left, s);
s += ',';
DFS1(root->right, s);
}
// TreeNode*&必须加引用,不然左右子树和root没有办法连接上
void DFS2(vector<string>& words, int& i, TreeNode*& root) {
if(i >= words.size()) {
return;
}
string word = words[i];
++i;
if(word == "NULL") {
return;
}
else {
root = new TreeNode(stoi(word));
DFS2(words, i, root->left);
DFS2(words, i, root->right);
}
}
public:
// Encodes a tree to a single string.
string serialize(TreeNode* root) {
string s;
DFS1(root, s);
return s;
}
// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
// 解码字符串,保存到数组中
vector<string> words;
int i = 0, j = 0;
while(j < data.length()) {
if(data[j] == ',') {
words.emplace_back(data.substr(i, j-i));
i = j+1;
}
++j;
}
words.emplace_back(data.substr(i, j-i));
TreeNode* root = nullptr;
int index = 0;
DFS2(words, index, root);
return root;
}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
###
思路:
用一个栈来进行匹配即可;
本来是打算用计数的方式来判断的(省点空间),结果不仅实现复杂,而且不能判断不同类型括号之间扫描的先后顺序,因此也不能判断是否合规,如{[}]
的这个例子,所以还是老老实实地用栈做吧;
注意,也不能用多个栈,不然不同类型括号之间还是不能判断扫描的先后顺序;
代码:
class Solution {
public:
bool isValid(string s) {
stack<char> stack;
for(int i=0;i<s.length();++i) {
switch(s[i]) {
case '(':
case '[':
case '{':
stack.push(s[i]);
break;
case ')':
if(!stack.empty() && stack.top() == '(') {
stack.pop();
}
else {
return false;
}
break;
case ']':
if(!stack.empty() && stack.top() == '[') {
stack.pop();
}
else {
return false;
}
break;
case '}':
if(!stack.empty() && stack.top() == '{') {
stack.pop();
}
else {
return false;
}
break;
default:
return false;
}
}
if(!stack.empty()) {
return false;
}
else {
return true;
}
}
};
思路:
和剑指offer算法题01的3. 包含min函数的栈同题;
一度想用队列(滑动窗口)的那种方法来做,就是用一个双向队列:
val
比队列尾的元素大,则逐一弹出,直到不比它大;val
比队列尾的元素小,则逐一弹出,直到不比它小;但实际上不需要这么复杂,用一个栈来实现即可:
val
比栈顶的元素大,则压入;val
比栈顶的元素小,则压入;先入后出结构和先入先出结构的单调栈内的单调性刚好相反,但与栈和队列的性质对应,即:
也就是说,栈用栈辅助,队列用双向队列辅助,虽然双向队列本质上还是一个单调栈;
代码:
class MinStack {
private:
stack<int> s;
stack<int> min_s;
public:
MinStack() {
}
void push(int val) {
s.push(val);
if(min_s.empty() || val<=min_s.top()) {
min_s.push(val);
}
}
void pop() {
if(min_s.top() == s.top()) {
min_s.pop();
}
s.pop();
}
int top() {
return s.top();
}
int getMin() {
return min_s.top();
}
};
k
个元素后才开始记录结果,并不是从一开始就记录最大值;class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> q; // front是最大值,其余单调非增
vector<int> re;
for(int i=0;i<nums.size();++i) {
if(q.empty()) {
q.push_back(nums[i]);
}
else {
while(!q.empty() && q.back()<nums[i]) {
// 相等的仍保留在q中
q.pop_back();
}
q.push_back(nums[i]);
}
if(i >= k-1) {
// 够滑动窗口才开始记录最大值
re.push_back(q.front());
}
if(i>=k-1 && q.front()==nums[i-(k-1)]) {
// 弹出q中即将不在滑动窗口内的元素
q.pop_front();
}
}
return re;
}
};
class Solution {
public:
/*
单调栈:
1. 维护一个栈顶是最小值,从顶到底递增的栈
2. 记录每天的下标即可
*/
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> s;
vector<int> re(temperatures.size(), 0);
for(int i=0;i<temperatures.size();++i) {
if(s.empty()) {
// 栈为空
s.push(i);
}
else {
// 栈不为空
while(!s.empty() && temperatures[s.top()]<temperatures[i]) {
// 处理比今天温度低的记录
re[s.top()] = i - s.top();
s.pop();
}
s.push(i);
}
}
return re;
}
};
class Solution {
public:
/*
单调栈:
1. 每个柱子仅考虑以它的最大高度可以构成的最大矩形
2. 维护一个单调栈,栈顶到底单调递减
*/
int largestRectangleArea(vector<int>& heights) {
stack<int> s;
// 在前后增加高度为0的柱子作为哨兵,以减少讨论
vector<int> new_heights(heights.size() + 2, 0);
for(int i=0;i<heights.size();++i) {
new_heights[i+1] = heights[i];
}
int re_max = 0;
for(int i=0;i<new_heights.size();++i) {
if(s.empty()) {
s.push(i);
}
else {
while(!s.empty() && new_heights[s.top()]>new_heights[i]) {
int cur = s.top(); // 当前栈顶柱子,用于计算面积
s.pop();
int width = i - s.top() - 1; // 宽取栈内前一个柱的下标
int height = new_heights[cur]; // 高取当前柱的高度
// 面积 = 底 * 高
int square = width * height;
if(square > re_max) {
re_max = square;
}
}
s.push(i);
}
}
return re_max;
}
};