Bootstrap

【预览】计算机考研复试、保研算法机试|笔记和代码模板|最全总结

【预览】计算机考研复试、保研算法机试|笔记和代码模板|最全总结

在好久之前我就写好了这一份笔记,尽管覆盖了很多知识点,但美中不足的一点是,在我整理笔记的时候,都没有给代码写注释(因为自己写的代码自己都看得懂嘛)。

对于需要快速学习机试重点知识的人,以及在考前要快速复习的人来说,看一遍(没有任何注释的)完整版笔记是一个过于庞大的任务。

那么在这里,我为大家总结了一份精华版考研保研参考算法笔记,添加了更多的说明和注释,方便我自己查阅,也给大家的复习提供了方便。

参考书和刷题题库

  1. 《算法笔记》,里面的代码都是很好的模版
  2. 洛谷(按照专题来刷)
  3. 《剑指Offer》,比较经典的算法和数据结构题目,有标准答案
  4. 《算法竞赛入门经典》
  5. 北大百练OJ POJ,适合刷题
  6. 浙大PAT甲级(想去浙大的话要刷,并且建议提前报名PAT考试,可以抵充机试的分数)
  7. Leetcode Medium以上难度的题 按照专题刷
  8. Leetcode Top100 Liked Question
  9. N诺课本和题库

必考算法

  • 快速排序
  • 质数有关的计算
  • 最小公倍数、最大公约数
  • 栈和队列(相互转化)
  • leetcode的链表题
  • 二叉树的四种遍历
  • 二叉树重建(重点)
  • 二叉树最小/最大深度
  • 4钟最短路算法(Dijkstra、Bellman-ford、Floyd和SPFA)
  • 最小生成树(Prim和Kruskal)
  • 并查集
  • 数字三角形
  • 最长上升子序列
  • 最长公共子序列
  • 背包问题

快速排序

快排基于分治,先分再排序

快排是不稳定的,如果想让快排变得稳定需要把每个元素变成独一无二的二元组<ai,i>表示位置

暴力的快排:时间复杂度O(n)

  1. 开辟新的数组 a[ ] b[ ]
  2. 选取q[left-right]中某个数作为x 小于等于x放在a数组 大于x放在b数组
  3. a[ ] 和 b[ ]先后放在q[ ]

优美的快排:

最左和最右各一个指针i和j

i往右跑跑跑(往中间) 跑到i指向的数大于x

j往左跑跑跑 跑到j指向的数小于等于x

i和j所指的数交换

重点:

  1. 初始化的时候i和j都分别往外走一个 i=left-1,j=right+1
  2. 在跑完一遍,i与j重合之后分别对两边进行快排。更新的时候是(left,j)和(j+1,right)

🔓解锁完整版笔记后方可查看代码~

质数

判断质数

试除法,一直除到大于等于n/i的时候停

bool is_prime(int n)
{
    if(n<2) return false;
    
    for(int i=2;i<=n/i;i++)
        if(n%i==0) return false; // 有因子 不是质数
    return true; // 没有任何一个数可以整除它
}
筛质数-朴素版

用质数i的倍数筛出2-n之间的质数

false表示质数,true表示不是质数

const int N=1000010;
bool st[N]; // 记录某个数是否是质量数
int primes[N]; // 记录质数
int cnt=0; // 记录质数的个数

void get_primes(int n)
{
    for(int i=2;i<=n;i++) // 从2到n
    {
        if(!st[i]) // st[i]是false表示此数是质数
        {
            primes[cnt++]=i;
            // 所有这个数的倍数都不是质数
            for(int j=i+i;j<=n;j+=i) st[j]=true;
        }
    }
}
筛质数-线性筛法

比朴素筛法更高效,每次都用一个数的最小质数因子筛掉合数,每个数都只会被筛一遍。

🔓订阅本专栏,解锁完整版笔记后方可查看代码~

质因子分解
#include<iostream>
#include<algorithm>
#include<unordered_map>

using namespace std;

int n;


int main()
{
    scanf("%d",&n); // 对n个数字进行分解质因数
    while(n--)
    {
        unordered_map<int,int> primes; // 记录每个质因子和出现的次数
        int x;
        scanf("%d",&x);
        
        for(int i=2;i<=x;i++)
            while(x%i==0)
            {
                x/=i;
                primes[i]++;
            }
        if(x>1) primes[x]++;
        
        for(auto prime:primes)
            printf("%d %d\n",prime.first,prime.second);
    }
    
}
试除法求所有约数

🔓订阅本专栏,解锁完整版笔记后方可查看代码~

最大公约数和最小公倍数

最大公约数gcd计算

if(b==0) return a;

else return gcd(b,a%b);

最小公倍数(a*b)/gcd(a,b);

最小公倍数=两个数的乘积/最大公约数

#include<iostream>
#include<stdlib.h>

using namespace std;

int gcd(int a, int b)
{
    if(b==0) return a;
    return gcd(b,a%b);
}

int main()
{
    int a,b;
    scanf("%d %d",&a,&b);
    printf("最小公约数是:%d\n",gcd(a,b));
    printf("最大公倍数是:%d",(a*b)/gcd(a,b));
}

栈和队列

括号匹配

输入括号序列,判断是否合法

思路:

对于左括号,直接压入栈中。

对于右括号,弹出栈顶第一个括号,看是否匹配。

1.如果栈是空的,不合法,退出

2.如果匹配,继续操作

3.如果不匹配,表示序列不合法,退出

如果括号序列输入结束,栈中为空,表示序列合法。否则不合法。

🔓订阅本专栏,解锁完整版笔记后方可查看代码~

链表

重点操作

1.对于头结点可能改变的情况:使用虚拟头结点dummy

初始化虚拟头结点:auto dummy=new ListNode(-1); dummy->next=head;

最后返回虚拟头结点:return dummy->next;

2.插入结点node->next=pre->next; pre->next=node;

3.删除结点node->next=node->next->next; //删除了node的下一个节点

删除链表倒数第k个点

leetcode19题

思路:

1.使用虚拟头结点dummy(就不需要区分删除的是否是第一个节点的情况)

2.求链表总长度n,然后找到倒数第k+1个点(删除的时候需要从前一个点来删)


🔓订阅本专栏,解锁完整版笔记后方可查看代码~

合并两个有序链表

leetcode 21

思路:

1.两个链表用两个指针,一起向后跑并不断把更小的加入当前链表

2.直到两个链表指针有一个为空(也可能两个链表都为空)

3.查看两个链表是否为空(至多有一个不为空)然后把不为空的链表接到当前链表的末尾

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        // 虚拟头节点
        auto dummy=new ListNode(-1);
        auto tail=dummy; // 新链表的尾指针

        while(l1 && l2){ // 两个链表都不为空的时候
            if(l1->val < l2->val){
                tail=tail->next=l1;
                l1=l1->next;
            }
            else{
                tail=tail->next=l2;
                l2=l2->next;
            }
        }
        // 至少有一个链表为空
        
        if(l1) tail->next=l1;
        if(l2) tail->next=l2;
        return dummy->next; // 虚拟头节点的下一个节点(真实头节点)
    }
};
合并k个升序链表

leetcode 22

class Solution {
public:
    // 结构体内重载运算符
    struct Cmp{
        bool operator() (ListNode* a, ListNode* b){
            return a->val>b->val;
        }
    };
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        priority_queue<ListNode*,vector<ListNode*>,Cmp> heap;
        auto dummy=new ListNode(-1),tail=dummy;

        for(auto l:lists) if(l) heap.push(l);

        while(heap.size())
        {
            auto t=heap.top();
            heap.pop();
            tail=tail->next=t;
            if(t->next) heap.push(t->next);
        }
        return dummy->next;
    }
};

双指针算法/滑动窗口

最长不包含重复数字/字母的连续子序列

暴力做法:

算法复杂度为 O ( n 2 ) O(n^2) O(n2)

枚举j,从后往前枚举i,移动到有重复字母的时候停。

双指针做法:

🔓订阅本专栏,解锁完整版笔记后方可查看代码~

二叉树的遍历

前中后序列本质上的区别就是访问操作的位置。

前序

leetcode 144

// 递归前序遍历
class Solution{
    public:
    vector<int> ans;
    vector<int> preorderTraversal(TreeNode* root){
        dfs(root);
        return ans;
    }
    
    void dfs(TreeNode* root){
        if(!root) return;
        // 访问操作的位置决定了是前、中还是后序
        ans.push_back(root->val);
        dfs(root->left);
        dfs(root->right);
    }
};
// 迭代前序遍历

🔓订阅本专栏,解锁完整版笔记后方可查看代码~

中序

leetcode 94

//递归写法
class Solution{
    public:
    vector<int> ans;
    vector<int> inorderTraversal(TreeNode* root){
        dfs(root);
        return ans;
    }
    
    void dfs(TreeNode* root){
        if(!root) return;
        dfs(root->left);
        ans.push_back(root->val);
        dfs(root->right);
    }
};
//迭代而不是递归的中序遍历

🔓订阅本专栏,解锁完整版笔记后方可查看代码~

   
后序

leetcode 145

// 递归的后序遍历
class Solution {
public: 
    vector<int> ans;
    vector<int> postorderTraversal(TreeNode* root) 
    {
        traverse(root);
        return ans;
    }    

    void traverse(TreeNode* node)
    {
        if(!node) return;
        traverse(node->left);
        traverse(node->right);
        ans.push_back(node->val);
    }
};

迭代法
由于后序遍历顺序是左、右、根,其逆序则为根、右、左。因此,可以先以根、右、左的顺序遍历整个二叉树(此时与前序遍历相似);然后将遍历后的结果进行逆序。
即每到一个节点N,先访问它。接着将N的左子树压入栈,然后遍历右子树。遍历完整棵树后,将结果序列逆序。

解题步骤
1.先将节点本身的值放入容器;
2.接着将左子节点放入栈中;
3.然后遍历右子树节点,即更新右子树节点为当前节点;
4.当右子树遍历到叶节点后,开始弹出该栈顶元素;
5.若栈中弹出的元素非空,则执行1~4;若为空,则不进行任何处理,继续弹出栈顶元素;
6.直至站内元素和当前元素为空,此时,对容器进行逆序,然后返回逆序后的结果即可。

// 迭代的后序遍历(逆向操作然后reverse一下)

🔓订阅本专栏,解锁完整版笔记后方可查看代码~


层序遍历

leetcode 102

利用队列这种数据结构,用BFS来遍历每一层

注意如何区分层号(提前记录当前层的节点个数)

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> res;
        queue<TreeNode*> q;

        if(root) q.push(root);

        while(!q.empty()){
            vector<int> level;
            int len=q.size(); // 当前层的节点个数

            while(len--){
                auto t=q.front();
                q.pop();
                level.push_back(t->val);

                if(t->left) q.push(t->left);
                if(t->right) q.push(t->right);
            }
            
            res.push_back(level); // 把本层的加入答案res里
        }
        return res;
    }
};
锯齿形层序遍历(拓展)

leetcode 103

就在原有锯齿形遍历的基础上,加一个判断层的单双号,然后决定是否要reverse

// 锯齿形遍历

🔓订阅本专栏,解锁完整版笔记后方可查看代码~

    

二叉树的重建

必须有的是中序的序列,中序+前序和中序+后序都可以

前+中序列重建树

leetcode 105


🔓订阅本专栏,解锁完整版笔记后方可查看代码~

后+中序列重建树

leetcode 106


🔓订阅本专栏,解锁完整版笔记后方可查看代码~

平衡二叉树判断

leetcode 110

在dfs的过程中,对每一个访问的节点计算左右子树的高度,并且判断是否平衡。如果有一个节点不平衡,那么子树就不平衡。

dfs函数返回的是某节点的高度!

class Solution {
public:
    bool res;
    bool isBalanced(TreeNode* root) {
        res=true;
        dfs(root);
        return res;

    }

    int dfs(TreeNode* root){
        if(!root) return 0;
        int lh=dfs(root->left); // 左子树高度
        int rh=dfs(root->right); // 右子树高度
        if(abs(lh-rh)>1)
            res=false;  // 在计算树的高度的任何一个步骤中如果有
        return max(lh,rh)+1;
    }
};
二叉树的最小深度

leetcode 111

最小深度:根节点到叶子节点的最短距离

计算最小深度:

  1. u是叶节点:深度为1

  2. u不是叶子节点,ab是左右子树

    • a,b都不空min(f(a),f(b))+1

    • a不空,b空 f(a)+1

    • a空,b不空 f(b)+1


🔓订阅本专栏,解锁完整版笔记后方可查看代码~

二叉树的最大深度

简单的dfs,每访问一个节点都计算该节点左右子树的高度,然后返回该节点的高度

计算:1+max(左子树高度,右子树高度)


🔓订阅本专栏,解锁完整版笔记后方可查看代码~
二叉树的路径总和

leetcode 112

f(u) 表示从根走到u节点的总和/sum-总和

对于u的左右子树a和b

f(a)=f(u)+a->val

f(b)=f(u)+b->val

如果一个节点是叶子节点且f(u)==sum或者f(u)==0


🔓解锁完整版笔记后方可查看代码~

最短路问题

初始化的操作需要用到头文件cstring

void *memset(void *s, int ch, size_t n);

函数解释:将s当前位置后面的n个字节用 ch 替换并返回 s 。

举例:把数组g全部设置为正无穷

memset(g,0x3f,sizeof g);

邻接矩阵建图
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=510;
int n,m;
int g[N][N]; // 邻接矩阵来存放各条边的长度
int dis[N]; // 后面用最短路算法时候的dis

int main()
{
  cin>>n>>m;
  memset(g,0x3f,sizeof g); // 初始化为正无穷
  
  while(m--)
  {
    int a,b,c;
    cin>>a>>b>>c;
    g[a][b]=min(g[a][b],c); // 处理重边的情况 取权值较小的边
  }
  return 0;
}
邻接表建图
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=510;
int n,m;
int w[N],e[N],ne[N],h[N];
// w是权值 e是这条边的第二个点 ne是指向下一条边的指针 h是各节点的表头指针
int idx=0;

void add(int a, int b, int c)
{
  w[idx]=c;
  e[idx]=b;
  ne[idx]=h[a];
  h[a]=idx++;
}

int main()
{
    scanf("%d %d",&n,&m); // n表示点的个数,m表示边的个数
    
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d %d %d",&a,&b,&c);
        add(a,b,c);
    }
    
    // 图就建好了
  
  
  return 0;
}
朴素dijkstra

朴素dijkstra的复杂度是 O ( n 2 ) O(n^2) O(n2),使用邻接矩阵存放边的信息。

dijkstra算法适用于单源最短路问题。

注意:

首先把dis数组设置为正无穷(表示不可到达),再将起点设置为距离0

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=1010;
int n,m;
int g[N][N];
int dis[N];
bool st[N];

int dijkstra()
{
  🔓解锁完整版笔记后方可查看代码~
}

int main()
{
  cin>>n>>m;
  memset(g,0x3f,sizeof g);
  
  while(m--)
  {
    int a,b,c;
    cin>>a>>b>>c;
    g[a][b]=min(g[a][b],c);
  }
    
  cout<<dijkstra()<<endl;
  
  return 0;
}
堆优化dijkstra

堆优化dijkstra的复杂度是 O ( m l o g 2 n ) O(mlog_2n) O(mlog2n)。使用priority_queue来表示堆。

使用邻接表存放边的信息,适用于单源最短路。

#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<queue>
using namespace std;

const int N=100010;
typedef pair<int,int> PII;

int n,m;
int h[N],e[N],ne[N],w[N],dis[N],idx=0;

void add(int a, int b, int c)
{
    w[idx]=c;
    e[idx]=b;
    ne[idx]=h[a];
    h[a]=idx++;
}

int dijkstra()
{
   🔓订阅本专栏,解锁完整版笔记后方可查看代码~
}

int main()
{
    scanf("%d %d",&n,&m);
    memset(h,-1,sizeof h);
    
    while(m--)
    {
        int a,b,c;
        scanf("%d %d %d",&a,&b,&c);
        add(a,b,c);
    }
    cout<<dijkstra()<<endl;
    return 0;
}

Bellman-ford算法

for循环n次,每次遍历所有边。

假设遍历的那条边是a->b,每次把dis[b]更新一次,dis[b]=min(dis[b],dis[a]+w(a,b)) 看是现在有的路径更短还是经过b到a到路径更短

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=510,M=10010;
int n,m,k;
int dis[N],backup[N];

struct Edge
{
  int a,b,w;
}edges[M];

int bellman_ford()
{
  🔓订阅本专栏,解锁完整版笔记后方可查看代码~
}

int main()
{
  cin>>n>>m>>k;
  for(int i=0;i<m;i++)
  {
    int a,b,w;
    scanf("%d%d%d",&a,&b,&w);
    edges[i]={a,b,w};
  }
  int t=bellman_ford();
  if(t==-1) printf("impossible\n");
  else printf("%d\n",t);
}

Floyd算法

Floyd算法是算法复杂度最高的, O ( n 3 ) O(n^3) O(n3)

Floyd算法可以解决多源汇的最短路问题。

SPFA算法

SPFA算法的一般时间复杂度是O(m),最坏时间复杂度是O(nm).

在算法竞赛中写最短路的题目一般用SPFA!SPFA过不了再用堆优化dijkstra算法。

SPFA算法核心:只修改最短距离有变化的点的出边

// SPFA算法
🔓订阅本专栏,解锁完整版笔记后方可查看代码~

拓扑排序

只有有向无环图才有拓扑序列。

拓扑排序不是唯一的!

BFS法拓扑排序(常用)

拓扑排序(bfs法)的算法思路:

第一步:所有入度为0的点进入队列

第二步:

while(队列不为空)

​ 取出队头的点;

​ 枚举从队头的点出去的所有边,删掉那些边;

​ 如果某个被删掉的边的b点的入度变为了0,将这个点加入队列;

拓扑排序-BFS(数组版)


🔓订阅本专栏,解锁完整版笔记后方可查看代码~

拓扑排序-BFS(队列queue版)


 🔓订阅本专栏,解锁完整版笔记后方可查看代码~

DFS法拓扑排序:

🔓订阅本专栏,解锁完整版笔记后方可查看代码~

并查集

并查集初始化
const int N=100010;
int p[N];
for(int i=0;i<n;i++) p[i]=i;
并查集查找
int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
并查集合并

注意:有的时候需要判断两个点在不在同一个集合,有点时候不要判断(如果维护了一个在根节点的元素,就需要判断.

把两个点(两组点)合并为同一个集合:

if(find(a)!=find(b))
{
    p[a]=p[b];
}

最小生成树

Kruskal算法

Kruskal算法适合用于稀疏图。重点在于如何给struct结构的边按照权值weight排序。

Kruskal算法思想:

每次取最短的边 判断是否形成环或者说这条边的两个点是否已经相连(通过并查集判断)

第一步:所有边按照权重从小到大排序

第二步:从小到大 枚举每条边ab 权重c

如果ab不连通(加ab不产生回路) 将这条边加入集合

在kruskal算法中,不需要存储邻接表或邻接矩阵,只需要把每条边用struct结构体存下来。

#include<iostream>
#include<algorithm>
using namespace std;

const int N=200010;
int n,m;
int p[N]; // 并查集的辅助数组

struct Edge
{
    int a,b,w;
    // 重载运算符 在sort的时候就会按照这里的规则排序
    bool operator<(const Edge &W) const
    {
        return w<W.w;
    }
}edges[N];

int find(int x)
{
    if(x!=p[x])
        p[x]=find(p[x]);
    return p[x];
}

int main()
{
    scanf("%d %d",&n,&m);
    
    for(int i=0;i<m;i++)
    {
        int a,b,w;
        scanf("%d %d %d",&a,&b,&w);
        edges[i]={a,b,w};
    }
    
    sort(edges,edges+m);
    
    for(int i=1;i<=n;i++) p[i]=i; // 初始化并查集
    
    int res=0,cnt=0;
    
    for(int i=0;i<m;i++)
    {
        int a=edges[i].a,b=edges[i].b,w=edges[i].w;
        a=find(a),b=find(b);
        if(a!=b)
        {
            p[a]=b;
            res+=w;
            cnt++;
        }
    }
    if(cnt<n-1) printf("impossible\n");
    else printf("%d\n",res);
}
Prim算法

prim算法适用于稠密图,用的比较少 不需要掌握

归并排序

归并排序和快速排序都是用的是分而治之的思想,先递归排序再归并合二为一

归并排序算法思想:

第一步:找分界点mid

第二步:递归排序

第三步:归并 把两个有序序列变为一个有序序列 这一步复杂度为O(n) 每个元素都只会被比较一次


🔓解锁完整版笔记后方可查看代码~

动态规划DP

数字三角形

求一个数字三角形的路径最大/最小值


🔓订阅本专栏,解锁完整版笔记后方可查看代码~

最长上升子序列

求最长的单调递增子序列(的长度)

#include<iostream>
#include<algorithm>

using namespace std;
const int N=1010;
int n;
int a[N],f[N];

int main()
{
  scanf("%d",&n);
  for(int i=1;i<=n;i++) scanf("%d",&a[i]);
  
  for(int i=1;i<=n;i++)
  {
    f[i]=1; // 初始化 最大长度为1
    // 遍历前面的每个字符
    for(int j=1;j<i;j++)
      // 如果前面的某个字符小于这个字符
      if(a[j]<a[i])
        f[i]=max(f[i],f[j]+1);
  }
  
  int res=0;
  for(int i=1;i<=n;i++) res=max(res,f[i]);
  return 0;
}

求最长的单调递增子序列(的序列和长度)


🔓订阅本专栏,解锁完整版笔记后方可查看代码~
最长公共子序列

重点在于把最后一个字符分情况讨论。


🔓订阅本专栏,解锁完整版笔记后方可查看代码~

背包问题

01背包

当空间优化为1维后,只有完全背包问题的体积是从小到大循环的

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1010;

int n,m;
int v[N],w[N];
int f[N];

int main()
{
  cin >> n >> m;
  
  for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
  
  for(int i=1;i<=n;i++)
    for(int j=m;j>=v[i];j--) //用的是上一层的状态
      f[j]=max(f[j],f[j-v[i]]+w[i]);
  
  cout<<f[m]<<endl;
  
  return 0;
}
多重背包-每个物品有s[i]个

朴素版多重背包(类似完全背包,只是k=0,1,2…s[i],多了一个判断)


🔓订阅本专栏,解锁完整版笔记后方可查看代码~

完全背包-每个物品有无限个

🔓订阅本专栏,解锁完整版笔记后方可查看代码~

分组背包-每组若干物品 一组内只能选一个

思路类似多重背包

#include<iostream>
#include<algorithm>
using namespace std;

const int N=110;

int n,m;
int v[N][N],w[N][N],s[N];
int f[N];

int main()
{
  cin>>n>>m;
  
  for(int i=1;i<=n;i++)
  {
    cin>>s[i];
    for(int j=0;j<s[i];j++)
    	cin>>v[i][j]>>w[i][j];
  }
  for(int i=1;i<=n;i++)
  	for(int j=m;j>=0;j--)
  		for(int k=0;k<s[i];k++)//用的是本层状态 所以体积从小到大
  			if(v[i][k]<=j)
  				f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
  cout<<f[m]<<endl;
  return 0;
}
;