算法笔记

目录

排序

全排序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-多个字符

数据结构细节

循环队列


排序

全排序Next Permutation

  • 从后往前,找第一个满足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:

  1. 计数时,不能前半数组元素比后半大时计算,应该要<=时计算,这时才知道最后i应该加几个逆序,否则会出现重加的问题
  2. 当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长度的时间,划分点选择任一概率相同,T(n) = \frac{1}{n}\sum_{k=0}^{n-1}(T(k-1)+T(n-k)) = \frac{2}{n}\sum_{k=0}^{n-1}(T(k)),k-1和n-k在k=0,1,...,n-1都算过,相当于每个k求了两次,然后通过T(n)-T(n-1)求递推式的解

稳定快排

给记录多加一个编号域,做双关键字的快排

学习自  我已飞过 的答案

堆排序 -- TopK问题

维护一个含K个数的最小堆,遍历数组,比根小则丢弃,最后堆剩下的即为TopK,O(NlogK)

希尔排序(缩小增量排序,优化的插入排序)

查找

二分查找-- 最大最小值问题 力扣410

插值查找(二分查找的优化)

思想:当要查找的数位于数组的边缘时,可以将二分的分割点倾斜以减小查找区间大小

大数据的中位数--快排分割思想+外存

递增二维矩阵,查找某个值

  • 比较右上角元素与值
    • 若大,则右边一列可以扔掉,查看第1-倒数第二列的矩阵
    • 若小,则左边一行可以扔掉,查看第2-最后一行的矩阵

二维矩阵找第K元素

  • 每个位置遍历,在左下角、右上角进行二分,查找比当前位置小的元素个数
  • 复杂度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或者排序解决了

有序数组之和的TopK问题

大顶堆,堆中保存(数值,左元素编号,右元素编号),先让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+滑窗+前缀和数组)

  • 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

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))

0-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种选择(甲乙向右、下的四种组合)

字符串分割成字典个数

  • 动规存储字符串分割的个数、存储字典,从头开始找单词,然后递归剩余字符串

数字转字母的编码数(1-a,26-z)

小坑:10和20是一种编码

数字字符串 移掉K位数字后最小

思路:单调增栈,

新元素<栈顶时,K-1,弹出栈顶,新元素入栈;

>=时,直接入栈

中序转后序

中序s转后序(符号栈、后序栈)
        遍历s:
            数字    入后序栈
            (          入符号栈
            )        弹出符号栈第一个(之前的符号进入后序栈
            操作符    弹出符号栈中比当前操作符优先级低的符号到后序栈,最后当前操作符入符号栈
            
        最后符号栈全部入后序栈 

接雨水--找先减后增的连续序列

单调栈+前缀数组(存前i项之和)

维护一个递减栈,若小于栈顶,直接入栈,

大于栈顶时,判断右边元素是否更大,更大就一直右移,弹出比当前元素小的栈顶,直到栈顶比当前元素大,然后计算一次雨水(填满时的格子  距离*min(栈顶、当前元素) - 未填时的格子数  )

滑动窗口

 

最大无重复子串长度

最小包含字串长度

  • 滑窗,使用一个HashMap统计每个字符差的数目,一个整数记录当前窗口差的数目
  • O(n)

求最小连续子数组包含(1-m)数字各1各以上

双指针,右边先移动到所有全>1

移动左边直至某个数个数为0,移动右边直至遇到这个数

三数之和为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上时,可分两种情况进行讨论
  1. i放到n上,其他n-2个又构成了全错位排列,有f(n-2)个情况
  2. i不放到n上,这时候i的角色与n等价,所以变了n-1的全错位排列,有f(n-1)个情况

i可以取1到n-1,故 f(n) = (n-1)*[ f(n-2)  + f(n-1) ]

判断[2,n]内素数个数

方法1:

  • 概述:若x非素数,则存在i*j=x,则min(i,j)<=sqrt(x),所以可以在[2,根号i]遍历判断是否x整除它们
  • 时间复杂度:O(n\sqrt{n})

方法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存在

十进制转二进制、十六进制

难点:

  1. 进制转换
    1. 从低位开始处理每个数字
    2. 用数字  乘以  10^位置得到整数值t
    3. while循环处理这个值,就像小学除法,移位求余
    4. 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++;
      }
      

       

    5. 每个位乘以对应的幂级数转化成整数
  2. 题目设定转化的二进制长度一定是16位,有个超界需要判断
    1. 如果转化后二进制超过16,则超界
  3. 负数,补码:取反+1

小坑:to_string(‘A')时得到的居然是65,用 string s = 'A' t得到的才是字符

力扣319 求因子个数为奇数

答案:只有平方数(2^2,3^2)的因子个数才为奇数,其他必然有因子配对

rand3生成rand5,randX生成randY

rand3生成rand5:3*(rand3)+rand3 = [0,8],如果为8则再生成一次,然后返回%5的结果

randX生成randY:X*(randX)+randX = [0,X*X-1=Z],选一个k使得随机数>k*Y-1时重新生成,<时就模Y

力扣233 统计1-n中数字1个数

按个、十、百...位数统计,分开整10幂次数(如连续10000-20000这10000个数)与1000-1564这种

数学规律,每10^{k}个数里会出现连续的10^{k-1}的第k-1位为1的情况,所以整10幂次根据这个计数

非整10幂次则另外:当第k为>1时,该位1的个数为10^{k-1},k==1时,该位1的个数为剩余k-1位组成的数+1

字符串

KMP模板匹配

计算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+整数倍环长,所以它相当于在环里溜达了几圈又回来了入口点,所以两者相遇

两个链表是否相交,找到相交点

 两个链表都走到尾部,如果两个引用相同,则相交

让第一个链表的尾节点指向第二个链表的头节点,这样第一个链表就变成了一个带环的链表,变成了环入口点问题

反转

  • 头插法

数组

整数数组,求和为K的对数

  • 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)

正方形顺时针90度旋转

号从1-n的数法时,n是正方形的边长

  • 旋转后的行号是旋转前的列号;
  • 旋转后的列号+旋转前的行号=n+1

所有子数组的最小值之和(力扣907)

一个数的计算次数 = 当它为子数组最右边时最小的长度 * 当它为子数组最左边时最小的长度

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)的倍数升水,欧几里得求最大公约数

公司真题

 

虾皮19年秋招 笔试题4 二维01数组,找0中到所有1距离最小

每一点到(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<

虾皮19年秋招 笔试题3 实现Linux * 匹配0-多个字符

细节:模板串遍历到*号时,

判断*号是否还有字符,没有匹配串后面就全部匹配上了;

有的话就循环在匹配串往后找跟模板串后面字符一致的,每找到一次就递归调用,从模板串*后面那个位置开始匹配

#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

分类:

  • 按子节点个数可分为:二叉树、多叉树
  • 二叉树
    • 按结构 可分为普通二叉树、完全二叉树、满二叉树
    • 按是否有序可分为 普通二叉树、二叉查找树、二叉平衡树

 

你可能感兴趣的:(笔记,春招)