数据结构笔记(自用)
数据结构
存取结构:随机存取、顺序存取
存取结构:分为随机存取和非随机存取(又称顺序存取)
1、随机存取就是直接存取,可以通过下标直接访问的那种数据结构,与存储位置无关,例如数组。非随机存取
就是顺序存取了,不能通过下标访问了,只能按照存储顺序存取,与存储位置有关,例如链表。
2、顺序存取就是存取第N个数据时,必须先访问前(N-1)个数据 (list),随机存取就是存取第N个数据时,
不需要访问前(N-1)个数据,直接就可以对第N个数据操作 (array)。
存储结构:随机存储、顺序存储
存储结构:分为顺序存储和随机存储
1.顺序存储结构
在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。
顺序存储结构是存储结构类型中的一种,该结构是把逻辑上相邻的节点存储在物理位置上相邻的存储单元中,
结点之间的逻辑关系由存储单元的邻接关系来体现。由此得到的储结构为顺序存储结构,通常顺序存储结构是
借助于计算机程序设计语言(例如c/c++)的数组来描述的。
顺序存储结构的主要优点是节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),
结点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,
由该序号可以直接计算出来结点的存储地址。但顺序存储方法的主要缺点是不便于修改,对结点的插入、删除运算时,
可能要移动一系列的结点。
2、随机存储结构
在计算机中用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
它不要求逻辑上相邻的元素在物理位置上也相邻。因此它没有顺序存储结构所具有的弱点,但也同时失去了顺序表可随机存取的优点。
随机存储最典型的代表为链式存储:
链式存储结构特点
1、比顺序存储结构的存储密度小 (每个节点都由数据域和指针域组成,所以相同空间内假设全存满的话顺序比链式存储更多)。
2、逻辑上相邻的节点物理上不必相邻。
3、插入、删除灵活 (不必移动节点,只要改变节点中的指针)。
4、查找结点时链式存储要比顺序存储慢。
5、每个结点是由数据域和指针域组成。
数组
数组的存储结构
一维数组
A[0...n-1]为例,存储关系
L是每个数组元素所占存储单元
多维数组
对于多维数组,有两种映射方法:按行优先和按列优先。
以二维数组为例,按行优先存储的基本思想是:先行后列。
先存储行号较小的元素,行号相等先存储列号较小的元素。
设二维数组行下标与列下标的范围分别为
,则存储结构关系式为:
若l1 l2均为0,则上式变成
矩阵压缩存储
先看行优先还是列优先,然后看要求的元素在上三角还是下三角,有时候要转化
压缩存储:指为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是为了节省存储空间。
特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的矩阵。常见的特殊矩阵有对称矩阵、上(下)三角矩阵、对角矩阵等。
对称矩阵
任意一个n阶方阵A的任意元素aij=aji,则称为一个对称矩阵。
对于n nn阶方阵,其中的元素可以划分为3个部分,即上三角区、主对角线和下三角区。
对称矩阵上下三角区元素相同,因此存放在一维数组
中,及aij存放在bk中,只存放主对角线和下三角区(或上三角区)
采用行优先存储,存放主对角线和下三角区
若数组下标从0开始,则在数组B中的下标为
因此元素下标对应关系

若存放主对角线和上三角区


三角矩阵
除了对角线外和上/下三角区,其余的元素都相同
压缩策略:按行优先原则,将三角区和主对角线元素存入一维数组,并在最后一个位置存放常量C
下三角:

上三角:

三对角矩阵

存储策略:只存储主对角线及其上、下两侧次对角线上的元素外,其他零元素一律不存储。
需要用一个一维数组B 来存储三对角矩阵中位于三对角线上的元素。同样要区分两种存储方式:即行优先方式和列优先方式。
行优先:
那么数组B中,位于Aij(i<=j),前边的元素个数为
于是有
对于i的求法,详细如下
稀疏矩阵
采用三元组表或者十字链表存储
顺序表、链表
用一组地址连续的存储单元依次存储线性表中的数据元素,逻辑上相邻的物理位置上也相邻
顺序表的实现
静态分配实现顺序表
#include <stdio.h>
#define MaxSize 10
//结构定义
Typedef struct {
ElemType data[MaxSize];
int length;
}SqList;
//实现顺序表
void InitList(Sqlist &L){
int i;
for(i=0;i<MaxSize;i++)
L.data[i]=0; //防止脏数据
L.length=0;
}
动态分配实现顺序表
#include <stdio.h> //用到malloc和free函数
#define InitSize 10
typedef struct{
int *data;//定义数据元素的类型为int型
int Maxsize;
int length;
}Seqlist
//初始化
void InitList(Seqlist &L){
L.data=(int *)malloc(InitSize*sizeof(int));
L.length=0;
L.MaxSize=InitSize;
}
//申请新的空间
void IncreaseSize(Seqlist &L, int len){
int *p=L.data;
L.data=(int *)malloc((L.MaxSize+len)sizeof(int)); //申请一块新空间
for(int i=0;i<l.length;i++){
L.data[i]=p[i]; //将数据复制到新区域
}
L.MaxSize=L.MaxSize+len;
free(p); //释放原来的内存空间,p也会被自动收回
}
单链表
单链表指线性表的链式存储,用一组任意的存储单元来存储数据元素;
而为了建立元素之间的线性关系,对每个链表结点,还要存放一个指向后继的指针;
头指针用以标识单链表,如果其值为 NULL,说明为一个空表;
在第一个结点前附加一个结点,成为头结点,可以不记录信息,也可以记录表长。设置头结点,便于空表与非空表的统一处理。
单链表的基本操作
建表:
头插法:将存有读入数据的新结点插入到当前链表表头,使用头插法会导致读入数据与生成链表顺序相反

//核心代码
s->next=head;
head=s;
尾插法:增加一个尾指针,以使新结点直接插入到表尾。

//核心代码
r->next=s;
r=r->next;
查找o(n):
按序号查找
按值查找
插入结点o(n):
前插操作

p=GetElem(L,i-1);//查找插入位置前驱
s->next=p->next;//1操作
p->next=s;//2操作
删除结点o(n)

p=GetElem(L,i-1);
q=p->next;
p->next=q->next;
free(q);
求表长o(n)
双链表
在单链表基础上增加前驱指针。

双链表的插入:

//1 2步必须在4步之前
//p的next应该最后改,否则找不到下一个元素
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
双链表的删除

p->next=q->next;
q->next->prior=p;
free(q);
循环链表
对于循环单链表,尾结点指针不是指向 NULL,而是头结点;
对于循环双链表,在循环单链表基础上,头结点的前驱指针指向尾结点

L->next==L;//判空
循环双链表

L->next==L;
L->prior==L;//判空
静态链表
用数组的方式实现的链表
#define MaxSize 10 //静态链表的最大长度
struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
};
typedef struct Node SLinkList[MaxSize];
初始化静态链表:
把a[0]的next设为-1
把其他结点的next设为一个特殊值用来表示结点空闲,如-2
查找:
从头结点出发挨个往后遍历结点
时间复杂度:O(n)
优点:增删操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找,容量固定不变
适用的场景:1不支持指针的低级语言2数据元素固定不变的场景
栈、队列
栈的数学性质:n个不同的元素进栈,出栈元素不同排序的个数为
称为卡特兰数
顺序栈
顺序栈:利用一组地址连续的存储单元存放自栈底到栈顶的元素,同时附设一个指针(top)指示当前栈顶元素的位置
typedef struct{
Elemtype data[MaxSize];
int top;
}SqStack
/*初始设置S.top=-1
栈空:S.top==-1 栈满:S.top==MaxSize-1 栈长:S.top+1

共享栈
第一个栈从数组头开始存储,第二个栈从数组尾开始,两个栈向中间拓展。
当top1+1 == top2或者top1 == top2-1时,整个存储空间被占满,发生上溢。
与普通栈一样,共享栈出栈入栈的时间复杂度仍为O(1)
顺序队列

/*q.front==q.rear==0;队空
front指向队头元素
rear指向队尾元素的下一个位置
进队:先入队,然后尾指针加1
出队:先取队头值,然后头指针加1
不能用q.front==MaxSize判断队满(假溢出)*/
循环队列
将存储队列的元素的表从逻辑上视为一个环,称为循环队列
队尾指针rear
- 若指向队尾元素的下一个位置(下次出队的元素) 则初始值置为0
- 若指向队尾元素(已经出队的元素,现在为空),则初始值置为n-1

/*初始:q.front=q.rear=0;
队首指针进1:q.front=(q.front+1)%MaxSize;
队尾指针进1:q.rear=(q.rear+1)%MaxSize;
队列长度:(q.rear-q.front+MaxSize)%MaxSize;
1、牺牲一个单元来区分队满和队空:队满条件(q.rear+1)%MaxSize==q.front;
2、类型中增设元素个数数据成员
队空:Q.size == 0;
队满:Q.size == MaxSize;
两者都有q.front==q.rear;
3、类型中增设tag数据成员
tag=0时,因删除导致rear==front 队空
tag=1时,因插入导致rear==front 队满
*/
栈的链式存储
没有头结点的单向链表:

队列的链式存储

/*
带头指针、尾指针的链表
q.front==NULL&&q.rear==NULL;队空
*/

双端队列
判断是否满足题设条件,代入验证即可。
栈和队列的应用
栈_括号匹配
栈_表达式求值
计算后缀表达式的值
- 顺序扫描表达式中的每一项
- 根据每一项的类型做出相应操作,如果是操作数则压入栈中,如果是操作符,则弹出栈中的两个操作数并把计算完成后的数重新压入栈中
- 当表达式所有项都扫描并处理完后,栈顶存放的就是最后的计算结果
中缀表达式转化为后缀表达式
-
从左向右扫描中缀表达式
-
遇到数字时,加入后缀表达式
-
遇到符号时
①若为'(',入栈②若为')',则依次把栈中运算符加入后缀表达式,直到出现'(',从栈中删除'('
③若为其他运算符,当其优先级高于栈顶运算符时,直接入栈;否则从栈顶开始依次弹出比当前处理运算符优先级高和优先级相当的运算符,直到一个比它优先级低或者遇到了一个左括号位置
栈_递归
讲递归算法转化成非递归算法,通常需要借助栈来实现这种转化
如斐波那契数列
int Fib(int n)
{
if(n==0)
return 0; //边界条件
else if (n==1);
return 1; //边界条件
else
return Fib(n-1)+Fib(n-2); //递归表达式
}
队列_层次遍历
层次遍历二叉树

- 根节点入队
- 若队空(所有结点已处理完毕),则结束遍历,否则重复3操作
- 队列中第一个结点出队,并访问,若其有左孩子,则将其左孩子入队,若其有右孩子,则将其右孩子入队。
队列_计算机系统
解决主机与外部设备之间不匹配的问题
设置一个打印数据缓冲区,主机把要打印输出的数据依次写入这个缓冲区,写满后就暂停输出,转去做其他事情,打印机就从缓冲区中按先进先出顺序依次取出数据打印
解决多用户引起的资源竞争
操作系统通常按照每个请求在时间上的先后顺序,把它们排成一个队列,每次把CPU分配给队首请求的用户使用
树
关于性质的计算、推导
树的性质
-
树中结点数等于所有结点的度数+1
每个结点的度数 = 孩子结点的个数,最后再加上根结点,就是树中的结点数了 -
度为m的树中,第i层的结点数至多为mi-1
\[\begin{align} &证明如下\\ &第 i 层上的结点数 = 第 i-1 层结点的度数*m\\ &也就是m*(i-1层结点数)=m*m*(i-2层结点数)=...=m^{i-1}*第一层结点数即1 \end{align} \] -
高度为h的m叉树至多有(mh-1)/(m-1)个结点
\[S=1*m*m^2*m^3*...*m^{h-1}=(m^h-1)/(m-1) \] -
高度为h的m叉树至少有h个结点;高度为h,度为m的树至少有h+m-1个结点
高度为h的m叉树不见得度为m,只是它强调每个结点的度最大为m,因此当它高度为m时允许每个结点度为h,这样串成一串下来,这时它的结点是最少的,此时除了叶子结点外每一个结点的度为1。
度为m的树则又多加了一个限制:至少有一个结点度为m,这时我们可以在高度为h的m叉树的基础上再多增加几个结点使其度数增加到m,由于高度是h已经限定死了,我们不能在叶子结点上增加,这样会导致h增加,因此我们只能再额外增加m-1个,使得某一个非叶子结点的度从1增加到m。 -
具有n个结点的m叉树的最小高度为⌈logm(n(m-1)+1)⌉
求最小高度也就是每层结点数最多时的高度,即该树是一棵完全m叉树,设其高度为h。\[n<=(m^h-1)/(m-1)\\ h>=log_m(n(m-1)+1)\\ 故h为⌈log_m(n(m-1)+1)⌉\\ 另外,实际上有 (m^{h-1}-1)/(m-1) +1≤ n ≤ (m^h-1)/(m-1),\\故最小高度h也可以为 ⌊log_m((n-1)(m-1)+1)⌋ + 1。 \] -
度为m的树和m叉树的区别
| 度为m的树 | m叉树 |
|---|---|
| 任意结点度数最多等于m | 任意结点度数最多等于m |
| 至少有一个结点的度等于m | 允许所有结点的度都小于m |
| 不允许为空树 | 可以为空树 |
二叉树的性质
-
非空二叉树上的叶子结点数等于度为2的结点数加1,即n0=n2+1
\[\begin{align} &设度为0,1,2的结点个数分别为n_0,n_1,n_2,设B为分支总数\\ &结点总数n=n_1+n_2+n_0\\ &分支总数B=n_1+2n_2=n+1\\ &n_1+n_2+n_0=n_1+2n_2+1\\ &n_0=n_2+1 \end{align} \] -
非空二叉树上第k层至多有2k-1个结点(k>=1)
-
高度为h的二叉树至多有2h-1个结点(h>=1)
-
对完全二叉树从上到下,从左到右依次编号0,1,2...n,左孩子为2i,右孩子为2i+1,父亲为
结点i所在的层次(深度)为
-
具有n个结点的完全二叉树的高度为
\[\lfloor log_2n \rfloor+1 或者\lceil log_2(n+1) \rceil \]\[\\ 计算: 2^{h-1}-1<n<=2^{h}-1或者2^{h-1}<=n<2^h \]
完全二叉树的叶子节点在最后两层
存储
二叉树的顺序存储
按完全二叉树进行存储编号

二叉树的链式存储
结构体
typedef struct BiTNode{
ElemType data;//数据域
struct BiTNode *lchild,*rchild;//左右孩子
}BiTNode,*BiTree;

在有n个元素的二叉链表中,含有n+1个空链域
多叉树、森林的存储
双亲表示法
用一组连续空间存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组的位置,根节点下标为0,伪指针域为-1.


孩子表示法
将每个结点的孩子节点都用单链表连接起来形成一个线性结构,n个结点就有n个孩子链表

孩子兄弟表示法
又称二叉树表示法,以二叉链表作为存储结构,每个结点包括三部分:结点值、指向结点的第一个孩子结点的指针,指向结点下一个兄弟结点的指针。


二叉树的遍历
先序遍历
中序遍历
后序遍历
层次遍历
先序和中序 后序和中序可唯一确定一颗二叉树 先序和后续不行
线索二叉树(算法)

typedef struct TBNode
{
char data;
int ltag,rtag;
struct TBNode *lchild;
struct TBNode *rchild;
}TBNode;
标识域:
- 如果ltag=0,表示指向节点的左孩子。如果ltag=1,则表示lchild为线索,指向节点的直接前驱
- 如果rtag=0,表示指向节点的右孩子。如果rtag=1,则表示rchild为线索,指向节点的直接后继
中序线索二叉树的构造
树、森林和二叉树的转化
将树转换成二叉树的步骤是:
(1)加线。就是在所有兄弟结点之间加一条连线;
(2)抹线。就是对树中的每个结点,只保留他与第一个孩子结点之间的连线,删除它与其它孩子结点之间的连线;
(3)旋转。就是以树的根结点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。
森林转换为二叉树
森林是由若干棵树组成,可以将森林中的每棵树的根结点看作是兄弟,由于每棵树都可以转换为二叉树,所以森林也可以转换为二叉树。
将森林转换为二叉树的步骤是:
(1)先把每棵树转换为二叉树;
(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子结点,用线连接起来。当所有的二叉树连接起来后得到的二叉树就是由森林转换得到的二叉树。
二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程,其步骤是:
(1)若某结点的左孩子结点存在,将左孩子结点的右孩子结点、右孩子结点的右孩子结点……都作为该结点的孩子结点,将该结点与这些右孩子结点用线连接起来;
(2)删除原二叉树中所有结点与其右孩子结点的连线;
(3)整理(1)和(2)两步得到的树,使之结构层次分明。
二叉树转换为森林
二叉树转换为森林比较简单,其步骤如下:
(1)先把每个结点与右孩子结点的连线删除,得到分离的二叉树;
(2)把分离后的每棵二叉树转换为树;
(3)整理第(2)步得到的树,使之规范,这样得到森林。
| 树 | 森林 | 二叉树 |
|---|---|---|
| 先根遍历 | 先序遍历 | 先序遍历 |
| 后根遍历 | 中序遍历 | 中序遍历 |
哈夫曼树
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
哈夫曼树又称最优树
哈夫曼树的构造:
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
要求手绘、文字描述建树过程
查找哈夫曼树(译码)
先统计各个结点出现的频率(权值)然后建树,左路经为0,右路径为1,进行编码
哈夫曼编码不会出现相同的前缀
并查集
将元素划分为互不相交的树,表示多个集合
基本操作:
查:从指定元素出发,一路向北找到根节点
并:两个集合合并为一个集合,让其中一个树的根节点连接到另一个树的根节点作为孩子节点
存储结构:双亲表示法

代码实现
初始化:
并、查:

并的时间复杂度o(1),查的时间复杂度为o(n)
优化:在每次union创建树时,小树合并进大树
用根节点的绝对值表示树的结点总数,小树合并进大树

该方法构建的树高不超过
优化后的Find时间复杂度为O(log2n)
进一步优化(压缩路径):Find操作,先找到根节点,再将查找路径上的所有节点都挂到根节点
下

可将Find函数的时间复杂度进一步降低
二叉排序树、平衡二叉树
二叉排序树(BST):
- 若它的左子树不为空,则左子树上所有结点的值均小于它根节点的值
- 若它的右子树不为空,则右子树上所有结点的值均大于它根节点的值
- 它的左右子树也分别为二叉排序树
查找、插入、构建、删除比较简单,不多赘述
平衡二叉树:
- 左右子树深度之差的绝对值不超过1;
- 左右子树仍然为平衡二叉树.
平衡二叉树的调整:
左旋:

- 当前操作节点是66 (66这个节点是最小失衡树的根节点)
- 断开该节点的右孩子连接线 (此时变成了两棵树,设以66为根节点的树为原根树,以77为根节点的树为新根树)
- 判断新根树的根节点的左子树是否为空
- 若空,直接把原根树作为新根树的左子树。
- 若不空:
- 将新根树的根节点的左子树独立出来,设其名为新原独树。
把新原独树作为原根树的右子树。
把原根树作为新根树的左子树。
- 将新根树的根节点的左子树独立出来,设其名为新原独树。
右旋:

- 当前操作节点是A (A这个节点是最小失衡树的根节点)
- 断开该节点的根节点的左孩子连接线 (此时变成了两棵树,设以A为根节点的树为原根树,以B为根节点的树为新根树)
- 判断新根树的根节点的右子树是否为空
- 若空,直接把原根树作为新根树的右子树。
- 若不空:
- 将新根树的根节点的右子树独立出来,设其名为新原独树。
把新原独树作为原根树的左子树。
把原根树作为新根树的右子树。
- 将新根树的根节点的右子树独立出来,设其名为新原独树。
四种调整方式
-
LL 对最小失衡根节点进行一次右旋
-
RR 对最小失衡根节点进行一次左旋
-
LR
![image-20221002213700896]()
对最小失衡树的根节点的左孩子(节点B)实行一次左旋,得到下图,
![image-20221002213723439]()
再对最小失衡树的根节点(节点A)(!!!当前的最小失衡树概念基于最初的树,不是已经经过左旋的树!!!)实行一次右旋,成功恢复平衡。
![image-20221002213841904]()
-
RL
![image-20221002213925132]()
对最小失衡树的根节点的右孩子(节点C)实行一次右旋,得到下图
![image-20221002214004994]()
再对最小失衡树的根节点((节点A)(!!!当前的最小失衡树概念基于最初的树,不是已经经过右旋的树!!!))实行一次左旋,成功恢复平衡。
![image-20221002214055092]()
图

图的一些概念
图(Graph):
由顶点的有穷非空集合和顶点之间边的集合组成。
通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
注意:线性表和树中可以没有元素;而在图中可以没有边,但是不允许没有顶点,。
网:
带有权重的图
环:
第一个顶点和最后一个顶点相同的路径;
无向边:
若顶点Vi和Vj之间的边没有方向,称这条边为无向边(Edge),用(Vi,Vj)来表示。
有向边/弧(Arc):
若从顶点Vi到Vj的边有方向,称这条边为有向边。
用<Vi, Vj>来表示,其中Vi称为弧尾(Tail),Vj称为弧头(Head)。
权(Weight):
表示从图中一个顶点到另一个顶点的距离。
度:
与顶点相连接的边数;
出度、入度:
有向图中的概念,出度表示以此顶点为起点的边的数目,入度表示以此顶点为终点的边的数目;
图的分类
按边的类型分类:
- 无向图(Undirected graphs): 图中任意两个顶点的边都是无向边。
- 有向图(Directed graphs): 图中任意两个顶点的边都是有向边。
按边的数量分类:
稀疏图与稠密图:
有很少条边或弧的图称为稀疏图,反之称为稠密图。
按有无环分类:
- 简单图: 不存在自环(顶点到其自身的边)和重边(完全相同的边)的图
- 简单环: 除去第一个顶点和最后一个顶点后没有重复顶点的环;
其他图的分类:
- 无向完全图: 无向图中,任意两个顶点之间都存在边。
- 有向完全图:有向图中,任意两个顶点之间都存在方向相反的两条弧。
- 连通图:任意两个顶点都相互连通的图;
极大连通子图:
包含竟可能多的顶点(必须是连通的),即找不到另外一个顶点,使得此顶点能够连接到此极大连通子图的任意一个顶点;
连通分量:
极大连通子图的数量;
强连通图:
此为有向图的概念,表示任意两个顶点a,b,使得a能够连接到b,b也能连接到a 的图;
性质:有 N 个顶点的强连通图,至少有 N 条边,最多有 N(N - 1) 条边。
强连通分量:
有向图中的极大强连通分量
存储结构
邻接矩阵
图的邻接矩阵表示法,又称数组表示法。它采用两个数组来表示图:
- 用于存储顶点信息的一维数组;
- 用于存储图中顶点之间关联关系的二维数组,称为邻接矩阵。
无权图的邻接矩阵:

带权图的邻接矩阵

邻接表
邻接表表示法是图的一种链式存储结构,基本思想是只存储图中存在的边的信息。
在邻接表中,对图的每个顶点建立一个带头结点的边表,每个边表的头结点又构成一个表头结点表。



多重邻接表、十字链表
十字链表:十字链表可以看成是将有向图的邻接表和逆邻接表结合起来的一种链表。
有向图中的每一个顶点在十字链表中对应有一个结点,称为顶点结点;
有向图中的每一条弧在十字链表中对应有一个结点,称为弧结点。


邻接多重表
用于存储无向图

顶点结构定义
- data:数据域,存放顶点数据
- firstedge:第一条边的指针边
边表节点结构定义:
- ivex:边的两个顶点其中一个顶点 i 的序号
- ilink:边的两个顶点其中一个顶点 i 的相连的下一条边
- jvex:边的两个顶点其中一个顶点 j 的序号
- -jlink:边的两个顶点其中一个顶点 j 的相连的下一条边
- -info:信息域,可以存放边的信息
- -mark:标记域,可以存放标记信息

最小生成树
树:无环连通图。
性质:N 个结点的树,有 N - 1条边。
一个连通图的生成树是指一个极小连通子图,它含有图中的全部顶点,但只有足以构成一棵树的 n-1 条边。
在一个连通网的所有生成树中,各边代价之和最小的那棵生成树称为该连通网 最小代价生成树(MST),简称为 最小生成树。
利用 普里姆(Prim)算法和 克鲁斯卡尔(Kruskal)算法可以生成一个连通网的最小代价生成树。
普里姆算法
基本步骤:(选点)
假设N=(V,{E})是连通网,,TE为最小代价生成树中边的集合。
① 初始U={u0}(u0∈V),TE=空集;
② 在所有u∈U,v∈V-U的边中选一条代价最小的边(u0,v0)并入集合TE,同时将v0并入U;
③ 重复②,直到U=V为止。
即每次选择某一顶点的权值最小的边。

克鲁斯卡尔算法
判断最小生成树唯一
求MST是否唯一
1.对图中的每条边,是否有相同权值的边。如果有进行标记。
2.求MST
3.求MST之后,如果MST没有包含标记的边,则唯一,如果含有,依次删除标记的边,再求MST,看求得MST权值与原值相同则不唯一。
基本步骤:(选边)
假设N=(V,{E})是连通网,将N中的边按权值从小到大的顺序排列。
① 将n个顶点看成n个集合。
② 按权值从小到大的顺序选择边,所选边应满足两个顶点不在同一个顶点集合内,将该边放到生成树边的集合中,同时将该边的两个顶点所在的顶点集合合并。
③ 重复②直到所有的顶点都在同一顶点集合内。

① 将待选的边按权值从小到大的顺序排列,得:(B,C),5;(B,D),6;(C,D),6;(B,F),11;(D,F),14;(A,B),16;(D,E),18;(A,E),19;(A,F),21;(E,F),33。
顶点集合状态:{A},{B},{C},{D},{E},{F}。
最小生成树的边的集合:{ }。
② 从待选边中选一条权值最小的边:(B,C),5。
顶点集合状态变为:{A},{B,C},{D},{E},{F}。
最小生成树的边的集合:{(B,C)}。
③ 从待选边中选一条权值最小的边:(B,D),6。
顶点集合状态变为:{A},{B,C,D},{E},{F}。
最小生成树的边的集合:{(B,C),(B,D)}。
④ 从待选边中选一条权值最小的边:(C,D),6,由于C,D在同一顶点集合{B,C,D}内,故放弃,重新从待选边中选一条权值最小的边:(B,F),11。
顶点集合状态变为:{A},{B,C,D,F},{E}。
最小生成树的边的集合:{(B,C),(B,D),(B,F)}。
⑤ 从待选边中选一条权值最小的边:(D,F),14,由于D,F在同一顶点集合{B,C,D,F}内,故放弃,重新从待选边中选一条权值最小的边:(A,B),16。
顶点集合状态变为:{A,B,C,D,F},{E}。
最小生成树的边的集合:{(B,C),(B,D),(B,F),(A,B)}。
⑥ 从待选边中选一条权值最小的边:(D,E),18。
顶点集合状态变为:{A,B,C,D,F,E}。
最小生成树的边的集合:{(B,C),(B,D),(B,F),(A,B),(D,E)}。
至此,所有顶点都在同一顶点集合{A,B,C,D,F,E}中,算法结束,最小生成树构造完毕,最小代价为5+6+11+16+18=56。
最短路径
两顶点之间权值之和最小的路径。无权图相当于每条边的权值都是1。
不能有负权环。
迪杰斯特拉(Dijkstra)算法
求从某个源点到其余各点的最短路径 — Dijkstra算法
单源最短路径算法,计算一个顶点到其它所有顶点的路径。不能有负权边
时间复杂度O(ElogV),E边数量,V节点数量,
过程:用带权的邻接矩阵表示有向图, 对Prim算法略加改动就成了Dijkstra算法
- 初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。
- 从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
- 以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
- 重复步骤b和c直到所有顶点都包含在S中。
弗洛伊德(Floyd)算法
每一对顶点之间的最短路径 — Floyd算法
Floyd-Warshall算法的时间复杂度为O(V3)
基本思想:
- 从vi到vj的所有可能存在的路径中,选出一条长度最短的路径。
- 若<vi,vj>存在,则存在路径{vi,vj};
- 若<vi,v1>,<v1,vj>存在,则存在路径{vi,v1,vj};
- 若{vi,…,v2}, {v2,…,vj}存在,则存在一条路径{vi, …, v2, …vj};
- 依次类推,则vi至vj的最短路径应是上述这些路径中,路径长度最小者。
具体做法为:
- 第一步,让所有边上加入中间顶点0,取A[i][j]与A[i][0]+A[0][j]中较小的值作A[i][j]的值,完成后得到A(0)
- 第二步,让所有边上加入中间顶点1,取A[i][j]与A[i][1]+A[1][j]中较小的值,完成后得到A(1)…
- 如此进行下去,当第n步完成后,得到A(n-1),A(n-1)即为我们所求结果,A(n-1)[i][j]表示顶点i到顶点j的最短距离。

拓补排序
AOV网络:有向图,用顶点表示活动,用弧表示活动的先后顺序
AOE网络:有向图,用顶点表示事件,用弧表示活动,用权值表示活动消耗时间
定义:给出有向图G=(V,E), 对于V中的顶点的线性序列(v1,v2,...,vn), 如果满足如下条件: 若在G中从顶点 vi 到vj有一条路经, 则在序列中顶点vi必在顶点vj之前; 则称该序列为G的一个拓扑序列。
构造有向图的一个拓扑序列的过程称为拓扑排序。
实际意义:如果按照拓扑序列中的顶点次序进行每一项活动,就能够保证在开始每一项活动时,他的所有前驱活动均已完成,从而使整个工程顺序执行。
说明:
在AOV网中, 若不存在回路, 则所有活动可排成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,那么该序列为拓扑序列。
拓扑排序列不是唯一的。
拓扑排序方法:
在AOV网中选一个入度为0的顶点( 没有前驱) 且输出之;
从AOV网中删除此顶点及该顶点发出来的所有有向边;
重复(1)、(2)两步, 直到AOV网中所有顶点都被输出或网中不存在入度为0的顶点。

关键路径
事件的最早发生时间ve[k]:事件的最早发生时间ve[k]是指从源点开始到顶点vk的最大路径长度。这个长度决定了从顶点vk发出的活动能够开工的最早时间。
事件的最晚发生时间vl[k]:,事件的最晚发生时间vl[k]是指在不推迟整个工期的前提下,事件vk允许的最晚发生时间。
活动的最早开始时间e[i]:若活动ai是由弧<vk , vj>表示,则活动ai的最早开始时间应等于事件vk的最早发生时间。因此,有:e[i]=ve[k]
活动的最晚开始时间l[i]:活动ai的最晚开始时间是指,在不推迟整个工期的前提下, ai必须开始的最晚时间。若ai由弧<vk , vj>表示,则ai的最晚开始时间要保证事件vj的最晚发生时间不拖后。因此,有:l[i]=vl[j]-weight<vk , vj>
关键路径:在AOE网中,从始点到终点具有最大路径长度(该路径上的各个活动所持续的时间之和)的路径称为关键路径。
关键活动:关键路径上的活动称为关键活动。关键活动:e[ ai]=l[ ai]的活动
对AOE网有待研究的问题是:
(1)完成整个工程至少需要多少时间?
完成整个工程的时间即求解出AOE网由源点到汇点的最长路径长度。
(2)那些活动是影响工程进度的关键?
关键活动的延期必然导致整个工期的推迟。也就是说处于关键路径上的关键活动是没有松弛时间的。而处于非关键路径上的活动可以有适当的松弛时间。
求解过程
-
输入e条弧<vk , vj>,建立AOE网的存储结构。
-
从源点v1出发,令ve[1]=0。按拓扑有序序列次序求其余各顶点的最早发生时间ve[k](2<=k<=n),ve[k]=max{ve[j]+weight(<vj , vk>)}。如果得到的拓扑有序序列中顶点个数小于网中顶点的个数n,说明网中存在环路,不能求关键路径算法终止,否则执行步骤(3)。
-
从汇点vn出发,令vl[n]=ve[n],按逆拓扑有序序列求其余各顶点的最晚发生时间vl[k](n-1>=k>=1),vl[k]=min{vl[vj]-weight(<vk , vj>)}。
-
根据各顶点的ve值和vl值,求每条弧的最早开始时间e[ai]。e[ai]等于弧ai的弧尾顶点vk的最早发生时间ve[k]。
-
根据各顶点的ve值和vl值计算每条弧的最晚开始时间l[ai]。l[ai]等于弧头顶点vk的最晚发生时间减去弧ai的权值。
-
若某条弧ai满足e[ai]=l[ai]则为关键活动,由所有关键活动构成的网的一条或几条关键路径。
实例: https://blog.csdn.net/muziyang555/article/details/105218843
查找
查找的基本概念
查找:在数据集合中寻找满足某种条件的数据元素的过程称为查找
查找表: 相同类型的数据元素(对象)组成的集合,每个元素通常由若干数据项构成。
关键字(Key): 数据元素中某个(或几个)数据项的值,它可以标识一个数据元素。若关键字能唯一标识一个数据元素,则关键字称为主关键字;将能标识若干个数据元素的关键字称为次关键字。
查找/检索: 根据给定的K值,在查找表中确定一个关键字等于给定值的记录或数据元素。
平均查找长度:在查找的过程中,一次查找的长度是指需要比较的关键字的次数。平均查找长度则是所有查找过程中进行关键字的比较的次数的平均值。

如何判断查找算法的优劣性:查找运算时间主要花费在关键字比较上,通常把查找过程中执行的关键字平均比较个数(也称为平均查找长度ASL)作为衡量一个查找算法效率优劣的标准。
顺序查找法
它的查找过程是:从第一个(或者最后一个)记录开始,逐个进行记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功。如果查找了所有的记录仍然找不到与给定值相等的关键字,则查找不成功。
下面是顺序表
int Search_Seq(SSTable ST, int key){
//在顺序表ST中顺序查找其关键字等于key的数据元素。若找到,则函数值为
//该元素在表中的位置,否则为0
for (int i=ST.length; i>=1; --i)
if (ST.R[i].key==key) return i; //从后往前找
return 0;
}// Search_Seq
顺序查找时间复杂度:O(n)
带监视哨的顺序查找:
/*具体实现就是将数组的第0位置空,在查找时将要查找的key插入作为监视哨。
这样的好处是不用每次循环都检查查找是否结束,减少了元素比较次数,最后的返回值要么是元素下标要么是数组第0位(这种情况就是到了监视哨)。*/
int Search_Seq(SSTable ST, int key){
//在顺序表ST中顺序查找其关键字等于key的数据元素。若找到,则函数值为
//该元素在表中的位置,否则为0
ST.R[0].key = key; //“哨兵”
for(int i = ST.length; ST.R[i].key!=key; --i) ; //从后往前找
return i;
}// Search_Seq
分块查找法
分块查找,又称索引顺序查找,性能介于顺序查找和折半查找之间。
需建立一个 索引表 。

索引表包括两项内容: 关键字项(其值为该子表内的最大关键字)和指针项(指示该子表的第一个记录在表中的位置)。
索引表按关键字有序,则表或者有序或者分块有序。
“分块有序”指第二个子表中所有记录的关键字均大于第一个子表中的最大关键字,第三个子表中的所有记录的关键字均大于第二个子表中的最大关键字,......,依次类推。
因此,分块查找分两步:
1) 确定待查记录所在块(将key与索引表中值比较,可用顺序查找,也可用折半查找)
2) 在块中顺序查找
算法分析:
优点:在表中插入、删除元素时,只要找到该元素对应块即可,由于块内是无序的,故不需要大量移动,插入、删除较容易(相比于折半查找的有序)。
如果线性表既要快速查找又经常动态变化,可采用分块查找。 (?在该块插入后,块后面的块内容需要移动吧?)
缺点:增加索引表存储空间,并对初始索引表进行排序运算。
折半查找法
折半查找又称为二分查找,折半查找的作用对象是有序的查找表,也就是说,我们的查找表是已经排好序的。之所以称为折半查找,是因为在每次关键字比较时,如果不匹配,则根据匹配结果将查找表一份为二,排除没有关键子的那一半,然后在含有关键字的那一半中继续折半查找。

int Search_Bin(SSTable ST,int key) {
// 在有序表ST中折半查找其关键字等于key的数据元素。若找到,则函数值为
// 该元素在表中的位置,否则为0
int low=1,high=ST.length; //置查找区间初值
int mid;
while(low<=high) {
mid=(low+high) / 2;
if (key==ST.R[mid].key) return mid; //找到待查元素
else if (key<ST.R[mid].key) high = mid -1; //继续在前一子表进行查找
else low =mid +1; //继续在后一子表进行查找
}//while
return 0; //表中不存在待查元素
}// Search_Bin
特别需要注意:折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
折半查找时间复杂度是O(logn)
树形查找
二叉树搜索树
二叉排序树,又称二叉查找树,是一种对排序和查找都很有用的特殊二叉树
定义
二叉排序树或者是一棵空树,或者具有以下定义:
1)若左子树不为空,左子树上所有结点值均小于根结点值;
2)若右子树不为空,右子树上所有结点值均大于根结点值;
3)左右子树也分别为二叉排序树。
递归定义。有定义可得性质:中序遍历二叉树可得到结点递增的有序序列。
查找
模仿折半查找易得非递归查找算法,以下给出递归形式
BSTree searchBST(BSTree T, KeyType key) {
if (!T || T.data == key) return T; // 找到则返回T,找不到则返回空
else if (T.data < key) return SearchBST(T.right, key);
else return SearchBST(T.left, key);
}
算法分析:
二叉排序树上的查找和折半查找相差不大。但二叉排序树可更好地维护表的有序性,无需移动记录,只需移动指针即可完成插入和删除操作。
因此,对需要经常进行插入、删除和查找运算的表,采用二叉排序树比较好。
插入
当树中不存在关键字等于key的结点时才进行插入。
新插入的结点一定是一个新添加的叶子结点(?一定是叶子结点?),且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子节点。
void insertBST(BSTree T, ElementType e) {
if (!T) {
S = new BSTNode;
S.data = e;
S.lchild = S.rchild = NULL;
T = S;
}
else if (T.data > e.data) insertBST(T.lchild, e);
else if (T.data < e.data) insertBST(T.rchild, e);
}
平衡二叉树
对于二叉排序树,树的高度越小,查找速度越快,因此,希望二叉树的高度尽可能小。
平衡二叉树是一种特殊类型的二叉排序树。
平衡二叉树或者是空树,或者是具有如下特征的二叉排序树:
1)左子树和右子树的深度之差的绝对值不超过1;
2)左子树和右子树也是平衡二叉树。
具体见树的应用一节
红黑树
B树、B+树
散列(Hash)表
散列表:关键字和存储地址具有直接关系
同义词:不同的关键字通过散列函数映射到同一个值
冲突:通过散列函数确定的位置已经存放元素
装填因子:表中记录数/散列表长度(装填因子越大,发生冲突概率越高,散列表查找效率越低)
哈希函数构造方法:
目的:尽可能减少冲突
1.除留余数法:H(key) = key % p
取一个不大于 m 但最接近 m 或等于 m 的质数
2.直接定址法:H(key) = key 或者 H(key) = a - key
适用于关键字基本连续(不会产生冲突),例如学号
3.数字分析法:若不同数码出现概率不同,选择数码分布较为均与的若干位作为散列地址
适用于已知的关键字集合,例如手机号的存储,选择后几位作为散列地址
4.平方取中法:计算关键字的平方,并取其中间几位数字作为散列函数
处理冲突的方法
1.开放定址法:散列表中的空闲地址既能存放同义词,也能存放非同义词
d为增量序列
设表长为16,在第一次计算散列函数时,仅有可能存放在[0,12]的位置上,[13,15]只有通过同义词冲突时才有可能被存放
①线性探测法:发生冲突的时,依次查找下一个位置是否为空,d为0、1、2、3...
查找失败时,查找数组中的空位置算一次查找
删除元素时,不能简单的物理上删除元素,而是作删除标记,逻辑上删除(副作用:在查找的时候,可能会因逐一遍历逻辑删除的结点而浪费时间)
查找效率分析:
A查找成功:每个查找元素从初始散列函数计算得到的位置移动到的目标元素的移动次数相加 / 表中总元素个数
B查找失败:每个查找元素从初始散列函数计算得到的位置移动到第一个空元素的移动次数相加 / 第一次散列函数可能取值的个数
缺点:容易造成同义词、非同义词聚集的现象,从而严重影响查找效率
②平方探测法:d为0的平方,1的平方,-1的平方,2的平方,-2的平方...
散列表长度必须是一个可以表示成4k+3的素数,又称二次探测法
可以避免堆积现象,缺点是不能探测到散列表所有单元,但至少能探测一半单元
③伪随机法:d为伪随机数列
④再散列法:第一个散列函数冲突时,使用第二个散列函数进行再散列
最多经过m-1次探测,就会回到初始位置
2.拉链法:所有的发生冲突的同义词用链表存储,适用于经常增、删的情况
依次查找每个结点上挂着的指针的个数 / 第一次散列函数可能取值的个数(即0 - 12)
查找失败的时候空指针不算一次查找,即记作0

字符串模式匹配
首先要了解 最长相等前后缀,比如串ababa,它的最长相等前后缀就是aba。
手算next数组:
例如:求串 a b c a c 的next数组,首先画一个表格
| 编号 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 串S | a | b | c | a | c |
| 部分匹配值 | 0 | 0 | 0 | 1 | 0 |
部分匹配值就是 最长相等前后缀的长度
next数组就是把所有的PM的值向右移一位,第一位补-1,也就是
-1 0 0 0 1
再加1
[next] 0 1 1 1 2
这里需要注意的一个点是:next数组可以从-1开始,也可以从0开始(两个都对)。如果题目要求从0开始,那么直接对刚刚求得的-1开始的next数组每个数都+1就OK。
如果题目给的字符串的下标i是从0开始的,那么next数组就是从-1开始的;如果字符串下标是从1开始的,那么next数组就是从0开始的。比如本题,字符串编号是从1开始的,那么最后算出来的next数组应该从0开始。
手算nextval数组:
在next数组的基础上手算nextval数组:
| 编号 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| S | a | b | c | a | c |
| next | 0 | 1 | 1 | 1 | 2 |
| nextval | 0 | 1 | 1 | 0 | 2 |
首先编号为1的字符的nextval值是0,这是规定的。
然后算编号为2的字符的nextval值,先看其next值,为1。那么,找到编号为1的字符,为a。比较a和我们当前算的编号为2的字符b,两者不同 ,直接把编号为2的 next的值落到下面 的nextval
再算编号为3的,和上面同理,先看next的值是1,找编号1对应的字符,即a。还是和当前字符c不同,直接把 next的值落到下面
再算编号为4的,看它的next值是1,找编号1对应的字符,即a。和当前字符相同了,那么直接把编号为1的 nextval复制 到当前nextval。
最后算编号5的nextval,和上面方法同理,为2.
总结一条口诀,相同用底下,不同用上面。
查找算法的分析及应用
排序
排序的基本概念
#define MAXSIZE 20//设记录不超过20个
typedef int KeyType;//设关键字为整型量(int)
typedef struct{
KeyType key;//关键字
InfoType otherinfo;//其他数据项
}RedType;
typedef struct{
RedType r[MAXSIZE + 1];//存储顺序表的向量
int length;//
}
直接插入排序
/*基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
有序插入:在有序序列中插入一个元素,保持序列有序,有序长度不断增加
*/
void InsertSort(SqList &L){
int i, j;
for(j = 2; i < L.length; ++i){
if(L.r[i].key < L.r[i-1].key){//若<,需要将L.r[i]插入有序子表
L.r[0] = L.r[i];//复制为哨兵
for(j = i - 1; L.r[0].key < L.r[j].key;--j){
L.r[j+1] = L.r[j];//记录后移
}
L.r[j+1] = L.r[0];//插入到正确位置
}
}
}
折半插入排序
void BInsertSort(SqList &L){
for(int i = 2; i < L.length; ++i){//依次插入第2~n个元素
L.r[0] = L.r[i];//当前插入元素存到哨兵位置
int low = 1;
int high = i - 1;//采用二分查找法查找插入位置
while(low <= high){
int mid = (low + high)/2;
if(L.r[0].key < L.r[mid].key) high = mid - 1;
else low = mid + 1;
}//循环结束,high+1则为插入位置
for(j = i - 1; j >= high + 1; j--) L.r[j+1] = L.r[j];//移动元素
L.r[high+1] = L.r[0];
}
}
起泡排序
/*基本思想:每趟不断将记录两两比较,并按“前小后大的”规则交换*/
void bubble_sort(SqList &L){
int m, i, j;
RedType x;//交换时临时存储
for(m = 1; m < n - 1; m++){
for(j = 1; j < n - m; j++){
if(L.r[j].key > L.r[j + 1].key){
x = L.r[j];
L.r[j] = L.r[j + 1];
L.r[j+1] = x;
}
}
}
}
优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素
/*改进的冒泡排序*/
//改进的冒泡排序算法
void bubble_sort(SqList &L){
int m, i, j;
RedType x;//flag作为是否有交换的标记
for(m = 1; m < n -1 && flag == 1; m++){
flag = 0;
for(j = 1; j <= n-m; j++){
if(L.r[j].key > L.r[j+1].key){
flag = 1;//发生交换。flag置为1,若本堂没发生交换,flag 保持0
x = L.r[j];
L.r[j] = L.r[j+1];
L.r[j+1] = x;
}
}
}
}
简单选择排序
/*
基本思想:再待排序的数据中选出最大(小)的元素放在其最终的位置
基本操作:
首先通过n-1此关键字比较,从n个记录中找出关键字最小的记录,讲它与第一个记录交换
再通过n-2次比较,从剩余的n-1个记录中找出关键字次笑的记录,讲它与第二个记录交换
重复上述步骤,共进行n-1躺排序后,排序结束
*/
void SelectSort(SqList &L){
for(int i = 1; i < L.length;++i){
int k = i;
for(int j = i+1; j < L.length; j++){
if(L.r[j].key < L.r[k].key) k = j;//记录最小的位置
}
if(k!=i) L.r[i] = L.r[k];//交换
}
}
简单选择排序是不稳定排序
希尔排序
/*基本思想:先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。*/
void ShellSort(SqList &L, int dlta[], int t){
//按增量序列dlta[0,……,t-1]对顺序表L作希尔排序
for(int k = 0; k < t; ++k){
ShellInsert(L,dlta[k]);//一趟增量为dlta[k]的插入排序
}
}
void ShellInsert(SqList &L, int dk){
for(int i = dk+1; i = L.length;i++){
if(r[i].key < r[i-dk].key){
r[0] = r[i];
for(j = dk; j > 0&& r[0].key < r[j].key; j= j-dk)
r[j+dk] = r[j];
r[j+dk] = r[0];
}
}
}
希尔排序是不稳定的排序
快速排序
/*
基本思想:任取一个元素为中心,所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表。对各个子表重新选择中心元素并依次规则调整,知道每个子表的元素只剩一个。
每一趟的子表的形成是采用哦才能够两头向中间交替式逼近法
由于每趟中对各子表的操作都相似,可采用递归算法
*/
void Qsort(SqList &L, int low, int high){
if(low < high){
pivotloc = Partition(L,low, high);
//讲L.r[low...high]一分为二,pivotloc为枢轴元素排好序的位置
Qsort(L,low, pivotloc-1);//对低子表递归排序
Qsort(L,pivotloc + 1, high);
}
}
int Partition(SqList &L, int low, int high){
L.r[0] = L.r[low];
pivotkey = L.r[low].key;
while(low < high){
while(low < high && L.r[high].key >= pivotkey) --high;
L.r[low] = L.r[high];
while(low < high && L.r[low].key <= pivotkey) ++low;
L.r[high] = L.r[low];
}
L.r[low] = L.r[0];
return low;
}
//自己写的
int locat(vector<int>& nums,int low,int high)
{
int key=nums[low];
while(low<high)
{
while(nums[high]>=key&&low<high) high--;
nums[low]=nums[high];
while(nums[low]<=key&&low<high) low++;
nums[high]=nums[low];
}
nums[low]=key;
return low;
}
void Qsort(vector<int>& nums, int low,int high)
{
if(low<high)
{
int adkey = locat(nums,low,high);
Qsort(nums,low,adkey-1);
Qsort(nums,adkey+1,high);
}
}
堆排序
/*堆排序:若再输出堆顶的最小值(最大值)后,使得剩余N-1个元素的序列重新又建成一个堆,则得到n个元素的次小值(次大值)……如此反复,便能得到一个有序序列,这个过程称之为堆排序
如何在输出堆顶元素后,调整为一个新的堆
小根堆:
输出堆顶元素之后,以堆中最后一个元素替代之;
然后将根结点值与左右子树的根结点值进行比较,并与其中小值进行交换
重复上述操作,直至叶子结点,将得到新的堆,成这个从堆顶至叶子调整过程为“筛选”
*/
void HeapAdjust(elem R[],int s, int m){
//已知R[S..m]中记录的关键字除R[s]之外均满足堆的定义,本函数调整R[s]的关键字,使R[s..m]成为一个大根堆
rc = R[s];
for(j = 2 * s; j <= m; j*=2){
if(j < m && R[j] < R[j + 1]) ++j;//j为key较大的记录的下标
if(rc > R[j]) break;
R[s] = R[j]; s= j;//rc应插入在位置s上
}
R[s] = rc;
}
二路归并排序
基本思想:将两个或两个以上的有序序列归并为一个有序序列
基数排序
(1)通过键值得各个位的值,将要排序的元素分配至一些桶中,达到排序的作用
(2)基数排序法是属于稳定性的排序,基数排序法是效率高的稳定排序法
(3)基数排序是桶排序的扩展
实现原理:
将所有待比较数值(自然数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
步骤:
(1)确定数组中的最大元素有几位(MAX)(确定执行的轮数)
(2)创建0-9个桶(桶的底层是队列),因为所有的数字元素都是由0-9的十个数字组成
(3)依次判断每个元素的个位,十位至MAX位,存入对应的桶中,出队,存入原数组;直至MAX轮结束输出数组。
(4)具体实现步骤如下图

外部排序
在内存中进行的排序是内部排序,而在许多应用中,经常需要对大文件进行排序,因为文件中的记录很多、信息量庞大,无法将整个文件复制进内存中进行排序。因此,需要将待排序的记录存储在外存中,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间地交换。这种排序方法就称为外部排序。
- 外部排序指待排序文件较大,内存中一次性放不下,需存放在外存地文件地排序。
- 为减少平衡归并中外存读写次数所采取地方法:增大归并路数和减少归并段个数
- 利用败者树增大归并路数
- 利用置换-选择排序增大归并段长度来减少归并段个数
- 由长度不等地归并段,进行多路平衡归并,需要构造最佳归并树
文件通常是按块存储在磁盘上的,操作系统也是按块对磁盘上的信息进行读写的。因为磁盘读 / 写的机械动作所需的时间远远超过内存运算的时间(相对而言可以忽略不记),因此在外部排序过程中的时间代价主要考虑访问磁盘的次数,即I/O次数。
外部排序通常采用归并排序法。它包括两个相对独立的阶段:
- 根据内存缓冲区大小,将外存上的文件分成若干长度为t的子文件,依次读入内存并利用内部排序方法对他们进行排序,并将排序后得到的有序子文件重新写回外存,称这些有序子文件为归并段或顺串。
- 对这些归并段进行逐趟归并,是归并段逐渐由小到大,直至得到整个有序文件位置。
在外部排序中实现两两归并时,由于不可能将两个有序段及归并结果段同时存放在内存中,因此需要不停地将数据读出、写入磁盘,而这会耗费大量的时间。一般情况下:
外部排序的总时间 = 内存排序所需的时间 + 外存信息读取的时间 + 内部归并所需的时间
显然,外存信息读取地时间远大于内部排序和内部归并地的时间,因此应着力减少I/O次数。由于外存信息的读/写是以“磁盘块”为单位进行的
因此增大归并路数可以减少归并趟数,进而减少总的磁盘I/O次数。




优化:多路归并 减少归并的趟数



一般的,对r个初始归并段,做K路平衡归并。
K路平衡归并:
最多只能有k个段归并为一个
k叉树 ,第一趟可将r个初始归并段归并为[r/k],以后每趟归并并将m个归并段归并成[m/k]个归并段,直至最后形成一个大的归并段位置。
树的高度 = [logkr]=归并趟数S。可见,只要增大归并路数k,或减少初始归并段个数r,都能减少归并趟数S,进而减少读写磁盘的次数,达到提高外部排序速度的目的。

优化:减少归并段的数量


多路平衡归并


败者树


对于k路归并,第一次构造败者树需要对比关键字K-1次

有了败者树,选出最小元素,只需要对比关键字
除了胜者,对比次数为树高h-1


置换-选择排序(生成初始归并段)
构建一个比内存更大的初始归并段





第一个归并段完成

第二个归并段完成

最佳归并树

构造哈夫曼树



如果减少一个归并段

错误的!!!
添加虚段



排序算法的分析和应用

快(排)些(希)以nlog2n的时间归(并)队(堆)

从平均情况看:堆排序、归并排序、快速排序胜过希尔排序。
从最好情况看:冒泡排序和直接插入排序更胜一筹。
从最差情况看:堆排序和归并排序强过快速排序。、
关键字位置问题
直接插入排序、折半插入排序:在最后一趟排序前,没有一个关键字到达其最终位置
快速排序:每一趟排序后有一个关键字到达最终位置
归并排序:在一次排序结束后不一定能选出一个关键字放到其最终位置上
希尔排序:不能保证每趟排序至少能将一个关键字放到其最终位置上
适用情况
直接插入排序、冒泡排序:适用于序列基本有序的情况
快速排序:待排序列越接近无序,算法效率越高
简单选择排序、归并排序:执行次数与初试序列无关
堆排序:适合关键字数很多的情况
基数排序:适合序列中关键字很多,但组成关键字的取值范围较小的情况







浙公网安备 33010602011771号