目录
排序
全排序Next Permutation
归并排序 -- 数组中的逆序对个数
快排
稳定快排
堆排序 -- TopK问题
希尔排序(缩小增量排序,优化的插入排序)
查找
二分查找-- 最大最小值问题 力扣410
大数据的中位数--快排分割思想+外存
递增二维矩阵,查找某个值
二维矩阵找第K元素
大数据中查找数(外存排序、压缩+哈希)
有序数组之和的TopK问题
动态规划
限制长度的最大连续子序列和(dp+滑窗+前缀和数组)
最小编辑距离
最长公共子序列LCS
0-1背包
完全背包
二维地图左下到右上路线总数(障碍)
地图中左上角走到右下,再回到左上,两条路径不能重复的最小代价
字符串分割成字典个数
数字转字母的编码数(1-a,26-z)
栈
数字字符串 移掉K位数字后最小
中序转后序
滑动窗口
最大无重复子串长度
最小包含字串长度
二叉树
最近公共祖先节点
数学
'全错位'排列问题
判断[2,n]内素数个数
布隆过滤器
十进制转二进制、十六进制
字符串
KMP模板匹配
字典序最小问题
链表
单链表排序
环入口点
两个链表是否相交,找到相交点
反转
数组
整数数组,求和为K的对数
正方形顺时针90度旋转
图论
拓扑排序
最大匹配(最小覆盖)--匈牙利算法
智力题
公司真题
虾皮19年秋招 笔试题4 二维01数组,找0中到所有1距离最小
虾皮19年秋招 笔试题3 实现Linux * 匹配0-多个字符
数据结构细节
循环队列
树
- 从后往前,找第一个满足V[i]
- 然后从后往前找第一个j,V[j]>V[i]
- 交换i,j,让[i+1,最后)变成递增序列
void mergeSortCalNiXu(vi &V,int l,int r,int &ans){ if((r-l)<=1) return; int mid = l+(r-l)/2; //递归合并左右子数组 mergeSortCalNiXu(V,l,mid,ans); mergeSortCalNiXu(V,mid,r,ans); vi data(r-l); int k=0; int i=l,j=mid; while(i
note:
- 计数时,不能前半数组元素比后半大时计算,应该要<=时计算,这时才知道最后i应该加几个逆序,否则会出现重加的问题
- 当i还有剩时,加上右边数组长度的逆序对数目
区间左闭右开,第二重while循环 移动指针时用开,i或j==哨兵时,交换哨兵,最后递归时注意mid不要再参与
//采取交换pilot两侧的目标,更快速 template
void my_qsort(vector &V,int start,int end){//qucik sort if(start>=end) return; int mid=start+(end-start)/2;//pilot int i=start,j=end; while(true){ while(V[i] j) break; swap(V[i],V[j]); if(i==mid) mid=j;//哨兵位置发生变化 else if(j==mid) mid=i; i++; j--; } //上述操作之后 哨兵位于i位置,j在其左侧 my_qsort(V,start,j); my_qsort(V,i,end); } 平均复杂度计算 :假设T(k)表示k长度的时间,划分点选择任一概率相同,,k-1和n-k在k=0,1,...,n-1都算过,相当于每个k求了两次,然后通过T(n)-T(n-1)求递推式的解
给记录多加一个编号域,做双关键字的快排
学习自 我已飞过 的答案
维护一个含K个数的最小堆,遍历数组,比根小则丢弃,最后堆剩下的即为TopK,O(NlogK)
思想:当要查找的数位于数组的边缘时,可以将二分的分割点倾斜以减小查找区间大小
- 比较右上角元素与值
- 若大,则右边一列可以扔掉,查看第1-倒数第二列的矩阵
- 若小,则左边一行可以扔掉,查看第2-最后一行的矩阵
- 每个位置遍历,在左下角、右上角进行二分,查找比当前位置小的元素个数
- 复杂度O(nm*log(nm))
大数据由于数据量较大,本质还是查找,所以依旧可以分为两种思路:散列、排序再查找,大数据的与众不同在于数据不能直接放进内存,需要拆分,通过外存作为中转,外存排序
压缩+哈希
举例:40亿32bit数组找是否存在某个数
- 40亿*32bit = 2^2*2^5^10亿 = 2^7*10*9 = 2^37,因为2^10=1024可以约等于1000
- 而32bit系统内存最大是2^32 bit,所以存不下
- 但是这题设置32bit,以及40亿这个数,就是想我们用压缩的方式,bitmap,用1bit表示一个32bit数是否存在的情况,这样内存就够用了
- 解法:32bit内存全用来存储,第i位=1表示i这个数存在于数组,然后查这个数位置上的0,1就判断出是否存在了
举例2:手机号MD5加密之后,如何快速查找?1000万的数据2G的内存怎么实现高效?
- MD5加密后得到128bit=2^7bit
- 1000万*2^7 = 10^7*2^7 = 2^27*10 = 2^30*1.25
- 2G = 2^31次方
- 内存存的下,这题是128bit数据就不能像上面题那么做了,bitmap内存需要2^128
- 这题用hash表
外存排序
举例:40亿32bit数组找是否存在某个数
- 大小分析见上文
- 编程珠玑里提供了方案,通过根据第一位是否为0,将数组分成两部分,再根据第二位,分成4部分,依次类推,分到某一部分4G内存可以完全存下了,就可以扔到内存hash或者排序解决了
大顶堆,堆中保存(数值,左元素编号,右元素编号),先让A[n-1]+B[m-1]入堆,
然后弹出堆顶元素,将堆顶元素相近的两个元素入堆(i-1,j)(i,j-1),调整堆结构,
重复上一行操作,直至弹出K个
#include
#include using namespace std; //C++中的堆 int maint(){ vector V = {3,5,2,6,4); make_heap(V.begin(),V.end(),greater ());//建堆 //最后插入元素 V.push_back(7); push_heap(V.begin(),V.end());//调整最后一个元素的堆位置 pop_heap(V.begin(),V.end());//弹出堆顶元素 int x = V.pop_back(); sort_heap(V.begin(),V.end(),greater ());//堆排序 }
- dp[i]表示以i结尾时答案,s[i]表示该答案下子序列长度
- 状态转移方程:
- if s[i-1] < m:
- if V[i]>=V[i]+dp[i-1]:
- dp[i] = V[i]
- s[i] = 1
- else:
- dp[i] = V[i]+dp[i-1]
- s[i] = s[i-1]+1
- else:
- j = i-s[i-1] //找i-1答案的左边界+1,左边界直接扔掉
- while(j
- 求[j,i-1)和sum,可用前缀和优化
- if V[i]>=V[i]+sum:
- dp[i] = V[i]
- s[i] = 1
- else:
- dp[i] = V[i]+sum
- s[i] = (i-1-j)+1
dp[i][j]表示s1串前i子串与s2前j子串的最小编辑距离
- if s1[i]==s2[j]) dp[i][j] = dp[i-1][j-1];
- else dp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+1)
- 3个数依次表示
- 删除i或在j后插入
- 删除j或在i后插入
- 修改i或j
LCS(m,n)表示串1前m个字符组成的子串 与 串2前n个字符组成的子串的最长公共子序列
- if(s1[m]==s2[n]) LCS(m,n) = LCS(m-1,n-1)+1
- else LCS(m,n) = max(LCS(m-1,n), LCS(m,n-1))
for i=1..N: for v=V..0: f[v]=max{f[v],f[v-cost]+weight} //保证每件物品只选一次,从f[i-1][]中肯定没有选过第i件
for i=1..N: for v=0..V: f[v]=max{f[v],f[v-cost]+weight} //允许从f[i][]中选择
细节:注意答案可能超过INT上限,得用long long
- 可看做两人同时走,不能重叠,则有x1+y1 = x2+y2, (x,y)是走到的坐标
- s(x1,y1,x2) 表示走到(x1,y1)(x2,y2)时的最小代价,y2可由其他三个计算得到
- s(x1,y1,x2) = v(x1,y1)+v(x2,y2)+Max(s(x1+1,y1,x2+1),s(x1,y1+1,x2+1),s(x1+1,y1,x2),s(x1,y1+1,x2))
- Max里的表示两人的4种选择(甲乙向右、下的四种组合)
- 动规存储字符串分割的个数、存储字典,从头开始找单词,然后递归剩余字符串
小坑:10和20是一种编码
思路:单调增栈,
新元素<栈顶时,K-1,弹出栈顶,新元素入栈;
>=时,直接入栈
中序s转后序(符号栈、后序栈)
遍历s:
数字 入后序栈
( 入符号栈
) 弹出符号栈第一个(之前的符号进入后序栈
操作符 弹出符号栈中比当前操作符优先级低的符号到后序栈,最后当前操作符入符号栈
最后符号栈全部入后序栈
单调栈+前缀数组(存前i项之和)
维护一个递减栈,若小于栈顶,直接入栈,
大于栈顶时,判断右边元素是否更大,更大就一直右移,弹出比当前元素小的栈顶,直到栈顶比当前元素大,然后计算一次雨水(填满时的格子 距离*min(栈顶、当前元素) - 未填时的格子数 )
- 滑窗,使用一个HashMap统计每个字符差的数目,一个整数记录当前窗口差的数目
- O(n)
双指针,右边先移动到所有全>1
移动左边直至某个数个数为0,移动右边直至遇到这个数
排序+双指针
第一重循环遍历第i个数,第二重循环遍历右边的每个数(第一个指针),第二个指针从尾往前移动
问题描述:二叉树中,找节点p,q的最近公共祖先节点
解题思路:
- 最近公共祖先节点是唯一一个能在左子节点中找到p或q,在右节点中找到另一个节点的节点
- 递归
//针对根 // 最近公共祖先节点是第一个 在左子节点中找到p或q,在右节点中找到另一个节点的 TreeNode* ans; TreeNode* find_ancestor(TreeNode *T,TreeNode *p, TreeNode *q){ if(T==p or T==q) return T; if(q==p) return q; TreeNode* left_ans,*right_ans; if(T->left){ //找p或q left_ans = find_ancestor(T->left,p,q); } if(T->right){ right_ans = find_ancestor(T->right,p,q); } if(left_ans and right_ans){ ans = T; //结束 return T; } return NULL; //1. p,q==root,返回root }
#include
using namespace std; struct TreeNode{ TreeNode *left,*right; int val; TreeNode(int v):val(v),left(NULL),right(NULL){} }; vector Inorder(TreeNode *T){ stack S; vector ans; TreeNode *p = T; while(p || !S.empty()){ while(p){//让节点p以及其左边一条线的子孙全部依次入栈 S.push(p); p = p->left; } p = S.top();//取出最左边节点 S.pop(); V.push_back(p->val); p = p->right;//开始走右边 } return ans; } int main(){ TreeNode *T=new TreeNode(5); T->left = new TreeNode(3); T->left->right = new TreeNode(4); T->left->right->left = new TreeNode(1); T->right = new TreeNode(2); vector ans = Inorder(T); for(auto a:ans){ cout<
n个信封,全部装错了,问有多少种排列方式
- 假设f(i)表示i个信封的排列方式,f(1),f(n-1)已知,n>=2;
- 考虑f(n),
- 当n放到位置i上时,可分两种情况进行讨论
- i放到n上,其他n-2个又构成了全错位排列,有f(n-2)个情况
- i不放到n上,这时候i的角色与n等价,所以变了n-1的全错位排列,有f(n-1)个情况
i可以取1到n-1,故 f(n) = (n-1)*[ f(n-2) + f(n-1) ]
方法1:
- 概述:若x非素数,则存在i*j=x,则min(i,j)<=sqrt(x),所以可以在[2,根号i]遍历判断是否x整除它们
- 时间复杂度:
方法2:诶式筛
- 概述:若x是素数,则2*x,3*x,...这些比x大的整数倍一定是合数,可以过滤掉
- 时间复杂度:O(nloglogn)),不知道咋算的。。
int countPrimes(int n) { int ans=0; vector
isPrimed(n+1,1); int j; for(int i=2;i<=n;i++){ if(isPrimed[i]==1){//i是质数 ans++; j=i*2; for(;j<=n;j+=i){ isPrimed[j]=0;//它的整数倍不是 } } } return ans; } 方法3:线性筛
- 概述:用一个数组保存所有素数,遍历x,将x与数组所有素数相乘的结果标记
- 时间复杂度:O(n),因为每个数跟素数相乘结果唯一,所有合数只会被标记一次
int countPrimes(int n) { int ans=0; vector
isPrimed(n+1,1),primed; int j; for(int i=2;i<=n;i++){ if(isPrimed[i]==1){//i是质数 primed.push_back(i); ans++; } for(auto a:primed){ if(a*i<=n) isPrimed[a*i]=0; else break; } } return ans; }
理论介绍 :判断一个key是否在集合中
- 插入元素:
- 准备一个很长的二进制列表(初始时每位为0)、一系列hash函数
- 对插入的key分别计算hash值并映射到列表中,将映射位置置于1
- 查询元素:
- 对key同样分别计算hash映射,查询所有映射位是否全为1
- 若存在不为1的位,则key肯定不在;否则key可能存在集合中
优点:常量级的空间、时间复杂度;缺点:误认为不存在的key存在
难点:
- 进制转换
- 从低位开始处理每个数字
- 用数字 乘以 10^位置得到整数值t
- while循环处理这个值,就像小学除法,移位求余
j=0; while(t!=0){ two[j] += t%2; //进位 if(two[j]>1){ two[j+1]+=two[j]/2; two[j]%=2; } t/=2; j++; }
- 每个位乘以对应的幂级数转化成整数
- 题目设定转化的二进制长度一定是16位,有个超界需要判断
- 如果转化后二进制超过16,则超界
- 负数,补码:取反+1
小坑:to_string(‘A')时得到的居然是65,用 string s = 'A' t得到的才是字符
答案:只有平方数(2^2,3^2)的因子个数才为奇数,其他必然有因子配对
rand3生成rand5:3*(rand3)+rand3 = [0,8],如果为8则再生成一次,然后返回%5的结果
randX生成randY:X*(randX)+randX = [0,X*X-1=Z],选一个k使得随机数>k*Y-1时重新生成,<时就模Y
按个、十、百...位数统计,分开整10幂次数(如连续10000-20000这10000个数)与1000-1564这种
数学规律,每个数里会出现连续的的第k-1位为1的情况,所以整10幂次根据这个计数
非整10幂次则另外:当第k为>1时,该位1的个数为,k==1时,该位1的个数为剩余k-1位组成的数+1
计算next[i]时应该使用s[i-1]比较!! 因为求的是(i-1的后缀)与字符串前缀
问题描述:一个长N的字符串,可以选择N次从首部或尾部取一个字符,取出的字符拼成一个新串,求最小字典序的串
方法:双指针+贪婪法
难点:左右指针指向的字符相同时,应该同时往里移动,直至找到左右指针不同的,如果右边小,则将右边的都取走
中心拓展:以每一个位置、或两个位置为中心,向两边拓展
时间复杂度:O(N^2)
遍历,转成数组,然后数组排序,再转成链表
双指针slow,fast,slow每次走一步,fast每次走两步,等相遇时
令fast=head,两个都开始走一步,再次相遇的点就是环的入口点解释:
- 假设非环部分长为l1,环长为l2,相遇时slow走了l3步
- 则l3=k*l2,k为正整数,就是说相遇时slow走的步数一定是环长的整数倍
- 当fast从头开始再次走到入口点时,slow就走了l1+整数倍环长,所以它相当于在环里溜达了几圈又回来了入口点,所以两者相遇
两个链表都走到尾部,如果两个引用相同,则相交
让第一个链表的尾节点指向第二个链表的头节点,这样第一个链表就变成了一个带环的链表,变成了环入口点问题
- 栈
- 头插法
- Set+遍历,用Set存储所有遍历的值,若Set已存在与当前遍历值相加为K的,计数+1,最后计数要除以2
- 时间:O(nlogn),Set红黑树查询logn * 遍历n
- 空间:O(n)
- 排序+栈:先排序,然后维护一个递增栈,遍历数组,若当前元素+栈顶
则出栈顶(后面元素都更大,不可能找到跟顶配对的数),=则计数
- 时间:排序O(nlogn)+遍历O(n)
- 空间:O(n)
- 双指针:排序,每次给左指针i找合适的,j从右往左移,若i+j >k,让j左移,
- 时间:排序O(nlogn)+遍历O(n)
- 空间:O(1)
号从1-n的数法时,n是正方形的边长
- 旋转后的行号是旋转前的列号;
- 旋转后的列号+旋转前的行号=n+1
一个数的计算次数 = 当它为子数组最右边时最小的长度 * 当它为子数组最左边时最小的长度
3 1 2 4
考虑1,它满足是某个子数组最右边的数且是最小的个数为2,最左边为3,所以计算次数为6,用两个数组分别记录这个值
优先级队列,入度数组
用于图的最大匹配问题(等价于最小覆盖问题)
背景知识
二分图: 如果将图中节点分为两个子集,子集中任意两个节点无边相连
匹配: 边的集合,如果其中两边没有交点 (匹配边用红线)
最大匹配: 在图的all匹配中,边的个数最多的
完美匹配: 所有顶点都是匹配中的边覆盖,完美匹配一定是最大匹配
交替路径: 从未匹配点出点,交替经过未匹配边、匹配边、。。。形成的路径
增广路径: 一条连通两个未匹配点的交替路径
由增广路的定义可以推出下述三个结论:
(1)P的路径个数必定为奇数,第一条边和最后一条边都不属于M。 M为匹配边的集合
(2)将M和P进行取反操作可以得到一个更大的匹配 。
(3)M为G的最大匹配当且仅当不存在M的增广路径。
所以匈牙利算法:
(1)置M为空
(2)从图中找一条增广路径P,通过异或获得更大的匹配M'代替M (结论2)
(3)重复(2)直至再找不到增广路径(dfs)
有两个桶,其中一个中有5个球,另一个有7个球,A、B两个人轮流取球,每次只能从一个桶至少取一个球,那么有没有一种方式,让先手必赢?
- 当两个桶各剩1时,先手必胜;
- 每次从某个桶取的数必然<=桶数-1,除非只剩1个
- 做法:左右桶不等时,从多的桶里取多的那部分,让两桶相同;后手无论取多少最后都会变成1,1的,所以先手必胜
有a升和b升的杯子,求最少多少次能得到c升水?
能倒出gcd(a,b)的倍数升水,欧几里得求最大公约数
每一点到(x,y)距离为 行间距+列间距
所以所有1到(x,y)距离和为
每一行的1个数*行间距+每一列的1个数*列间距
#include
using namespace std; typedef long long ll; typedef vector vll; typedef vector > vvll; typedef vector vi; typedef vector > vvi; typedef vector vs; typedef vector vb; #define UF(i,start,end) for(auto i=start;i =end;i--) int main(){ freopen("temp.in","r",stdin); int n; while(cin>>n){ vvi V(n,vi(n)); vi H(n,0),L(n,0);//行、列的1个数 UF(i,0,n){ UF(j,0,n){ cin>>V[i][j]; if(V[i][j]==1){ H[i]++; L[j]++; } } } int ans=INT_MAX,nans; UF(i,0,n){ UF(j,0,n){ if(V[i][j]==0){ nans=0; UF(k,0,n){ nans+=L[k]*abs(j-k)+H[k]*abs(i-k); } ans=min(ans,nans); } } } cout<
细节:模板串遍历到*号时,
判断*号是否还有字符,没有匹配串后面就全部匹配上了;
有的话就循环在匹配串往后找跟模板串后面字符一致的,每找到一次就递归调用,从模板串*后面那个位置开始匹配
#include
using namespace std; typedef long long ll; typedef vector vll; typedef vector > vvll; typedef vector vi; typedef vector > vvi; typedef vector vs; typedef vector vb; #define UF(i,start,end) for(auto i=start;i =end;i--) /* 思路1 判断s[l,r)是否匹配,匹配则输出 过50%,超时 思路2 AC */ bool token;//是否有匹配的标志 //从s的r位置 与p的i位置进行匹配,l是s的起点 void solve(string &p,string &s,int l,int r,int i){ int n=p.length(),m=s.length(); char c1,c2; while(i >p>>s){ int n=p.length(), m=s.length(); token=true; UF(l,0,m){ solve(p,s,l,l,0); } if(token) cout<<-1<<" "<<0<
rear尾指针:指向队尾元素的下一个位置,即待插入位置
front头指针:指向头元素位置
队列长度:MaxSize
空:rear=front
满:约定队列保留一个空间,以区分空与满,所以当(rear+1)%MaxSize=front时,队列满了,元素数目为MaxSize-1
分类:
- 按子节点个数可分为:二叉树、多叉树
- 二叉树
- 按结构 可分为普通二叉树、完全二叉树、满二叉树
- 按是否有序可分为 普通二叉树、二叉查找树、二叉平衡树