数据结构与算法分析
概述
数据:信息的载体
-
存储结构
- 顺序存储
- 链式存储
- 索引存储
- 散列存储
数据元素:数据的基本单元
数据对象:数据的一个子集
数据类型:值的集合和此集合的一组操作
- 原子类型
- 结构类型
- 抽象数据类型
数据结构
算法
-
特性
- 有穷性
- 确定性
- 可行性
- 输入
- 输出
-
目标
- 正确性
- 可读写
- 健壮性
- 效率与低存储量需求
-
时间复杂度:一个语句被重复执行的次数
- 加法法则
- 乘法法则
-
渐进时间复杂度
-
空间复杂度:所耗费的存储空间
原地工作为O(1)
线性表
特征
- 个数有限
- 元素有逻辑上顺序 和 先后顺序
- 元素都是 数据元素,每个元素都是 单个元素,其数据类型相同,各存储空间大小相同
- 元素具有抽象性,仅讨论元素间逻辑关系,不考虑元素的具体内容
顺序存储(顺序表):由一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻
- 静态分配:数组大小空间事先固定,一旦空间满,不可扩充,再增加数据产生移除
- 动态分配:存储数组的空间是在程序执行过程中通过动态分配语句分配的,一旦空间满,可另外开辟新空间,以替换原存储空间
链式存储
-
单链表
-
建表
- 头插法
- 尾插法
-
按序查找
-
按值查找
-
插入结点
-
删除结点
-
表长
-
-
双链表
- 插入结点
- 删除结点
-
循环链表
- 循环单链表
- 循环双链表
-
静态链表
栈、队列、数组
Stack 栈:先进后出
-
顺序存储
- 顺序栈:
栈顶S.top;初始S.top=-1;栈顶:S.data[S.top];
进栈:未满时,S.top += 1
出栈:非空时,S.top -= 1
栈空:S.top = -1;栈满:S.top == MaxSize - 1;栈长:S.top + 1 - 共享栈:
0号栈空:top0 = -1;1号栈空:top1 = MaxSize - 1;
栈满:top1 - top0 = 1;
进栈:top0 += 1,top1 -= 1;
出栈:top0 -= 1,top += 1;
- 顺序栈:
-
链式存储(链栈)
-
应用:括号匹配、表达式求值、递归
Queue 队列:先进先出
-
顺序存储:
队空(初始):Q.front == Q.rear == 0;
进队:未满时,Q.rear += 1;
出队:非空时,Q.front += 1
【入队上溢,假溢出】-
循环队列:
初始:Q.front = Q.rear = 0;
队首指针进1:Q.front = (Q.front + 1) % MaxSize;
堆尾指针进1:Q.rear = (Q.rear + 1) % MaxSize;
队长:(Q.rear + MaxSize - Q.front) % MaxSize;-
- 牺牲一个单元以区分队满和队空
队空条件:(Q.rear + 1) % MaxSize == Q.front;
队满条件:Q.front == Q.rear;
队中元素个数:(Q.rear - Q.front + MaxSize) % MaxSize
- 牺牲一个单元以区分队满和队空
-
- 增设表示元素个数的数据成员
队空条件:Q.size == 0;
队满条件:Q.size == MaxSize;
两种情况都有 Q.front == Q.rear
- 增设表示元素个数的数据成员
-
- 增设 tag 数据成员
队空:tag = 0时,若因删除(出队)导致Q.front == Q.rear
队满:tag = 1时,若因插入(入队)导致Q.front == Q.rear
- 增设 tag 数据成员
-
-
-
链式存储
带头结点方便于出队,
不存在队满和溢出情况-
双端队列
仍是线性结构- 不受限
- 输出端受限
- 输入端受限
-
-
应用:层次遍历、计算机系统
数组
-
一维数组
-
二维数组
- 行优先(先行i 后列j)
h2列下标范围 - 列优先(先列j 后行i)
h1行下标范围
- 行优先(先行i 后列j)
-
矩阵
-
对称矩阵
-
三角矩阵
- 上三角矩阵
- 下三角矩阵
-
对角矩阵(带状矩阵)
以对角线为准的两侧是非0元素空间- 三对角矩阵
-
稀疏矩阵:非0元素非常少时,非零元素及其行列构成一个三元组(行标,列标,值),通常采用十字链表法
-
串
存储结构
- 定长顺序存储:串长只能 小等于 MAXLEN,超出部分【截断】
- 堆分配存储:仍以一组地址连续的存储单元存放串值字符序列,但 是在程序执行过程中动态分配的存储空间。
malloc()为每个新产生的串分配一块实际串长所需的存储空间,若分配成功,返回起址指针,作为串的基地址;若失败,返回NULL,已分配空间由free()释放。 - 块链存储:类似线性表的 链式存储结构,采用链表方式存储串值,每个结点(块)只能放一个字符,形成链表(块链结构)最后一个结点通常以#补足
模式匹配:子串(模式串)在主串位置
-
简单模式匹配
m 模式串长;n 主串长; int i = 1, j = 1; while(i <= S.length && j <= T.length) { if (S.ch[i] == T.ch[j]) { ++i; ++j; } // 继续匹配后继字符 else { i = i - j + 2; j = 1; } // 指针后退重新开始匹配 } if( j > T.length) { return i - T.length; } return 0;
-
KMP(改进模式匹配)
m 模式串长;n 主串长; // 获取next数组 get_next(String 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],循环继续 } } // KMP(String S, String T, int next[]) { int i = 1, j = 1; while (i <= S.length && j <= T.length) { if (j == 0 || S.ch[i] == T..ch[j]) { ++i; ++j; } // 继续比较后继字符 else { j = next[j] } // 模式串右移 } if (j > T.length) { return i - T.length; } else { return 0; } } -
-
KMP进一步优化next数组
get_nextVal(String T, int nextval[]) { int i = 1, j = 0; nextval[1] = 0; while (i < T.length) { if (j == 0 || T.ch[i] == T.ch[j]) { ++i; ++j; if (T.ch[i] != T.ch[j]) { nextval[i] = j; } else { nextval[i] = nextval[j]; } } else { j = nextval[j]; } } }```
树
性质
二叉树
-
性质
-
特殊二叉树
-
满二叉树
-
完全二叉树(无单独的右孩子)
-
BST(二叉排序二叉树):左子树上所有结点值均小于右子树结点的值
- 构造
CreateBST(BiTree &T, KeyType str[], int n) { T = NULL; int i = 0; while (i < n) { BST_Insert(T, str[i]); i++; } }
- 插入(时间复杂度O(n))
BST_Insert(BiTree &T, KeyType k) { if (T == NULL) { T = (BiTree) malloc(sizeof(BSTNode)); T->key = k; T->lchild = T->rchild = NULL; return 成功 } elif (k == T->key) { return 失败 } elif (k < T->key) { return BST_Insert(T->lchild, k); } // 插入T的左子树 return BST_Insert(T->rchild, k); // 插入T的右子树 } }
-
删除
时间复杂度O(n)-
- 若删除叶子结点,直接删除
-
- 若结点z 只有 左子树或右子树,先让该子树成为z父节点的子树,再删除z结点(替换z)
-
- 若结点z 有左右子树,则令z的直接后继(或直接前驱)替代z,然后从二叉树排序树中删除这个直接后继(或直接前驱),问题转换为1或2的情况
-
-
查找
- 最坏情况:单支斜二叉树
- 最好情况:平衡二叉树
-
平衡二叉树:树上任一结点的左子树和右子树的深度之差(平衡因子)不超过1
-
插入
- LL(右单旋转):在结点A左孩子(L)的左子树(L)上插入新结点,以A为根的子树失衡(平衡因子2),右旋操作
(1)将A的 左孩子B 向上旋替换A成为 根结点;
(2)将 A结点 向下选成为 B右孩子;
(3)原B的右子树 变为 A 的左子树 - RR(左单旋转):在结点A右孩子(R)的右子树(R)上插入新结点,以A为根的子树失衡(平衡因子-2),左旋操作
(1)将A的 右孩子B 向上旋替换A成为 根结点;
(2)将 A结点 向下选成为 B左孩子;
(3)原B的左子树 变为 A 的右子树 - LR(先左后右双旋转):在结点A左孩子(L)的右子树(R)上插入新结点,以A为根的子树失衡(平衡因子2),先左后右旋操作
(1)将A的 左孩子B 的 右子树根节点C 向左上旋替换B;
(3)原C右孩子 变为 B 左孩子;
(4)再把C 右上旋 替换 A;
(5)原C左孩子 变为 A右孩子 - RL(先右后左双旋转):在结点A左孩子(R)的右子树(L)上插入新结点,以A为根的子树失衡(平衡因子-2),先右后左旋操作
(1)将A的 左孩子B 的 右子树根节点C 向右上旋替换B;
(3)原C右孩子 变为 B 左孩子;
(4)再把C 左上旋 替换 A;
(5)原C左孩子 变为 A右孩子
- LL(右单旋转):在结点A左孩子(L)的左子树(L)上插入新结点,以A为根的子树失衡(平衡因子2),右旋操作
-
查找
-
-
-
存储结构
- 顺序存储
- 链式存储
-
遍历
- 先序(根左右)
preOrder(BiTree t) { if (T != NULL) { visit(T); preOrder(T.lchild); preOrder(T.rchild); } } - 中序(左根右) inOrder(BiTree t) { if (T != NULL) { inOrder(T.lchild); visit(T); inOrder(T.rchild); } }
- 后序(左右根)
postOrder(BiTree t) { if (T != NULL) { postOrder(T.lchild); postOrder(T.rchild); visit(T); } }
- 层序(从上到下,从左到右)
levelOrder(BiTree T) { initQueue(Q); BiTree p; EnQueue(p); // 根入队 while(!isEmpty(Q)) { DeQueue(Q, p); // 出队 visit(p); if (p->lchild != NULL) { EnQueue(Q, p->lchild); // 左子树不空,入队 } if (p->rchild != NULL) { EnQueue(Q, p->rchild); // 右子树不空,入队 } } }```
-
线索二叉树:按一定规则将二叉树结点排列成一个线性序列,除首尾结点,每个结点都有 直接前驱 和 直接后继
线索化 将 将二叉链表中 的 空指针 改为 指向前驱或后继 的线索- 中序构造:指针p指向正在访问的结点,pre指向p的前驱。
- InThread(ThreadTree &p, ThreadTree &pre) { if (p != NULL) { InThread(p->lchild, pre); // 递归线索化左子树 if (p->lchild == NULL) { // 左子树为空,建立前驱线索 p->lchild = pre; p->ltag = 1; } if (pre != NULL && pre->rchild == NULL) { pre->rchild = p; // 建立前驱结点的后继线索 pre->rtag = 1; } pre = p; // 标记当前结点成为刚刚访问过的结点 InThread(p->rchild, pre); // 递归,线索化右子树 }
- 中序构造:指针p指向正在访问的结点,pre指向p的前驱。
createInThread(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
InThread(T, pre); // 非空二叉树,线索化
pre->rchild = NULL; // 线索化二叉树
pre->rtag = 1; // 处理遍历的最后一个结点
}
}
- 中序遍历:过程中,检查p左指针是否为空,则指向pre;若pre右指针是否为空,则指向p。
先找到序列中的第一个结点,然后依次找结点的后继,直至后继为空。
- 第一个结点
```c
ThreadNode *FirstNode(ThreadNode *p) {
while (p->ltag == 0) { p = p->lchild; } // 最左下的结点(不一定是叶子结点)
return p;
}
- 结点p后继
ThreadNode *NextNode(ThreadNode *p) {
if (p->rtag == 0) { return FirestNode(p->rchild); }
return p->rchild; // 直接返回线索
}
- 不含头结点的 中序线索二叉树 的 中序遍历算法
InOrder(ThreadNode *T) {
for (ThreadNode *p = FirestNode(T); p != NULL; p= NextNode(p)) {
visit(p);
}
}
- 先序
- 后序
森林
-
存储结构(可顺序,也可链式)
- 双亲表示法:一组连续的空间存储每个结点,同时每个结点增设一个 伪指针,用以指示其双亲结点在数组中位置
- 孩子表示法:将每个结点的孩子结点的孩子结点都用单链表链接起的线性结构(n个结点即n个孩子链)
- 孩子兄弟表示法(二叉树表示法):
指向结点第一个孩子的指针、结点值、指向结点下一个兄弟结点的指针
比较灵活,方便进行树转换成二叉树、查找结点孩子;
为查找双亲结点还需增设parent域指向父结点
-
遍历(森林非空)
-
先序遍历
- (1)访问森林中的第一棵树中根结点;
- (2)先序遍历第一棵树中根结点的子树森林;
- (3)先序遍历除去第一棵树之后剩余的树构成的森林
-
中序遍历
- (1)中序遍历森林中第一棵树的根结点的子树森林;
- (2)访问第一棵树的根结点
- (3)中序遍历除去第一棵树之后剩余的树构成的森林
-
树的遍历
- 先根遍历:若树非空,先访问根结点,再依次遍历根结点的每棵树,遍历子树时采取 根左右
- 后根遍历:若树非空,先依次遍历根结点的每棵树,再访问根结点,遍历子树时采取 左根右
并查集
应用
-
BST
-
平衡二叉树
-
哈夫曼树:WPL(带权路径长度)最小的二叉树
-
哈夫曼编码
(一般左子树0右子树1)- 固定长度编码
- 可变长度编码:不同字符用不等长的二进制表示
图
图
-
有向图:有向边(弧)的有限集合
-
强连通(有向图)
- 强连通图:有向图中,任意两个顶点都是连通的
- 强连通分量:有向图中的极大强连通子图
-
有向树:一个顶点的入度为0,其余顶点的入度均为1的有向图
-
-
无向图:无向边(边)的有限集合
-
连通(无向图)
- 连通图:无向图中,任意两个顶点都是连通的
- 连通分量:无向图中的极大连通子图
-
-
简单图:不存在重复边、不存在顶点到自身边
-
多重图:两个顶点间的边数大于1,由允许顶点仅通过一条边与自身关联
-
完全图(简单完全图)
- 无向完全图:有n(n-1)/2条边
- 有向完全图:有n(n-1)条弧
-
子图
-
生成树:包含图中全部顶点的一个 极小连通子图,n-1条边
生成森林:在非连通图中,连通分量的生成树构成非连通图 的 生成森林 -
度
-
无向图:全部顶点的度为顶点数的两倍,2e
-
有向图 TD(v) = ID(v) + OD(v),
全部顶点的 出度之和 与 入度之和 相等,等于边数- 入度ID(v):以顶点v为终点的有向边数目,
- 出度OD(v):以顶点v为起点的有向边的数目
-
-
稠密图:相对边数多
稀疏图:相对边数少 -
路径:顶点之间的边,
路径长度:顶点上的边数目
若一图有n个顶点,且大于n-1条边,则一定有环
第一个顶点:回路
最后一个顶点:环- 简单路径:顶点不重复的路径;
简单回路:除第一个顶点和最后一个顶点外 的 顶点不重复出现的 回路
- 简单路径:顶点不重复的路径;
-
距离:若顶点间不存在路径,距离为 无穷大
存储结构
- 邻接矩阵法:一维数组 存 顶点信息,二维数组 存 边信息(各顶点邻接关系,带权图即权值),对存储空间消耗大
- 邻接表:每个顶点建立一个单链表(无向图:边表,有向图:出边表)
- 十字链表:有向图的链式存储结构
- 邻接多重表:无向图的一种链式存储结构
遍历
- BFS(广度优先):类似于二叉树层序遍历,最坏情况下空间复杂度O(|V|)
bool visisted[MAX_VERTEX_NUM]; // 访问标记数组
BFS_Traverse(Graph g) {
for (int i = 0; i < g.vexnum; i++) { visited[i] = false; } // 标记数组 初始化
InitQueue(Q); // 初始化辅助队列
for (int i = 0; i < g.vexnum; i++) { // 从 0 号顶点开始遍历
if (!visited[i]) { BFS(g. i); } // 每个连通分量调用一次BFS
}
}
BFS(Graph g, int v) {
visit(v); // 访问初始顶点 v
visited[v] = True; // 对 v 进行已访问标记
Enqueue(Q, v); // 顶点 v 入队列Q
while( ! isEmpty(Q) ) {
DeQueue(Q, v); // 顶点v 出队列
// 检测v所有邻接点
for ( w = FirstNeighbor(g, v); w >= 0; w = NextNeighbor(g, v, w) ) {
if ( !visited[w] ) {
visit(w); // w 为 v 的尚未访问的邻接顶点
visited[w] = TRUE; // 对 w 进行已访问标记
Enqueue(Q, w); // 顶点 w 入队列Q
}
}
}
}
- DFS(深度优先):类似树的先序遍历,递归算法,空间复杂度 O(|V|)
bool visited[MAX_VERTEX_NUM]; // 访问标记数组
DFS_Traverse(Graph g) {
for ( int v = 0; v < g.vexnum; ++v ) { visited[v] = FALSE; } // 初始化已访问标记数据
for ( int v = 0; v < g.vexnum; ++v ) {
if ( ! visited[v] ) { DFS(g, v); }
}
DFS(Graph g, int v) {
visit(v); // 从顶点 v 出发,深度优先遍历图
visted[v] = TRUE; // 对 v 进行已访问标记
for ( w = FirstNeighbor(g, v); w >= 0; w = NextNeighbor(g, v, w) ) {
if ( ! visited[w] ) { DFS(g, w); } // w 为 v 尚未访问的邻接顶点
}
}
应用
-
最小生成树:
树形不唯一;
边的权值之和唯一;
边数为顶点数减1-
- Prim:类似寻找最短路径
-
- Kruskal:按权值的递增次序选择合适的边进行构造
-
-
最短路径
-
- Dijkstra(基于贪心策略)
-
- Floyd(动态规划)
-
-
拓扑排序
-
AOV网:顶点表示活动,有向边表示活动间先后关系
-
-
关键路径
-
AOE网:顶点表示事件,有向边表示活动
- 最长工期为【关键路径】,关键路径上的活动为【关键活动】,不可延期
- 最早开始时间
- 最迟开始时间
-
-
DAG图(有向无环图):有向图中不存在环,是描述公共子式的工具
查找
静态(查询元素及其属性) / 线性结构
-
顺序(线性)查找
(可为链式结构)-
一般线性表
-
有序表
-
-
折半(二分)查找
(只适用于有序顺序表,
不适合链式存储)
Binary_Search(SeqList L, ElemType key) {
int low = 0, high = L.TableLen - 1, mid;
while (low <= high) {
mid = (low + high) / 2; // 取中间位置
if (L.elem[mid] == key) { return mid} // 查找成功返回所在位置
elif (L.elem[mid] > key) { high = mid - 1; } // 从前半部分查找
else { low = mid + 1; } // 从前半部分查找
}
return 失败
}
-
-
分块(索引顺序)查找
动态结构,适合快速查找-
(1)查找表分为若干块;
(2)块内元素可以无序,但块之间须有序;
(3)建立索引表,各块最大的元素和第一个元素的地址,并按关键字有序排序;
(4)首先在索引表确定待查找记录的所在块(可顺序或折半);
(5)块内顺序
-
动态(查询元素及其属性、插入删除) / 树形结构
-
二叉排序树
-
平衡二叉树
-
B树
(B-树,多路平衡查找树,
平衡因子为0)-
条件
一棵m阶(所有结点中孩子个数最大值)B树或空树- (1)树中每个结点至多有m棵树,即至多含有m-1个关键字
- (2)若根结点 不是终端结点,则至少有两棵子树
- (3)除根结点外的所有非叶子结点 至少有[m/2]棵子树,即至少有[m/2]-1个关键字
- (4)所有非叶子结点的结构如下
- (5)所有的叶子结点都出现在同一层次上,并且不带信息(可视为外部结点 或 折半查找判定树的查找失败结点)
-
高度(磁盘存取次数)
-
查找
-
插入
- (1)定位
- (2)插入
-
删除
-
- 直接删除关键字
-
- 兄弟够借
-
- 兄弟不够借
-
-
-
B+树
(B树的变形,B*树是B+树的变形)-
条件
- (1)每个分支结点最多有m棵子树(孩子结点)
- (2)非叶根结点至少有两棵子树,其他每个分支结点至少有[m/2]棵子树
- (3)结点的子树个数 与 关键字个数 相等
- (4)所有叶结点包含全部关键字 及 指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来
- (5)所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及其指向其子结点的指针
-
散列结构
-
散列表
-
散列表:根据关键字而直接进行访问的数据结构
散列建立了关键字 和 存储地址 之间的一种直接映射关系 -
散列函数:一个查找表中的关键字映射成该关键字对应的地址的函数
-
构造方法
-
- 直接定址法:简单、不会产生冲突,适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则造成存储空间浪费
-
- 除留余数法:简单常用,选好p,能使每个关键字通过该函数转换后等概率地映射到散列空间上的任一地址,从而尽可能减少冲突可能性
-
- 数字分析法:关键字是r进制数,而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一下,每种数码出现的机会均等;
而在某些位上分布不均,只能某几种数码经常出现,此时选取数码分布较为均匀的若干位作为散列地址。
适用于已知关键字集合,若更换关键字,需要重新构建散列函数
- 数字分析法:关键字是r进制数,而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一下,每种数码出现的机会均等;
-
- 平方取中法:取关键字的平方值的中间几位作为散列地址(具体取位数须视情况而定)。分布较均匀,适用于关键字每位的取值都不够均匀或均小于散列地址所需的位数。
-
-
处理冲突
-
- 开放定址法
- 线性探测法
- 平方探测法(二次探测法)
- 再散列法(双散列法)
- 伪随机序列法
-
- 拉链法(链接法):将所有同义词存储在一个线性链表中,,该线性链表由其散列地址唯一标识
-
-
性能分析
散列表的查找效率取决于:散列函数、处理冲突方法、装填因子
-
排序
内部排序:内存中进行
-
插入排序
- 直接插入排序
InsertSort(ElemType 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];
}
- 折半插入排序
HalfInsertSort(ElemType A[], int n) {
int i, j, low, high, mid;
for (i = 2; i <= n; i++) {
A[0] = A[i];
low = 1; high = i - 1; // 每次折半查找范围
while (low <= high) {
mid = (low + high) / 2; // 取中间点
if (A[mid] > A[0]) {high = mid - 1} // 查找左半子表
else {low = mid + 1} // 查找右半子表
}
for (j = i - 1; j > high + 1; --j) {
A[j + 1] = A[j]; // 统一后移元素,空出插入位置
}
A[high + 1] = A[0]; // 插入
}
- 希尔排序
ShellSort(ElemType A[], int n) {
for (dk = n/2; dk >= 1; dk = dk / 2) { // 步长变化
for (i = dk + 1; i <= n; ++i) {
if (A[i] < A[i - dk]) { // 需将A[i]插入有序增量子表
A[0] = A[i]; // 暂存在A[0]
for (j = i - dk; j > 0 && A[0] < A[j]; j -= dk) {
A[j + dk] = A[j]; // 记录后移,查找插入的位置
}
A[j + dk] = A[0]; // 插入
}
}
}
}
-
交换排序
- 冒泡排序
BubbleSort(ElemType A[], int n) {
for (i = 0; i < n - 1; i++) {
flag = false;
for (j = n - 1; j > 1; j--) {
if (A[j - 1] > A[j]) {
swap(A[j - 1], A[j]);
flag = true;
}
}
if (flag == false) return;
}
}
- 快速排序
(基于分治)
QuickSort(ElemTpye A[], int low, int high) {
if (low < high) { // 递归跳出条件
int pivotpos =Partition(A, low, high); // 划分出两个子表
QuickSort(A, low, pivotpos - 1); // 依次对两个子表进行递归排序
QuickSort(A, pivotpos + 1, high);
}
Partition(ElemType A[], int low, int high) {
ElemType 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] = pivot; // 枢轴元素存放的最终位置
return low; // 返回枢轴元素的最终位置
}
-
选择排序
- 简单选择排序
SelectSort(ElemType A[], int n) {
for (int i = 0; i < n - 1; i++) { // 一个进行 n - 1 趟
min = i; // 记录最小元素位置
for (int j = i + 1; j < n; j++) { // 在A[i ... n - 1]中选择最小的元素
if ( A[j] < A[min] ) { min = j; } // 更新最小元素
}
if (min != i) { swap(A[i], A[min]); } // 共移动元素 3 次
}
- 堆排序
// 构造大根堆
BuildMaxHeap(ElemType A[], int len) {
for (int i = len / 2; i > 0; i--) { // 从 i = [n/2] ~ 1,反复调整堆
HeadAdujst(A, i, len);
}
}
HeadAdjust(ElemType A[], int k, int len) { // 将元素 k 为根的子树进行调整
A[0] = A[k]; // A[0[暂存子树的根结点
for (int i = 2*k; i <= len; i *= 2) { // 沿key较大的子结点向下筛选
if (i < len && A[i] < A[i + 1]) { i++; } // 取k值较大的子坐标
if (A[0] >= A[i]) { break; } // 筛选结束
else { A[k] = A[i]; k = i; } // 将A[i]调整至父结点上,修改k值,以便继续向下筛选
}
A[k] = A[0]; // 被筛选结点的值放入最终位置
}
// 排序算法
HeapSort(ElemType A[], int len) {
BuildMaxHeap(A, len); // 初始建堆
for (int i = len; i > 1; i--) { // n - 1趟的交换和建堆过程
swap(A[i], A[1]); // 输出堆顶元素 与 堆底元素 进行交换
HeadAdjust(A, 1, i - 1); // 调整,将剩余的 i - 1 个元素整理成堆
}
- 归并排序
(基于分治)
ElemType *B = (ElemType *) malloc ((n + 1) *sizeof(ElemType)); // 辅助数组B
// 将A的两段A[low...mid] 和 B[mid+1...high]各自有序,将它们合并成一张有序表
Merge(ElemType A[], int low, int mid, int high) {
for (int k = low; k <= high; k++) {
B[k] = A[k]; // 将A中所有元素复制到B中
}
for (int i= low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
if (B[i] <= B[j]) { A[k] = B[i++]; } // 复制较小值到A中
else { A[k] = B[j++]; }
}
// 两个循环只会指向一个
while(i <= mid) { A[k++] = B[i++]; } // 若第一张表未检测完,复制
while(j <= high) { A[k++] = B[j++]; } // 若第二张表未检测完,复制
}
// 归并排序
MergeSort(ElemType 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); // 归并
}
}
-
基数排序
- MSD(最高位优先)
- LSD(最低位优先)
外部排序:由于文件信息量过大(存放在外存),需要进行多次内存与外存之间交换
-
方法
-
多路平衡归并与败者树
-
置换-选择排序(生成初始归并段)
-
最佳归并树
树和森林遍历 与 二叉树遍历 对应关系
(亦有将森林的中序遍历称后根遍历)
- 先序遍历
- 先根遍历
- 先序遍历
- 中序遍历
- 中序遍历
- 后根遍历