第1章 绪论
1.1 数据结构基本概念
1.1.1 基本概念和术语
- 数据:信息的载体
- 数据元素:数据的基本单位,可由若干数据项组成
如:学生记录是一个数据元素,由学号、姓名等数据项组成
- 数据项:构成数据元素的不可分割的最小单位
- 数据对象:具有相同性质的数据元素的集合,是数据的一个子集
- 数据结构:相互之间存在一种或多种特定关系是数据元素的集合。数据元素相互之间的关系称为结构
- 抽象数据类型(ADT):定义一个ADT就是定义了数据的逻辑结构、数据的运算,也就是定义了一个数据结构
- 数据结构三要素:逻辑结构、存储结构、数据的运算
- 一个算法的设计取决于所选定的逻辑结构,二算法的实现依赖于所用的存储结构
严书拓展
- 数据类型:一个值的集合和定义在这个值集上的一组操作的总称
按"值"的不同特性分为:
- 非结构的原子类型:C语言中的基本类型(int、char、枚举等)、指针类型、空类型
- 结构类型:数组等
结构类型可以看成由一种数据结构和定义在其上的一组操作组成- 抽象数据类型:指一个数学模型以及定义在该模型上的一组操作
- 原子类型:变量的值不可分解
- 固定聚合类型:其值由确定数目的成分按某种结构组成(e.g. 复数由两个实数依据确定的次序关系构成)
- 可变聚合类型:值的成分的数目不确定
1.1.2 数据结构三要素
- 逻辑结构
- 与数据的存储无关,是独立于计算机的
- 分为线性结构和非线性结构
- 线性结构:数据元素之间只存在一对一的关系
- 非线性结构:数据元素之间存在一对多的关系
- 存储结构
- 包括数据元素的表示和关系的表示
- 是用计算机语言实现的逻辑结构,依赖于计算机语言
- 有顺序存储、链式存储、索引存储、散列存储(哈希存储)
- 数据的运算
- 包括运算的定义和实现
- 运算的定义是针对逻辑结构的
- 运算的实现是针对存储结构的
习题
- 有序表是指关键字有序的线性表,仅描述元素之间的逻辑关系,既可以链式存储,也可以顺序存储,属于逻辑结构
- 逻辑结构独立于存储结构,存储结构不能独立于逻辑结构而存在
- 循环队列是用顺序表表示的队列,是一种数据结构
- 栈是一种抽象数据类型,可以采用顺序存储或链式存储,只表示逻辑结构
- 存储数据要存储数据元素的值和数据元素之间的关系
- 链式存储设计时,各个不同结点的存储空间可以不连续,但结点内的存储单元地址必须连续
1.2 算法和算法评价
1.2.1 算法的基本概念
- 算法是对特定问题求解步骤的一种描述,是指令的有限序列,其中的每条指令表示一个或多个操作
算法的特性:
- 有穷性:算法必须是有穷的,而程序可以是无穷的
- 确定性:对相同的输入只能得到相同的输出
- 可行性
- 输入:可以有零个或多个输入
- 输出:至少有一个输出
算法的目标:
- 正确性
- 可读性
- 健壮性
- 高效率与低存储需求
1.2.2 算法效率的度量
- 时间复杂度
- 依赖于问题的规模n和待输入数据的性质(如输入数据元素的初始状态)
非递归算法的效率
递归算法的效率
- 空间复杂度
习题
- 求归并排序时间复杂度
第2章 线性表
2.1 线性表的定义与基本操作
-
线性表的定义:具有相同特征的数据元素的一个有限序列
-
线性表的长度可以为0,表示一个空表
-
存储结构:顺序存储(顺序表)和链式存储(链表)两种
-
线性表的基本操作
2.2 顺序表
2.2.1 顺序表的定义
2.2.2 顺序表的基本操作
- 插入
- 删除
- 查找
2.3 链表
2.3.1 单链表
顺序单链表(带头结点)A和B合并为一个顺序单链表C
void merge(LNode *A,LNode *B,LNode *&C)
{
LNode *p = A->next; //p跟踪A的最小值结点
LNode *q = B->next; //q跟踪B的最小值结点
LNode *r; //r跟踪C的终端结点
C = A; //将A的头结点设定为C的头结点
C->next = NULL;
r = C;
free(B); //释放B的头结点
while(p != NULL && q != NULL)
{
if(p->data <= q->data)
{
r->next = p;
p = p->next;
r = r->next;
}
else
{
r->next = q;
q = q->next;
r = r->next;
}
}
r->next = NULL;
//将剩下的直接连接到C的末尾
if(p != NULL)
r->next = p;
if(q != NULL)
r->next = q;
}
尾插法建立链表
- 将数组a中的n个元素用尾插法建立链表C
void creatlistR(LNode *&C,int a[],int n)
{
LNode *s,*r; //s指向新申请的节点,r指向C的终端结点
int i;
C = (LNode *)malloc(sizeof(LNode)); //申请C的头结点空间
C->next = NULL;
r = C;
for(i = 0; i < n; i++)
{
s = (LNode *)malloc(sizeof(LNode));
s->data = a[i];
r->next = s;
r = r->next;
}
r->next = NULL;
}
头插法建立链表
void creatlistF(LNode *&C,int a[],int n)
{
LNode *s;
int i;
C = (LNode *)malloc(sizeof(LNode)); //申请C的头结点空间
C->next = NULL;
for(i = 0; i < n; i++)
{
s = (LNode *)malloc(sizeof(LNode));
s->data = a[i];
//下面两句是头插法的关键
s->next = C->next; //s所指新节点的指针域next指向C中的开始结点
C->next = s; //头结点的指针域next指向s结点,是的s成为新的开始结点
}
r->next = NULL;
}
尾插法与头插法的图解
其他基本操作
- 插入
- 后插
- 前插
- 删除
2.3.2 双链表
尾插法建立双链表
void creatDlistR(LNode *&C,int a[],int n)
{
LNode *s,*r; //s指向新申请的节点,r指向C的终端结点
int i;
C = (LNode *)malloc(sizeof(LNode)); //申请C的头结点空间
C->prior = NULL;
C->next = NULL;
r = C;
for(i = 0; i < n; i++)
{
s = (LNode *)malloc(sizeof(LNode));
s->data = a[i];
r->next = s;
//开始与单链表不同
s->prior = r;
r = s;
}
r->next = NULL;
}
双链表的插入
s->next = p->next;
s->prior = p;
p->next = s;
s->next->prior = s;
双链表的删除
q = p->next;
p->next = q->next;
q->next->prior = p;
free(q);
2.3.3 循环链表
比较简单,此处省略
2.3.4 静态链表
2.4 顺序表与链表的比较
- 存储分配的方式:
顺序表的存储空间是一次性分配的
链表的存储空间是多次分配的 - 存储密度:
顺序表存储密度 = 1
链表存储密度 > 1 - 存取方式:
顺序表可随机存取
链表只能顺序存取 - 插入/删除
顺序表需要移动多个元素
链表不需要移动元素,只用修改指针
2.5 逆置问题(重点)
天勤上的,王道没有
- 将长度为n的数组的前端k个元素逆序后移动到数组后端,要求原数组中的数据不丢失,其余元素位置无关紧要
逆转整个数组
void reverse(int a[], int left, int right, int k)
{
int temp;
for(int i = left, j = right; i < left + k && i < j; ++i, --j)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
- 将长度为n的数组的前端k个元素保持原序移动到数组后端,要求原数组中的数据不丢失,其余元素位置无关紧要
将前k个元素逆置,再将整个数组逆置
void moveToEnd(int a[], int k, int n)
{
reverse(a, 0, k - 1, k);
reverse(a, 0, n - 1, k);
}
- 循环左移p个位置
将0p-1位置的元素逆置,再将pn-1位置的元素逆置,最后将整个数组逆置
void moveP(int a[], int n, int p)
{
reverse(a, 0, p - 1, p);
reverse(a, p, n - 1, n - p);
reverse(a, 0, n - 1, n);
}
习题
- 线性表的顺序结构是一种随机存取的存储结构(不是顺序存取的存储结构)
存取方式即为读写方式
-
链式存储结构比顺序存储结构更能方便的表示各种逻辑结构
-
队列需要在表头删除元素,表尾插入元素,用带尾指针的循环链表更方便
-
用给定的n个元素的以为数组,建立一个有序单链表的最低时间复杂度是O(nlogn)
-
删除链表的最后一个元素的时间复杂度是O(n),就算有尾指针也是,因为要遍历到其前驱结点,把next指向NULL
-
从顺序表汇总删除其值在给定值s到t之间(包含s和t,s<t)的所有元素,若s和t不合理或顺序表为空,则显示出错信息并退出运行
-
空间换时间的思想
第3章 栈和队列
3.1 基本概念
-
n个不同元素进展,不同的出栈顺序有 1 n + 1 C 2 n n \frac{1}{n+1}C^n_{2n} n+11C2nn种
-
进栈:先动指针再进元素
stack[++top] = x;
- 出栈:先取元素再动指针
x = stack[top--];
- 入队
rear = (rear + 1) % maxSize;
data[rear] = x;
- 判断队空/满
一般情况下front所指的位置为空,所以先变指针再出队
rear所指的位置为尾元素,所以先变指针再进队
- 双端队列
3.2 栈的应用
括号匹配问题
表达式问题
计算
转换
递归问题
3.3 队列的应用
- 树的层次遍历
- 图的广度优先遍历
- 操作系统中的FCFS
3.4 特殊矩阵的压缩存储
对称矩阵
三角矩阵
带状矩阵
稀疏矩阵
习题
- 链表不带头结点且所有操作均在标头进行,只有表头结点指针,没有表尾指针的单向循环链表最不适合做为链栈(删除需要找到前一个结点,即表尾结点,时间复杂度为O(N))
- 栈只可能发生上溢
- 用数组A[0…5]实现循环队列,当前raer和front值分别为1和5,若进行两次入队操作和一次出队操作后,rear和front的值为:3和0(不是4!)
- 对链表实现的队列来说,若头尾指针都有,则非循环的比循环的更好
第4章 串
4.1 朴素模式匹配算法
4.2 KMP算法
求next数组(重点)
-
方法一:划线试探法
详情见24王道课视频
-
方法二:最长公共前后缀法
同样先划线,next数组的值等于其前最长公共前后缀数+1
void get_next(SString T, int next[]){
int i = 1, j = 0;
next[1] = 0;
while(i < T.length)
{
if(j == 0 || T.ch[i] == T.ch[j])
{
i++;
j++;
next[i] = j; //若Pi=Pj,则next[j+1]=next[j]+1
}
else
j = next[j]; //否则令j=next[j]
}
}
优化(nextval)
第5章 树与二叉树
5.1 基本概念与性质(重点)
- 除根节点外,任何结点有且仅有一个前驱
- 结点的度:有几个分支
- 树的度:各结点度的最大值
常考性质
- 结点数 = 总度数+1
- 度为m的树,第i层最多有 m i − 1 m^{i-1} mi−1个结点
- m叉树第i层最多有 m i − 1 m^{i-1} mi−1个结点
- 高度为h的m叉树最多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1个结点
- 高度为h的m叉树至少有h个结点
- 高度为h,度为m的树至少有h+m-1个结点
- 具有n个结点的m叉树的最小高度为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1) \rceil ⌈logm(n(m−1)+1)⌉
5.2 二叉树
5.2.1 几种常见的二叉树
满二叉树
叶子结点都在最后一层,不存在度为1的结点
- 若高度为h,则有 2 h − 1 2^h-1 2h−1个结点
- 按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父节点为 ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor ⌊2i⌋
完全二叉树
每个结点都与高度为h的满二叉树中的编号一一对应
- 叶子结点只能出现在最后两层
- 最多只有一个度为1的结点(只有左孩子)
- 若共有n个结点,则 i ≤ ⌊ n 2 ⌋ i \leq \lfloor \frac{n}{2} \rfloor i≤⌊2n⌋为分支结点,为叶子结点 i > ⌊ n 2 ⌋ i > \lfloor \frac{n}{2} \rfloor i>⌊2n⌋
二叉排序树
- 左子树上的所有结点的关键字都小于根节点的关键字
- 右子树上的所有结点的关键字都大于根节点的关键字
平衡二叉树
- 任一结点的左子树和右子树的深度之差不超过1
5.2.2 二叉树的性质(重点)
-
度为0的结点 = 度为2的结点 + 1
-
2叉树第i层最多有 2 i − 1 2^{i-1} 2i−1个结点
-
高度为h的2叉树最多有 2 h − 1 2^h-1 2h−1个结点
-
有n个结点的完全二叉树的高度为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1) \rceil ⌈log2(n+1)⌉或 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n \rfloor + 1 ⌊log2n⌋+1(计算i所在的层次也是一样)
-
完全二叉树若已知结点数为n,可推出度为0/1/2的结点数的个数
5.2.3 二叉树的遍历
先序遍历
- 根左右
Status PreOrderTraverse(BiTree T){
if(T == NULL)
return OK;
else
{
visit(T); //访问根节点
PreOrderTraverse(T->lchild); //递归遍历左子树
PreOrderTraverse(T->rchild); //递归遍历右子树
}
}
中序遍历
- 左根右
Status InOrderTraverse(BiTree T){
if(T == NULL)
return OK;
else
{
InOrderTraverse(T->lchild); //递归遍历左子树
visit(T); //访问根节点
InOrderTraverse(T->rchild); //递归遍历右子树
}
}
后续遍历
- 左右根
Status PostOrderTraverse(BiTree T){
if(T == NULL)
return OK;
else
{
PostOrderTraverse(T->lchild); //递归遍历左子树
PostOrderTraverse(T->rchild); //递归遍历右子树
visit(T); //访问根节点
}
}
层序遍历(队列)
根据遍历序列构造二叉树
-
前序+中序
-
后序 + 中序
-
层序 + 中序
5.2.4 线索二叉树
概念
线索化
- 先序遍历线索化会有"爱的魔力转圈圈"问题,要加个判断语句
在线索二叉树中找前驱后继(重点)
5.3 树与森林
5.3.1 存储结构
-
双亲表示法(顺序存储)
-
孩子表示法(顺序+链式存储)
-
孩子兄弟表示法(链式存储)
5.3.2 树、森林与二叉树的转换
-
树→二叉树
-
森林→二叉树
-
二叉树→树
-
二叉树→森林
5.3.3 树与森林的遍历
树的遍历
-
先根遍历
-
后根遍历
-
层序遍历
森林的遍历
- 先序遍历
- 中序遍历
5.4 树与二叉树的应用
5.4.1 Huffman树(最优二叉树)
在含有n个带权叶结点的二叉树中,WPL最小的二叉树即为Huffman树
- 结点的带权路径长度:从树的根到该结点的路径长度 × 该结点的权值
- 树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和
5.4.2 并查集
对Union优化
- 思路:合并时小树进大树,使得树尽量"矮"
- Find的时间复杂度从O(n)变为O(logn)
对Find优化
- 思路:压缩路径,先找到根节点,再将查找路径上的所有结点都挂到根节点下,使得树尽量"矮"
优化总结
习题
-
结点按完全二叉树层序编号的二叉树中,第i个结点的左孩子编号为2i:错,不一定有左孩子
-
设二叉树有2n个结点,m < n,则不可能存在2m个度为1的结点
-
三叉树的结点数为50,则最小高度为5
-
对任意一颗高度为5且有10个结点的二叉树,若采用顺序存储结构保存,每个结点占1个存储单元(仅存放结点的数据信息),则存放该二叉树所需的存储单元数至少 为31 (任意→考虑最坏情况)
-
二叉树中可用后续遍历找到祖先到子孙的路径
-
线索二叉树是一种物理结构
-
线索化后仍不能有效求解后序线索二叉树中求后序后继
-
后序线索树的遍历需要栈的支持
-
一棵有2011个结点的树,其中叶结点个数为116,则对应的二叉树中无右孩子的结点个数为?
-
设哈夫曼编码的长度不超过4,若已对两个字符编码为1和01,则最多还能对4个字符编码
-
度为m的哈夫曼树中,叶结点为n,则非叶结点个数为?
第6章 图
6.1 图的基本概念(重点)
- 图不可为空(边可以没有,一定有一个顶点)
- 并非任意挑几个边几个顶点就是子图
- 生成子图:包含所有顶点
- 连通分量:无向图的极大连通子图
- 强连通分量:有向图的极大连通子图
n个顶点的树有n-1条边
n个顶点的图若边 >n-1则一定有回路
6.2 图的存储
6.2.1 邻接矩阵法
6.2.2 邻接表法
6.2.3 十字链表法(有向图)与邻接多重表(无向图)
空间复杂度O(V+E)
6.3 图的遍历
6.3.1 广度优先搜索(BFS)
6.3.2 深度优先搜索(DFS)
6.4 图的应用(重点)
6.4.1 最小生成树
问题模型:道路规划,使每个城市都联通,且花销最少
Prim算法
- 以顶点为主
时间复杂度: O ( V 2 ) O(V^2) O(V2)
Kruskal算法
- 以边为主
时间复杂度: O ( E l o g 2 E ) O(Elog_{2}{E}) O(Elog2E)
6.4.2 最短路径
问题模型:寻找两个顶点之间的最短路径
BFS算法
Dijkstra算法
不断加顶点,算新的最短路径
Floyd算法
尝试以所有结点为中转点,路径是否变小
6.4.3 有向无环图(DAG)描述表达式
6.4.4 拓扑排序
- AOV网:用有向无环图表示一个工程,顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于Vj进行
逆拓扑排序
6.4.5 关键路径
- AOE网:以顶点表示时间,有向边表示活动,边上的权值表示完成该活动的开销
- 正看最大,反看最小
- 活动的最早发生时间就是该结点所连的弧尾的事件的最早发生时间
- 活动的最迟发生时间 = 该活动所连弧头时间的最晚发生时间 - 该活动所花费的时间
–
习题
- 路径:由顶点和相邻顶点序偶构成的边所形成的的序列
- 一个有n个顶点和n条边的无向图一定是有环的(n-1条边必连通)
- 连通分量:极大连通子图(连通且尽量有多的边)
- 生成树:包含所有顶点的极小连通子图(连通且边尽可能少)
- 简单路径:没有重复的顶点
- V < C E − 1 2 + 1 V < C_{E-1}^{2} + 1 V<CE−12+1时无向图必连通; V > E + 1 V > E + 1 V>E+1时无向图必不连通
- 最小生成树的n-1条边中并不能保证是n-1条最小的边
第7章 查找
7.1 查找的基本概念
- 关键字:唯一标识该元素的某个数据项的值
- 平均查找长度
7.2 顺序查找与折半查找
7.2.1 顺序查找
哨兵:保证查找时不会越界
7.2.2 折半查找
仅适用于有序表,适合顺序存储结构,不适合链式存储结构
- 对比失败后low = mid +1或high = mid - 1
性质
- 对任何一个结点:右子树结点数 - 左子树结点数 = 0或1
- 折半查找树预定是平衡二叉树,结点个数为n时,树高为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_{2}^{(n+1)} \rceil ⌈log2(n+1)⌉
- 若折半查找树中共有n个成功结点,则失败结点有n+1个
- 折半查找的时间复杂度
7.2.3 分块查找
- 块内无序,块间有序
low>high,在low所在的分块查找
7.3 树形查找(重点)
7.3.1 二叉排序树(BST)
- 左子树结点值 < 根节点值 < 右子树结点值
- 用中序遍历可得到一个递增的有序序列
- 平均查找长度(ASL)
二叉排序树的删除
- 删除叶结点:直接删除
- 目标结点只有左子树/右子树,则让子树直接代替准备删除结点的位置
- 目标结点有两颗子树
- 法一:用右子树中最小的结点代替
- 法二:用左子树中最大的结点代替
7.3.2 平衡二叉树(AVL)
- 任一结点的左子树和右子树的高度之差不超过1
- 结点平衡因子 = 左子树高度 - 右子树高度
平衡二叉树的插入(保持平衡)
-
LL
-
RR
- 代码实现
-
LR
-
RL
练习
平衡二叉树的删除
- 用前驱替代
- 用后继替代
7.3.3 红黑树(RBT)
- 考纲新添内容,插入和删除考的可能性不大,了解大致过程即可
红黑树的概念与性质
性质:
- 从根节点到叶结点的最长路径不大于最短路径的2倍
- 若有n个内部结点的红黑树高度 h ≤ O ( l o g 2 ( n + 1 ) ) h \leq O(log_{2}^{(n+1)}) h≤O(log2(n+1))
红黑树的插入
3直接插
25、18直接插
…
红黑树的删除(不重要)
7.4 B树和B+树
7.4.1 B树
基本概念
B树的高度
对于含有n个关键字的m阶B树 l o g m ( n + 1 ) ≤ h ≤ l o g ⌈ m 2 ⌉ n + 1 2 + 1 log_{m}{(n+1)} \leq h \leq log_{\lceil \frac{m}{2} \rceil}{\frac{n+1}{2}} + 1 logm(n+1)≤h≤log⌈2m⌉2n+1+1
B树的插入
B树的删除
- 删除非终端结点:用直接前驱(左子树中最右下的结点)/直接后继(右子树中最左下的结点)替代。即转换为删除终端结点
- 删除终端结点
- 兄弟够借:
借右兄弟:
借左兄弟:
- 兄弟不够借
7.4.2 B+树
- B+树的查找,无论是否找到,最终一定要走到最下面一层结点(B树可能不用)
7.5 散列表
7.5.1 散列表的基本概念
- 装填因子: α = 表中记录数 n 散列表长度 m \alpha = \frac{表中记录数n}{散列表长度m} α=散列表长度m表中记录数n
α \alpha α 越大说明表中记录数越多,说明表装的越满,冲突的可能性越大,查找的次数越多
- 散列表的平均查找长度依赖于散列表的装填因子
- 散列表的查找效率取决于:散列函数、冲突处理、装填因子
7.5.2 散列函数
- 除留余数法
- 直接定址法
- 数字分析法
- 平方取中法
7.5.3 拉链法处理冲突
拉链法的平均查找长度
9.5.4 开放定址法处理冲突
- 开放定址情况下不能随便删除表中已有元素
- 线性探测法
- 平方探测法
- 双散列法
- 伪随机序列法: d i = 伪随机数序列 d_i = 伪随机数序列 di=伪随机数序列
线性探测法的查找效率
聚集(堆积)现象
- 线性探测法容易引起聚集问题
- 堆积问题是由于同义词之间与非同义词之间发送冲突引起的
习题
-
判断能否构成折半查找中关键字比较序列
-
高度为h的AVL至少有多少结点
-
正常AVL中序遍历得到的是升序序列,而这道题说的是降序序列。坑…
-
高度为5的3阶B树至少有31个结点,最多有121个结点 (注意问的是结点数还是关键字数)
-
线性探测法的ASL
第8章 排序
8.1 插入排序
直接插入排序
void InsertSort(int A[], int n)
{
int i, j;
for(i = 2; i <= n; i++)
{
if(A[i] < A[i - 1])
{
A[0] = A[i]; //复制为哨兵
for(j = i - 1; A[0] < A[j]; j--)
{
A[j + 1] = A[j]; //记录后移
}
A[j + 1] = A[0]; //插入到正确位置
}
}
}
ps.对链表进行插入排序,移动元素的次数变少,但关键字对比次数仍为 O ( n 2 ) O(n^2) O(n2)数量级
折半插入排序
- 用折半查找的方式找到应插入的位置
- 时间复杂度和稳定性同直接插入
void InsertSort(int A[], int n)
{
int i, j, low, high, mid;
for(i = 2; i <= n; i++)
{
low = 1;
high = i - 1;
while(low <= high) //折半查找
{
mid = (low + high)/2;
if(A[mid] > A[0]) //找左半边
hgih = mid - 1;
else //找右半边(保持稳定性)
low = mid + 1;
}
for(j = i - 1; j >= high + 1; j--) //后移
A[j + 1] = A[j];
A[high + 1] = A[0]; //插入到正确位置
}
}
希尔排序
void ShellSort(int A[], int n)
{
int d, i ,j;
for(d = n / 2; d >= 1; d = d / 2) //步长变化
{
for(i = d + 1; d <= n; i++)
{
if(A[i] < A[i - d]) //将A[i]插入有序增量子表
{
A[0] = A[i]; //暂存在哨兵位置
for(j = i - d; j > 0 && A[0] < A[j]; j -= d) //插入排序
A[j + d] = A[j];
A[j + d] = A[0]; //插入到正确位置
}
}
}
}
8.2 交换排序
- 根据序列中两个元素的关键字的比较结果来兑换这两个记录在序列中的位置
冒泡排序
void BubbleSort(int A[], int n)
{
for(int i = 0; i < n - 1; i++)
{
bool flag = false; //本次冒泡排序是否发生交换
for(int j = n - 1; j < i; j-->) //一趟冒泡过程,从后往前找
{
if(A[j - 1] > A[j])
{
swap(A[j - 1], A[j]); //交换
flag = true;
}
}
if(flag = false)
return;
}
}
快速排序
ps.上图最好和最坏复杂度写反了
//用第一个元素将序列划分为左右两部分
int Partition(int A[], int low. int high)
{
int pivot = A[low]; //将第一个元素作为基准
while(low < high)
{
while(low < high && A[high] >= pivot)
--high; //从后往前找比基准小的元素
A[low] = A[high]; //比基准小的移动到左端
while(low < high && A[low] <= pivot)
++low; //从前往后找比基准大的元素
A[high] = A[low]; //比基准大的移动到右端
}
A[low] = A[high]; //将基准放到最终位置
return low; //返回基准所在的位置
}
//快速排序
void QuikSort(int A[], int low, int high)
{
if(low < high)
{
int pivotpos = Partition(A, low, high); //划分
QuikSort(A, low, pivotpos - 1); //划分左子表
QuikSort(A, pivotpos + 1, high); //划分右子表
}
}
8.3 选择排序
简单选择排序
void SelectSort(int A[], int n)
{
for(int i = 0; i < n - 1; i++) //共n-1趟
{
int min = i; //记录最小元素的位置
for(int j = i + 1; j < n; j++)
if(A[j] < A[min]) //找最小元素
min = j; //更新最小元素位置
if(min != i)
swap(A[i], A[min]); //交换
}
}
堆排序
在顺序存储的完全二叉树中,非终端节点编号 i ≤ ⌊ n / 2 ⌋ i \leq \lfloor n/2 \rfloor i≤⌊n/2⌋
//建立大根堆
void BuildMaxMap(int A[], int len)
{
for(int i = len / 2; i > 0; i--) //从后往前调整所有非终端结点
HeadAdjust(A, i ,len);
}
//将以k为根的子树调整为大根堆
void HeadAdjust(int A[], int k, int len)
{
A[0] = A[k]; //暂存
for(int i = 2 * k; i <= len; i *= 2)
{
if(i < len && A[i] < A[i + 1]) //选左右孩子中更大的
i++;
if(A[0] > A[i]) //满足大根堆,退出
beak;
else //不满足大根堆,小元素下坠
{
A[k] = A[i]; //将A[i]放到上面
k = i; //修改k值,继续向下比较
}
}
A[k] = A[0]; //最终位置
}
//堆排序
void HeapSort(int A[], int len)
{
BuildMaxHeap(A, len); //初始建堆
for(i = len; i > 1; i--) //n-1趟交换和建堆过程
{
swap(A[i], A[1]); //每次把最大的元素放在最后
HeadAdjust(A, 1, i - 1); //声誉元素调整为堆
}
}
8.4 归并排序
- 归并太虚的性能与输入元素序列无关
- m路归并每次选出一元素需要对比关键词m-1次
int *B = (int *)malloc(n * sizeof(int)); //辅助数组B
//将A[low..mid]和A[mid+1..high]归并
void Merge(int A[], int low, int mid, int high)
{
int i, j, k;
for(k = low; k <= high; k++)
B[k] = A[k]; //将A数组复制到B中
for(i = low, j = mid + 1, k = i; i<= mid && j <= high; k++)
{
if(B[i] < B[k]) //将小的放到A中
A[k] = B[i++];
else
A[k] = B[j++];
}
//将数组中剩余部分直接放入
while(i <= mid)
A[k++] = B[i++];
while(j <= high)
A[k++] = B[j++];
}
//归并排序
void MergrSort(int A[], int low, int high)
{
if(low < high)
{
int mid = (low + high) / 2;
MergeSort(A, low, mid); //归并左半部分
MergeSort(A, mid + 1, high); //归并右半部分
Merge(A, low, mid, high); //归并
}
}
8.5 基数排序
8.6 各种内部排序的比较
8.7 外部排序
8.7.1 外部排序基本原理
8.7.2 外部排序的优化
- 多路平衡归并
败者树
- 减少多路归并时的对比次数
置换-选择排序
最佳归并树
习题
- 对任意n个关键字进行基于比较的排序,至少要进行 l o g 2 ( n ! ) log_{2}^{(n!)} log2(n!) 次关键字之间的两两比较
例如,对1,2,3(n=3)进行从小到大排序,即序列有6(3!)种可能,我们要从这6种可能中找到唯一的一个序列1,2,3。针对3个元素中的任意两个元素,比如1和2,一定要求1在2前面,对此只剩3个序列满足要求,之后依次进行对数折半筛选,只剩唯一一个序列。注意到每次筛选都是折半的,因此是以2为底的对数运算。
-
n
-
n个关键字的小根堆中,关键字最大的记录可能存储在 ⌊ n / 2 ⌋ + 1 n \lfloor n/2 \rfloor + 1 ~ n ⌊n/2⌋+1 n中
满二叉树最后一个非叶结点为 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor ⌊n/2⌋
-
堆排序和希尔排序用到了顺序表随机存储的特点,用链表实现更慢
-
设有5个初始归并段,每个归并段有20个记录,采用5路平衡归并的方法,若不采用败者树,总的比较次数为369次;若采用败者树,总的比较次数为300次