文章目录
- 数据结构
- 动态规划
- 分治
- 查找
- 双指针
- 位运算
- 数学
- Hot 100
- 2两数之和(中等)
- 19删除链表的倒数第N个节点(中等)
- 31下一个排序(数组)(中等)
- 39组合总和(中等)(回溯)
- 48 旋转图像(中等)(找规律)
- 64 最小路径和(中等)
- 75颜色分类(中等)(双指针)
- 96不同的二叉搜索树(中等)(动态规划)
- 114二叉树展开为链表(中等)
- 128 最长连续序列(中等)(hash)
- 136只出现一次的数字(简单)
- 139单词拆分(中等)(动态规划)(待完善)
- 152 乘积最大子数组(中等)(动态规划)
- 867 链表的中间节点
- 148排序链表(中等)(归并 快排)
- 160相交链表(简单)(特殊)
- 207. 课程表(中等)(待补充)(拓扑)
- Sort的实现原理及相关问题
- 215 数组中的第k个最大元素
- 221 最大正方形(中等)(动态规划)
- 234回文链表(简单)
- 238 除自身以外数组的乘积(中等)
- 240搜索二维矩阵2
- 279完全平方数(中等)(背包 动态规划)(待完善)
- 287寻找重复数(中等)(二分 快慢)(特别注意)
- 347 前k个高频元素(中等)(重要)
- 394. 字符串解码(中等)(辅助栈)(重要)
- 406 根据身高重建队列(中等)(贪心)(特别)
- 448 找到所有数组中消失的数字(简单)(数组的原地修改)
- 461 汉明距离(简单)(位运算)
- 494 目标和(中等)(回溯)(背包)(待补充)
- 01背包和完全背包练习题(待补充)
- 543 二叉树的直径(简单)(递归)
- 560 和为k的子数组(中等)(前缀+哈希)
- 前缀的题目(待补充)
- 前缀和哈希优化的题目(待补充)
- 617合并二叉树(简单)(可以复习)
- 416 分割等和子集(中等)(背包问题 )(待补充)
- 538把二叉树转换成累加树(中等)(技巧)
- 581最短无序连续子数组(中等)(规律)
- 437 路劲之和3(中等)(回溯 + 前缀和)
- 621任务调度器(中等)
补充的题目
数据结构
59-2队列的最大值(中等)(单调队列)
- 使用单调队列实现作为辅助实现就可以了
- 有一个忽视的知识点,
队列是尾部加入数据,头部输出数据,所以最先删除的是front。这和我构建的单调队列是相反的,我是把新数据放入头部,删除数据从尾部开始
class MaxQueue {
public:
deque<int> deque;//构建单调队列
queue<int> que;//构建优先队列
MaxQueue() {
}
int max_value()
{
if(deque.empty()==true ) return -1;
return deque.back();
}
//我们还是喜欢从头部放入数据
void push_back(int value)
{
que.push(value);
while(deque.empty()!=true && value > deque.front())
{
deque.pop_front();
}
deque.push_front(value);
}
//我们还是喜欢从尾部删除数据
int pop_front()
{
if (deque.empty() == true) return -1;
//队列是尾部添加元素 头部删除元素
int temp = que.front();
que.pop();//删除元素
if( deque.empty()!=true && deque.back() == temp)
{
deque.pop_back();
}
return temp;
}
};
/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue* obj = new MaxQueue();
* int param_1 = obj->max_value();
* obj->push_back(value);
* int param_3 = obj->pop_front();
*/
- 第二遍复习 没啥问题
动态规划
46把数字翻译成字符串(中等)
-
很容易想到动态规划,递推公式也比较容易想到。情况比较少
- dp[i] 表示以长度为i的字符串数字有几种翻译的方法 下标表示长度
- 递推公式 当倒数第二个数是1 说明必定有两种可能,最后一个数子单独或者最后两个数字一起 dp【i】=dp[i-1]+dp[i-2]。因为如果确定下来以哪种方式翻译就由之前几种方案决定 了。 当倒数第二个数是2 倒数第一个小于等于5则递推公式如上。其他的dp【i】=dp【i-1】
- 出现dp【i-2】所以把dp【0】和dp【1】确定就好
-
第二个代码为状态压缩的代码
class Solution {
public:
int translateNum(int num)
{
string s =to_string(num);
vector<int> dp(s.size()+1);
dp[0]=1;
dp[1]=1;
//dp下标表示长度 从长度为2开始
for(int i =2;i<=s.size();i++)
{
//当前下标为i-1 前一个下标为i-2
if(s[i-2] == '1' || (s[i-2]=='2'&&s[i-1] <= '5'))
dp[i]=dp[i-1]+dp[i-2];
else
{
dp[i]=dp[i-1];
}
}
return dp[s.size()];
}
};
)
class Solution {
public:
int translateNum(int num) {
string s = to_string(num);
int cur = 1, pre = 1, lastpre = 1;
for(int i = 2; i < s.size()+1; ++i)
{
if(s[i-2] == '1' ||(s[i-2] == '2' && s[i-1] <= '5'))
cur = pre + lastpre;
else
cur = pre;
lastpre = pre;
pre = cur;
}
return cur;
}
};
- 第二遍复习
- 主要就是思考过程 这个区别于一个 从1开始计算 分大情况 0 和 非0 这个题不用
- 直接分成一种解法 和二种解法 因为二种解法的可能比较少 所以放在 if 其他放在else 看代码注释
class Solution {
public:
//动态规划
//下标表示长度 因为如果为空是一种
//dp【i】表示长度为i的字符串有多少种解法 一般就是考虑 最后一个元素和倒数第二个元素
//分析情况
//区别之前解码 这个解码从0开始 从1开始考虑的情况比较复杂(大情况 最后一个元素0还是非0)
//怎么分情况最后 只有一种解法的 倒数第二位不是1 也不是 2 倒数第二位是2 倒数第一位大于5 其他的都归成一类
int translateNum(int num)
{
string s =to_string(num);
int n =s.size();
if(n==0 || n==1) return 1;
vector<int> dp(n+1);
dp[0]=1;
dp[1]=1;
for(int i=2;i<=n;i++)
{
//当前下标为i-1 前一个下标为i-2
if(s[i-2] == '1' || (s[i-2]=='2'&&s[i-1] <= '5'))
dp[i]=dp[i-1]+dp[i-2];
else
{
dp[i]=dp[i-1];
}
}
return dp[n];
}
};
47礼物的最大价值(中等)
- 比较简单 和 几个方案是一样的 没什么区别 。
- 递归公式是dp[i][j]=max(dp[i-1][j]+dp[i][j-1])+grid[i][j];
- dp[][] 表示走到当前位置礼物的最大价值
- 初始化第一行和第一列 小的动态规划 累加
class Solution {
public:
int maxValue(vector<vector<int>>& grid)
{
if(grid.size()==0) return 0;
int n= grid.size();
int m =grid[0].size();
//初始化边界
vector<vector<int>> dp(n,vector<int>(m));
dp[0][0]=grid[0][0];
for(int i =1;i < m;i++)
{
dp[0][i]=dp[0][i-1]+grid[0][i];
}
for(int i =1;i < n;i++)
{
dp[i][0]=dp[i-1][0] +grid[i][0];
}
for(int i =1 ;i<n;i++)
{
for(int j=1;j<m;j++)
{
//dp[i][j]=max(dp[i-1][j]+dp[i][j-1])+num[i][j];
if(dp[i-1][j] >dp[i][j-1]) dp[i][j] =dp[i-1][j]+grid[i][j];
else dp[i][j]= dp[i][j-1]+grid[i][j];
}
}
return dp[n-1][m-1];
}
};
- 做过很多次 没什么问题
48 最长不含重复字符的子字符串(中等)
-
思考还是以当前坐标为结尾还是长度。想了想还是以当前坐标为结尾比较好。毕竟这样下一个元素还能更上一个找到联系
-
递归公式,如果当前元素与上一个元素为结尾的字符串没有冲突那就+1 ,如果有冲突 那就从后向前重新找最长的字符串(我觉的这边可能要构建map来判重)
-
看了题解 跟我想的有一点点区别
-
主要分为三种情况
- 如果 s【j】在hash从来没有出现 毫无以为最大长度为 dp【j-1】+1
- 如果 s【j】 在hash出现过需要分两种情况
- 他出现在dp【j-1】的范围之外(
就比如dp【i-1】最大长度是下标2-5 然后下标6的元素和第下标1冲突了 那没关系 dp【j】=dp【j-1】+1
) - 如果在范围内 那就要缩减范围 dp【j】=j-i
举个例子 2 3 4 2 坐标 3-0=3 刚好等于新序列的长度(3 4 2)
; 如何判断在判断范围内外呢 用 j-i 和 dp【i-1】(tmp)进行比较 。比如 1 2 3 4 5。其中 2 3 4前一个长度为3 5-1=4(j-i)大于tmp 那就是超过范围
- 他出现在dp【j-1】的范围之外(
class Solution {
public:
int lengthOfLongestSubstring(string s)
{
unordered_map<char, int> hash;//元素 最后一次出现的下标
int res = 0; //时刻保存最大值
int tmp = 0;//表示前一个值
int len = s.size();
int i;//表示取出的下标
for(int j = 0; j < len; j++)
{
if(hash.find(s[j]) == hash.end())
{
//i = - 1;
hash[s[j]] = j; // 更新哈希表 保存这一次出现的下标
tmp=tmp+1;
}
else //出现了重复
{
i = hash[s[j]]; // 获取索引 i 获得最后一次出现索引的位置
if(tmp < j-i)//但是出现的重复是在上一个序列的范围外的
{
tmp =tmp+1;//那么长度还是dp【i-1】+1
}
else//在dp【i-1】的长度返回内的 那么就要缩减长度了
{
tmp=j-i;
}
hash[s[j]] = j; // 更新哈希表 保存这一次出现的下标
}
res = max(res, tmp); // max(dp[j - 1], dp[j])
}
return res;
}
};
- 第二遍复习 修改成更符合dp的写法,很聪明的方法 主要是注意我们 dp【j】 和 hash[s[j]]=j;
class Solution {
public:
int lengthOfLongestSubstring(string s)
{
if(s.size()==0) return 0;
if(s.size()==1) return 1;
int i;
unordered_map<char,int> hash;
int len = s.size();
vector<int> dp(len,1);//初始化为1 最起码为1
//把第一个值挂上去
hash[s[0]]=0;
int result=1;
for(int j=1;j<len;j++)
{
if(hash.count(s[j])==0)//表示hash没有出现过
{
hash[s[j]] = j;//挂上值 和 下标
dp[j]=dp[j-1]+1;
}
else//出现重复了
{
i = hash[s[j]];//保存一下最近一次出现的下标
int temp = dp[j-1];//范围 在图上画一下 0(i) 1 2 3 4(j)
if(j-i >temp)//想想一下 外面的圈大于里面的 那没事
{
dp[j]=dp[j-1]+1;
}
else
{
// dp[i]=j-i+1;//自己举个例子就好
dp[j]=j-i;//不要+1 老是写错
}
//无论重复了 是不是在 范围内 都要修改
hash[s[j]]=j;
}
result=max(result,dp[j]);
}
return result;
}
};
n个骰子的点数(中等)
- 解题思路如下
- dp【i】【j】其中i表示几个骰子 j表示骰子的总和是多少。dp的含义是出现的次数而非概率。
- 确定递推公式 从上图可知 dp【i】【j】=dp【i-1】【j-1】+dp[i-1]dp[j-2]+… dp[i-1][j-6]
当然前提规则是 j -k >=i-1 因为i个骰子 取值范围是 i 到 6i 所以 最多就是 dp【i-1】【i-1】到dp【i-1】【6i-6】故 j -k >=i-1
- 初始化 dp【1】【…】
- 这题比较特殊一开始确实没想到这么做
class Solution {
public:
vector<double> twoSum(int n) {
//n <= 11
vector<vector<double>>dp(n + 1, vector<double>(6*n + 1, 0));
vector<double> ans;
for(int i = 1; i <= n; i ++){
for(int j = i; j <= 6*i; j ++){
if(i == 1) {
dp[i][j] = 1;
continue;
}
for(int k = 1; k <= 6; k ++){
if(j - k >= i - 1) dp[i][j] += dp[i - 1][j - k];
}
}
}
for(int i = n; i <= 6*n; i ++){
ans.push_back(dp[n][i] * pow(1.0/6, n));
}
return ans;
}
};
class Solution {
public:
vector<double> twoSum(int n) {
//n <= 11
vector<double>dp(6*n + 1, 0);
vector<double> ans;
for(int i = 1; i <= n; i ++){
for(int j = 6*i; j >= i; j --){
dp[j] = 0;
if(i == 1) {
dp[j] = 1;
continue;
}
for(int k = 1; k <= 6; k ++){
//需要保证***总和得大于等于***数
if(j - k >= i - 1) dp[j] += dp[j - k];
}
}
}
for(int i = n; i <= 6*n; i ++){
ans.push_back(dp[i] * pow(1.0/6, n));
}
return ans;
}
};
class Solution {
public:
vector<double> dicesProbability(int n)
{
vector<double> result;
//dp范围 下标表示个数最大值为n所以n+1行. 最大值6n 所以 6n+1
vector<vector<double>> dp(n+1,vector<double>(6*n+1,0));
//初始化
for(int j=1;j<=6;j++)
{
dp[1][j]=1;
}
for(int i=2;i<=n;i++)
{
for(int j=i;j<=6*i;j++)
{
//这边的k 和循环用来控制递推公式的
for(int k=1;k<=6;k++)
{
//注意看这边的j-k 和 i-1对应的位置
if(j-k>=i-1) dp[i][j]+=dp[i-1][j-k];
}
}
}
for(int i=n;i<=6*n;i++)
{
result.push_back(dp[n][i]*1.0/pow(6,n));
}
return result;
}
};
235二叉树的最近公共祖先1(简单)(重)
-
要注意到题目给出的信息,二叉搜索树 表示这个树是有序的
参考连接 -
题解利用了性质:
其实只要从上到下遍历的时候,cur节点是数值在[p, q]区间中则说明该节点cur就是最近公共祖先了。
-
所以我们需要先序遍历进行判断,后续遍历进行节点的返回(而且是立即返回根据什么情况返回的 )所以代码是理解判断的,区别于第二题需要遍历整棵树(因为只有遍历整棵树才能确保返回的值一定是公共祖先)
-
这个题特殊在于他有判断 相当于三个独立的判断
-
注意看最后一个代码 进行的修改可能更好理解了 就是把遇到共同祖先的判断 和 为空的判断作为递归终止条件放在最前面。那么后序的节点返回就可以理解成是以那种方式返回的,如果是非空节点 那就一路返回
TreeNode* left = traversal(cur->left, p, q);
if (left != NULL)//这个判断表示如果不是终止条件返回的NULL那必然是下一层返回的结果 直接返回
{
return left;
}
class Solution {
private:
TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) {
if (cur == NULL) return cur;
//情况一
if (cur->val > p->val && cur->val > q->val)// 左
{
TreeNode* left = traversal(cur->left, p, q);
if (left != NULL)//这个判断表示如果不是终止条件返回的NULL那必然是下一层返回的结果 直接返回
{
return left;
}
}
//情况二
if (cur->val < p->val && cur->val < q->val) // 右
{
TreeNode* right = traversal(cur->right, p, q);
if (right != NULL) {
return right;
}
}
//情况三 不包含前面两种情况 那就只可能是 《=cur《= 在两个值之间
return cur;
}
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
return traversal(root, p, q);
}
};
class Solution {
private:
TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q)
{
//也可以把这个看做终止条件
if (cur == NULL) return cur;
if ( cur->val <= p->val && cur->val >= q->val) return cur;
if ( cur->val >= p->val && cur->val <= q->val) return cur;
//情况一
if (cur->val > p->val && cur->val > q->val)// 左
{
TreeNode* left = traversal(cur->left, p, q);
if (left != NULL)//这个判断表示如果不是终止条件返回的NULL那必然是下一层返回的结果 直接返回
{
return left;
}
}
//情况二
if (cur->val < p->val && cur->val < q->val) // 右
{
TreeNode* right = traversal(cur->right, p, q);
if (right != NULL) {
return right;
}
}
//情况三 不包含前面两种情况 那就只可能是 《=cur《= 在两个值之间
return cur;//这个只是必须写的 其实不会走到这里
}
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
return traversal(root, p, q);
}
};
- 第二遍复习,这种中途退出 尽管不是判断 我们喜欢用void类型 修改代码如下
/**
* 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:
int flat=0;//两个节点必定存在的
TreeNode* result;//用于保存结果
void traversal(TreeNode* root, TreeNode* p, TreeNode* q)
{
//递归终止条件
if(flat==1) return;
if(root == NULL) return;
if ( root->val <= p->val && root->val >= q->val)
{
flat=1;
result=root;
return;
}
if ( root->val >= p->val && root->val <= q->val)
{
flat=1;
result=root;
return;
}
//这边 不需要进入两边
//情况一
if (root->val > p->val && root->val > q->val)// 左
{
traversal(root->left, p, q);
}
//情况二
if (root->val < p->val && root->val < q->val) // 右
{
traversal(root->right, p, q);
}
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
traversal(root, p, q);
return result;
}
};
236二叉搜索树的共同祖先2(中等)(重)
-
题目一样 不是搜索树
-
之前写过 注意这边的写法是全部遍历的一遍
-
这个终止条件只有一个 不能和上一题一样,因为我们就是需要后序全部遍历一遍(递归函数写在一起 判断放在后序 )
-
后序在二叉树并不是只能做节点的返回,还可以进行判断 求和 许多的操作
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == q || root == p || root == NULL) return root;//表示三个地方开始返回
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left != NULL && right != NULL) return root;//后续遍历看看那什么情况返回的
if (left == NULL && right != NULL) return right;
else if (left != NULL && right == NULL) return left;
else { // (left == NULL && right == NULL)
return NULL;
}
}
};
- 第二遍复习
- 做过很多次了
分治
17. 打印从1到最大的n位数(简单)
- 方法很多 要么回溯 要么正常做法
class Solution {
public:
vector<int> ans;
int pos = 0;
vector<int> printNumbers(int n) {
string s = "0123456789";
string str = "";
dfs(s, str, n);
return ans;
}
void dfs(string &s, string &str, int k){
if(str.length()== k){
if(pos==0){pos=1;return;} //前导零的去除
ans.push_back(atoi(str.c_str()));
return ;
}
for(int i=0; i<s.length();++i){
str+=s[i];
dfs(s, str, k);
str.pop_back();
}
}
};
查找
34 在排序数组中查找元素的第一个和最后一个(中等)
-
看到题的第一个想法就是二分查找,找到这个数 然后while定位它的左右边界 边界相减+1就是个数(可能存在一个问题就是太慢了 不符合二分查找的思想 比如 2 3 3 3 3 3 3 3 9).所以题解估计很难用这种方法来求解
-
这个写法是把 target>=middle 归为一类 来找右边界。
-
target<=middle 归为一类 来找左2边界。存在优化空间(这题最难的是细节的处理)
-
比如退出条件 》= 退出的结果是左边界的左边一个 右边界的右边一个,举个例子
//当left超过right退出 当单独一个数 6 7 我们找6 此时left 和 right都是指向6退出就会指向7
-
需要考虑的点很多,比如分3个情况 result初始值设为《-1的值 也不能是-1
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
// 情况一
if (leftBorder == -2 || rightBorder == -2) return {-1, -1};
// 情况三
if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1};
// 情况二
return {-1, -1};
}
private:
int getRightBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
} else { // 寻找右边界,nums[middle] == target的时候更新left
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
int getLeftBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
right = middle - 1;
leftBorder = right;
} else {
left = middle + 1;
}
}
return leftBorder;
}
};
class Solution {
public:
//寻找右边界 所以 target >= mid 归为一边 因为这样才可以把 left尽可能右移到边界
int getright(vector<int>& nums, int target)
{
int left = 0;
int right = nums.size()-1;
int result = -2; //设置一个初始值 不正常的就行 也不要-1这种
while(left<=right) //当left超过right退出 当单独一个数 6 7 我们找6 此时left 和 right都是指向6退出就会指向7
{
int mid =left+(right-left)/2;
if(target >= nums[mid])
{
left = mid+1;//
result = left;//记录一下右边界
}
else
{
right=mid-1;
}
}
return result;
}
//寻找左边界 所以target <=mid
int getleft(vector<int>& nums,int target)
{
int left =0;
int right=nums.size()-1;
int result=-2;//
while(left<=right)
{
int mid =left+(right-left)/2;
if(target<=nums[mid])
{
right= mid-1;
result=right;
}
else
{
left=mid+1;
}
}
return result;
}
vector<int> searchRange(vector<int>& nums, int target)
{
int left_result =getleft(nums,target);
int right_result = getright(nums,target);
//如果跳出了两个值还没有变化(没有=过) 那就说明不存在
if (left_result == -2 || right_result == -2) return {-1, -1};
if(right_result-left_result>1) return {left_result+1,right_result-1};//因为返回的结果是左边界的左边一个 右边界的右边一个
//这个是情况2 这个会走到
return{-1,-1};
}
};
- 第二遍复习
- 很聪明的想法,就是当我们去找左右边界 如果 left=mid 很容易陷入死循环 所以不如去找右边界的右边一个
- 注意main中的三种判断条件
53 在排序数组中查找数字1(简单)
- 修改一下返回值就好了时间复杂度是logn
class Solution {
public:
//寻找右边界 所以 target >= mid 归为一边 因为这样才可以把 left尽可能右移到边界
int getright(vector<int>& nums, int target)
{
int left = 0;
int right = nums.size()-1;
int result = -2; //设置一个初始值 不正常的就行 也不要-1这种
while(left<=right) //当left超过right退出 当单独一个数 6 7 我们找6 此时left 和 right都是指向6退出就会指向7
{
int mid =left+(right-left)/2;
if(target >= nums[mid])
{
left = mid+1;//
result = left;//记录一下右边界
}
else
{
right=mid-1;
}
}
return result;
}
//寻找左边界 所以target <=mid
int getleft(vector<int>& nums,int target)
{
int left =0;
int right=nums.size()-1;
int result=-2;//
while(left<=right)
{
int mid =left+(right-left)/2;
if(target<=nums[mid])
{
right= mid-1;
result=right;
}
else
{
left=mid+1;
}
}
return result;
}
int search(vector<int>& nums, int target)
{
int left_result =getleft(nums,target);
int right_result = getright(nums,target);
//如果跳出了两个值还没有变化(没有=过) 那就说明不存在
if (left_result == -2 || right_result == -2) return 0;
if(right_result-left_result>1) return right_result-1-left_result;//因为返回的结果是左边界的左边一个 右边界的右边一个
//这个是情况2
return 0;//这个是不可能走到的
}
};
35搜寻插入位置(简单)(重)
-
目标值在数组所有元素之前 [0, -1]
-
目标值等于数组中某一个元素 return middle;
- 目标值插入数组中的位置 [left, right],return right + 1(
举个例子 3 7 找 5 :1 l =0 r =1 mid =0 由于小了2 l=mid+1=1 r=1 mid=1 3 于是 r = mid-1=0退出 所以 返回left 或者right+1
)- 目标值在数组所有元素之后的情况 [left, right], return right + 1
mid = left + ((right - left) / 2
这个是偏向右边的
- 目标值插入数组中的位置 [left, right],return right + 1(
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效
int mid = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[mid] > target) {
right = mid - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[mid] < target) {
left = mid + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return mid;
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0, -1]
// 目标值等于数组中某一个元素 return middle;
// 目标值插入数组中的位置 [left, right],return right + 1
// 目标值在数组所有元素之后的情况 [left, right], return right + 1
return left;//或者right+1
}
};
53 0~n-1中缺失的数字(简单)
- 第一个想法就是中间的值(left+right)/2比较
- 一开始写错了 举了个例子 012 3 5 下标(0 1 2 3 4 )分两种情况 还有就是判断返回值的问题
- 0 1 3初始 l r m
- 0 2 1 判断相等
- 2 2 2 不想等 改变r
- 2 1 …跳出循环 我们要返回 i
class Solution {
public:
int missingNumber(vector<int>& nums)
{
int n =nums.size();
int left =0;
int right =n-1;
while(left <= right)
{
int mid =left +(right-left)/2;
if(nums[mid] == mid )//说明在右边
{
left=mid+1;
}
else
{
right=mid-1;
}
}
return left;
}
};
- 第二遍复习
- 缺少一个值 重复一个值都是用二分
双指针
58 - I. 翻转单词顺序(简单)
- 之间的代码存在漏洞
s.erase(s.begin() + k-1, s.end()); 主要是这句话 如果k=0 他不会给报错 但是不会给你删除 所以稍微修改了1
- 还有就是我们用的是for循环 最后把 i = j;其实吧指向了一个空格 你还需要后移一格 所以for循环的i++也用的上
class Solution {
public:
string reverseWords(string s) {
reverse(s.begin(), s.end());//首先翻转整个列表
int n = s.size();//求一下长谷
int k = 0;//用来向前移动的
for (int i = 0; i < n; i ++ ) //一次循环就是翻转一个单词
{
if (s[i] == ' ') continue;//如果是空格后移
int j = i;//去除出空格后 就可以开始把i复制给j了
//如果没到结尾 那就一直j 这边的j你想想是不是可以表示个数 他就是最后一个字母的后一位
while (j < n && s[j] != ' ') j++;
reverse(s.begin() + i, s.begin() + j);//翻转 是j不是j-1 因为要表示最后有一个字母的后一个
while( i < j) s[k++] = s[i++];//(易错)开始前移动 这边很巧妙 并且是i<j j表示最后字母的下一个坐标
if (k != 0) s[k ++ ] = ' ';//用来填充一个空格 一开始不用填充,移动后 马上填充一个空格 注意此时k表示的是空白点的下一个
i = j;//修改初始点
}
if(k!=0) s.erase(s.begin() + k-1, s.end());//删除后面的空格
else s.erase(s.begin() , s.end());
return s;
}
};
18删除链表的节点(简单)
- 这个很容易 pre(temp)前 cur 后 但是为了防止head被删除的可能性 所以设置一个岗哨指向head 如果head被删除 temp=head-》next 也可以 。就不用特殊判断了
class Solution {
public:
ListNode* deleteNode(ListNode* head, int val) {
//定义一个哨兵保存头结点
ListNode *temp = new ListNode(-1);
temp->next=head;
ListNode *cur = head;
ListNode *temp1 =temp;
while(cur != nullptr )
{
if(cur->val == val)
{
temp->next=cur->next;
break;
}
else
{
temp = cur;
cur=cur->next;
}
}
return temp1->next;
}
};
- 第二遍复习 没什么问题
位运算
56 - II. 数组中数字出现的次数 II
- 可以看出比较慢 毕竟循环在哪里 如果正常做我会用hash来做
class Solution {
public:
int singleNumber(vector<int>& nums) {
int counts[32] = {0}; // C++ 初始化数组需要写明初始值 0
for(int i =0 ;i<nums.size();i++)
{
for(int j= 0;j<32;j++)//当时当前位数
{
counts[j]+= nums[i]&1;//每次取最低位
nums[i]=nums[i]>>1;
}
}
int result=0;//存放结果
for(int i =31;i>=0;i--)//把数组里面的数组成一个01二进制
{
result =result << 1;
result =result | (counts[i]%3);
}
return result;
}
};
还有一个比较巧妙的方式,当然我觉的直接第一个i和第三个i+2比较就好了每次后移3个i+3就行了.
- 分成两种情况 那个值在前面或者中间
- 或者在最后
class Solution {
public:
int singleNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size()-2; i += 3)
if (nums[i] != nums[i+2]) return nums[i];
return nums.back();
}
};
数学
14减绳子(中等)
-
这边用动态规划求解比较好解释
-
dp[i] = max(dp[j]*dp[i-j], dp[j1]*dp[i-j1]…)很容易想想 当前的最优解由子结构构成(由于这边j需要遍历 所以需要一个for循环)
-
这个题比较特殊的一个地方在于必须分成2个,这导致存在几个特殊的存在
(1 2 3 的长度。如果分成2段的成绩 会比不分的更小,这就导致了当它作为别人的子段时候,不应该用继续分的结果计算,而是用不分的结果计算,所以当遇到这几个特殊情况需要提前返回,初始化用本身不分段的程度 )循环从4开始
-
简单的说dp动态方程隐藏了一个条件就是 当你子段>=4 那就需要继续分解所得到的结果一定最大
class Solution {
public:
int cuttingRope(int n)
{
vector<int> dp(n+1,0);//下标表示长度
if( n<=3) return n-1;
//
dp[0]=0;
dp[1]=1;
dp[2]=2;
dp[3]=3;
for(int i=4;i<=n;i++)
{
for(int j = 1;j<=i/2;j++)
{
dp[i]=max(dp[i],dp[j]*dp[i-j]);
}
}
return dp[n];
}
};
class Solution {
public:
int cuttingRope(int n) {
/*
* 三种特殊情况:
* 1、长度为1时,没法剪,最大乘积为0
* 2、长度为2时,最大乘积为1 × 1 = 1
* 3、长度为3时,最大乘积为1 × 2 = 2
*/
if (n <= 3) return n - 1;
/*
* 创建动态规划数组,所有元素初始化为0
* dp[i]表示剪长度为i的绳子所能得到的最大乘积,dp[n]表示长度为n的绳子
* 所以数组的长度要为n-1
*/
vector<int> dp(n + 1, 0);
/*
* 上面分析过长度小于等于3时存在的特殊情况,
* 所以当绳子剪过之后,有一段长度小于等于3时,就不应该继续剪,否则乘积就会变小
* 则在动态规划数组中,小于等于3的索引所指的元素应该等于其索引的值
* 代表剪过的绳子到这长度就不要继续剪了
*/
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
dp[3] = 3;
/*
* 外层循环i表示每一段要剪的绳子,去掉特殊情况从4开始
* 内层循环j表示将绳子剪成长度为j和i-j的两段
* (j只需要遍历到i/2就可以了,两边对称的。比如4剪成 1|3 和 3|1 结果是一样的)
* 这样双层循环就相当于从下向上完成了剪绳子的逆过程
* (剪绳子本来是将大段的绳子剪成小段,然后再在每小段上继续剪)
* 双层循环中外层循环从4开始一直到原始绳子长度n,每一段都到内层循环进行剪绳子
* 这样就得到长度在[4, n]区间内的每段绳子剪过之后的最大乘积
* dp[i]记录当前长度绳子剪过之后的最大乘积
*/
for (int i = 4; i <= n; i++) {
for (int j = 1; j <= i / 2; j++) {
dp[i] = max(dp[i], dp[j] * dp[i - j]);
}
}
/* 返回剪绳子的最大乘积 */
return dp[n];
}
};
- 第二遍复习
- 就是特殊情况初始值 1 2 3 分开得到的结果更小
14减绳子2(中等)
-
唯一的不同就是需要取余(感觉限定了范围),这就导致动态规划不能用了。
-
如果只是对结果区域 , 会报错超过了范围
-
如果中间进行取余,结果会错误
-
通过找规律,进行如下代码编写
class Solution {
public:
int cuttingRope(int n) {
if( n<=3 )
return n-1;
vector<long long> dp( 1001, 0 );
dp[1]=1,dp[2]=2,dp[3]=3;
dp[4]=4;
for( int i=5; i<=n; i++ ){
dp[i]=( dp[i-3] * 3 ) % 1000000007;
}
return dp[n];
}
};
n 乘积 子数字
2 1 1 1
3 2 1 2
4 4 2 2
5 6 2 3
6 9 3 3
7 12 2 2 3
8 18 2 3 3
9 27 3 3 3
10 36 2 2 3 3
11 54 2 3 3 3
12 81 3 3 3 3
13 108 2 2 3 3 3
14 162 2 3 3 3 3
15 243 3 3 3 3 3
16 324 2 2 3 3 3 3
17 486 2 3 3 3 3 3
18 729 3 3 3 3 3 3
19 972 2 2 3 3 3 3 3
20 1458 2 3 3 3 3 3 3
21 2187 3 3 3 3 3 3 3
22 2916 2 2 3 3 3 3 3 3
23 4374 2 3 3 3 3 3 3 3
24 6561 3 3 3 3 3 3 3 3
25 8748 2 2 3 3 3 3 3 3 3
26 13122 2 3 3 3 3 3 3 3 3
27 19683 3 3 3 3 3 3 3 3 3
28 26244 2 2 3 3 3 3 3 3 3 3
29 39366 2 3 3 3 3 3 3 3 3 3
44 数字序列中某一位的数字(中等)(待复习)
- 主要就是参考题解 加上一些自己的理解和修改
- 我们要明白需要的几个变量 start digit count
- 第一步就是计算对应的位数,特别之处在于先去除小的,他这边同时修改了n方便了后边的计算很巧妙的方法。
正常的思维是所有的count累加 找到第一个大于n的数,这边是从小的count减去 找到第一个 >n的数
- 第二步就是定位对应的数字 通过n和位数和start很容易定位
- 第三步 通过求余也很容易找到对应的数字
class Solution {
public:
int findNthDigit(int n) {
int digit=1;//位数从1开始 1 2 3
long start=1;//开始从1开始 1 10 100 1000
long count =9;//位数数量 9 190 2700 公式是=9*start*digit
//第一步 找到对应的位数 digit(因为知道位数 就知道 位数数量)所以已知位数数量反推 位数
while(n > count )//退出条件 小于对于位数的最大值了(在这个区间了)
{
n = n- count;//从低位开始减去
digit+=1;//指向下一个位数
start*=10;//指向下一个start
count=9*start*digit;
}
//第二步 找到对应的数字
//
//long num = start + (n-1)/digit; 这个是题解的方式 下面是我自己的理解
//向上取整 找到第几个数字 例如 start=100 n=7 表示第三个数 结果就是start+ceil-1
long num = start + ceil((n*1.0/digit))-1;
//第三步 确定最终指向数字的哪一个数位
//自己举个例子 位数3 100 101 102 4%3=1 5%3=2 6%3=0 减去1方便计算
string s = to_string(num);
int res = s[(n - 1) % digit] - '0'; // 获得 num 的 第 (n - 1) % digit 个数位,并转
return res;
}
};
Hot 100
2两数之和(中等)
- 写代码建议先写上步骤 再具体写
- 比如空判断 补上0节点
- 求和 记得加上进位
- 求下一个 进位 和真实位数值
- 创建新的节点挂上
- 新链表 temp后移
- l1 和 l2 后移
- 退出循环还需要进行一步判断
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* p=new ListNode(-1);//头结点,为了方便后面用new
ListNode* re=p;//结果结点
int jin=0;//进位
while(l1!=NULL||l2!=NULL){//l1和l2中有一者不为空
if(l1==NULL)
l1=new ListNode(0);//l1比l2短,所以将l1补0
if(l2==NULL)
l2=new ListNode(0);//l2比l1短,所以将l2补0
int n=l1->val+l2->val+jin;//计算l1和l2的和
int realNum=n%10;//记录到新结点中的数字
jin=n/10;//进位
p->next=new ListNode(realNum);将新节点添加进来
p=p->next;//p后移
l1=l1->next;//l1后移
l2=l2->next;//l2后移
}
if(jin>0)//如果加到最后一位仍然有进位的时候
p->next=new ListNode(jin);//把进位直接补全到最后
return re->next;//由于一开始有头结点,所以答案是re->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* addTwoNumbers(ListNode* l1, ListNode* l2)
{
//创建一个岗哨
ListNode* temp =new ListNode(-1);
ListNode* temp1 = temp;
int jin =0; //创建一个标量保存进位
while(l1!=NULL || l2!=NULL) //退出条件就是两个同时为空
{
//特殊处理 为空就挂上一个1
if(l1 == NULL)
{
l1= new ListNode(0);
}
if(l2 == NULL)
{
l2 =new ListNode(0);
}
//求和(包括上一次的进位)
int n = l1->val+l2->val+jin;
//求进位 和实际放入的值 下一次用
jin = n/10;
int realsum = n % 10;
//挂入新的节点
temp->next = new ListNode(realsum);
temp=temp->next;//后移 保存下一个节点
//全部后移
l1=l1->next;
l2=l2->next;
}
if(jin>0)
{
temp->next=new ListNode(1);
}
return temp1->next;
}
};
19删除链表的倒数第N个节点(中等)
-
很容易想到最快的方法是双指针
-
分析过程
- 首先我想到可能删除第一个节点 所有设置岗哨
- 第二个想到的问题,不同与找到那个节点,删除需要找到倒是第k+1个节点,所以循环次数应该是k+1次。这边我第一次写错了…写成了k-1
- 第三个遗漏的问题就是记得及时释放空间,岗哨 。
-
其他方法有1计算长度 2 通过栈的方式很容易找到要删除的节点和前驱节点。时间复杂度和空间复杂度都是o(n)
/**
* 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* removeNthFromEnd(ListNode* head, int n)
{
//我觉的先创建一个岗哨 如果删除的是第一个节点也好返回
ListNode* temp = new ListNode(-1);
temp->next = head;
ListNode* temp1=temp;
ListNode* temp2=temp;
//举个例子 这次我不是找到那个节点 我是要找到删除节点的前一个方便我删除所以n-1
for(int i = 0;i<n+1;i++)
{
temp2=temp2->next;
}
while(temp2!=NULL && temp1!=NULL)
{
temp2=temp2->next;
temp1=temp1->next;
}
temp1->next=temp1->next->next;
ListNode* ans =temp->next;
delete temp;
return ans;
}
};
31下一个排序(数组)(中等)
- 总而言之
- 第一步 从后向前找到第一个相邻升序 i 和 j
- 第二步 从后向前(到j)找到第一个大于i的值下标设为k
- 第三步 交换 i k 并且reserve j 到 end
- 如果整个循环找到到第一步 那就整个翻转返回
第三步和第四步可以合并写在一起,如下代码
比较重要的是我们根据while设置两个退出条件 退出后记得判断哪一种退出的
class Solution {
public:
void nextPermutation(vector<int>& nums)
{
//1第一步 从后向前 找升序
int i =nums.size()-2;//如果用i 和 i--要用i--进行越界判断。 所以我们用i 和 i++ 对i进行判断就好
while(i>=0 && nums[i] >= nums[i+1])
{
i--;//移动到下一个
}
//跳出循环两种可能 1越界了 2要么找到了
if(i>=0)
{
int k = nums.size()-1;
//这边不同判断k的边界 最坏的情况就是k=j
while(nums[k]<=nums[i])
{
k--;
}
//第二部
swap(nums[i],nums[k]);
//第三步
reverse(nums.begin()+i+1,nums.end());
}
else
{
//第四部 特殊情况
reverse(nums.begin(),nums.end());
}
}
};
class Solution {
public:
void nextPermutation(vector<int>& nums)
{
//1第一步 从后向前 找升序
int i =nums.size()-2;//如果用i 和 i--要用i--进行越界判断。 所以我们用i 和 i++ 对i进行判断就好
while(i>=0 && nums[i] >= nums[i+1])
{
i--;//移动到下一个
}
//跳出循环两种可能 1越界了 2要么找到了
if(i>=0)
{
int k = nums.size()-1;
//这边不同判断k的边界 最坏的情况就是k=j
while(nums[k]<=nums[i])
{
k--;
}
//第二部
swap(nums[i],nums[k]);
//第三步
}
reverse(nums.begin()+i+1,nums.end());
}
};
39组合总和(中等)(回溯)
-
第一个想法就是回溯,非a就是b 如果和超过了那就回退 并且for循环后面的就不走了,提前排序
-
看了题解发现想法一样
- 有一个需要注意的 我们不要同时出现 2 3 和3 2 。这个排序问题 所以就要设置start 每次都比之前的大。
第二个就是可以重复!!! 所以start从i开始而不是从i+1开始
-
特别注意第二个代码,我对题解进行的修改,用时更少,把终止条件放在for循环 及时退出(特殊)。特别注意退出前把当前位置的sum回溯。一个for循环控制的是一个格子,所有退出吧当前格子的数减去 返回上一个格子继续考虑
- 这段代码sum可以传入形参,修改成如下
backtracking(candidates,target,sum+candidates[i],i);
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates,int& target,int& sum,int start)
{
//递归终止条件 2种
if(sum == target)
{
result.push_back(path);
}
if(sum > target)
{
return;
}
//递归回溯
for(int i =start;i<candidates.size();i++)
{
sum = sum+candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i);//特别注意这边传入的是i
sum =sum-candidates[i];//两种情况 符合条件条件如result 回溯 不符合 一样回溯
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
result.clear();
path.clear();
//sort(candidates.begin(),candidates.end());
int sum = 0;
backtracking(candidates,target,sum,0);
return result;
}
};
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates,int& target,int& sum,int start)
{
//递归终止条件 2种
if(sum == target)
{
result.push_back(path);
}
//递归回溯
for(int i =start;i<candidates.size();i++)
{
sum = sum+candidates[i];
if (sum > target)
{
sum = sum - candidates[i];
break;
}
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i);//特别注意这边传入的是i
sum =sum-candidates[i];//两种情况 符合条件条件如result 回溯 不符合 一样回溯
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
result.clear();
path.clear();
sort(candidates.begin(),candidates.end());
int sum = 0;
backtracking(candidates,target,sum,0);
return result;
}
};
48 旋转图像(中等)(找规律)
-
第一个想法就是 从坐标中找到规律进行移动,或者多次翻转(但是没找到规律啊)
-
看了题解:
顺时针90度应该是左上/右下对角线翻转+左右翻转,或者右上/左下对角线翻转+上下翻转。时间复杂度n^2
-
左右翻转可以用双指针 或者 第二种方式
class Solution {
public:
void rotate(vector<vector<int>>& matrix)
{
int n =matrix.size();
//左上 右下进行对折翻转 举个例子就是坐标对调 遍历一个三角就好
for(int i =0;i < n ;i++)
{
for(int j =0;j<i;j++)
{
swap(matrix[i][j],matrix[j][i]);
}
}
// 再沿竖线进行左右翻转 行数不变 列数 = n-i 和i
for(int i =0;i<n;i++)
{
for(int j = 0, k = n - 1; j < k ; j++, k--) //类似于双指针,由两端向中心靠齐
swap(matrix[i][j],matrix[i][k]);
}
}
};
class Solution {
public:
void rotate(vector<vector<int>>& matrix)
{
int n =matrix.size();
//左上 右下进行对折翻转 举个例子就是坐标对调 遍历一个三角就好
for(int i =0;i < n ;i++)
{
for(int j =0;j<i;j++)
{
swap(matrix[i][j],matrix[j][i]);
}
}
// 再沿竖线进行左右翻转 行数不变 列数 = n-i 和i
for(int i =0;i<n;i++)
{
//for(int j = 0, k = n - 1; j < k ; j++, k--) //类似于双指针,由两端向中心靠齐
// swap(matrix[i][j],matrix[i][k]);
for(int j =0;j<= (n-1)/2;j++)
{
swap(matrix[i][j],matrix[i][n-1-j]);
}
}
}
};
64 最小路径和(中等)
- 很明显就是dp,写了一遍没啥问题
class Solution {
public:
int minPathSum(vector<vector<int>>& grid)
{
int n = grid.size();
int m=grid[0].size();
vector<vector<int>> dp(n,vector<int>(m));
//初始化
dp[0][0] =grid[0][0];
//初始化第一列
for(int i = 1;i<n;i++)
{
dp[i][0]=grid[i][0]+dp[i-1][0];
}
for(int i = 1;i<m;i++)
{
dp[0][i]=grid[0][i]+dp[0][i-1];
}
for(int i =1;i<n;i++)
{
for(int j=1;j<m;j++)
{
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
return dp[n-1][m-1];
}
};
75颜色分类(中等)(双指针)
- 方法一就和奇偶排序一样(或者说快慢指针 k 和 i 遇到奇数进行交换
不存在奇数换奇数 因为k都是经过i走过的路 k走过的都会变成偶数或者自己和自己更换
) 只是进行了两次而已 o(n) - 方法二 排序 无论是快排 归并 时间复杂度都是 nlogn
- 方法三 就是遇到0放在开头 遇到2放在结尾,但是这个有很多注意事项。
就是放在结尾的你需要考虑2替换2的情况 所以把原先if更换为while进行遍历替换 这个我没想到 这个写法最简便
while (i <= p2 && nums[i] == 2)
swap(nums[i], nums[p2--]);
- 还有一个就是把判断2的放在0的前面是有原因的,更换完可能吧0换在i的位置,此时不能后移要继续进行判断。否则就是我代码2的复杂写法
class Solution {
public:
void sortColors(vector<int>& nums)
{
int n =nums.size();
int k=0;
for(int i =0;i<n;i++)
{
if(nums[i]==0)
{
swap(nums[k++],nums[i]);
}
}
for(int i =k;i<n;i++)
{
if(nums[i]==1)
{
swap(nums[k++],nums[i]);
}
}
}
};
class Solution {
public:
void sortColors(vector<int>& nums)
{
int n =nums.size();
int k=0;
int k2=n-1;
for(int i =0;i<=k2;i++)
{
if(nums[i]==0)
{
swap(nums[i],nums[k++]);
}
if(nums[i]==2)
{
//找到一个不是2的位置
while(i<k2 && nums[k2]==2) k2--;
if(i<k2)
{
swap(nums[i--],nums[k2--]);
}
}
}
}
};
class Solution
{
public:
void sortColors(vector<int> &nums)
{
int n = nums.size();
int p0 = 0, p2 = n - 1;
for (int i = 0; i <= p2; i++)
{
while (i <= p2 && nums[i] == 2)//防止p2位置原本就是2,交换后2被遗漏在了前面
swap(nums[i], nums[p2--]);
if (nums[i] == 0)
swap(nums[i], nums[p0++]);
}
}
};
96不同的二叉搜索树(中等)(动态规划)
-
看到题目属实没啥思路…难道思考每次多一个数会多几种变化?
- G(n)其中的n表示有几个连续的树 数字1-3 和 2-4种类结果是一样的。
- 外层循环控制整数变化 内层循环控制跟的变化 比如整数 2(根有1 2) 整数3(跟有1 2 3)
属实有点难想了
class Solution {
public:
int numTrees(int n)
{
vector<int> dp(n + 1, 0);//下标代表整数 所以n+1 最大值+1
dp[0] = 1;
dp[1] = 1;
//外层循环从2开始 0和1初始化了
for(int i =2;i<=n;i++)
{
//内层循环控制跟f累加
for(int j = 1;j<=i;j++)
{
dp[i]=dp[i]+dp[j-1]*dp[i-j];
}
}
return dp[n];
}
};
114二叉树展开为链表(中等)
- 和之前做过的有一题很像,题目要求先序遍历的顺序一致,第一个想法就是先序序列递归,需要一个保存上一个节点,不断next当前节点,然后当前节点作为pre节点。
- 我的第一次写法不符合题目left为空的要求 我们要先把它保存下来 然后再遍历处理
/**
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<TreeNode*> ans;
void dfs(TreeNode* root)
{
//递归终止条件
if(root == nullptr) return;
//先序遍历 跟左右
//逻辑处理
ans.push_back(root);
dfs(root->left);
dfs(root->right);
}
void flatten(TreeNode* root)
{
if(root ==NULL) return;
dfs(root);
for(int i=0;i<ans.size()-1;i++)//前后挂钩
{
ans[i]->right=ans[i+1];
ans[i]->left=NULL;
}
}
};
128 最长连续序列(中等)(hash)
- 注意存在重复的数字的
- 我觉的要么就排序 找到所有的连续长度(两两差值 《=1 最后收尾元素相减+1知道长度)
- 题解方式如下 外层循环需要 O(n) 的时间复杂度,只有当一个数是连续序列的第一个数的情况下才会进入内层循环
利用前驱判断 剩下很多功夫
不知道为啥 auto的方式更省时间
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> hash;
for(auto x : nums) hash.insert(x); //放入hash表中
int res = 0;
for(auto x : hash)
{
if(!hash.count(x-1))
{
int y = x; //以当前数x向后枚举
while(hash.count(y + 1)) y++;
res = max(res, y - x + 1); //更新答案
}
}
return res;
}
};
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> set;
//全部插入 取出重复
for(int i =0;i<nums.size();i++)
{
set.insert(nums[i]);
}
int result=0;//保存最大长度
for(int i =0;i<nums.size();i++)
{
if(set.count(nums[i]-1)==0)//表示前驱不存在
{
int x =nums[i];//保存第一个值
int y = x;
while(set.count(y)==1) y++;//退出时指向第一个不存在的下标
result=max(result,y-x);
}
}
return result;
}
};
136只出现一次的数字(简单)
位运算求解 或者一次循环判断求解,但是第二个方法注意就是需要先排序,题目没有排序...如果题目又说排序 第二种方法可能更容易(所以第一种方法更快)
class Solution {
public:
int singleNumber(vector<int>& nums)
{
int result =0;
for( auto x : nums)
{
result=result^x;
}
return result;
}
};
class Solution {
public:
int singleNumber(vector<int>& nums)
{
sort(nums.begin(),nums.end());
for(int i =1;i<nums.size();i+=2)
{
if(nums[i]!=nums[i-1])
return nums[i-1];//一定是前面那个数 i移动是偶数上 不同的数在奇数上
}
//循环走完了 还没有退出 那就是出现在最后一个位置了
return nums[nums.size()-1];
}
};
139单词拆分(中等)(动态规划)(待完善)
- 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度)
- 代码回想录题解 背包问题
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for (int i = 1; i <= s.size(); i++) //外层循环表示当前长度的字符串
{
for (int j = 0; j < i; j++) //分成两部分进行判断 多种分发
{
string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
if (dp[j] && wordSet.find(word) != wordSet.end() )
{
dp[i] = true;
}
}
}
return dp[s.size()];
}
};
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
int start=0;
for (int i = 0; i < s.size(); i++) //i表示终止点
{
string word = s.substr(start, i - start+1); //substr(起始位置,截取的个数)
if (wordSet.find(word) != wordSet.end())
{
//更换起始点
if(i+1 < s.size()) start=i+1;
}
}
//退出之前看看最后一次到底有没有在就行了
string word = s.substr(start, s.size() - start); //substr(起始位置,截取的个数)
if (wordSet.find(word) != wordSet.end())
{
return true;
}
else
{
return false;
}
}
};
152 乘积最大子数组(中等)(动态规划)
- 很容易想到 dp动态规划 下标表示以当前数值为结尾的子数组。但是需要想到 可能存在 23-2 此时以-2为结尾的最大值是-2 但是如果下一个数也是负数那就需要保留整串了,
-对于乘法,我们需要注意,负数乘以负数,会变成正数,所以解这题的时候我们需要维护两个变量,当前的最大值,以及最小值,最小值可能为负数,但没准下一步乘以一个负数,当前的最大值就变成最小值,而最小值则变成最大值了
dp_max[i]=max(max(dp_max[i-1]*nums[i],nums[i]),dp_min[i-1]*nums[i]);
dp_min[i]=min(min(dp_min[i-1]*nums[i],nums[i]),dp_max[i-1]*nums[i]);
看了题解思路写了代码 有一个地方写错了就是result最大值初始化为nums[0]忘记了
当然可以空间优化 因为每次只用到前一个值,第二个代码有简介版本
class Solution {
public:
int maxProduct(vector<int>& nums)
{
int n =nums.size();
if(n == 0) return 0;
else if(n == 1) return nums[0];
vector<int> dp_min(n);
vector<int> dp_max(n);
dp_min[0]=nums[0];
dp_max[0]=nums[0];
int result=nums[0];
for(int i=1;i<n;i++)
{
dp_max[i]=max(max(dp_max[i-1]*nums[i],nums[i]),dp_min[i-1]*nums[i]);
dp_min[i]=min(min(dp_min[i-1]*nums[i],nums[i]),dp_max[i-1]*nums[i]);
result=max(result,dp_max[i]);
}
return result;
}
};
867 链表的中间节点
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
关于下一题的重要补充
- quick = head 偶数指向中间一组的后一个 奇数指向中间
- quick = head->next 偶数指向中间一组的前一个 奇数指向中间
- quick = head->next->next 偶数指向中间一组的前一个 奇数指向中间的前一个
148排序链表(中等)(归并 快排)
- 题目没有说不能修改节点内的值,算是取巧的方法
- 归并排序 和 快速排序
自己写了一遍如下,特别注意链表的断开 和 快慢指针的设置
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptrptr) {}
* ListNode(int x) : val(x), next(nullptrptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* sortList(ListNode* head)
{
//1递归终止条件 只有一个节点 或者一个节点没有
if (head == nullptr || head->next == nullptr)
{
return head;
}
//2当前层的逻辑 一分为二 把一个链表彻底分为两个链表
ListNode* midNode = middleNode(head);
ListNode* rightHead = midNode->next;//下一个节点为第二个链表的头节点
midNode->next = nullptr;//第一个链表的尾部设置为NULL为了后面有序链表的合并
//3递归调用
ListNode* left = sortList(head);
ListNode* right = sortList(rightHead);
// 4 归并 有序链表的合并
return mergeTwoLists(left, right);
}
// 找到链表中间节点(876. 链表的中间结点)
ListNode* middleNode(ListNode* head)
{
//切记 我们要返回的是第一个链表的最后一个节点slow
//所以不能用fast=head 对于偶数个节点 会分成3 和 1
//只能用 fast=head->next =head->next->next
ListNode* slow = head;
ListNode* fast = head->next->next;//这样确保所得中是偏向前的
//ListNode* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// 合并两个有序链表(21. 合并两个有序链表)
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* temp = new ListNode(-1);
ListNode* temp1 = temp;
while(l1 != nullptr && l2 != nullptr) {
if(l1->val < l2->val)
{
temp->next = l1;
l1 = l1->next;
temp = temp->next;
} else
{
temp->next = l2;
l2 = l2->next;
temp = temp->next;
}
}
if(l1!=nullptr)
{
temp->next=l1;
}
if(l2!=nullptr)
{
temp->next=l2;
}
return temp1->next;
}
};
160相交链表(简单)(特殊)
第二次遇到,写错了,错误代码如下,原因在于如果a走到了空,此时a=b就算一步,不能a=b 然后 next连续走两步。第二段代码正确
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
if (headA == nullptr || headB == nullptr)
{
return nullptr;
}
ListNode* pa=headA;
ListNode* pb=headB;
while(headA!=headB)
{
if(headA==NULL)
{
headA=pb;
}
if(headB==NULL)
{
headB=pa;
}
headA=headA->next;
headB=headB->next;
}
return headA;
}
};
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
if (headA == nullptr || headB == nullptr)
{
return nullptr;
}
ListNode* pa=headA;
ListNode* pb=headB;
while(headA!=headB)
{
if(headA==NULL)
{
headA=pb;
}
else
headA=headA->next;
if(headB==NULL)
{
headB=pa;
}
else
headB=headB->next;
}
return headA;
}
};
207. 课程表(中等)(待补充)(拓扑)
Sort的实现原理及相关问题
215 数组中的第k个最大元素
-
内置实现
-
第二个就是自己实现归并。
if((j > r|| a[i]<=a[j] && i<=mid) )这句话的判断顺序不能颠倒,会数组越界。
-
第三个就是快排 代码复习一下
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
sort(nums.begin(),nums.end());
int l=nums.size();
return nums[l-k];
}
};
class Solution {
public:
vector<int> t;
void merge_sort(vector<int>& a,int l,int r)
{
//递归终止条件
if(l == r) return;
//处理当前层的逻辑
int mid = (l+r)/2;
//进入下一层
merge_sort(a,l,mid);
merge_sort(a,mid+1,r);
//汇总结果
int i=l;
int j=mid+1;
for(int k=l;k<=r;k++)
{
//2 种情况 //注意
if((j > r|| a[i]<=a[j] && i<=mid) )
{
t[k] = a[i];
i++;
}
else
{
t[k]=a[j];
j++;
}
}
for(int k=l;k<=r;k++)
{
a[k]=t[k];
}
}
int findKthLargest(vector<int>& nums, int k)
{
vector<int> t_new(nums.size());
t =t_new;
merge_sort(nums,0,nums.size()-1);
return nums[nums.size()-1-k+1];
}
};
#include<iostream>
using namespace std;
void quickSort(int s[], int l, int r)
{
//终止条件
if (l>= r) //元素个素只有一个
{
return;
}
//处理当前层的逻辑
int i = l, j = r, x = s[l];//枢纽
while (i < j)//注意是从右边开始 因为我们是取左边第一个为枢纽 那个值可以填入
{
while(i < j && s[j]>= x) // 从右向左找第一个小于x的数
j--;//下一个待比较的值
if(i < j && s[j]<x ) //右边第一个小于枢纽的值 填入到 当前i的位置
{
s[i]=s[j];
i++;//i值被覆盖了 指向下一个待比较的数字
}
while(i < j && s[i]< x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
{
s[j]=s[i];
j--;//指向下一个待比较的数字
}
}
s[i] = x;
//进入下一层
quickSort(s, l, i - 1); // 递归调用
quickSort(s, i + 1, r);
}
int main()
{
int array[]={34,65,12,43,67,5,78,10,3,70},k;
int len=sizeof(array)/sizeof(array[0]);
cout<<"The orginal arrayare:"<<endl;
for(k=0;k<len;k++)
cout<<array[k]<<",";
cout<<endl;
quickSort(array,0,len-1);
cout<<"The sorted arrayare:"<<endl;
for(k=0;k<len;k++)
cout<<array[k]<<",";
cout<<endl;
system("pause");
return 0;
}
221 最大正方形(中等)(动态规划)
- 太难想到动态规划了,没有做空间优化代码如下
- 参考链接
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
int m=matrix.size(),n=matrix[0].size();
vector<vector<int>> dp(m,vector<int>(n,0));
int max=0;
for(int i=0;i<m;i++)
{
dp[i][0]=matrix[i][0]-'0';
//这个方法好 就修改一次
if(max==0&&matrix[i][0]=='1')
max=1;
}
for(int j=0;j<n;j++)
{
dp[0][j]=matrix[0][j]-'0';
if(max==0&&matrix[0][j]=='1')
max=1;
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
if(matrix[i][j]=='1')
{
dp[i][j]=min(min(dp[i-1][j-1],dp[i-1][j]),dp[i][j-1])+1;
if(dp[i][j]>max) max =dp[i][j];
}
else
dp[i][j]=0;
}
}
return max*max;
}
};
234回文链表(简单)
- 两个想法 全部取出来然后reverse判断是不是一样
- 要么就头尾指针,不过这个不是双向链表 所以一样要取出到数组然后再进行比较
题解有一个方法很巧妙 就是快慢指针找到中间节点,然后翻转前半段,最后比较(一般就会要求这种解法)
自己写了一下翻转的写法,有几个注意的点 一个是翻转后半段方便比较,因为如果奇数个此时翻转前半段必然可能第一个值不想等(还需要考虑奇数偶数的关旭)第二个就是快慢指针找中间节点 我这边估计前半段《=后半段,这样后面比较的时候只要比较前半段长度的次数就好,无需判断奇数偶数节点(这个方法无需额外的空间也更快 0(n))
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
bool isPalindrome(ListNode* head)
{
vector<int> vec;
while(head!=nullptr)
{
vec.push_back(head->val);
head=head->next;
}
int n= vec.size()-1;
for(int i =0,j=n;i<=j;i++,j--)
{
if(vec[i]!=vec[j])
{
return false;
}
}
return true;
}
};
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverselist(ListNode* head)
{
//翻转链表 3个一组
ListNode* pre=nullptr;
ListNode*cur=head;
ListNode*next =nullptr;
while(cur!=nullptr)
{
next=cur->next;//保存要修改的指向
cur->next=pre;
pre=cur;
cur=next;
}
return pre;
}
bool isPalindrome(ListNode* head)
{
if(head==nullptr||head->next==nullptr) return true;
//我觉的喜欢前半段长度 <=后半段长度还是用如下的方式
ListNode* slow =head;
ListNode* quick=head->next->next;
while(quick!=nullptr && quick->next!=nullptr)
{
//(参考链表排序哪一章节)
//偶数节点 slow会指向中间一组的前一个
//奇数节点 slow会指向中间的前一个
slow=slow->next;
quick=quick->next->next;
}
ListNode* head1=slow->next;//保存一下后半段的头节点
slow->next=nullptr;
ListNode* cur1 =head;//前半段
ListNode* cur2 =reverselist(head1);
//这边特别注意 我们前半段固定小于=后半段 所以以前半段长度为基准
while(cur1!=nullptr)
{
if(cur1->val!=cur2->val) return false;
cur1 =cur1->next;
cur2=cur2->next;
}
return true;
}
};
238 除自身以外数组的乘积(中等)
之前做过类似的很容易想到用类似dp的方法去做这个题,但是要想想用常数的空间(看了题解发现之前的方法就是常数(因为输出的数组不算辅助空间,差点忘记了))
- 之前的题目(66 构建乘积数组)重新写了一遍确实没啥问题,就是一些细节的处理
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums)
{
int n =nums.size();
vector<int> dp(n);//从第0行开始
dp[0]=1;//
//注意 第i行用的是第i-1的数 比如第二行用第一个数 所以是i-1
for(int i=1;i<n;i++)
{
dp[i]=dp[i-1]*nums[i-1];
}
//从后向前 处理另外一个三角形
//从倒数第二行还是
int temp=1;//需要一个中间值保存
//倒数第二行 用倒数第一个数 +1
for(int i =n-2;i>=0;i--)
{
temp=temp*nums[i+1];
dp[i]=dp[i]*temp;
}
return dp;
}
};
240搜索二维矩阵2
- 首先我觉的对每一行每一列做二分查找太蠢了
做过两中题1 本题这种 2就是每一行可以拼接有序 二分查找的
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target)
{
int n =matrix.size();
if(n == 0)
return false;
int m=matrix[0].size();
int i= 0;
int j =m-1;
while(i<n && j>=0)
{
if(matrix[i][j] < target)
{
//去掉一行
i++;
}
else if(matrix[i][j] > target)
{
//去掉列
j--;
}
else
{
return true;
}
}
return false;
}
};
279完全平方数(中等)(背包 动态规划)(待完善)
- 第一个想法难道先找最大的完全平方数填充?我咋感觉真是呢
代码回想 重新归纳一下背包问题的动态规划
// 版本一
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i <= n; i++) { // 遍历背包
for (int j = 1; j * j <= i; j++) { // 遍历物品
dp[i] = min(dp[i - j * j] + 1, dp[i]);
}
}
return dp[n];
}
};
287寻找重复数(中等)(二分 快慢)(特别注意)
总结:数组中查找某一个数 可以在二分上思考一下下
- 注意题目需要使用常量的额外空间,所以排序也不行,hash也不行。
- 第一个想法就是双指针两层for循环遍历比对
- 这也能想到快慢指针,属实有点变态参考链接
- 方法二:二分查找(这题只能< 如果是<=跳不出循环)
出现这种问题找个边界上的测试 就行了 1233
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int fast = 0, slow = 0;
while(true){
fast = nums[nums[fast]];
slow = nums[slow];
if(fast == slow)
break;
}
int finder = 0;
while(true){
finder = nums[finder];
slow = nums[slow];
if(slow == finder)
break;
}
return slow;
}
};
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n=nums.size()-1;
int left=1,right=n;
while(left<right){
int mid=left+(right-left)/2;
int count=0;
for(int num:nums)if(num<=mid)count++;//统计小于等于这个数的
if(count>mid)right=mid;//如果这个数量大于这个mid说明就在小于等于这边否则在另外一边 包含mid
else left=mid+1;
}
return left;
}
};
时间复杂度是nlogn 空间复杂度o1
347 前k个高频元素(中等)(重要)
- 频率我们很容易想到hash,然后根据hash的值频率进行排序。
- 具体步骤
- 1就是构建hash进行计数
- 2次数通过迭代器或者auto把值 放入vector 二维数组或者一维+pair
- 3 构建cmp 修改sort的排序规则 (注意cmp的传入参数)
- 4 遍历输出k个
bool cmp(const pair<int,int> &p1,const pair<int,int> &p2)
{
return p1.second > p2.second;//从大到小
}
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k)
{
unordered_map<int, int> map;
for (auto i: nums)
map[i]++;
//这边也可以是二维数组 也可以
vector<pair<int, int>> temp;
int i = 0;
//这边可以使用迭代器 或者auto的方式
for (auto it = map.begin();it != map.end(); it++)
{
//temp.emplace_back(make_pair(it->first, it->second));
temp.emplace_back(pair(it->first, it->second));
}
sort(temp.begin(), temp.end(), cmp);
vector<int> res(k);
for (int i = 0;i < k;i++)
res[i] = temp[i].first;
return res;
}
};
394. 字符串解码(中等)(辅助栈)(重要)
、
- 第一个想法就是栈的方向思考了 它这个解码要从最内部的括号开始解码 ,自己没写出来
需要两个辅助栈和两个临时变量,分为4种情况
- 当 c 为数字时,将数字字符转化为数字 multi,用于后续倍数计算,存入临时变量
- 当 c 为字母时,在 res临时变量尾部添加 c;
- 当 c 为 [ 时,将当前 multi 和 res 入栈,并分别置空置 00.这也叫状态清空
- 当 c 为 ] 时,stack 出栈,拼接字符串 res = last_res + last_multi * res,其中:
需要自己演变一下这个过程,我的理解因为res临时变量始终表示需要重复的量,字符串栈中的表示的是对于当前阶段只需要重复一次的,因为都是[之前的。阶段变化一定是从内部考虑的。如果是并列的【】【】那就是前面先考虑。当我们从]取出后并没有入栈,因为还可能重复
- 下面是我看的比较好的一个简洁代码
对入栈的问题需要考虑3个 1 栈元素是什么 2 什么时候入栈 3 什么时候出栈
对于这类问题 我觉的应该从一个最简单的结构进行分析 例如a3[bbb] 分解成最简单的一个结构
class Solution {
public:
string decodeString(string s) {
//两个栈分别压int res和用pair
stack<pair<int, string>> sta;
int num = 0; string res = "";
//循环检查字符串
for (int i = 0; i < s.size(); i++) {
//遇到数字则存入num
if (s[i] >= '0'&&s[i] <= '9') {
num *= 10;
num += (s[i] - '0');//这里括号是否需要
}
else if (s[i] == '[') {//遇到[压栈数字和字符串,置零置空
sta.push(make_pair(num, res));
num = 0;
res = "";
}
else if (s[i] == ']') {//遇到]出栈数字和字符串,组装
int n = sta.top().first;//n指示的是res的循环次数,不是a的
string a = sta.top().second;
sta.pop();
for (int i = 0; i < n; i++) a = a + res; //循环n次
res = a;
}
else {//遇到字符存入字符
res += s[i];
}
}
return res;
}
};
406 根据身高重建队列(中等)(贪心)(特别)
- 第一眼看没啥思路,难不成递归遍历所有情况?
-
题解总结:
(套路):一般这种数对,还涉及排序的,根据第一个元素正向排序,根据第二个元素反向排序,或者根据第一个元素反向排序,根据第二个元素正向排序,往往能够简化解题过程。
-
在本题目中,我首先对数对进行排序,按照数对的元素 1 降序排序,按照数对的元素 2 升序排序。原因是,按照元素 1 进行降序排序,对于每个元素,在其之前的元素的个数,就是大于等于他的元素的数量,而按照第二个元素正向排序,我们希望 k 大的尽量在后面,减少插入操作的次数(或者说保证正确性) 比如(5,2)(5,3)很明显顺序相同的身高按照第二个元素升序 降序必然会错误
-
步骤
- 1 排序规则:按照先H高度降序,K个数升序排序
- 2 历排序后的数组,根据K插入到K的位置上
-
注意
- cmp的用法
- insert的用法
-
问题
- vector的插入底层实现需要很多时间 所以可以修改成链表
注意看代码的区别,list是双向迭代器 只能++ -- 而vector deque是随机访问迭代器 可以 + 3 -4 这种 所以代码有所区别
- vector的插入底层实现需要很多时间 所以可以修改成链表
class Solution {
public:
//如果是类内写cmp 需要加上static
bool static cmp(const vector<int>& a, const vector<int>&b)
{
if(a[0]>b[0]) return true;//true表示ab位置不变
else if(a[0] < b[0]) return false;//false 表示改变
else
{
//或者如下简洁写法 直接把要求写在return后面
return a[1]<b[1];
}
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people)
{
sort(people.begin(),people.end(),cmp);
vector<vector<int>> result;
for(int i =0;i<people.size();i++)
{
int position = people[i][1];
result.insert(result.begin()+position,people[i]);//参数一 iterator loc
}
return result;
}
};
// 版本二
class Solution {
public:
// 身高从大到小排(身高相同k小的站前面)
static bool cmp(const vector<int> a, const vector<int> b) {
if (a[0] == b[0]) return a[1] < b[1];
return a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort (people.begin(), people.end(), cmp);
list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
for (int i = 0; i < people.size(); i++) {
int position = people[i][1]; // 插入到下标为position的位置
std::list<vector<int>>::iterator it = que.begin();
while (position--) { // 寻找在插入位置
it++;
}
que.insert(it, people[i]);
}
return vector<vector<int>>(que.begin(), que.end());
}
};
448 找到所有数组中消失的数字(简单)(数组的原地修改)
-
其实我自己第一个想法就是归为 第一次遍历,把数字交换对应的下标的位置(比如1 放入下标0 i-1的位置)如果 (1)情况1 两个数字一样 那就当前置为0 (2)如果交换的位置是0 那就换过去,把自己置为0 后移(3)如果遇到0就后移(4) 如果两个数字不一样都不是零 这边应该需要一个while不断交换。 第二次遍历就应该找0了
总结 两种方法都是类似的 就是希望在原本数组信息做标记 但是又能保存原来的信息 ,一个是+n 后% 一个是修改正负 -
看了题解,发现自己的方法还是太笨了,正常的用辅助数组的方法是出现的我们在对应的数组位置标记上。
-
方法一如下:
那如何在原数组进行标记么,我们完全可以把原本的正数变成负数,这样就可以保存原来的信息,比如一开始修改后边的数组元素 把正数变成负数,等便利到的时候我们可以取出取绝对值就好了。
-
自己写有一个地方写错了 第一个循环要进行判断,如果修改的值已经是负数了 那就不要进行改变 否则负负为正 判断错误
-
方法二如下
class Solution {
public:
vector<int> findDisappearedNumbers(vector<int>& nums)
{
int n=nums.size();
vector<int> res;
if(n<=0) return res;
for(int i =0;i<n;++i)
{
int index =abs(nums[i])-1;//表示我要修改的坐标
//注意这边的判断!!!!!!!
if(nums[index]>0) nums[index]=-nums[index];
}
for(int i=0;i<n;++i)
{
if(nums[i]>0) res.push_back(i+1);
}
return res;
}
};
class Solution {
public:
vector<int> findDisappearedNumbers(vector<int>& nums) {
vector<int> res;
if(nums.empty()) return nums;
for(int i=0;i<nums.size();i++)
{
int index=(nums[i]-1)%nums.size();
nums[index]+=nums.size();
}
for(int i=0;i<nums.size();i++)
{
if(nums[i]<=nums.size())
res.push_back(i+1);
}
return res;
}
};
。
461 汉明距离(简单)(位运算)
- 涉及到二进制那肯定想到位运算,很简单 很容易想到先用异或找到不同位,然后用& 统计1的个数
class Solution {
public:
int hammingDistance(int x, int y)
{
int result= x^y;
int count=0;
while(result!=0)
{
result =result & (result-1);
count++;
}
return count;
}
};
494 目标和(中等)(回溯)(背包)(待补充)
- 我的第一个想法:遇到最长最短 方案数可以想象动态规划
- 我的第二个想法: 回溯 非加 就是减
- 看了题解…确实想的差不多
- 参考链接
01背包和完全背包练习题(待补充)
543 二叉树的直径(简单)(递归)
- 不是很难,
就是要把这个问题转换一下,其实就是比较每一个子树(左子树最大深度和右子树最大深度的和),取其中的最大值
很容易出错的是我们默认以为最大直径就是过跟节点的左子树深度加右子树的深度,然后再主函数调用两次dfs 最后相加那么就错了
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int max_result=0;
int dfs(TreeNode* root)
{
//递归终止条件
if(root==nullptr) return 0;//表示当前深度为0
int left= dfs(root->left);
int right =dfs(root->right);
max_result =max((left+right),max_result);
return max(left,right)+1;//还是照样保存当前子树的最大深度
}
int diameterOfBinaryTree(TreeNode* root)
{
dfs(root);
return max_result;
}
};
560 和为k的子数组(中等)(前缀+哈希)
- 看到连续子数组 第一个想法就是滑动窗口…或许先来个排序 试试看吧,写完发现错了…这不能修改数组的顺序 要么怎么叫连续子数组呢。。想的太简单了
-
简单的说前缀和是思想,从3层循环暴力 减少到2层,哈希表是优化,从二层循环减少到一层
-
简单的说前缀表就是把问题转换成
两数之差 会不会等于 k 情况的判断。
-
我们之前做过两次for循环求两数之和的题 通过hash减少时间复杂度 这题同理
-
特别的在于 前缀表的定义决定写法,如果下标表示长度 add(n+1) 长度从0开始 那么任意两数相减就可以遍历所有的情况 如果下标表示实际坐标 那么还要还要考虑 a-b的a本身
特别重要 注意下面 1 2代码的区别 -
当然代码还可以简洁 前缀表的空间还可以省去 代码一的改进,两个并列for循环可以同时进行 修改为第三个代码
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n=nums.size();
vector<int>add(n,0);
add[0]=nums[0];
for(int i=1;i < n;i++)add[i]=add[i-1]+nums[i];
int ans=0;
unordered_map<int,int> map;
map[0]=1;//这边是判断等于本身的情况
for(int i=0; i<n ;i++)
{
if(map.count(add[i]-k))ans+=map[add[i]-k];
map[add[i]]++;
}
return ans;
}
};
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n=nums.size();
vector<int>add(n+1,0);
//add[0]=nums[0];
for(int i=1;i <= n;i++)add[i]=add[i-1]+nums[i-1];
int ans=0;
unordered_map<int,int> map;
for(int i=0; i<=n ;i++)
{
if(map.count(add[i]-k))ans+=map[add[i]-k];
map[add[i]]++;
}
return ans;
}
};
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n=nums.size();
unordered_map<int,int> map;
int pre = 0;
int count =0;
map[0] = 1;
for(int i=0; i<n ;i++)
{
pre =pre+nums[i];
if(map.count(pre-k)==1)
{
count+=map[pre-k];
}
map[pre]++;
}
return count;
}
};
前缀的题目(待补充)
前缀和哈希优化的题目(待补充)
617合并二叉树(简单)(可以复习)
想要沿用对称二叉树传入两个跟节点和构建二叉树返回值为节点的想法
,如果一边为空 那就直接返回另外一个树的节点 后面 就不用遍历了。- 看了题解和自己想的一样,没有任何区别,前序+后续,前序不断创建新的节点,后续不断从后向前把节点串起来,注意看注释。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* dfs(TreeNode* root1, TreeNode* root2)
{
//终止条件 这种写法包含了 两边为空返回为null 和 只有一边为空的情况
if(root1 == nullptr)
{
return root2;
}
if(root2 ==nullptr)
{
return root1;
}
//处理当前层的逻辑
//构建新节点 挂上下一层传递上来的值 把当前层返回给上一层
TreeNode* new_node = new TreeNode(root1->val + root2->val);
new_node->left = mergeTrees(root1->left, root2->left);
new_node->right = mergeTrees(root1->right, root2->right);
return new_node;
}
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2)
{
TreeNode* root = dfs(root1,root2);
return root;
}
};
416 分割等和子集(中等)(背包问题 )(待补充)
- 想到的回溯
538把二叉树转换成累加树(中等)(技巧)
- 第一眼看题目都提示了二叉搜索树,那直接中序,更换一下顺序 右中左不就好了 累加,没啥问题,一遍过了
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int sum =0;
void dfs(TreeNode* root)
{
if(root==nullptr) return;
dfs(root->right);
sum=sum+root->val;
root->val=sum;
dfs(root->left);
}
TreeNode* convertBST(TreeNode* root)
{
dfs(root);
return root;
}
};
581最短无序连续子数组(中等)(规律)
- 如果就是找到规律,简单的说如果把一个完全升序的序列修改中间一段无序就很容易找到规律
- 12345678 12563478 例子一
- 方法一:双指针遍历,两个指针都需要从头遍历到尾,左指针不断后移保存当前的最大值,找到
最后一个
比最大值小的。右指针向左移动,不断保存最小值,找到最后一个
比最小值大的数。0(n)的时间复杂度 - 方法二:对整个序列排序 找到第一个和最后一个不同的元素
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums)
{
int n =nums.size();
int right_max = INT_MIN;
int left_min =INT_MAX;
int left=0;
int right=0;
for(int i =0 ; i < n ; i++)
{
if(nums[i] >= right_max) right_max=nums[i];//正常情况
else
{
right=i;
}
if(nums[n-i-1] <= left_min) left_min=nums[n-i-1]; //正常情况
else
{
left=n-i-1;
}
}
if (right==0) return 0;
else return right-left+1;
}
};
437 路劲之和3(中等)(回溯 + 前缀和)
-
题目分为4种
- 从根结点到页节点
- 从根节点到任意节点
- 从任意节点到叶结点
- 从父子节点到子节点
-
第一眼看到这个题目,其实没有想到前缀和+回溯来解决。
但是看了题解很容易理解,如果想到用前缀和来解题(单独思考一条路劲),然后再想到如果想到从左子树返回到父子节点再进入右子树需要修改当前前准和 和 map的值就可以想到用回溯
-
和之前做的一个题很像,我们只要知道有几条路径,不需要知道具体的路劲,所以键是sum 值是数量
-
老样子要么一开始定义路径为0的数量为1,这样是为了找到本身,之前讲过
-
回溯什么东西么1 当你从下一层退回到上一层首先前缀和需要修改回去,第二个就是map对于当前层的前缀和减去
认真思考
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
unordered_map<int,int> map;//键表示sum综合 值表示数量
int res=0;//保存结果
//先序处理
void dfs(TreeNode* root,int& sum,int& cur_sum)
{
//递归终止条件
if(root==nullptr) return;
//处理当前层的逻辑
//1更新当前层的前缀和
cur_sum=cur_sum+root->val;
//2判断有没有符合条件的 有就统计结果 没有啥事没做
if(map.count(cur_sum-sum)==1)
res =res+map[cur_sum-sum];
//3 不管有没有符合的 把当前前缀和 放入map
map[cur_sum]++;
//4递归
dfs(root->left,sum,cur_sum);
dfs(root->right,sum,cur_sum);
//5回溯
map[cur_sum]--;
cur_sum=cur_sum-root->val;
return;
}
int pathSum(TreeNode* root, int targetSum)
{
map[0]=1;//表示本身为结果 不是差为结果
int cur_sum=0;
dfs(root,targetSum,cur_sum);
return res;
}
};
621任务调度器(中等)
- 有种感觉就是一种排序好,第二种插入,然后第三种插入,但是如何选择哪一种先插入呢。直接把冷却最长的先放入,但是冷却时间一直那肯定想到就是任务多的哪一个
- 看了题解有很像相似的想法,但是没有题解想的那么仔细
- 题解链接
- 主要理解下面这个图,
概括起来就是创建一个桶,长度为任务最多的那个数量,宽度为冷却时间+1.桶有一个缺口就是最后一个任务后面不用等待了所以需要计算并列最多的任务数。当这个桶装满了(相当于没有空闲的时间)剩下的任务可以扩充桶的宽度然后放入任务(需要多少扩充多少,不存在剩余时间),此时时间就是任务的数量。所以最后返回值就是桶的原始大小和任务数的最大值
class Solution {
public:
static bool cmp(int& p1,int& p2)
{
return p1>p2;
}
int leastInterval(vector<char>& tasks, int n)
{
int len =tasks.size();
int same=1;//表示数量也表示下标
vector<int> vec(26);
for(auto x:tasks)
{
vec[x-'A']++;
}
sort(vec.begin(),vec.end(),cmp);
while(same<vec.size() && vec[same]==vec[0]) same++;//找到相同最大
return max(len,same+(n+1)*(vec[0]-1));
}
};