数据结构总结

一、线性表

  • 一组先后有序存储的数据,分为顺序存储和链式存储。
  • 我们把链式存储第一个结点叫做头指针,不存储数据,当头指针的next为空时,线性表空;链式线性表插入数据一般使用头插法,这样更简单而且不用再单独用一个指针。
    顺序
Typedef struct
{
	ElemType data[MAXSIZE];
	int length;
}

链式:

Typedef struct Node
{
	ElemType data;
	struct Node *next;
}
  • 另外还有一种特殊的结构,静态链表,就是用一个固定的数组来存储数据,不过数组的每个数据并不直接是一个数,而是我们定义的数据类型,这种数据类型内部有两个数据域,一个存真正的数据,一个存下一个结点的下标,相当于next指针。
    静态链表的第一个结点我们用来存储数组中还未被使用的第一个元素的下标,链表的最后一个元素我们用来存储第一个存有元素的下标,相当于头结点。
  • 双向链表,多了一个指向前驱结点的指针。

二、栈和队列

1. 栈

后进先出,top指针即可确定一个栈
顺序

typedef struct
{
	ElemType data[MAXSIZE];
	int top;
}SqStack;

链式

typedef struct StackNode
{
	ElemType data;
	struct StackNode *next;
}*LinkStackPtr;
typedef struct LinkStack
{
	LinkStackPtr top;
	int count;
}LinkStack;
  • 链式存储栈时,入栈同样用头插法,这样在弹栈时更方便,也不用记录其他数据。
  • 栈的应用:递归、四则运算等
2. 队列

先进先出,用front和rear指针来确定队列。
顺序

typedef struct
{
	ElemType data[MAXSIZE];
	int front;
	int rear; //若循环队列rear每次计算只需要%MAXSIZE即可,front==rear说明队满
}

链式

typedef struct QNOde
{
	ElemType data;
	struct QNode *next;
}QNode,*QueuePtr;
typede struct
{
	QueuePtr front,rear;
}LinkQueue;

三、串

  • 就是一个字符串,每一个元素是一个字符,也分为顺序存储和链式存储,链式存储一个结点可以存多个字符。
KMP模式匹配算法
  • 例如主串S=”abcab123abcabdababc”,模式T=”abcabdab”
    当匹配到第5个字符abcab时,全部成功,但是第六个字符1和d失败,这时候朴素匹配算法是将模式串向后移动1位然后重头开始匹配;
  • KMP对这种情况做一个改进,观察发现第6个字符的前两个字符45匹配成功了,而且与在它们之前有一模一样的12字符组ab,也就是说,他们之前的字符组12拿到这个位置45上可以直接匹配成功,那么我们可以不再进行多余的比较,直接把模式串移动到这个45位置,然后我们比较6和模式串的3,看是否成功;
  • 这里是c和2,匹配失败,而c之前没有重复的字符串,所以我们只能一个个向后移动重新匹配。也就是说,当我们匹配失败的时候,要观察失败字符之前的字符的特点,将模式串按照特定的数字向后移动。关键就在于模式串每个字符匹配失败时,应该如何移动模式串。
  • 把每个元素匹配不成功时跳转的位置存储到next数组
  • 约定next[0] = -1;
  • 找到该字符的前后缀,前后缀中最长的相等的字符数,即为该字符的next,下次比较时直接拿该字符与下标为其next的字符比较。

求j的next数组的具体过程:

  • j-1与next[j-1]比较,若相等说明j的前缀字符比j-1多一个相等的,而j-1已知next,所以next[j]= next[j-1]+1;若不相等,j-1 = next[j-1],也就是把j和next[next[j-1]]位置的字符比较,这是因为匹配不成功时,我们首先已知j-1之前的字符后缀和next[j-1]之前的字符s完全匹配,那我们可以找到s的前后缀相等部分,也就是next[next[j-i]],我们可以找到前缀字符串里的重复部分,也就next[j-1]里前缀等于后缀的部分,注意这一段和j-1的完全相等,比如串abc abd abc abf h,求next[h]时,f和d匹配失败,我们接着找next[d],发现可以将之前的分为两个串,那说明d的前缀和f的后缀相等next[d]长度时可以匹配的,我们拿d和c匹配。如果next[next[j-1]]为-1,说明已经是第一个字符,也就是前面的字符没有重复的前后缀,即使匹配成功了,到这里不相等,我们也得从头开始匹配,所以next[j] = 0。
  • 求得next数组后,模式串匹配时,比较当前两个字符,如果成功,各自向后移动一位,如果失败,将S[i]与T[next[j]]比较,如果T[next[j]]==-1,当前字符指针都要向后移动,就是拿下一个S和第一个T匹配,直到i或j超出S或T的长度,跳出循环后判断是否匹配成功。

四、树

二叉树的链式存储

typedef struct BiTNode
{
	ElemType data;
	struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
1. 遍历(递归)
OrederTraverse(BiTree T)
{
	if(T==null)
		return ;
	访问结点;//1
	OrderTraverse(T.lchild);  //2
	OrderTraverse(T.rchild);  //3
	//改变1,2,3步骤的顺序即可得到前、中、后序遍历结果
}
2. 线索二叉树

将二叉树左/右子结点为空的指针指向其前驱/后继,在每个结点类型中添加两个标识符ltag/rtag,标记现在的左右指针指向孩子还是前驱后继。

3. 赫夫曼树

树上每一个到达每一个结点的路径都是有权值的,可以用赫夫曼树构造赫夫曼编码,让左孩子路径为0,右孩子路径为1,每个叶子结点的路径即为一个赫夫曼编码,这样可以保证每个编码不会称为其他编码的前缀,使得解码唯一确定。

五、图

1. 邻接矩阵存储

用两个数组来存储图,一个是一维数组,存储所有的顶点信息;另一个是二维数组,用来存储所有边或弧的信息,数组的下标对应的即为一维数组中相同下标的顶点。

typedef struct
{
	VertexType vexs[MAXSIZE];
	EdgeType arc[MAXSIZE][MAXSIZE];
	int numVertexs, numEdges;
}MGraph;
2. 邻接表存储

用一个顺序表存储顶点,每个顶点类型存储顶点数据和指向它的第一个邻接边的指针firstedge,每个边类型存储边的权值,以及边尾结点,以及指向下一个邻接边的指针。
这里边的添加同样用头插法。

typedef struct EdgeNode  //边表
{
	int adjVex;  //邻接点下标
	EdgeType weight;
	struct EdgeNode *next;
}
typedef struct VertexNode
{
	VertexType data;
	EdgeNode *firstedge;  //边表的头指针
}VertexNode,AdjList[MAXVEX];
typedef struct
{
	AdjList adjlist;
	int numVertexs,numEdges;
}GraphAdjList;
3. 深度优先遍历(DFS)

用到,从选取的第一个顶点开始,入栈,若栈不为空,弹栈,访问该顶点,并将该顶点的邻接顶点全部入栈。这样我们会访问一直寻找顶点的邻接点的邻接点访问,直至没有邻接点,回溯,继续找到邻接点访问。

4. 广度优先遍历(NFS)

用到队列,从选取的第一个顶点开始,入队列,若队列不为空,出队,访问该顶点,并将该顶点的未被访问过的邻接顶点全部入队。使用队列,我们会将顶点的邻接点全部访问完以后,才会进入其邻接点的邻接点访问,有点像层序遍历。

通用函数

void fun(BiTree T)
{
	初始化visit数组,全部未访问过;
	for i = 0 to G.numVertexs do
	{
		if(!visit[i])
		{
			visit = true;
			访问G.vexs[i];
			入栈(队列);
			while(栈(队列)!= null)
			{
				弹栈(出队);
				p = firstEdge;
				while(p!=null)
				{
					if(p.adjvex 未被访问)
					{
						访问;
						标记已被访问;
						入栈(队列);
					}
					p = p -> next;
				}
			}
		}
	}
}
5. 最小生成树——普利姆算法(Prim)

需要两个数组,数组下标对应图中顶点顺序存储的下标,lowcost[]存顶点到生成树的最短边权重、adjVex[]存这条边的另一个顶点,将开始结点加入最小生成树集合,处理方式可以为把它的lowcost设置为0,找到所有与它相邻的顶点,修改他们的若这条边的权重小于对应的lowcost,修改lowcost、adjVex对应修改;再从lowcost中选择权重最小的且未加入生成树的点,加入生成树集合,重复上述操作,每循环因此加入一个点,所以循环顶点总数次。

6. 最小生成树——克鲁斯卡尔算法(Kruskal)
  • 每次从图中选取最小的边,判断是否会与当前的最小生成树形成回路,若不会,则将这条边加入最小生成树,否则,删掉这条边。
  • 怎么判断会不会形成回路呢,我们把一条边较小的一头记为begin,另一头记为end,把最小生成树的根记作整个树中最大的顶点,对每个可以加入生成树的边,将begin的parent记作end。拿到一个新的边时,我们寻找它的头尾结点的根结点(根结点的parent为0),若他们根结点相同,则连通,这条边加入会形成回路。
  • 实现过程:(也可以事先把所有边按权重排序,然后循环所有边即可)从图中选取权值最小的边,判断是否会与当前的最小生成树形成回路,找begin和end的parent,用while循环,while(parent[begin]!= 0)begin= parent[begin],两个parent不相等,则不连通,则将这条边加入最小生成树,且修改parent[begin] = end,将边标记为已处理。
7. 最短路径——迪杰斯特拉算法(Dijkstra)
  • 类似于prim算法,不过prim求的是点到整个生成树的最短边,只考虑两个点之间的直接权重,而这里求的点到固定点的最短距离,需要考虑整个路径上的权重。
  • 这个算法需要3个数组,Visited[]用来存储该路径的最短路径是否已经找到,Distance[]用来存储每个顶点的最短路径总长,Parent[]用来存到该点的最短路径的前一个结点
  • 我们先把v0顶点的visited修改为true,然后将与它相邻的顶点的distance修改为边权重,找到visited为false且distance最小的点,修改其visited,parent,对于这个新的结点,继续找与它相邻的结点,比较相邻结点的distance和当前结点的distance+边权值哪个更小,后者更小说明经过该点到相邻点比他的父顶点到相邻点更近,修改这类相邻点的parent和distance,然后再找最小的未被访问的点,重复上述操作。
8. 最短路径——弗洛伊德算法(Floyd)

计算任意两个点之间的最短路径。

  • 因为要计算整个图任意两点之间的路径,用到两个二维数组,Distance[]存储任意两个点之间最短路径的长度,Parent[]存储任意两点之间最短路径的前驱结点。
  • 我们用到三个for循环,都是for k/i/j =0 to numVertexs,内两层的意思是,对图中每个顶点,计算它到其它所有顶点的最短路径,这个路径是通过当前的路径和经过k的路径比较得到的,怎么求得的呢,比较i->ji->k+k->j哪个更小,记录的最短路径和计算两点经过下标k后路径最短的k(也有可能是原先的方式最短),修改Distance和Parent。
  • 整个循环的意思是,选取一个中间结点,比较任意两个顶点经过k以后的最短路径情况,当k循环完以后,任意顶点经过所有k的最短路径就求出来了。
9. 拓扑排序

解决一个工程能否顺序完成。

  • 拓扑排序要记录每个顶点的入度,可以单独用一个数组来存储,也可以在顶点表数据结构中增加一个存放入度的数据in;同时需要一个count来记录拓扑序列中的顶点数,与图的顶点数相比较,判断图中是否有环。
  • 先将所有入度为0的顶点入栈(队列也可以),若栈不为空,弹栈,处理该顶点,并将count计数+1,将它的出边的末尾顶点入度-1,若修改后入度为0,入栈,重复栈不为空操作,直至栈为空,若此时count<G.numVertexs,说明图中有环。
10. 关键路径算法

解决工程完成需要的最短时间。

  • 先求出每个事件(顶点)的最早开始时间(etv)最晚开始时间(ltv),再根据这个顺序求得每个活动(边)的最早开始时间(ete)最晚开始时间(lte)ete = lte 的活动即为关键活动
公式

etv[ i ] = max{ (etv[ i -1] + w< i-1, i >), etv[ i ] };
ltv[ i ] = min{ (ltv[i + 1] - w< i+1, i >), ltv[ i ] };
ete[ i ] = etv [ i ];
lte [ i ] = ltv[ i + 1 ] - w< i, i+1>;

公式理解

etv:每个顶点的最早发生时间,前一个事件结束后触发该事件,所以要根据前一个事件最早发生时间加上活动的时间,与当前存储的最早发生时间来比较,要保证所有前驱活动都完成才能开始,所以选择最大的,也就是相对最晚的。
ltv:每个顶点的最晚发生时间,应该是保证所有后继活动都能完成的情况下,使得耗时最长的后继活动刚好完成。用后继事件的最晚发生事件减去活动耗时,即为当前事件的最晚发生时间,选择相对较大的,才能保证后继活动全部都能完成。
ete:活动的最早开始时间应该是和前驱事件的最早发生时间相等。
lte:活动的最晚开始时间,为它后继事件的最晚发生时间减去活动时间,因为活动最晚开始,完成活动后,得到事件即为最晚发生。

拓扑排序的使用

拓扑排序可以得到所有事件的发生顺序,用拓扑排序求得事件发生顺序,把它们压入栈,取出时就是逆序,同时在求这个序列过程中,可以顺便求得etv,第一个事件etv=0,后面的事件根据公式就可以求出,可以保证得到的序列中,每个顶点不会受之后的顶点影响,按照拓扑序列的正序和反序,使得对当前结点求etv和ltv时,前驱和后继都已经确定了数据。

六、查找

1. 二叉排序(搜索)树

树中每个结点的左子树都小于它,右子树都大于它。

  • 不管是查找还是添加,我们都可以用排序树的性质,一直选择往左还是右搜索,对查找,若一直到叶子结点还未找到,说明没有这个结点,对于插入,一直到叶子结点还未找到,那我们可以直接在这个叶子结点的左或右孩子上添加这个结点,因为查找是满足排序树特点来查的,所以要插入的结点和这个叶子结点一定满足排序树的规则。
  • 对于删除,若存在这个点,要判断这个点是只有左或右一个孩子还是有两个孩子只有一个孩子,那么把它的孩子接到父结点即可,如果有两个孩子,那么我们要找到它的左孩子的最右孩子或者它的右孩子的最左孩子,这两个结点都是它的子树中刚好排在他前后的结点,用这个结点替换他,排序树可以保持特性。
2. 散列表查找(哈希表Hash)

哈希函数就是拿到数据的键值key,用哈希函数直接计算出它在数组中存储的位置。
主要关注哈希函数的选择和冲突的处理。

七、排序

1. 冒泡排序
2.选择排序
3. 插入排序
4. 希尔排序

将序列按照每间隔k个增量为一组分为几个组,组内插入排序,每次成倍缩小增量,直至增量为1;

5. 堆排序

分为大根堆和小根堆,先建立好堆,从最后一个顶点开始循环堆,将末尾顶点和堆顶交换(意思是加入已排好序列中),交换后对根做处理使之依然为大根堆。

6. 归并排序

从算法名字来看就是将两个序列按顺序归并为一个序列,这个算法从单个数字为一个组开始,两两归并,将归并后的几个序列继续两两归并,直至整个数组排好序。
用k记录当前每个分组的大小,归并时需要两个组的起止以及分隔位置,非递归算法要注意最后不满足等长数组的序列。

7. 快速排序

一种可以一次性找到元素位置的算法。

  • 首先将要排序的元素拿出来存好作为参考点,然后处理数组,找到一个合适的位置使得这个位置之前的元素都小于参考点,之后的元素都大于参考点,那么这个位置参考点的最终位置。
  • 用low和high两个指针指向头尾,若list[low]<list[0],low向后移一位,list[high]>list[0],high向前移一位,都不满足条件时,交换两个位置上的元素,继续,直到两个指针相遇,low=high,这个位置就是我们要找的位置。
  • 然后我们对此元素的左右两个分段递归排序,递归最内层是只有一个元素,也就是low=high。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值