保研/面试复习-数据结构与算法-万字总结(近三万字)

以下是笔者整理的保研/面试容易被问到的算法问题,包含最短路径,dfs,bfs,最小生成树MST(krusal和prim),KMP(这个可能较难,如果算法不是问得很深,一般不会问到),十种排序算法(大部分都有代码实现,非伪代码的代码是笔者写的,且可以在DEVc++上跑通),链表相关的简单操作。内容详细通俗易懂,且大部分常用算法都有实现代码或者PAT、PTA例题。非常值得学习,我甚至敢说,基本层面的算法,看我这一篇就够了。(后面几期可能会出操作系统、计算机网络等等复习问题,另外链表这一块写的比较乱,是因为以后可能会出一期专门总结Leetcode上面的链表相关的题目)

目录

以下是笔者整理的保研容易被问到的算法问题,包含最短路径,dfs,bfs,最小生成树MST(krusal和prim),KMP(这个可能较难,如果算法不是问得很深,一般不会问到),十种排序算法(大部分都有代码实现),链表相关的简单操作。内容详细通俗易懂,且大部分常用算法都有实现代码或者PAT、PTA例题。非常值得学习,我甚至敢说,基本层面的算法,看我这一篇就够了。(后面几期会出操作系统、计算机网络等等复习问题)

1.最短路径

Dijkstra

Floyd

Bellman-Ford

SPFA

2.DFS

大致思路

伪代码

例题

3.BFS

大致思路:

代码

例题

4.MST

Krusal

Prim

5.KMP

前提next

代码求next

核心算法

总结

代码KMP

6.排序算法

快速排序

归并排序

插入排序

选择排序

冒泡排序

希尔排序

堆排序

计数排序

桶排序

基数排序

7.链表基本操作

逆序打印链表

逆置链表

删除链表中一个给定的节点

在无头链表的给定节点前插入一个节点

合并两个有序链表

查找单链表的中间节点

查找单链表倒数第k个节点

删除单链表倒数第k个节点

带环单链表


1.最短路径

Dijkstra

经典的单源最短路径算法

算法思想

采用了一种贪心的策略。

  1. 声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点集合T

  2. 初始时,源点到源点为0,多以dist[s]=0。对于s存在能直接到达的边(s,m),则dis[m]=w(s,m),同时吧所有其他的s不能直接到达的点的dis设为正无穷。初始时,集合T中只有顶点s。

  3. 然后从dist中选择最小值,则该值就是s到该值对应点的最短路径,并把该点放入集合T中。

  4. 假设新加的点值为N,是否存在点M,是dist[N]+w(N,M)<dist[M],有就替换dist[M]为前者的值,遍历完更新值后,然后继续从dist中选择最小值,返回操作3开始重复操作。

ps:适用于求单源、无负权的最短路,且如果最后得到的dist存在正无穷值,代表源点到该点不可达。所以可以用来判断两点是否可达

算法实例

例题

PTA 7-9旅游规划

题目描述

有了一张自驾旅游路线图,你会知道城市间的高速公路长度、以及该公路要收取的过路费。现在需要你写一个程序,帮助前来咨询的游客找一条出发地和目的地之间的最短路径。如果有若干条路径都是最短的,那么需要输出最便宜的一条路径。

输入格式

输入说明:输入数据的第1行给出4个正整数N、M、S、D,其中N(2≤N≤500)是城市的个数,顺便假设城市的编号为0~(N−1);M是高速公路的条数;S是出发地的城市编号;D是目的地的城市编号。随后的M行中,每行给出一条高速公路的信息,分别是:城市1、城市2、高速公路长度、收费额,中间用空格分开,数字均为整数且不超过500。输入保证解的存在。

输出格式

在一行里输出路径的长度和收费总额,数字间以空格分隔,输出结尾不能有多余空格。

输入样例

4 5 0 3
0 1 1 20
1 3 2 30
0 3 4 10
0 2 2 20
2 3 1 20

输出样例

3 40

题解

#include<cstdio>
#define INF 0x3f3f3f3f
using namespace std;
//存储两点之间的路径长度与花费 
int way[500][500][2]; 
int dis[500];//距离 
int cost[500]; //花费
bool visit[500]={false};//是否选中加入集合 
​
void dijkstra(int n){
    for(int k=0;k<n;k++){
        //找到dis集合中的最小值点 
        int min_point=INF, min_tmp=INF;;
        for(int i=0;i<n;i++){
            if(!visit[i] && min_tmp>dis[i]) {
                min_tmp=dis[i];
                min_point=i;
            }
        }
        //找不到结束 
        if(min_point==INF) break; 
        //找到,加入集合 
        visit[min_point]=true; 
        for(int i=0;i<n;i++){
            //更新最短路径顺便记录路径费用 
            if(!visit[i] && dis[min_point]+way[min_point][i][0]<dis[i]){
                dis[i]=dis[min_point]+way[min_point][i][0];
                cost[i]=cost[min_point]+way[min_point][i][1];
            }
            //长度相同,选择费用少的 
            else if(!visit[i] && dis[min_point]+way[min_point][i][0]==dis[i] && cost[min_point]+way[min_point][i][1]<cost[i]){
                cost[i]=cost[min_point]+way[min_point][i][1];
            }
        }
    }
} 
​
int main(){
    int n,m,s,d;
    int a,b,c,e;
    scanf("%d %d %d %d",&n,&m,&s,&d);
    //初始化 
     for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            way[i][j][0]=INF;
            way[i][j][1]=INF;
         } 
     }
     //建图
     for(int i=0;i<m;i++){
        scanf("%d %d %d %d",&a,&b,&c,&e);
        //无向图 
        way[a][b][0]=c;
        way[b][a][0]=c;
        way[a][b][1]=e;
        way[b][a][1]=e;
     } 
     //初始化cost和dis数组
     for(int i=0;i<n;i++){
        //能直接到达的赋值,不能直接到达的赋值inf 
        dis[i]=way[s][i][0];
        cost[i]=way[s][i][1];
     } 
     //到自己为0 
     dis[s]=0;
     cost[s]=0;
     //起点加入集合
     visit[s]=true; 
     //使用dijkstra求单源最短路径 
     dijkstra(n);
     printf("%d %d",dis[d],cost[d]);
} 

Floyd

经典的多源最短路径算法

算法特点

解决任意两点间的最短路径的一种算法,可以正确处理有向图或有向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包。

算法思想

经典的动态规划算法。

  1. 从任意节点i到任意节点j的最短路径存在两种可能:

    1. 直接从i到j

    2. 从i经过若干个节点k到j。

  2. 假设Dis(i,j)为节点i到节点j的最短路径的距离,对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j),这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。

算法描述

  1. 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。

  2. 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。

伪代码

简单粗暴O(n^3)

for(k=0;k<n;k++)
{ 
    for(i=0;i<n;i++)
        for(j=0;j<n;j++)
            if(A[i][j]>A[i][k]+A[k][j])
            {
                A[i][j]=A[i][k]+A[k][j];
            } 
} 

Bellman-Ford

允许负权边的单源最短路径算法

适用条件

  • 单源最短路径

  • 有向图&无向图(无向图可以看做(u,v),(v,u)同属于边集E的有向图)

  • 边权可正可负(如有负权回路输出错误提示)

  • 差分约束系统

算法步骤

  1. 初始化所有点。每一个点保存一个值,表示从原点到这个点的距离,将原点值设为0,其他的点设为无穷大。用数组Dis存。(这里和Dijkstra差不多,不过少了一个初始时把相邻点也填上去的操作)

  2. 进行循环,循环下标为1到n-1(n个顶点,至多循环n-1次)。在循环内部,遍历所有边,进行松弛(对于每一条边e(u, v),如果Dis[u] + w(u, v) < Dis[v],则另Dis[v] = Dis[u]+w(u, v)。w(u, v)为边e(u,v)的权值)计算。时间复杂度为O(V*E)。

  3. 遍历完后,需要对各边进行检查(时间复杂度为O(E)),判断是否有负权环路,即存在边(u,v),d(v) > d (u) + w(u,v)则表示途中存在从源点可达的权为负的回路。

SPFA

Shortest Path Faster Algorithm。

Bellman-Ford+队列优化,和BFS的关系更密一点,有时候被称为Moore-Bellman-Ford算法。

算法思路

该算法本质上和Dijkstra算法差不多,只不过Dijkstra算法是基于贪心的,而SPFA是基于先进先出的优先对列。

前提:带有负环的图是没有最短路径的,所以我们在执行算法的时候,要判断图是否带有负环,方法有两种:

  1. 开始算法前,调用拓扑排序进行判断(一般不使用)

  2. 如果某个点进入队列的次数超过N次则存在负环

算法步骤

  1. 初始时,初始化数组dis,除了起点赋为0外,其他赋为正无穷,这一步和Bellman-Ford算法一样。把起点入队列。

  2. 取出队头元素。对队头元素相邻边进行松弛操作。将被松弛且不在队列中的顶点加入到队列队尾。

  3. 重复上述操作,不断从队列中取出结点来进行松弛操作,直至队列为空。

2.DFS

大致思路

大致思路(纯自己话概括):访问图的某一起始点v,从v开始出发,访问其任一邻接顶点w1,然后从w1开始,访问与w1邻接但未被访问过的点w2,然后从w2出发,进行类似的操作,直到到达所有邻接点都被访问过的点u为止。然后需要回退,退回前一次访问过的顶点,看看是否还没有访问过得到邻接点。如果有就访问,重复上述类似操作,没有就继续回退,重复上述操作。知道图中所有点都被访问位置

  1. 选择起始顶点涂成灰色,表示访问了。

  2. 从该顶点的邻接顶点中选择一个,继续这个过程(即再寻找邻接结点的邻接结点),一直深入下去,直到一个顶点没有未被访问的邻接结点(即没有白色的点),涂黑并回退到上一层顶点

  3. 上一个顶点是否有白色的邻接结点,没有则涂黑回退,有则继续深入邻接结点访问。

ps:初始结点均为白色,被访问后为灰色,其邻接结点被访问完了就为黑色。

伪代码

void DFS(AMraph G, int v){  //图G为邻接矩阵类型
    cout << v;              //访问第v个顶点
    visited[v] = true;      //做标志
    for( w = 0; w < G.vexnum; w++)  //依次检查邻接矩阵v所在的行
    {
        if(G.arcs[v][w] != 0 && ! visited[w])   //如果w是v的邻接点,且w未被访问,则递归调用DFS
            DFS(G,w);
    }
}

例题

PAT甲级A1090

Highest Price in Supply Chain

题目描述

A supply chain is a network of retailers(零售商), distributors(经销商), and suppliers(供应商)-- everyone involved in moving a product from supplier to customer.Starting from one root supplier, everyone on the chain buys products from one's supplier in a price P and sell or distribute them in a price that is r% higher than P.  It is assumed that each member in the supply chain has exactly one supplier except the root supplier, and there is no supply cycle.Now given a supply chain, you are supposed to tell the highest price we can expect from some retailers.

输入描述

Each input file contains one test case.  For each case, The first line contains three positive numbers: N (<=105), the total number of the members in the supply chain (and hence they are numbered from 0 to N-1); P, the price given by the root supplier; and r, the percentage rate of price increment for each distributor or retailer.  Then the next line contains N numbers, each number Si is the index of the supplier for the i-th member.  Sroot for the root supplier is defined to be -1.  All the numbers in a line are separated by a space.

输出描述

For each test case, print in one line the highest price we can expect from some retailers, accurate up to 2 decimal places, and the number of retailers that sell at the highest price.  There must be one space between the two numbers.  It is guaranteed that the price will not exceed 1010.

输入例子

9 1.80 1.00
1 5 4 4 -1 4 5 3 6

输出例子

1.85 2

题解

#include<iostream>
#include<vector>
#include<iomanip>
using namespace std;

void dfs(int node,double P,double r,int &count,double &max,vector<vector<int> > &tree){
	if(tree[node].size()){ //有下一层 
		P*=r;
	} 
	else if(P>max){
		max=P;
		count=1;//跟新深度后,结点数重新开始计算 
	} 
	else if(P==max){ //相等则节点数+1 
		count++;
	} 
	for(int i=0;i<tree[node].size();i++){
		dfs(tree[node][i],P,r,count,max,tree);//遍历其邻居结点 
	}
} 

int main(){
	int  N,count=0,root;
	double r,P,max=0;
	cin>>N>>P>>r;
	r=r/100+1;
	vector<vector<int> > tree(N);
	
	int a[N];
	for(int i=0;i<N;i++) {
		cin>>a[i];
	} 
	//建图 
	for(int i=0;i<N;i++) {
		if(a[i]==-1){
			root=i;//确定root得到索引 
		} 
		else
		{
			tree[a[i]].push_back(i);//构造图结构,通过索引关系来构造
			//类似于二维数组,父亲索引(对应自己的值)和自己的索引(对应儿子的值)
		} 
	}
	
	//dfs遍历 
	dfs(root,P,r,count,max,tree);
	cout<<fixed<<setprecision(2)<<max<<" "<<count;
	
} 

PAT甲级A1094

The Largest Generation

题目描述

A family hierarchy is usually presented by a pedigree tree where all the nodes on the same level belong to the same generation.  Your task is to find the generation with the largest population.

输入描述

Each input file contains one test case.  Each case starts with two positive integers N (<100) which is the total number of family members in the tree (and hence assume that all the members are numbered from 01 to N), and M (<N) which is the number of family members who have children.  Then M lines follow, each contains the information of a family member in the following format:ID K ID[1] ID[2] ... ID[K]where ID is a two-digit number representing a family member, K (>0) is the number of his/her children, followed by a sequence of two-digit ID's of his/her children. For the sake of simplicity, let us fix the root ID to be 01.  All the numbers in a line are separated by a space.

输出描述

For each test case, print in one line the largest population number and the level of the corresponding generation.  It is assumed that such a generation is unique, and the root level is defined to be 1.

输入例子

23 13
21 1 23
01 4 03 02 04 05
03 3 06 07 08
06 2 12 13
13 1 21
08 2 15 16
02 2 09 10
11 2 19 20
17 1 22
05 1 11
07 1 14
09 1 17
10 1 18

输出例子

9 4

题解

#include<cstdio>
#include<vector>
using namespace std;
vector<int> v[100];
int book[100];//用于存储各个世代的节点数。 
void dfs(int node, int level){
	book[level]++;//该世代的节点数+1 
	for(int i=0;i<v[node].size();i++){
		dfs(v[node][i],level+1);
	} 
}

int main(){
	int N,M,a,b,c,level=1;
	scanf("%d %d",&N,&M);
	for(int i=0;i<M;i++){
		scanf("%d %d",&a,&b);
		for(int j=0;j<b;j++){
			scanf("%d",&c);
			v[a].push_back(c);//建图
		}
	}
	dfs(1,level);
	int maxnum = 0,maxlevel=1;
	for(int i=0;i<100;i++){
		if(book[i]>maxnum) {
			maxnum=book[i];
			maxlevel = i; 
		}
	} 
	printf("%d %d",maxnum,maxlevel);
} 

3.BFS

大致思路:

  1. 首先选择一个顶点作为起始顶点并放入队列,并将其染成灰色,其余点为白色。

  2. 从队列首部选出一个点并将该点涂黑表示访问过,并找出所有与之邻接的结点并依次放入队列中,染成灰色。

  3. 重复步骤2

ps:出队的顶点变成黑色,在队列里的是灰色,还没入队的是白色。

代码

例题

PAT甲级1094

BFS方法的题解

#include<cstdio>
#include<vector>
#include<queue>
using namespace std;
vector<int> v[100];
int level[100];//某结点所在世代数,索引表示结点值,值表示世代数 
int book[100]; //某世代的结点数,索引表示世代数,值表示结点数 
int main(){
	int n,m;
	scanf("%d %d",&n,&m);
	int a,b,c;
	for(int i=0;i<m;i++){
		scanf("%d %d",&a,&b);
		for(int j=0;j<b;j++){
			scanf("%d",&c);
			v[a].push_back(c); 
		}
	}
	queue<int> q;
	q.push(1);
	level[1]=1;//结点1在1层 
	while(!q.empty()){
		//取出队头元素并出栈 
		int index=q.front();
		q.pop();
		book[level[index]]++;//该层节点数+1
		for(int i=0;i<v[index].size();i++){
			//由于是子节点,所以世代数+1
			level[v[index][i]]=level[index]+1;
			//入栈该结点 
			q.push(v[index][i]);
			 
		} 
	} 
	int maxnum=0,maxlevel=0;
	for(int i=0;i<100;i++){
		if(book[i]>maxnum){
			maxnum=book[i];
			maxlevel=i;
		}
	}
	printf("%d %d",maxnum,maxlevel); 
} 

4.MST

连通图:在无向图中,若任意两个顶点vi与vj都有路径相通,则称该无向图为连通图。

强连通图:在有向图中,若任意两个顶点vi与vj都有路径相通,则称该有向图为强连通图。

生成树:如果连通图G的一个子图是一颗包含G的所有顶点的树,该子图称为G的生成树。

最小生成树:在连通图G的所有生成树中,所有边的代价和最小的生成树,成为最小生成树。

Krusal

算法思路

将边排序后从小到大依次检查直到所有边都得到连通。可通俗称为加边法。因为方法主要操作与边有关,所以适用于点稠密图。

算法输入

点集合V,边集合E

算法步骤

  1. 将所有边按价值从小到大排序,并初始化一个空点集A以及一个空边集B。

  2. 依次遍历所有边,若当前边存在一个点不属于A,则将该边加入B,并将边的该边中不属于A的点加A中;若当前边的两点都已经加入A中,则跳过该边。直到集合A=集合V为止。

例题

PTA 7-10 公路村村通

题目描述

现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。

输入格式:

输入数据包括城镇数目正整数N(≤1000)和候选道路数目M(≤3N);随后的M行对应M条道路,每行给出3个正整数,分别是该条道路直接连通的两个城镇的编号以及该道路改建的预算成本。为简单起见,城镇从1到N编号。

输出格式:

输出村村通需要的最低成本。如果输入数据不足以保证畅通,则输出−1,表示需要建设更多公路。

输入样例

6 15
1 2 5
1 3 3
1 4 7
1 5 4
1 6 2
2 3 4
2 4 6
2 5 2
2 6 6
3 4 6
3 5 1
3 6 1
4 5 10
4 6 8
5 6 3

输出样例

12

代码

#include<cstdio>
#include<algorithm> 
using namespace std;
//使用kruskal,也称为加边法,从小到大加边,但加边过程不能产生回路 
struct edge{
	int l,r,dis;
}e[3001];

//定义排序规则,边长度由小到大排序 
bool cmp(edge a,edge b){
	return a.dis<b.dis;
}

int n,m,root[1001];

int  findRoot(int x){
	if(x==root[x]){
		return x;
	}
	return findRoot(root[x]);
}

int kruskal(){
	int sum=0,count=0;
	//排序 
	sort(e,e+m,cmp);
	for(int i=0;i<m;i++){
		//得到两个节点相连的根 
		int root_l=findRoot(e[i].l);
		int root_r=findRoot(e[i].r);
		//如果不相等,代表可以加入无回路
		if(root_l!=root_r){
			//加入之后,根设置为相同了。
			root[root_l]=root_r; 
			sum+=e[i].dis;
			++count;
			//使n个村达到村村通只需要n-1条边 
			if(count==n-1) break; 
		}	
	}
	if(count!=n-1) return -1;//目前的资源还不足以达到村村通
	return sum; 
	
	
} 

int main(){
	scanf("%d %d",&n,&m);
	//建图 
	for(int i=0;i<m;i++){
		scanf("%d %d %d",&e[i].l,&e[i].r,&e[i].dis);	
	}
	//初始化root ,开始时每个顶点是一个独立的集合,根是自己本身 
	for(int i=1;i<=n;i++){
		root[i]=i;
	}
	printf("%d",kruskal());
} 

Prim

算法思路

任取一点后,通过这个点逐渐长出整个生成树。可通俗称为加点法。

因为方法主要操作与点有关,所以适用于边稠密图。

算法输入

点集合V,边集合E

算法步骤

  1. 从V中任取一点u,并设置两个点集合,U=u,R=V-u。其中U表示已经成为最小生成树一部分的点集。

  2. 设置一个visited数组,用于表示哪些点已经加入到U当中,同时设置一个cost数组,表示当前点连接到U上的代价。

  3. 通过U里面的点,先更新所有R里的点直接连接U里的点的代价并记录到cost数组,cost数组索引是R里面的点。然后选出所有连接两个点集的边中最小的边,把R里对应的点加入到U里。直到U等于V为止。

例题

PTA 7-10 公路村村通

题目描述

现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。

输入格式:

输入数据包括城镇数目正整数N(≤1000)和候选道路数目M(≤3N);随后的M行对应M条道路,每行给出3个正整数,分别是该条道路直接连通的两个城镇的编号以及该道路改建的预算成本。为简单起见,城镇从1到N编号。

输出格式:

输出村村通需要的最低成本。如果输入数据不足以保证畅通,则输出−1,表示需要建设更多公路。

输入样例

6 15
1 2 5
1 3 3
1 4 7
1 5 4
1 6 2
2 3 4
2 4 6
2 5 2
2 6 6
3 4 6
3 5 1
3 6 1
4 5 10
4 6 8
5 6 3

输出样例

12

代码

#include<cstdio>
#include<algorithm> 
#include<vector>
#define INF 0x3f3f3f3f
using namespace std;
int n,m,g[1001][1001],dis[1001];

int prime(){
	//是否选中加入集合 
	vector<bool> visited(1001,false);
	int sum=0;
	for(int i=1;i<=n;i++){
		int u=INF,min_val=INF;
		//找d集合中最小值点,这里逻辑类似于dijkstra 
		//不过 dijkstra中dis数组表示的是距起点的最小距离
		//而这里是距离已被访问的点集中的点的最小距离 
		for(int j=1;j<=n;j++){
			if(!visited[j] && dis[j]<min_val){
				u=j;
				min_val=dis[j];
			}
		}  
		// 找不到结束 
		if(u==INF) return -1; 
		//找到,加入集合
		visited[u]=true; 
		sum+=dis[u];
		for(int i=1;i<=n;i++){
			//这里和dijkstra有点不同,不需要左边加上dis[u]
			// 即dis[u]+g[u][i]<dis[i],因为我们现在要找的是到点集的任意点的最小距离,而不是到起点的最小距离。 
			if(!visited[i] && g[u][i]<dis[i]){
				//更新未加入集合中点到点集的最小距离 
				dis[i]=g[u][i];
			}		 
		} 
	} 
	return sum;
}

int main(){
	scanf("%d %d",&n,&m);
	//初始化图
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			//无向图 
			g[i][j]=INF;
			g[j][i]=INF; 
		}
	} 
	int a,b,dist;
	//建图
	for(int i=0;i<m;i++){
		scanf("%d %d %d",&a,&b,&dist);
		g[a][b]=dist;
		g[b][a]=dist;
	} 
	//第一个村庄为起点 
	fill(dis,dis+1001,INF); 
	dis[1]=0; 
	
	printf("%d",prime());
}

5.KMP

首先给定两个字符串,一个是S串,一个是T串,判断S中是否包含T。

前提next

该算法的一个核心就是理解next数组。

这里我们定义next[i]为T中前i+1位的字符串的"前缀"和"后缀"的最长的共有元素的长度-1。又可以理解为满足这些条件后的前缀的最后一个字符的索引。

例如我们分析ababaca

字符串"前缀"和"后缀"的最长的共有元素的长度next数组
a0next[0]=-1
ab0next[1]=-1
aba1next[2]=0
abab2next[3]=1
ababa3next[4]=2
ababac0next[5]=-1
ababaca1next[6]=0

代码求next

void get_next(int *next, char *T, int len)
{
	next[0] = -1;//-1代表没有重复子串
	int k = -1;
	for (int q = 1; q <= len; q++)
	{
		while (k > -1 && T[k+1] != T[q])//下一个元素不相等,把k向前回溯
		{
			k = next[k];
		}
		if (T[k+1] == T[q])//下一个元素相等,所以最长重复子串+1
		{
			k = k+1;
		}
		next[q] = k;//给next数组赋值
	}
}

分析一下代码中的重点,当下一个元素不等时,令k=next[k]回溯。这里的next[k]表示前k+1位(即索引为0~k)的字符串中的最长前后缀的前缀的索引。如果其后面一个仍然不配则继续以相同的算法进行回溯,这样得到了next数组。

得到next数组之后的核心算法。

核心算法

两串下一个字符不相等,向前回溯,效率高就是在这里,每次匹配失败,k不用直接变为0,从第一个字符开始重新匹配,而是变为最长重复子串的下一个字符,从中间开始匹配即可。

举个例子,如上图,假设匹配到A和B不相等时:

①暴力算法是把下面的向右移动一个距离,然后从头开始匹配。

②现在的算法是,B前面部分的字符串的的最长前后缀的前缀索引即next[k],这是重新从next[k]+1开始,即最长前后缀中前缀的下一个开始。还要相对与之前移动了B前面部分字符的长度 — 其最长前后缀的长度。而不是之前的只移动1位。也就是说该方法每次移动的距离增加且不再每次从头开始了。这就是其优化的地方。

总结

KMP效率高的两大原因:

①每次不匹配后,相对于暴力法的向右移动一位,KMP的B前面字符串长度(已匹配字符串长度)— 该部分字符串最长前后缀的长度。

②不与暴力法一样从头开始,而是从前缀的下一位开始。

代码KMP

int KMP(char *s, int len, char *p, int plen)//利用KMP算法匹配
{
	int *next = new int(plen);
	get_next(next, p, plen);
	int k = -1;
	for (int i = 0; i < len; i++)
	{
 		while (k > -1 && p[k+1]!=s[i])//两串下一个字符不相等,向前回溯(效率高就是在这里,每次匹配失败,k不用直接变为0,从第一个字符开始重新匹配,而是变为最长重复子串的下一个字符,从中间开始匹配即可)。
		{
			k = next[k];
		}
		if(p[k+1] == s[i])//两个串的字符相等,k+1来匹配子串的一个字符
		{
			k++;
		}
		if (k == plen-1)//匹配成功,返回短串在长串的位置。
		{
//			cout << "在位置" << i-(plen-1)<< endl;
//            k = -1;//重新初始化,寻找下一个
//            i = i-(plen-1);//i定位到该位置,外层for循环i++可以继续找下一个(匹配的地方可能有多个)
			return i-plen+1;
 
		}
	}
	return -1;
}

6.排序算法

快速排序

思路如下图

思想:

总结一下算法大致思想就是:

①选择一个基准key,然后定义两个指针i和j,一个从起点一个从终点开始。

②左移j直到遇到小于基准key的,然后把j指针所在位置值赋给i指针所在位置。

③右移i直到遇到大于基准key的,然后把i指针所在位置值赋给j指针所在位置。

④直到i与j相遇则停止。并把key的值赋给此时i指针指向的位置

⑤这样一次处理就完成了,使得大于key的在其右边,小于key的在其左边。

⑥以相遇位置划分左右区间,然后分别对左右区间进行递归处理,方式与上面相同。

代码:

#include<cstdio>
#include<vector>
using namespace std;
//快排 
 void quickSort(vector<int> &nums,int l,int r){
 	if(l+1>=r){
 		return; 
	 }
	 //使用双指针first和last 
	 int first =l,last=r-1,key=nums[first];//选定区间第一位为基准
	 while(first<last){
	 	//左移指针last,直到遇到小于key的 
	 	while(first<last && nums[last]>=key) --last;
		 //此时last指针所在值赋给first指针
		 nums[first]=nums[last];
		 //右移指针first,直到遇到大于key的 
		 while(first<last && nums[first]<=key) ++first;  
		 //此时first指针所在值赋给last指针
		 nums[last]=nums[first];
	 } 
	 //此时相遇的地方赋值为key  
	 nums[first]=key;//自此就完成了快排的一趟,将所有小于key的放在key左边,大于key的放在右边 
	 //递归处理左半部分和右半部分
	 //这里填first是因为在函数内部时,last=frist-1 
	 quickSort(nums,l,first);
	 quickSort(nums,first+1,r);
 } 
 
 int main(){
 	vector<int> nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
 	quickSort(nums,0,nums.size());
 	for(int i=0;i<nums.size();i++){
 		if(i==nums.size()-1) printf("%d\n",nums[i]);
 		else printf("%d ",nums[i]);
	}
 } 

复杂度分析:

平均时间复杂度:O(NlogN)

最差时间复杂度:O(N^2)

空间复杂度:根据实现方式的不同而不同

不稳定

归并排序

思想:

这个算法用我的话来讲,就是划分区间,划分到只有两个数的区间之后,然后从小区间往整个大区间开始构建排序。先处理小区间,然后处理小区间合并的大区间,这一处理过程类似于leetcode上的一题就是和并两个增序数组成一个增序数组。这题就是合并已排序好的小区间成一个排序好的大区间。

代码:

#include<cstdio>
#include<vector>
using namespace std;

//temp用来作为一个中间存储排序后的数组。 
void mergeSort(vector<int> &nums,int l,int r,vector<int> &temp){
	if(l+1>=r){
		return;
	}
	int mid=l+(r-l)/2;
	//划分区间 
	mergeSort(nums,l,mid,temp);
	mergeSort(nums,mid,r,temp);
	int p=l,i=l,q=mid;
	while(p<mid || q<r){
		//加上q>=r,因为防止nums[q]越界,又可以方便判断,因为q>=r则代表右区间已经分配合并用完了,需要左区间。 
		if(q>=r || (p<mid && nums[p]<=nums[q])) temp[i++]=nums[p++];
		else temp[i++]=nums[q++]; 
	}
	for(int i=l;i<r;i++){
		nums[i]=temp[i];//赋值 
	} 
} 

int main(){
	vector<int> nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
	vector<int> temp(nums.size());
 	mergeSort(nums,0,nums.size(),temp);
 	for(int i=0;i<nums.size();i++){
 		if(i==nums.size()-1) printf("%d\n",nums[i]);
 		else printf("%d ",nums[i]);
	}
}

复杂度分析:

平均时间复杂度O(nlogn)

空间复杂度:O(n)

稳定

插入排序

思想:

用我的话来讲,每次都要保持一个位置靠前的已经排序好的数组,后面的新数就是被插入到这个排序好的数组中。

也就是说到遍历到第i个元素时,表示前0~i-1已经排序好了,此时的第i个元素需要做的是和前面的元素依次比较交换位置(也可以理解为找一个合适的位置插入)使得前0~i项已经排好序。每进行一次插入,排好的数组增加一项。

代码:

#include<cstdio>
#include<vector>
using namespace std;

void insertSort(vector<int> &nums,int n){
	for(int i=1;i<n;i++){
		for(int j=i;j>=1&&nums[j]<nums[j-1];j--){
			swap(nums[j],nums[j-1]);
		}
	}
}

int main(){
 	vector<int> nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
 	insertSort(nums,nums.size());
 	for(int i=0;i<nums.size();i++){
 		if(i==nums.size()-1) printf("%d\n",nums[i]);
 		else printf("%d ",nums[i]);
	}
 } 

复杂度分析:

平均时间复杂度:O(N^2)

空间复杂度:O(1)

稳定

选择排序

思想:

用我的话来说。当遍历到i(假设i>0),代表前0~i-1项已经排序好,找出后面i+1~n-1以及第i项中最小项的索引,最后将其索引与i交换即可保证前0~i项已经排序好。

ps:选择排序的前0~i-1项排序好了和插入排序的概念不是完全相同的,前者是全局排序好了,即前0~i-1项和最终排序好的数组的前0~i-1项相同;而后者不一定和最终排序的数组的前0~i-1项相同,只能保证其目前的前0~i-1项是局部保持正确顺序的。

代码:

#include<cstdio>
#include<vector>
using namespace std;

void selectSort(vector<int> &nums,int n){
	for(int i=0;i<n-1;i++){
		int min=i;
		for(int j=i+1;j<n;j++){
			if(nums[j]<nums[min]){
				min=j;
			}
		}
		swap(nums[i],nums[min]);
	}
}

int main(){
 	vector<int> nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
 	selectSort(nums,nums.size());
 	for(int i=0;i<nums.size();i++){
 		if(i==nums.size()-1) printf("%d\n",nums[i]);
 		else printf("%d ",nums[i]);
	}
 } 

复杂度分析:

平均时间复杂度:O(N^2)

空间复杂度:O(1)

不稳定

冒泡排序

思想:

用我的话来讲就是由于相邻两项会根据大小交换,所以每次遍历一趟,交换完后,最大项就会被排到正确的位置。

代码:

#include<cstdio>
#include<vector>
using namespace std;

void bubbleSort(vector<int> &nums,int n){
	bool isSwap=false;
	for(int i=1;i<n;i++){
		isSwap=false;
		for(int j=1;j<n+1-i;j++){
			if(nums[j]<nums[j-1]){
				swap(nums[j],nums[j-1]);
				isSwap=true;
			}
		}
		if(!isSwap){
			break;
		}
	} 
}

int main(){
 	vector<int> nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
 	bubbleSort(nums,nums.size());
 	for(int i=0;i<nums.size();i++){
 		if(i==nums.size()-1) printf("%d\n",nums[i]);
 		else printf("%d ",nums[i]);
	}
 } 

复杂度分析:

平均时间复杂度:O(N^2)

空间复杂度:O(1)

稳定

希尔排序

思想:

就是通过步长来分组,一般其实步长是数组长度的一半,每趟排序,步长减半。对于分的组,使用插入排序进行排序。

代码:

#include<cstdio>
#include<vector>
using namespace std;

void shellSort(vector<int> &nums,int n){
	//根据步长由长到短分组,每次分组排序完,步长减小一倍。 
	for(int gap=n/2;gap>0;gap/=2){
		//对于每个分组使用插入排序 
		for(int i=gap;i<n;i++){
			int temp=nums[i];
			int j;
			for(j=i;j>=gap&&nums[j-gap]>temp;j-=gap){
				nums[j]=nums[j-gap];
			}	
            //nums[j]是nums[i]应该插入的位置
			nums[j]=temp;
		} 
	} 
}

int main(){
 	vector<int> nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
 	shellSort(nums,nums.size());
 	for(int i=0;i<nums.size();i++){
 		if(i==nums.size()-1) printf("%d\n",nums[i]);
 		else printf("%d ",nums[i]);
	}
 } 

复杂度分析:

平均时间复杂度:O(Nlog2N)

最差时间复杂度:O(N^2)

空间复杂度:O(1)

不稳定

堆排序

思想:

用的我的话来说。

①堆存储的数据结构,用数组,其中数组下标为的i节点的父节点为(i-1)/2;数组下标为i的父节点(假设其存在左子节点和右子节点)的左子节点为i*2+1,右子节点为i*2+2

②将其变为最大堆:

  1. 从最后一个父节点开始,若其小于较大的子节点,则与其交换值,同时还需要继续对子节点和孙子节点做同样的操作。

  2. 整个1是一次,直到对所有父节点处理完,这样就建成了最大堆。

③将堆顶元素与堆最后一个元素交换,此时堆尾是最大值,已排序好,就不需要参加以后的堆相关操作了。又因为堆顶改变,所以需要重新变为最大堆。之后重复步骤③。

代码:

#include<cstdio>
#include<vector>
using namespace std;

void maxHeap(vector<int> &nums,int start,int end){
	//父节点下小标与子节点下标
	int parent= start;
	int child=start*2+1;
	//要保证子节点索引未越界,是正常的 
	while(child<=end){
		//仍然先判断其是否越界 并选择较大的子节点 
		if(child+1<=end&&nums[child]<nums[child+1]){
			child++; 
		} 
		//如果父节点大于或等于较大的子节点 ,不用交换 
		if(nums[parent]>=nums[child]){
			return; 
		}
		else{//交换父子内容,再继续子节点和孙节点进行比较 
			swap(nums[parent],nums[child]);
			parent=child;
			child=parent*2+1; 
		}
	}
} 

//首先要理解,就是索引为i的节点的父节点索引为(i-1)/2 
void heapSort(vector<int> &nums,int n){
	//要从最后一个父节点开始,依次进行调整 
	//(n-1-1)/2=n/2-1 
	for(int i=n/2-1;i>=0;i--){
		//将堆转化为最大堆 
		maxHeap(nums,i,n-1); 
	}
	//将堆顶元素与堆尾元素互换
	//此时堆尾就是已排序好的最大值 
	//这里有点像选择排序,选择排序是每次选择最小的  
	for(int i=n-1;i>0;i--){
		swap(nums[i],nums[0]);
		//已经排序好了,后续不用参加堆建立了,所以是i-1 
		//由于交换后堆可能不是最大堆了,所以需要重新构建成最大堆
		//不过这次从第一个父节点开始 
		maxHeap(nums,0,i-1); 
	} 
}

int main(){
 	vector<int> nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
 	heapSort(nums,nums.size());
 	for(int i=0;i<nums.size();i++){
 		if(i==nums.size()-1) printf("%d\n",nums[i]);
 		else printf("%d ",nums[i]);
	}
 } 

复杂度分析

时间复杂度O(nlogn)

不稳定

计数排序

思想:

①先找出待排序数组的最大值,定义一个最大元素+1的大小的数组。

②使用该数组统计每个元素的次数,也就是count[temp]代表temp元素出现的次数的值。

③将count数组进行累加,就是该项的值等于该项前面所有项的和。这样得到的数组就是:count[i]表示值小于或等于i的数的个数(包括自己),那么代表i值应该放在第count[i]位,所以索引为count[i]-1,最后由于排序好了一个,那么相应的count[i]就要减少一个。(该元素出现的次数-1)

代码:

#include<cstdio>
#include<vector>
using namespace std;

void countSort(vector<int> &nums,int n){
	int *temp = new int[n];
	int max=nums[0];
	for(int i=1;i<n;i++){
		if(max<nums[i]){
			max=nums[i];
		}
	}
	int *count = new int[max + 1];
	for (int i = 0;i<=max;i++){
		count[i]=0;  //初始化
	}
	//计算出数组每个元素出现的次数 
	for(int i=0;i<n;i++){
		count[nums[i]]++;
	}
	//计算数组小于或等于该下标的元素的个数 
	for(int i=1;i<=max;i++){
		count[i]=count[i]+count[i-1];
	} 
	for(int i=n-1;i>=0;i--){
		//count[nums[i]]表示小于或等于nums[i]的元素个数
		//那么排序好后nums[i]应该放在第count[nums[i]]位
		//对应索引为count[nums[i]]-1。 
		//所以nums[i]排序后的索引是count[nums[i]]-1。 
		temp[count[nums[i]]-1]=nums[i];
		count[nums[i]]--; 
	}
	//赋值给原数组 
	for(int i=0;i<n;i++){
		nums[i]=temp[i];
	} 
	//释放空间 
	delete[] count; 
	delete[] temp; 
}

int main(){
 	vector<int> nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
 	countSort(nums,nums.size());
 	for(int i=0;i<nums.size();i++){
 		if(i==nums.size()-1) printf("%d\n",nums[i]);
 		else printf("%d ",nums[i]);
	}
 } 

复杂度分析:

当输入的数据是n个0到k之间的整数时,运行时间是O(n+k)。

计数排序需要两个额外的数组用来对元素进行计数和保存排序的输出结果,所以空间复杂度为O(k+n)。

稳定

桶排序

思想:

将待排序数据平均切分为几个区间(不同区间之间本身就是有序的),叫做桶,每个桶各自将元素排序好,再将桶内数据合并即可完成排序。

ps:计数排序可以看做是一种极端的桶排序,一个数就对应一个桶,一个桶只存放一个具体的数(而不是一个区间的数)。

主要是计数排序解决不了含有浮点数的排序,但桶排序可以。

桶排序应用的条件苛刻,桶排序必须将数据均匀分布在桶中,所以数据要分布比较均匀,而且数值要有一定的区分度,容易被分成m个有大小顺序的桶。

代码:

复杂度分析:

假设元素数量为n,m个桶,每个桶的元素个数bucketsize = n/m。我们可以在桶内选择O(nlogn)的快排,所以总的时间复杂度为O(n) + O(m * (n/m) * log(n/m)) = O(n) + O(n * log(n/m)),由于每个桶的元素个数bucketsize是我们自己设置的,当n等于m时,log(n/m)=0,所以得到时间复杂度为O(n)。所以桶排序最好和平均时间复杂度为O(n+k)。如果数据全在一个桶中,排序会退化为桶内的排序类型,所以最差时间复杂度可以是O(n²),即快排的最差情况。

空间复杂度O(n * k)

稳定的。

基数排序

思想:

其实这个算法就是相对于计数排序适用于大数据,因为数据很大时,要申请一个大数据+1大小的数组比较耗内存,所以采用基数排序。

可以理解为每个数据的低位到高位的每一位进行一次计数排序。

总结一下基数排序、计数排序、桶排序。

  • 基数排序:根据键值的每位数字来分配桶

  • 计数排序:每个桶只存储单一键值

  • 桶排序:每个桶存储一定范围的数据

基数排序不是直接根据元素整体的大小进行元素比较,而是将原始列表元素分成多个部分,对每一部分按一定的规则进行排序,进而形成最终的有序列表。

代码:

#include<cstdio>
#include<vector>
using namespace std;

//求待排序数组中数据的最大位数(10进制) 
int maxbit(vector<int> &nums,int n){
	int max=nums[0]; 
	for(int i=1;i<n;i++){
		if(max<nums[i]){
			max=nums[i];
		}
	}
	int d=1,p=10;
	while(max>=p){
		max/=10;
		++d;
	}
	return d;	 
}

void radixSort(vector<int> &nums,int n){
	int d=maxbit(nums,n);
	int *temp=new int[n]; 
    //因为一位数最大为9,所以建立一个9+1大小的数组
	int *count = new int[10];
	int radix=1,k; 
	//需要进行d次排序 
	for(int i=1;i<=d;i++){
		//每一次相当于对其位数采用计数排序。 
		for(int j=0;j<10;j++){
			count[j]=0;
		}
		for(int j=0;j<n;j++){
			k=(nums[j]/radix)%10;//根据其位数得到在桶中位置
			count[k]++; //得到每个桶的记录数 
		}
		for(int j=1;j<10;j++){
			count[j]=count[j-1]+count[j];
		}
		for(int j=n-1;j>=0;j--){
			k=(nums[j]/radix)%10;
			temp[count[k]-1]=nums[j];
			count[k]--; 
		} 
		for(int j=0;j<n;j++){
			nums[j]=temp[j];
		}
		radix*=10;
	} 
	//释放内存 
	delete[] temp;
	delete[] count;
}

int main(){
// 	vector<int> nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
 	vector<int> nums={12,3,51,7,2,6,4,118,9,26,8,7,6,0,3,5,9,4,21,0};
 	radixSort(nums,nums.size());
 	for(int i=0;i<nums.size();i++){
 		if(i==nums.size()-1) printf("%d\n",nums[i]);
 		else printf("%d ",nums[i]);
	}
 } 

复杂度分析:

总的时间复杂度为O(d*(n+r))。其中d为位数,n为最多元素个数,r为桶数。

空间复杂度是O(k+n)

稳定

7.链表基本操作

逆序打印链表

使用,递归先遍历再输出

void LinkListReversePrint(LinkNode* head){
    	//为空直接返回
        if(head == NULL){
            return;
        }
    	//递归调用
        LinkListReversePrint(head -> next);
    	//最后才打印
        printf("[%c | %p]",head ->data,head);
}

逆置链表

思路:

①创建三个节点,一个是当前节点cur、一个是cur上一个节点pre。另外就是tmp。

②每次循环,用tmp记录cur下一个节点,接着将cur的next指针指向上一个节点pre。这个时候就完成了一次反转操作。

③接着将pre和cur的记录往后移动一次,重复上面的操作。

public ListNode reverseList(ListNode head) {
		//申请节点,pre和 cur,pre指向null
		ListNode pre = null;
		ListNode cur = head;
		ListNode tmp = null;
		while(cur!=null) {
			//记录当前节点的下一个节点
			tmp = cur.next;
			//然后将当前节点指向pre
			cur.next = pre;
   
			//pre和cur节点都前进一位
			pre = cur;
			cur = tmp;
		}
		return pre;
	}

删除链表中一个给定的节点

思路:

①从要删除节点的后面节点入手。,将后面节点的值给要删除的节点。

②将要删除节点的指针指向后面节点的后面节点即可。

(ps:那么在物理上来看,其实是删除了要删除节点的后节点,只是把它的值给了要删除节点。)

void LinkListPop(LinkNode** phead, LinkNode* pos){
      if(phead == NULL || pos == NULL){
          return ;//非法输入
      }   
      if(*phead == NULL){
          return;//空链表
      }   
      pos -> data = pos -> next -> data;
      pos -> next = pos -> next -> next;
      LinkNodeDestory(pos->next);
  }

在无头链表的给定节点前插入一个节点

思路:

①还是从给定节点的后节点入手,新建一个节点,其值为给定节点的值,给定节点的值赋为插入节点的值。

②新建节点的指针指向给定节点的后节点,给定节点的指针指向新节点即可。

void LinkListInsertBefore(LinkNode** phead, LinkNode* pos, LinkNodeType value){
      if(phead == NULL || pos == NULL){
          return;//非法输入
      }
      LinkNode* new_node = LinkNodeCreate(pos -> data);
      pos -> data = value;
      new_node -> next = pos -> next;
      pos -> next = new_node;
  }

合并两个有序链表

思路:

①创建一个新链表,用于存放合并后的链表,并维护两个结点,头节点和尾节点。

②将两个链表的当前节点(起始当前节点均为各自的头节点)的值进行比较,然后将小的节点放在新链表(跟新链表放入前尾节点的指向,然后放入后修改尾节点)。将小节点的链表当前节点右移一位,代表已被用过的节点过滤掉。

③重复②,依次类推。最后哪个链表先结束,将将另外一个链表没有进行比较的部分拷贝在新链表后面即可。

  LinkNode* LinkListMerge(LinkNode* head1, LinkNode* head2){
      if(head1 == NULL || head2 == NULL){
          return NULL;
      }   

      LinkNode* new_head = NULL;
      LinkNode* new_tail = NULL;
      LinkNode* cur1 = head1;
      LinkNode* cur2 = head2;
      while(cur1 != NULL && cur2 != NULL){
          if(cur1 -> data <= cur2 -> data){
              if(new_head == NULL){
                  new_head = new_tail = cur1;
              }   
              else{
                  new_tail -> next = cur1;
                  new_tail = cur1;
              }   
              cur1 = cur1 -> next;
          }   
          else if(cur1 -> data > cur2 -> data){
              if(new_head == NULL){                                                                                                                                                            
                   new_head = new_tail = cur2;
              }   
              else{
                  new_tail -> next = cur2;
                  new_tail = cur2;

              }   
              cur2 = cur2 -> next;
           }   
      }
      return new_head ;
  }

查找单链表的中间节点

思路:

①使用快慢指针,慢指针每次走一步,快指针每次走两步。

②当然遍历的时候要注意块指针的结束条件,当为倒数第二个节点的时候也需要停止遍历,因为此时不够走两步。

  LinkNode* FindMidNode(LinkNode* head){
      LinkNode* slow = head;
      LinkNode* fast = head;
      while(fast != NULL && fast -> next != NULL){
          fast = fast -> next -> next;                                                                                                                                                         
          slow = slow -> next;
      }
      return slow;
  }

查找单链表倒数第k个节点

思路:

①使用快慢指针,这次两指针的速率一样,只是快指针先走k步后,慢指针才开始走。

②当快指针走到空时,慢指针所指位置即倒数第k个节点。

LinkNode* FindLastKNode(LinkNode* head,size_t K){
      LinkNode* fast = head;
      LinkNode* slow = head;
      size_t i = 0;
      for(; i < K && fast != NULL; ++i){
          fast = fast -> next;
      }
      if(i < K){
          return NULL;//链表节点小于K,直接返回空
      }
      while(fast != NULL){
          fast = fast -> next;
          slow = slow -> next;
      }
      return slow;
  }

删除单链表倒数第k个节点

思路:可使用上述方法找到该结点并常规法删除。

带环单链表

判断链表是否带环;若带环,求环的入口点、环的长度。

思路:

①使用快慢指针,fast每次走两步,slow每次走一步。若两指针相遇,则存在环,

②当两个指针相遇时,将fast放到链表头并速率调整为一次一步,fast和slow向前遍历,相遇点即为环的入口点(证明见142.环形链表相关结论数学性证明。_emttxdy的博客-CSDN博客

③从相遇点开始slow和fast继续按照原来的方式向前走slow = slow -> next; fast = fast -> next -> next;直到二者再次相遇,此时经过的步数就是环上节点的个数 。(还有一种简单思路,由于相遇点必定在环中,此时slow一次走一步,直到走一圈再次回到该相遇点,走的步数就是环的长度,下面有代码)。

//链表是否带环,时间复杂度O(n),没有开辟新空间,所以空间复杂度为O(1)
  LinkNode* HasCycle(LinkNode* head){
      LinkNode* fast = head;
      LinkNode* slow = head;
      while(fast != NULL && fast -> next != NULL){
          fast = fast -> next -> next;
          slow = slow -> next;
          if(fast == slow){
              return fast;
          }   
      }   
      return NULL; 
  }
  //环的长度,时间复杂度O(n),空间复杂度O(1)
  size_t GetCycleLen(LinkNode* head){
      LinkNode* meet_node = HasCycle(head);
      if(meet_node == NULL){
          return 0;//链表无环
      }
      size_t count = 1;
      LinkNode* cur = meet_node;
      for(; cur -> next != meet_node; cur = cur -> next){
          ++count;
      }
      return count;
  }
  //环的入口点,时间复杂度O(n),空间复杂度O(1)
  LinkNode* GetCycleEntry(LinkNode* head){
      LinkNode* meet_node = HasCycle(head);
      if(meet_node == NULL){
          return NULL;//没有环直接返回
      }
      LinkNode* cur1 = head;
      LinkNode* cur2 = meet_node;
      while(cur1 != cur2){
          cur1 = cur1 -> next;
          cur2 = cur2 -> next;
      }
      return cur1;
  }

  • 8
    点赞
  • 69
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 保研面试是一个对学生全面素质的考察,其中涉及到的专业知识也是面试官关注的重点之一。在准备保研面试资料时,复习pdf.rar可以作为辅助工具,帮助自己复习算法数据结构、操作系统、概率论、线性代数等相关知识。 首先,在复习算法方面,可以针对常见的排序算法(如冒泡排序、快速排序等)、查找算法(如二分查找)、图算法(如最短路径算法、最小生成树算法等)进行复习。通过理解算法的原理和实现,掌握它们的应用场景和效率分析方法,提升自己在面试中解决实际问题的能力。 其次,在复习数据结构方面,可以重点关注常见的数据结构,如数组、链表、栈、队列、树、图等。了解它们的基本性质、操作和应用,掌握它们之间的相互关系和优缺点。并且,要能够熟练地运用各种数据结构解决实际问题,在面试中展现自己的编程能力。 此外,操作系统也是保研面试中的一个重要考点。可以通过复习pdf.rar中的资料,了解操作系统的基本概念、类型、特点以及常见的操作系统原理和机制,如进程管理、内存管理、文件系统等。理解操作系统的工作原理和实现机制,可以更好地回答与操作系统相关的面试问题。 另外,概率论也是比较重要的一门学科,它在统计学、随机过程、信号处理等领域有广泛的应用。需要掌握基本的概率概念、概率分布、随机变量、期望、方差等,并能够灵活运用概率论的基本原理解决实际问题。 最后,线性代数也是应用较广泛的数学学科之一。需要了解线性方程组、矩阵、向量空间、特征值与特征向量等基本概念和性质,并能够熟练运用线性代数知识解决相关问题。 总之,复习pdf.rar中的资料可以作为复习保研面试算法数据结构、操作系统、概率论、线性代数等知识的辅助工具。但重点在于对这些知识进行深入理解和灵活运用,通过大量的练习和实践,提升自己的解决问题的能力,以在面试中展现出色的表现。 ### 回答2: 自己制作复习资料是提高保研面试准备效果的一种有效方法。首先,制作自己的复习pdf.rar可以确保资料内容与自己所需的面试知识点完全匹配。在复习过程中,可以根据自己的理解和重点进行归纳、总结,使得资料更符合个人学习特点和记忆习惯。 对于算法数据结构、操作系统、概率论、线性代数等科目,制作复习资料可以帮助梳理知识框架,理解知识点之间的联系。可以将各个知识点进行分类整理,标注重点和难点,方便复习时查阅。同时,复习资料还可以添加自己的理解和解题技巧,帮助巩固和加深记忆。 此外,制作复习资料也是一个复习的过程。在制作过程中,可以主动思考、查漏补缺,提高对知识点的理解和记忆。通过编写文、绘制思维导图、整理笔记等方式,可以将知识点更深入地掌握。制作的资料可以随时查阅,方便复习。 总之,制作自己的复习pdf.rar可以提高面试复习的效果。通过整理和梳理知识点,深入理解和记忆,提升面试的答题能力。此外,自己制作的资料还可以根据个人特点和喜好进行定制,提高学习的兴趣和效果。 ### 回答3: 所有复习的资料应该针对保研面试所需涉及的算法数据结构、操作系统、概率论、线代等内容进行整理。对于这些科目,可以从以下几个方面来进行复习: 首先,对于算法数据结构,可以准备一些经典问题和常见的数据结构实现,并对其原理和应用进行深入理解。可以通过刷题、看书或者参加相关的课程来提高自己的算法数据结构水平。 其次,对于操作系统,可以重点复习进程管理、内存管理、文件系统等方面的知识。可以了解操作系统的基本原理和相关的算法,掌握常见的操作系统概念和技术。 另外,对于概率论和线代,可以复习一些基本的概念和公式,并了解其在计算机科学中的应用。掌握线性代数中矩阵、向量等的基本概念和运算规则,并了解其在计算机图形学、机器学习等领域的重要性。 最后,除了复习这些具体的学科知识外,还应该注重提高自己的解决问题和思考能力。保研面试不仅仅是考察知识的掌握程度,更注重对问题的分析和解决能力。因此,可以通过做一些综合性的题目和实践项目,培养自己的思维和创新能力。 总之,自主整理的复习pdf.rar应该包含针对保研面试所需的算法数据结构、操作系统、概率论、线代等知识的复习资料。同时,要注重提高解决问题和思考能力,通过刷题、实践项目等方式进行综合性的训练。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值