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

以下是笔者整理的保研/面试容易被问到的算法问题,包含最短路径,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)

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
#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;kdis[i]) {
                min_tmp=dis[i];
                min_point=i;
            }
        }
        //找不到结束 
        if(min_point==INF) break; 
        //找到,加入集合 
        visit[min_point]=true; 
        for(int i=0;i

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;kA[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
#include
#include
using namespace std;

void dfs(int node,double P,double r,int &count,double &max,vector > &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>N>>P>>r;
	r=r/100+1;
	vector > tree(N);
	
	int a[N];
	for(int i=0;i>a[i];
	} 
	//建图 
	for(int i=0;i

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 (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
#include
using namespace std;
vector v[100];
int book[100];//用于存储各个世代的节点数。 
void dfs(int node, int level){
	book[level]++;//该世代的节点数+1 
	for(int i=0;imaxnum) {
			maxnum=book[i];
			maxlevel = i; 
		}
	} 
	printf("%d %d",maxnum,maxlevel);
} 

3.BFS

大致思路:

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

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

  3. 重复步骤2

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

代码

例题

PAT甲级1094

BFS方法的题解

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

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

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
#include 
#include
#define INF 0x3f3f3f3f
using namespace std;
int n,m,g[1001][1001],dis[1001];

int prime(){
	//是否选中加入集合 
	vector 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]

5.KMP

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

前提next

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

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

例如我们分析ababaca

字符串 "前缀"和"后缀"的最长的共有元素的长度 next数组
a 0 next[0]=-1
ab 0 next[1]=-1
aba 1 next[2]=0
abab 2 next[3]=1
ababa 3 next[4]=2
ababac 0 next[5]=-1
ababaca 1 next[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
#include
using namespace std;
//快排 
 void quickSort(vector &nums,int l,int r){
 	if(l+1>=r){
 		return; 
	 }
	 //使用双指针first和last 
	 int first =l,last=r-1,key=nums[first];//选定区间第一位为基准
	 while(first=key) --last;
		 //此时last指针所在值赋给first指针
		 nums[first]=nums[last];
		 //右移指针first,直到遇到大于key的 
		 while(first 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

复杂度分析:

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

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

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

不稳定

归并排序

思想:

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

代码:

#include
#include
using namespace std;

//temp用来作为一个中间存储排序后的数组。 
void mergeSort(vector &nums,int l,int r,vector &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=r,因为防止nums[q]越界,又可以方便判断,因为q>=r则代表右区间已经分配合并用完了,需要左区间。 
		if(q>=r || (p nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
	vector temp(nums.size());
 	mergeSort(nums,0,nums.size(),temp);
 	for(int i=0;i

复杂度分析:

平均时间复杂度O(nlogn)

空间复杂度:O(n)

稳定

插入排序

思想:

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

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

代码:

#include
#include
using namespace std;

void insertSort(vector &nums,int n){
	for(int i=1;i=1&&nums[j] 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

复杂度分析:

平均时间复杂度: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
#include
using namespace std;

void selectSort(vector &nums,int n){
	for(int i=0;i 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

复杂度分析:

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

空间复杂度:O(1)

不稳定

冒泡排序

思想:

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

代码:

#include
#include
using namespace std;

void bubbleSort(vector &nums,int n){
	bool isSwap=false;
	for(int i=1;i 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

复杂度分析:

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

空间复杂度:O(1)

稳定

希尔排序

思想:

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

代码:

#include
#include
using namespace std;

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

int main(){
 	vector 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

复杂度分析:

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

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

空间复杂度:O(1)

不稳定

堆排序

思想:

用的我的话来说。

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

②将其变为最大堆:

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

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

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

代码:

#include
#include
using namespace std;

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

//首先要理解,就是索引为i的节点的父节点索引为(i-1)/2 
void heapSort(vector &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 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

复杂度分析

时间复杂度O(nlogn)

不稳定

计数排序

思想:

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

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

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

代码:

#include
#include
using namespace std;

void countSort(vector &nums,int n){
	int *temp = new int[n];
	int max=nums[0];
	for(int i=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 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

复杂度分析:

当输入的数据是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张图片

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

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

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

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

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

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

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

代码:

#include
#include
using namespace std;

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

void radixSort(vector &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=0;j--){
			k=(nums[j]/radix)%10;
			temp[count[k]-1]=nums[j];
			count[k]--; 
		} 
		for(int j=0;j nums={1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
 	vector 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

复杂度分析:

总的时间复杂度为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;
  }

你可能感兴趣的:(保研复习,算法,数据结构,算法,面试,排序算法,链表)