快排:
原地,不稳定(不快)。
归并:
非原地,稳定。
都是O(nlogn)
快慢指针
移动0,删除重复元素保留一个,保留两个,保留三个,......(不删除,只移动)
二分查找的两种实现
记住口令:等于,不等于
两种实现方式
while(left<right){
mid=left+(right-left)/2;
if(target>nums[mid]){//小于就移动
left=mid+1;
}else right=mid;//第一个大于等于的位置不动
}
return right;
搜索插入位置(找第一个大于等于目标值的位置):
不等于方法,right=nums.size()
链表的反转:
反转整个链表,反转链表中的一部分(dummy,i)
栈和队列
检测出栈入栈顺序是否正确
#include <iostream>
#include <vector>
#include <stack>
#include <queue>
using namespace std;
int main() {
int k;
cin >> k;
while (k--) {
int n;
cin >> n;
vector<int> num;
for (int i = 0; i < n; ++i) {
int val;
cin >> val;
num.push_back(val);
}
queue<int> outStack;
for (int i = 0; i < n; ++i) {
int val;
cin >> val;
outStack.push(val);
}
stack<int> inputStack;
for (int i = 0; i < n; ++i) {
if (num[i] == outStack.front()) {
outStack.pop();
while (!inputStack.empty()) {
if (outStack.front() == inputStack.top()) {
outStack.pop();
inputStack.pop();
}
else break;
}
}
else {
inputStack.push(num[i]);
}
}
while (!inputStack.empty()) {
if (outStack.front() != inputStack.top()) {
cout << "false" << endl;
break;
}
}
if(inputStack.empty()) cout << "true" << endl;
}
return 0;
}
二叉树
二叉树有几种遍历方式---四种
前序遍历:先右后左
后续遍历:先左后右
层序遍历使用队列,先放左子节点,再放右子节点
前中后使用栈,层序遍历使用队列。
注意区分层序遍历和广度优先搜索:
前中后序遍历实现深度优先搜索,层序遍历实现广度优先搜索。
广度优先搜索的中心思想:每一次循环之后,队列中就是一层的结点数量。
二叉树的5种算法题
1,dfs三部求解路径和(最重要的一步--三部联合)
2,dfs三部求解反转二叉树(注意保留)
3,dfs三部求解相同二叉树(^运算符,0^1=1)
4,队列求解对称二叉树(我放我的左节点,你放你的右节点,我放我的右节点,你放你的作节点)
5,二叉树的最大深度
图
四联通解决岛屿问题
一维数组
941,有效山脉---
/*
两个循环判有效山脉,不仅要判断左边,还要判断右边
*/
189,旋转数组----1,公式:(0+3)%8=3,(7+3)%8=2
2,整体反转,部分反转
分发糖果--135
种花---605
找柠檬水---806
二维数组
1--1,旋转一位数组
1--2,旋转二维数组
螺旋矩阵两种题--54,59
杨辉三角两种题--118,119
两层for循环即可;
如果获取只是单层,直接填充单层,把单层作多层使用,每次多填充一位,改变前面的元素。
res[col]+=res[col-1];
对角线遍历--------498
1,两个方向走:左上和右下,所以需要定义一个往左上和右下走的二维数组。
2,它不是一层一层去遍历的,所以不要两层for循环,只和元素个数有关,所以:i<m*n即可;
3,怎么获取下一个元素下标:
当前下标为;row,col;
那么是往右上还是往坐下走呢,
表示方向的二维数组,第一个下标决定往上走还是往下走,
往同一个方向一直走,直到出了边界才改变方向,也就是改变第一个下标,也就是变为1,还是0,
di=1-di;
4,边界判断:
先判断复杂的右边界和下边界,需要移动两步;
左边界和上边界只需要移动一步;
(获取下标,判定是否合理)
字符串
字符串匹配
1--1,BM
1--2,重复的子字符串---459--本质上也是字符串匹配
2--1,反转字符串
2--2,反转字符串中的k个字符
在反转字符串的基础上,计算字符串反转的下标
2--3,反转单词----在反转字符串的基础上,计算字符串反转的下标
3,比较版本(长度不同和0比较)
4,整数转罗马数字(获取第一个小于等于的数值的罗马数值)
字符串分割函数
split--自己快速实现
数学
%---取最后一位,/---去除最后一位(除-->去除)
需要取位
1,整数反转和字符串反转
2,勾股定理的运用----a^2+b^2=N
a从1到sqrt(n),判断sqrt(N-a*a),也就是b,看看b是否为整数,如果b是一个整数,说明存在a^2+b^2=N,否则不存在。
以圆的方式理解:
a从1开始增加,加到a==sqrt(N),要不然就成不了以a,b为直角边的直角三角形了。
int main() {
int N;
cin >> N;
int n = sqrt(N);
bool res = false;
for (int a = 1; a < n; ++a) {
float b = sqrt(static_cast<double>(N - a * a));
int c = b;
if (c == b){
res = true;
break;
}
}
if (res)
cout << "true" << endl;
else cout << "false" << endl;
return 0;
}
字符数字相加题
需要取位(本质上就是要实现两个数据一位一位相加的结果)
2--1,数字和整数相加---989
2--2,加1----66
2--3,字符串相加----415
2--4,二进制求和---67
二进制求和 同之前的求和方法,几乎一模一样,只二进制是对2取余和求模
2--5,两数相加(链表)--2
注意:是逆序存储,所以很好处理,如果是非逆序,可以先将链表中的数据存储在数组中。
质数----204
每一个质数的倍数的数都不是质数,
排除了倍数的,剩下的就是质数
什么是质数:就是有除了1以外的倍数的数
如何呈现倍数增长:
for(int j=i;j<n;j+=i)
斜率比较--1232
快速幂
位运算
3,汉明距离总和
4,5,2的幂--231
6,^运算符-----只出现一次的数字--136,137(^&~),
只出现一次的两个数据--
滑动窗口
前缀和---643
注意:数组计算总和一定不要重复计算,而是要利用前缀和。
前缀和要加上第一个元素---0.作用就像链表的亚结点一样。
前缀和的规律:
每一个前缀和的元素都是第一个元素一直加到次元素的结果,所以区间[i,j]的总和为:
sum=preSum[j]-preSum[i-1];
也就是减去前面的区间就是这个区间的总和。
变量代替数组减少空间复杂度
糖果,643
滑动窗口基本代码格式
1,一直往右边走,没处理一个元素,判断是否满足条件。
2,如果满足条件进行处理,左边去除一个元素。
3,右边继续走。
滑动窗口使用条件
数组孩子字符串内的元素的移动必须是连续的子串移动方式。
1,滑动窗口题型----最长的连续子串
2,leetcode---424--替换后的最长子串
解法:
a,暴力:dfs找出所有子串,一一替换寻找。
#include <iostream>
#include <vector>
using namespace std;
int getMaxLen(vector<string>& nums, int k) {//aabaabaa
int maxLen = 0;
for (auto str : nums) {
int k2 = k, a = 0, b = 0;
for (char c : str) {
if (c == 'a') a++;
else b++;
}
char alph;
if (a >= b) alph = 'b';
else alph = 'a';
for (int i = 0; i < str.size(); ++i) {
if (str[i] == alph) k2--;
if (k2 < 0) break;
}
if (k2 >= 0) {
int strLen = str.size();
maxLen = max(maxLen, strLen);
}
}
return maxLen;
}
int main() {
int len, k;
cin >> len >> k;
string str, tmpstr;
cin >> str;
vector<string> numsStr;
for (int i = 0; i < str.size(); ++i) {
for (int j = i; j < str.size(); ++j) {
tmpstr.push_back(str[j]);
numsStr.push_back(tmpstr);
}
tmpstr.clear();
}
int max_len = getMaxLen(numsStr, k);
cout << max_len << endl;
return 0;
}
贪心
1,删除重复字符串---三类题
2,能否跳到最后一个下标的问题---55(统计随着下标变化的最大值--贪心)
方法:统计当前下标之前的所有跳法中跳的最远的距离,看能不能跳到当前下标。
如果计算到某一个下标时,之前的所有下标的可以跳的最远距离都跳不到这个下标,那么肯定是跳不到最后的。
3,最少跳跃次数---45---获取某个区间内可以跳跃的最远距离(贪心),看看这个最远距离也没有到达u最后。
回溯
1,字符串切割为斐波那契数---842 切割回文串
2,全排列:重复数据,重复路径
全排列的特点:
2-1.没有重复元素,find()即可;
2-2.含有重复元素的,如何去重:
需要通过辅助数组去重:排序,后前比较--》nums[i]==nums[i-1];
visited[i-1]表示同一个排列中的同一个位置,如果已经前面一个已经使用过了,后面这个与前面一个相对的就不再使用了。
3,组合:同一个元素无线使用,单次使用且有重复元素
全排列的重复路劲---单词组合的重复元素
全排列:
if(i>0&&nums[i]==nums[i-1]&&!visited[i-1]) continue;
每一级的遍历起始下标都从0开始
单词组合:
if(i>startIndex&&nums[i]==nums[i-1]) continue;
每一级的遍历起始下标都从startIndex开始
4,数组子集
几个基础:
获取所有子串
string str, tmpstr;
cin >> str;
vector<string> numsStr;
for (int i = 0; i < str.size(); ++i) {
for (int j = i; j < str.size(); ++j) {
tmpstr.push_back(str[j]);
numsStr.push_back(tmpstr);
}
tmpstr.clear();
}
动态规划
动态规划最重要就是数组和数组下标,要明白下标表示什么,值表示什么
什么题型可以使用动态规划
1,字符串子集,字符串子序列
子集:
回溯--每层切几个,从头切到尾
int lenStart=1;
for(int i=startIndex;i<s.size();++i){
int len=lenStart++;
string tmp=s.substr(startIndex,len);
if(isPalindrome(tmp)==false) continue;
path.push_back(tmp);
dfs(s,i+1,path,allRes);
path.pop_back();
}
lc---131
子序列:
回溯--下标加一
lc---516
回文串
1,最长回文子串--5
回文子串判断条件--相等1,2
2,回文子串数
解法同上
总结----统计回文子串的方法
3,分割回文串
dfs
int len=lenStart++;
4,最长回文序列--516
1)dfs会超时
2)动态规划:二维上填数组
dp[i][j]=max(dp[i][j-1],dp[i+1][j]);
dp[i][j-1]---左区间
dp[i+1][j]--右区间
5最长回文串(用一个字符串中的字符组成最长的回文串)--409
一个字符出现n次,那么它在回文串中
res+=n/2*2;
unordered_map
斐波那契
爬楼梯
dp[i]=dp[i-1]+dp[i-1];
tmp=cur+pre;
pre=cur;
cur=tmp;
从一级台阶和二级台阶都可以跳上三级台阶,那么跳上三级台阶的方法就是到达一级台阶的方法加上到达二级台阶的方法的总和
最少花费爬楼梯
dp[i]=min(pre+dp[i-1],cur+dp[i-2])
使用指针---O(1)
零钱兑换问题
先遍历目标值和先遍历硬币的区别:
先便利目标值:---322---求最少组合
一个目标值循环遍历一遍硬币,找到组合成本目标值的最小硬币数。
先遍历硬币:---518---求总组合数
先决条件:每一个目标值都初始化为0
所以每一个硬币循环遍历一遍所有的目标值,只要小于等于目标值
count[target]=count[target]+count[target-c];
二维网络路径问题
最小路径和--64
两边和中间
O(1)空间复杂度
不同路径--62
两边和中间
路径有障碍的问题--63
最长递增子数组--674
连续元素比较---和前一个数据比较加1
最大子数组之和--53
非连续元素比较---和之前的所有数据比较取大
int res=nums[0];
for(int i=1;i<len;++i){
nums[i]=max(nums[i-1]+nums[i],nums[i]);
res=max(res,nums[i]);
}
前面和当前取大
两个max
最长递增子序列--300
两个max
(最长递增数)--738
打家劫舍
题1--198
for(int i=2;i<len;i++){
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
}
题2--213
分为两个求
题3--337
偷或者不偷当前结点
cur[0]=max()
cur[1]=
背包问题
1,01背包
dp[i-1][c]---表示不放时,容量c的当前的最大价值就是上一个物品的价值;
V[i]+dp[]--表示放当前物品的价值
2,零钱兑换
硬币在外,不重复的匹配--322,
322--最少组合数---count[0]
518--总组合数---------count[0]=1
count[target]=count[target]+count[target-c];
硬币在内,重复匹配--------377,
3,完全平方数--279
1......sqrt(n)
4,目标和--494
(同硬币求组合和的问题)
(sum-neg)-neg=target
反着求--neg----0
5,分割等和子集 416
(同硬币求组合和的问题)
反着求--target----0
求组合类题型:
零钱,目标和,分割等和子集
二叉树
二叉树的两个遍历结果求另外一种遍历可能的结果
遍历的结果都是扎堆存放的。
前序或者后续找到根节点,中序找到根节点的左右子树。
前序遍历第一个是根节点,后续遍历最后一个是根节点。
所有的节点分成三部分:
前序遍历:中左右;
中序遍历:左中右;
后序遍历:左右中;
更具这个规律,每次从前续遍历中获取根节点,再让左子树的前序遍历和右子树的中序遍历获取左子树的根节点返回即可,右子树同理,后续遍历同理。
前序遍历和中序遍历得二叉树:
前序切割:根节点中序遍历中前面有几个,前序遍历就要往后切割几个。
TreeNode* getBinaryTreeFromPreMid(vector<int> preVec, vector<int> midVec) {
if (preVec.size() == 0) return nullptr;
//前序遍历中取根节点
TreeNode* root = new TreeNode(preVec[0]);
//中序遍历中找到位置
int index = 0;
for (int i = 0; i < midVec.size(); ++i) {
if (midVec[i] == preVec[0]) {
index = i;
break;
}
}
//left,上加1
vector<int> pre1(preVec.begin()+1,preVec.begin()+index+1);
vector<int> mid1(midVec.begin(),midVec.begin()+index);
root->left = getBinaryTreeFromPreMid(pre1,mid1);
//rihgt,都加1
vector<int> pre2(preVec.begin()+index+1,preVec.end());
vector<int> mid2(midVec.begin()+index+1,midVec.end());
root->right = getBinaryTreeFromPreMid(pre2,mid2);
return root;
}
void postOrder(TreeNode* root) {
if (root == nullptr) return;
postOrder(root->left);
postOrder(root->right);
cout << root->val << endl;
}
后续和中序遍历得二叉树:
TreeNode* getBinaryTreeFromMidPost(vector<int> postVec, vector<int> midVec) {
int len = postVec.size();
if (len== 0) return nullptr;
//后序遍历中取根节点
TreeNode* root = new TreeNode(postVec[len-1]);
//中序遍历中找到位置
int index = 0;
for (int i = 0; i < midVec.size(); ++i) {
if (midVec[i] == postVec[len-1]) {
index = i;
break;
}
}
//left
vector<int> post1(postVec.begin(), postVec.begin() + index);
vector<int> mid1(midVec.begin(), midVec.begin() + index);
root->left = getBinaryTreeFromMidPost(post1, mid1);
//rihgt
vector<int> post2(postVec.begin() + index, postVec.end()-1);
vector<int> mid2(midVec.begin() + index + 1, midVec.end());
root->right = getBinaryTreeFromMidPost(post2, mid2);
return root;
}
void preOrder(TreeNode* root) {
if (root == nullptr) return;
cout << root->val << endl;
preOrder(root->left);
preOrder(root->right);
}
为什么没有前序和后续得到二叉树
因为只根据前序和后续无法确定一颗唯一的二叉树,想要确定二叉树,必须要包含中序。
找规律
找规律,详细到每一步怎么做。
规律不是靠自己去猜的,而是在完成事件的过程中发现的;
把大问题从小问题看起,小问题满足的规律,大问题就满足了
表达式
设变量,列表达式的题
步骤:
1,题目类型
2,公式或者方法推导
3,写程序
题目类型
1,数学题
设变量,列表达式的题
根据规律找出计算结果表达式---链接
最大公约数(gcd)和最小公倍数(lcm)
最大公约数=A,B不断取余和A,B较小数的最大公约数,直到取余为0
最小公倍数=A*B/最大公约数
int gcd(int x, int y) {
while (y != 0) {
int r = x % y;
x = y;
y = r;
}
return x;
}
int gcd(int x, int y) {
if (y == 0) {
return x;
} else {
return gcd(y, x % y);
}
}
lcm=x*y/gcd;
(1),数值类型选择,如果数值范围比较大,计算结果可能超出,就必须使用大数据类型。
(2),规律可能有多层,规律找到越多,代码性能越好。
发现规律的方法:
1)常识,定理,公式.........
2)多列出几项,找规律。
2,纯纯的找规律的题型----链接