一:绪论
- 数据:信息的载体,所有能输入到计算机中,被计算机程序识别和处理的符号的集合
- 数据结构 = {D, R}
D:数据元素
R:数据元素间的关系
数据结构是指数据元素之间的逻辑关系,即数据的逻辑结构,与数据的存储无关- 抽象数据类型:信息隐蔽,数据封装,使用与实现相分离
- 面向对象 = 对象+类+继承+通信
1.数据的逻辑结构
逻辑结构包括:线性结构、非线性结构
- 线性结构:
- 集合中存在唯一一个“第一个元素”
- 集合中存在唯一一个“最后一个元素”
- 除最后一个元素之外,其他数据元素均有唯一的“后继”
- 除第一个元素之外,其他数据元素均有唯一的“前驱”
- 非线性结构:包括树形结构、图形结构
- 一对多
2.数据的物理结构
物理结构包括:
- 顺序存储方法、链式存储方法、索引存储方法、散列存储方法
3.算法的基本概念
算法的特性:
- 有穷性、确定性、输入、输出、可行性
算法的设计目标:
- 正确性、可使用性、可读性、健壮性、效率
算法的后期测试:
在算法中某些部位插装时间函数time(),测定算法完成吗某一功能所花费的时间
算法的事前估计:
- 空间复杂度,时间复杂度
加法规则:
- T(n,m) = T1(n) + T2(m) = O(max(f(n),g(m)))
算法原地工作:空间复杂度为O(1)
二:线性表
1.有序表的合并
- A和B是两个带头结点的单链表,其中元素递增有序,将A和B归并成一个按元素值非递减有序的链表C,C有A和B中的结点组成
void merge(LNode* A, LNode* B, LNode*& C)
{
LNode* p = A->next; //用来跟踪A的最小值
LNode* q = B->next; //用来跟踪B的最小值
LNode *r; //指向C的终端结点
C = A; //用A的头结点来做C的头结点
C->next = NULL;
free(B); //释放B的头结点
r = C; //初始头结点设为终端结点
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 = p == NULL?q:p; //将有剩余结点的链表连在C的尾部
}
2.链表的建立
- 尾插法
void CreateListR(LNode*& C, int a[], int n) //要改变的变量用引用型
{
LNode *s, *r; //s用来申请新的结点,r始终指向C的终端结点
int i;
C = (LNode*)mallco(sizeof(LNode)); //申请C的头结点空间
C->next = NULL;
r = C;
for(i = 0; i< n; i++)
{
s = (LNode*)mallco(sizeof(LNode)); //申请新结点空间
s->data = a[i]; //赋值
r->next = s; //用r接纳新节点
r = r->next; //r指向终端结点
}
r->next = NULL;
}
- 头插法
void CreateListF(LNode*& C, int a[], int n)
{
LNode *s;
int i;
C = (LNode*)mallco(sizeof(LNode)); //申请C的头结点空间
C->next = NULL;
for(i = 0; i< n; i++)
{
s = (LNode*)mallco(sizeof(LNode)); //申请新结点空间
s->data = a[i]; //赋值
//头插法的关键
s->next = C->next;
C->next = s;
}
}
3.双链表
- 插入
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);
4.逆置问题
fo(int i = left, j = right; i < j; i--, j--)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
三:栈和队列
1.栈
- 进栈:
stack[++top] = x; //先动指针再进栈
- 出栈:
x = stack[top--]; //先出栈再动指针
表达式的转换
中缀变后缀:
操作数的顺序不会发生变化,只有运算符的顺序可能发生变化。同时又没有括号。所以在转换的过程中,只要碰到操作数,可以直接输出,而遇到运算符和括号进行相应的处理即可。
转换原则如下:
- 从左到右读取一个中序表达式。
- 若读取的是操作数,则直接输出。
- 若读取的是运算符,分三种情况。
- A.该运算符为左括号( ,则直接存入堆栈。
- B.该运算符为右括号),则输出堆栈中的运算符,直到取出左括号为止。
- C.该运算符为非括号运算符,则与堆栈顶端的运算符做优先权比较,若较堆栈顶端运算符高或者相等,则直接存入堆栈;若较堆栈顶端运算符低,则输出堆栈中的运算符。
- 当表达式已经读取完成,而堆栈中尚有运算符时,则依次序取出运算符,直到堆栈为空,由此得到的结果就是中缀表达式转换成的后缀表达式。
中缀变前缀:
- 从右至左扫描中缀表达式,从右边第一个字符开始判断:
- 如果当前字符是数字,则分析到数字串的结尾并将数字串直接输出。
- 如果是运算符,则比较优先级。
- A. 如果当前运算符的优先级大于等于栈顶运算符的优先级(当栈顶是括号时,直接入栈),则将运算符直接入栈;
- B. 否则将栈顶运算符出栈并输出,直到当前运算符的优先级大于等于栈顶运算符的优先级(当栈顶是括号时,直接入栈),再将当前运算符入栈。
- C. 如果是括号,则根据括号的方向进行处理。如果是向右的括号,则直接入栈;
- D. 否则,遇向左的括号前将所有的运算符全部出栈并输出,遇右括号后将向左、向右的两括号一起出栈(并不输出)。
- 重复上述操作2.直至扫描结束,将栈内剩余运算符全部出栈并输出,再逆缀输出字符串。中缀表达式也就转换为前缀表达式了。
- 如果表达式结束,但栈中还有元素,将所有元素出栈,添加到前缀表达式中
2.队列
2.1 循环队列
//队空状态
qu.rear == qu.front
//队满状态
(qu.rear + 1) % maxSize == qu.front;
//入队
qu.rear = (qu.rear + 1) % maxSize;
qu.data[qu.rear] = x;
//出队
qu.front = (qu.front + 1) % maxSize;
x = qu.data[qu.front];
maxSzie为最大元素个数
2.2 双端队列
四:串
数据结构:详解KMP算法,手工求解next、nextval数组,求模式串的比较次数例题
五:数组、矩阵和广义表
1.对称矩阵
2.上/下三角矩阵
同对称矩阵
3.稀疏矩阵
3.1 顺序存储
- 三元组
typedef struct
{
int val; //非零值
int i, j; //该值所在的横、纵坐标
}Trimat;
//建立三元组
void createTrimat(int A[][maxSize], int m, int n, intB[][maxSize])
{
int k = 1;
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
if(A[i][j] != 0)
{
B[k][0] = A[i][j];
B[k][1] = i;
B[k][2] = j;
k++;
}
B[0][0] = k - 1; //非零元素个数
B[0][1] = m; //行数
B[0][2] = n; //列数
}
- 伪地址
3.2 链式存储
- 邻接表
将每一行的非零元素串连成一个链表, 链表结点中有两个分量,分别表示元素值和列号
2. 十字链表
行分量、列分量、数据域、指向下方结点的指针,指向右方结点的指针
//普通结点
typedef struct OLNode
{
int row, col;
struct OLNode* down, *right;
int val;
}OLNode;
//头结点
typedef struct
{
OLNode* chead, *rhead; //两个头指针
int m, n, k; //矩阵行数、列数、非零元素个数
}CrossList;
4.广义表
- 长度:广义表中最上层元素的个数
- 深度:表中括号的最大层数
- 表头:第一个元素
- 表尾:除表头之外的所有元素组成的表
A(a, B((), C(1))):
六:树与二叉树
1.基本概念
- 结点的度:拥有的分支个数
- 树的度:结点度的最大值
- 树的高度(深度):结点的最大层次
- 二叉树:每个结点最多只有两颗子树,且子树有左右之分,不能颠倒
- 满二叉树:所有分支结点都有左孩子和右孩子结点,且叶子结点都集中在二叉树的最下一层
- 完全二叉树:对满二叉树进行编号,从1开始,从上到下,自左至右进行,若对一颗深度为k,有n个结点的二叉树进行编号后,各节点的变化与深度为k的满二叉树中相同位置上的结点的编号均相同
1.1 二叉树的性质
- 若二叉树的叶子数为n0,度为2的结点数为n2,则n0=n2+1
总分支数=总结点数-1
- 二叉树的第i层上至多有2i-1个结点
- 深度为k的二叉树至多有2k-1个结点
- 对一颗有n个结点的完全二叉树的结点按层序编号,则对任一结点i有:
1.如果i = 1,则结点i是二叉树的根,无双亲;若i > 1,则其双亲是结点 ⌊i/2⌋
2.如果2i > n,则结点i为叶子结点,无左孩子;否则,其左孩子为结点2i
3.如果2i+1 > n,则结点i无右孩子;否则,其右孩子为结点2i+1 - 具有n个结点的完全二叉树的深度为⌊log2n⌋+1
1.2 二叉树的遍历
剪枝操作:当在左子树中找到满足要求的结点后,无需继续查找右子树,直接退出本层递归
t = search(p->lchild, key); //在左子树中寻找
if (t == false) //左子树中没有找到
search(p->rchild, key); //在右子树中寻找
- 层次遍历(队列)
void level(BTNode *p)
{
int front, rear; //队头、队尾指针
front = rear = 0;
BTNode *que[maxSize]; //循环队列
BTNode *q;
if(p != NULL)
{
rear = (rear + 1) % maxSize;
que[rear] = p; //根节点入队
while(front != rear)
{
front = (front + 1) % maxSize;
q = que[front]; //根节点出队
Visit(q); //访问根节点
if(q->lchild != NULL) //左子树不为空
{
rear = (rear + 1) % maxSize;
que[rear] = q->lchild; //左子树的根节点入队
}
if(q->rchild != NULL) //右子树不为空
{
rear = (rear + 1) % maxSize;
que[rear] = q->rchild; //右子树的根节点入队
}
}//end of while
}//end of if
}
- 非递归的遍历算法
- 先序遍历
void PreOrderNoRecursion(BTNode *bt)
{
if(bt == NULL) //空树
return;
BTNode *Stack[maxSize]; //定义栈
int top = -1; //初始化栈指针
BTNode *p;
Stack[++top] = bt; //根结点入栈
while(top != -1)
{
p = Stack[top--]; //出栈
visit(p); //访问
if(p->rchild != NULL) //右孩子存在
Stack[++top] = p->rchild; //右孩子入栈
if(p->lchild != NULL) //左孩子存在
Stack[++top] = p->lchild; //左孩子入栈
}
return;
}
- 中序遍历
- 根节点入栈
- 如果栈顶结点左孩子存在,左孩子入栈,若左孩子不存在,则输出栈顶指针并检查右孩子书否存在,存在则右孩子入栈
- 栈空时结束
void InOrderNoRecursion(BTNode *bt)
{
if(bt == NULL) //空树
return;
BTNode *Stack[maxSize]; //定义栈
int top = -1; //定义栈顶指针
p = bt;
/*出栈过程中会出现栈空但遍历未完成的情况,所以判断条件多一个p不为空*/
while(top != -1 && p != NULL)
{
while(p != NULL) //左孩子入栈
{
Stack[++top] = p;
p = p->lchild;
}
if(top != -1) //出栈
{
p = Stack[top--];
visit(p);
p = p->rchild;
}
}
}
- 后序遍历
逆后序遍历是先序遍历过程中对左右子树遍历顺序交换所得到的结果
- 用Stack1辅助做逆后序遍历(将先序遍历的左、右子树遍历顺序交换)
- 将Stack1的遍历结果顺序压入Stack2中,然后将Stack2中的元素全部出列,即得到后续遍历序列
void PostOrderNoRecursion(BTNode *bt)
{
if(bt == NULL)
return;
/*定义两个栈*/
BTNode *Stack1[maxSize], *Stack2[maxSize];
int top1 = -1, top2 = -1;
BTNode *p = NULL;
Stack1[++top] = bt;
while(top1 != -1)
{
p = Stack1[top1--];
Stack2[++top] = p;
/*先左孩子入栈,后右孩子入栈,和先序遍历顺序相反*/
if(p->lchild != NULL)
Stack1[++top1] = p->lchild;
if(p->rchild != NULL)
Stack1[++top1] = p->rchild;
}
while(top2 != -1)
{
/*出栈序列即为后续遍历序列*/
p = Stack2[top--];
visit(p);
}
}
1.3 线索二叉树
主要是中序线索二叉树
- tag=0,则表示child为指针,指向结点的左/右孩子,如果tag=1,则表示child为线索,指向结点的直接前驱/后继
typedef struct TBTNode
{
int data; //数据域
int ltag, rtag; //线索标记
struct TBTNode* lchild; //左孩子域
struct TBTNode* rchild; //右孩子域
}TBTNode;
/*通过中序遍历将二叉树线索化*/
void InThread(TBTNode *p,TBTNode *&pre)
{
if(p == NULL)
return;
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; //pre指向当前的p,作为p将要指向的下一个结点的前驱结点指示指针
InThread(p->rchild, pre); //递归,右子树线索化
}
void createInThread(TBTNode *root)
{
TBTNode *pre = NULL; //前驱结点指针
if(root != NULL)
{
InThread(root, pre);
pre->rchild = NULL;
pre->rtag = 1; //处理中序遍历最后一个结点
}
}
2. 树和森林与二叉树的相互转换
2.1 树转换为二叉树
- 将同一结点的个孩子结点用线连起来
- 将每个结点的分支结点从左往右除了第一个意外,其余的都减掉
2.2 二叉树转换为树
- 从左上到右下分为若干层,并调整至水平方向
- 找到每一层结点在其上一层的父结点
- 将每一层的结点与其父结点相连,且删除每一层结点之间的连接
2.3 森林转换为二叉树
- 先将森林中的所有树转换为二叉树
- 将第n+1棵二叉树作为第n棵二叉树的右子树
2.4 二叉树转换为森林
- 将有右孩子的二叉树的而孩子全部断开,直到不存在根节点有右孩子的二叉树为止
- 将每棵二叉树转换为树
3.树与二叉树的应用
3.1 赫夫曼树
基本概念
- 路径:从一个结点到另一个结点的分支所构成的路线
- 路径长度:路径上的分支数目
- 树的路径长度:从根到每个结点的路径长度之和
- 带权路径长度:从根到该结点之间的路径长度乘以该结点的权值
- 树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和
赫夫曼树的构造方法(给定n个权值)
- 将这n个权值分别看作只有根节点的n棵二叉树,这些二叉树构成的集合记为F
- 从F中选出两棵根节点的权值最小的树作为左、右子树,构成一棵新的二叉树,新的二叉树的根节点的权值为左、右子树根节点权值之和
- 重复上述步骤,直到F中只有一棵树为止
赫夫曼树的特点
- 权值越大的结点离根节点越近
- 树中没有度为1的结点
- 树的带权路径最短
3.2 二叉排序树(BST)
- 若左子树不为空,则左子树上所有关键字均不大于/不小于根节点的值
- 若右子树不为空,则右子树上所有关键字均不小于/不大于根节点的值
BTNode* BSTearch(BTNode* bt, int key)
{
if(bt == NULL)
return NULL;
else
{
if(bt->key == ley)
return bt;
else if (key < bt->key)
return BSTSearch(bt->lchild, key);
else
return BSTSearch(bt->rchild, key);
}
}
3.3 平衡二叉树(AVL)
- 所有结点为根的树的左右子树高度之差的绝对值不超过1
- 是一种特殊的BST
4.堆
- 是完全二叉树
- 根节点总小于/大于子女结点
七:图
1.基本概念
- 完全图:任意两个顶点都有边相连
有向完全图:n个顶点,n(n-1)条边
无向完全图:n个顶点,n(n-1)/2条边- 路径长度:路径上边的个数
- 简单路径:序列中顶点不重复出现
- 回路:起点顶点和终点顶点相同的路径
- 连通:两个顶点之间有路径
- 连通图(无向图):任意两个顶点之间连通
- 连通分量:极大连通子图
- 强连通图(有向图):任意两个顶点之间都有路径
- 强连通分量:极大强联通子图
- 无向图顶点度数之和 = 边数目的2倍
2.存储结构
2.1 邻接矩阵(顺序存储)
表示顶点之间的关系
设邻接矩阵A,Am中非零元素A[i][j]代表从i到j且路径长度为m的路径条数
typedef struct
{
int no; //顶点编号
char info; //其他信息
}VertexType; //顶点类型
typedef struct //图的定义
{
int edges[maxSize][maxSize]; //邻接矩阵
int n, e; //顶点数、边数
VertexType vex[maxSize]; //存放顶点信息
}MGraph;
- A[i][j] = 1 表示顶点i与顶点j邻接(i与j之间存在边/弧)
- A[i][j] = 0 表示顶点i与顶点j不邻接
- 无向图的邻接矩阵是对称的
2.2 邻接表(链式存储)
typedef struct ArcNode
{
int ajdvex; //该边所指向的结点的位置
struct ArcNode *nextarc; //指向下一条边的指针
int info; //其他信息(如权值)
}ArcNode;
typedef struct
{
char data; //顶点信息
ArcNode *firstarc; //指向第一条边的指针
}VNode;
typedef struct
{
VNode adjlist[maxSize]; //邻接表
int n, e; //顶点数、边数
}AGraph; //图的;邻接表类型
2.3 邻接多重表
- vertex存放和该顶点相关的信息
- firstedge指向第一条依附于该顶点的边
- mark为标记域,用于标记该条边是否被搜索过
- ivex和jvex为该边依附的两个顶点在图中的位置
- ilink指向下一条依附于ivex的边
- jlink指向下一条依附于jvex的边
- info为指向与边相关的各种信息的位置
3.图的遍历
3.1 深度优先搜索
任取一个顶点,访问,再检查这个顶点的所有邻接顶点,递归访问其中未被访问过的顶点
/*以邻接表位存储结构的图的DFS*/
void DFS(AGraph *G, int v)
{
ArcNode *p = G->adjlist[v].firstarc; //指向顶点v的第一条边
visit[v] = 1; //已访问
visit(v);
while(p != NULL)
{
if(visit[p->adjvex] == 0);
DFS(G, p->adjvex);
p = p->nextarc; //p指向顶点v的下一条边的终点
}
}
3.2 广度优先搜索
先访问起始节点v,然后选取与v邻接的所有顶点w1…wn,再依次访问与w1…wn邻接的全部顶点,以此类推,直到所有顶点被访问
- 任取图中一个顶点访问,入队,并将这个顶点标记为已访问
- 当队列不空是执行循环:出队,一次检查出队的所有邻接顶点,访问没有访问过的邻接顶点,并将其入队
- 队列为空时跳出循环
viud BFS(AGraph *G, int v, int visit[maxSize])
{
ArcNode *p;
int que[maxSize], front = 0, front = 0; //定义队列
visit[v] = 1; //访问
visit(v);
rear = (rear + 1) % maxSize; //入队
que[rear] = v;
while(front != rear)
{
front = (front + 1) % maxSize; //出队
j = que[front];
p = G->adjlist[j].firstarc; //p指向出队顶点j的第一条边
while(p != NULL)
{
if(visit[p->adjvex] == 0) //当前邻接顶点未被访问
{
visit[p->adjvex] = 1; //访问
visit(p->adjvex);
rear = (rear + 1) % maxSize;
que[rear] = p->adjvex;
}
p = p->nextarc; //指向j的下一条边
}
}
}
4.最小生成树
- 当带权连通图的任意一个环中所包含的边的权值均不相同时,其 最小生成树是唯一的
4.1 Prim算法
- 将v0到其他顶点的所有边当作候选边
- 从候选边中挑选出权值最小的边输出,并将与该边另一端相接的顶点v并入生成树中
- 考察剩余所有顶点vi,如果(v, vi)的权值比lowcost[vi]小,则用(v, vi)的权值更新lowcost[vi]
- 重复2、3步n-1次,使得其他n-1个顶点被并入至生成树中
vest[]数组
- vest[i] = 1 表示顶点i已被并入树中
- vest[i] = 0 表示顶点i未被并入树中
lowcost[]数组
- 存放当前生成树到剩余各顶点最短边的权值
void Prim(MGraph g, int v0, int& sum)
{
int lowcost[maxSize] = {}; //存放当前生成树到剩余各顶点最短边的权值
int vset[maxSize] = {}; //查看第i个顶点是否已被并入树中
int v = v0;
int i, j, k, min;
/*赋值*/
for (i = 0; i < g.n; i++)
{
lowcost[i] = g.edges[v0][i];
vset[i] = 0;
}
vset[v0] = 1; //将v0并入树中
sum = 0; //清零,用于累记树的权值
/*循环,将剩余顶点并入树中*/
for (i = 0; i < g.n - 1; i++)
{
min = INF;
/*选出边中的最小值*/
for (j = 0; j < g.n; j++)
if (vset[j] == 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;
}
vset[k] = 1; //并入树中
v = k;
sum += min; //记录权值
/*以刚并入的顶点v为媒介更新候选边*/
for (j = 0; j < g.n; j++)
if (vset[j] == 0 && g.edges[v][j] < lowcost[j])
lowcost[j] = g.edges[v][j];
}
}
4.2 Kruskal算法
每次找出候选边中权值最小的边,就将该边并入生成树中。重复此过程直到所有边都被检测完为止
- 将图中边按权值大小排序,然后从最小边开始扫描各边,并检测当前边是否为候选边,即是否该边的并入会产生回路,若不构成回路,则将该边并入当前生成树中,直到所有的边都被检测完为止。
并查集:
保存了几棵树,这些树可以通过树中一个结点找到其双亲结点,进而找到根节点
- 可以快速将两个含有多个元素的集合并为一个
- 可以方便地判断两个元素是否属于同一个集合
road[]:存放路中各边及其所连接两个顶点的信息
typedef struct
{
int a, b; //顶点信息
int value; //边的权值
}Road;
Road road[maxSize]; //记录路径
int v[maxSize]; //定义并查集
/*在并查集中寻找根节点*/
int getRoot(int a)
{
while (a != v[a])
a = v[a];
return a;
}
/*交换两值*/
void swap(int& a, int& b)
{
int temp = 0;
temp = a;
a = b;
b = temp;
}
/*排序*/
void sort(int n)
{
int min; //记录最小值
/*排序*/
for (int i = 0; i < n; i++)
{
min = i; //最小值赋值
for (int j = i + 1; j < n; j++)
{
if (road[j].value < road[min].value) //发现更小值
min = j; //更新最小值
if (min != i) //如果最小值发生变化,则需进行值的交换
swap(road[i].value, road[min].value);
}
}
}
/*最小生成树*/
void Kruskal(MGraph*& g)
{
int i, j, value = 0, a, b;
/*初始化并查集*/
for (i = 0; i < g->arcnum; i++)
v[i] = i;
sort(g->vexnum); //对road数组中的边按其权值大小排序
for (i = 0; i < g->vexnum; i++)
{
a = getRoot(road[i].a);
b = getRoot(road[i].b);
if (a != b)
{
v[a] = b;
/*输出*/
for (j = 0; j < g->vexnum; j++)
{
if (road[j].a == a && road[j].b == b)
value = road[j].value;
}
cout << g->vex[a] << "-<" << value << ">->" << g->vex[b] << " ";
}
}
}
5.最短路径
5.1 Dijkstra算法
辅助数组
- dist[i]:当前已找到的从V0到Vi的最短路径的长度,缺省为∞
- path[i]:从V0到Vi最短路径上的签一个顶点(Vi-1)
- set[i]:标记数组,为1表示Vi被并入最短路径中
- 过程:
例子:
- 输出路径:用栈
void print_path(int path[], int a)
{
int stack[maxSize], top = -1;
/*以由叶子结点到根节点的顺序将其入栈*/
while (path[a] != -1)
{
stack[++top] = a;
a = path[a];
}
stack[++top] = a;
while (top != -1)
cout << stack[top--] << ' ';
cout << endl;
}
- 算法实现
void Dijkstra(MGrapg g, int v, int dist[], int path[])
{
int set[maxSize];
int min, i, j, u;
/*初始化*/
for (i = 0; i < g.n; i++)
{
dist[i] = g.edges[v][i];
set[i] = 0;
if (g.edges[v][i] < INF)
path[i] = v;
else
path[i] = -1;
}
set[v] = 1;
path[v] = -1;
/*开始循环*/
for (i = 0; i < g.n - 1; i++)
{
min = INF;
/*从其余顶点中选出一个顶点,这个顶点的路径在通往所有剩余顶点的路径中是长度最短的*/
for (j = 0; j < g.n; j++)
if (set[j] == 0 && dist[j] < min)
{
u = j;
min = dist[j];
}
set[u] = 1;
/*以新并入的顶点作为中间点,对通往剩余顶点的路径进行检测*/
for (j = 0; j < g.n; j++)
{
/*判断u的加入是否会出现通往顶点j的更短路径*/
if (set[j] == 0 && dist[u] + g.edges[u][j] < dist[j])
{
dist[j] = dist[u] + g.edges[u][j];
path[j] = u;
}
}
}
/*函数结束后,dist数组中存放v点到其余顶点的最短路径长度,path中存放v点到其余顶点的最短路径*/
}
5.2 Floyd算法
例子:
- 输出路径
void print_path(MGrapg g, int u, int v, int path[][maxSize], int A[][maxSize])
{
if (A[u][v] == INF)
return;
else
{
if (path[u][v] == -1)
cout << g.edge[u][v] << ' ';
else
{
int mid = path[u][v];
print_path(g, u, mid, path, A); //处理mid前半段路径
print_path(g, mid, v, path, A); //处理mid后半段路径
}
}
}
- 算法实现
void Floyd(MGraph g, int path[][maxSize], int A[][maxSize])
{
int i, j, k;
/*初始化*/
for(i = 0; i < g.n; i++)
for (j = 0; j < g.n; j++)
{
A[i][j] = g.edges[i][j];
path[i][j] = -1;
}
/*以k为中间点,对所有的顶点对(i,j)进行检测和修改*/
for (k = 0; k < g.n; k++)
for (i = 0; i < g.n; i++)
for (j = 0; j < g.n; j++)
if (A[i][j] > A[i][k] + A[k][j])
{
A[i][j] = A[i][k] + A[k][j];
path[i][j] = k;
}
}
6.拓扑排序(AOV网)
活动在顶点上的网(Activity On Vertex network, AOV)
- 顶点表示活动
- 边表示活动的先后次序
- 没有回路
过程:
- 从有向图中选择一个没有前驱的顶点输出
- 删除该顶点,并删除从该顶点发出的所有边
- 重复以上两步,直到剩余的图中不存在没有前驱的顶点为止
逆拓扑排序:(AOV网)
- 在网中选一个没有后继的顶点(出度为0)输出
- 在网中删除该结点,并删除所有到达该顶点的边
- 重复上述两步,直到AOV网中已无出度为0的顶点为止
7.关键路径(AOE网)
活动在边上的网(Activity On Edge network, AOE)
- 边表示活动,权值代表活动的持续时间
- 顶点表示时间,时间是图中新活动开始或旧活动结束的标志
- 源点:入度为0的点,表示整个工程的开始
- 汇点:出度为0的点,表示整个工程的结束
- 关键路径:
- 从源点到汇点中具有最大路径长度的路径
- 关键路径的长度所代表的的时间就是完成整个工期的最短时间
- 关键活动:关键路径上的活动
求关键路径的方法:
- 关键活动:最早发生时间 = 最迟发生时间
- 剩余时间:活动最迟时间 - 活动最早发生时间
八:排序
1.冒泡排序
- 算法思想:
两两比较,经过一系列交换,直到最大的到最后- 时间复杂度:O(n2)
- 稳定
void bubble_sort(int arr[], const int num)
{
bool exchange = false;
int i, j;
for (i = num - 1; i >= 0; i--)
{
exchange = false; //标记是否发生交换
for (j = 0; j < i; j++)
if (arr[j] > arr[j + 1])
{
/*交换*/
swap(arr[j], arr[j + 1]);
/*标记*/
exchange = true;
}
/*没发生交换说明已经有序*/
if (!exchange)
break;
}
}
2.直接插入排序
- 算法思想:
每趟讲一个待排序的关键字按其值的大小插入到已经拍好的部分有序序列的适当位置上,直到所有待排关键字都被插入到有序序列中为止- 时间复杂度:O(n2)
- 稳定
void straight_insert_sort(int arr[], const int num)
{
int i, j;
for (i = 1; i < num; i++)
//swap(arr, j, j + 1)表示进行两个相邻数比较时,左边的数大于右边数时,才交换否则不交换
for (j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--)
swap(arr[j], arr[j + 1]);
}
3.希尔排序
- 算法思想:
通过不断缩小的增量分组,组内进行排序- 时间复杂度:
- 不稳定
void shell_sort(int arr[], const int num)
{
int i, j, temp;
int gap = num;
while (gap > 1)
{
gap = gap / 3 + 1; //求下一增量
for (i = gap; i < num; i++) //各子序列交替处理
{
if (arr[i - gap] > arr[i]) //逆序
{
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) //依次比较
arr[j + gap] = arr[j];
arr[j + gap] = temp; //将元素送回
}
}
}
}
4. 快速排序
- 算法思想:
每次选择一个基准数,比他小的放在左边,比他大的放在右边- 时间复杂度:O(nlog2n)
- 不稳定
int partition(int arr[], const int left, const int right)
{
/*基准元素*/
int pivotpos = left;
int pivot = arr[left];
/*检测整个序列,进行划分*/
for(int i = left + 1; i <= right; i++)
if (arr[i] < pivot) //将小于基准元素的值放到左边
{
pivotpos++; //每有一个小于它的元素,位置就要向右移一位
if (pivotpos != i)
{
/*交换*/
swap(arr[pivotpos], arr[i]);
change_times++;
}
}
/*将基准元素就位*/
arr[left] = arr[pivotpos];
arr[pivotpos] = pivot;
change_times++;
return pivotpos;
}
void quik_sort(int arr[], const int left, const int right)
{
if (left < right)
{
int pivotpos = partition(arr, left, right); //划分
quik_sort(arr, left, pivotpos - 1); //递归排序左侧
quik_sort(arr, pivotpos + 1, right); //递归排序右侧
}
}
5.选择排序
- 算法思想:
每次选出一个最小值放在最前面- 时间复杂度:O(n2)
- 不稳定
void select_sort(int arr[], const int num)
{
int i, j, min;
for (i = 0; i < num - 1; i++)
{
min = i; //设定最小值
for (j = i + 1; j < num - 1; j++)
{
if (arr[j] < arr[min]) //发现更小值
min = j; //更新最小值
}
if (min != i) //最小值发生变化
swap(arr[min], arr[i]); //交换
}
}
6.堆排序
- 算法思想:
调整为大根堆后,跟最后一个结点交换,再将其余的调整为大根堆- 时间复杂度:O(nlog2n)
- 不稳定
void shiftDown(int arr[], const int start, const int m)
{
/*j是i的左子女*/
int i = start;
int j = 2 * i + 1;
/*暂存子树根结点*/
int temp = arr[i];
/*循环构造大根堆*/
while (j <= m) //检查是否到最后位置
{
/*两个子女中j指向较大的*/
if (j < m && arr[j] < arr[j + 1])
j++;
if (temp >= arr[j]) //根节点大于子女结点
break;
else //根节点小于子女结点
{
arr[i] = arr[j]; //将根节点与较大的一个子女结点交换
i = j; //i下降到子女位置
j = 2 * i + 1;
}
}
arr[i] = temp; //temp中暂存元素放到合适位置
}
void heap_sort(int arr[], const int num)
{
int i;
/*自底向上将表转换为堆*/
for (i = (num - 2) / 2; i >= 0; i--)
shiftDown(arr, i, num - 1);
/*对表排序*/
for (i = num - 1; i >= 0; i--)
{
swap(arr[0], arr[i]); //将堆顶(最大元素)与最后一位元素交换
shiftDown(arr, 0, i - 1); //剩下的元素重新置为堆
}
}
7.归并排序
- 算法思想:
分治思想,先分,后合- 时间复杂度:O(n2)
- 稳定
void merge(int arr[], int arr2[], const int left, const int mid, const int right)
{
/*赋值*/
for (int i = left; i <= right; i++)
arr2[i] = arr[i];
/*检测指针*/
int s1 = left, s2 = mid + 1;
/*存放指针*/
int t = left;
/*两个表都未检测完,两两比较*/
while (s1 <= mid && s2 <= right)
{
if (arr2[s1] <= arr[s2])
arr[t++] = arr2[s1++];
else
arr[t++] = arr2[s2++];
}
/*若第一个表未检测完,复制*/
while (s1 <= mid)
arr[t++] = arr2[s1++];
/*若第二个表未检测完,复制*/
while (s2 <= right)
arr[t++] = arr2[s2++];
}
void merge_sort(int arr[], int arr2[], const int left, const int right)
{
/*终止条件*/
if (left >= right)
return;
int mid = (left + right) / 2; //递归划分为两个子列
merge_sort(arr, arr2, left, mid); //对左侧子列进行递归排序
merge_sort(arr, arr2, mid + 1, right); //对右侧子列进行递归排序
merge(arr, arr2, left, mid, right); //合并
}
8.基数排序
- 算法思想:
根据位数依次排序- 时间复杂度:O(d(n+rd))
- 稳定
/*基数排序(LSD)*/
//返回num的第n位的数
int getDigit(int num, int radix)
{
return ((num / radix) % 10);
}
//返回数组中元素的最大位数
int maxbit(int arr[], const int num)
{
int max = 1;
int stand = 10;
for (int i = 0; i < num; i++)
{
while (arr[i] >= stand)
{
stand *= 10;
max++;
}
}
return max;
}
void radix_sort(int arr[], const int num)
{
int radix = 1;
int digit = maxbit(arr, num); //最大位数
int i, j, d;
int* count = new int[10]; //计数器(实质上就是元素在桶内放置的位置)
int* bucket = new int[num]; //桶
start_time = clock(); //开始时间
/*从低位到高位进行排序*/
for (d = 1; d <= digit; d++)
{
/*初始化计数器*/
for (i = 0; i < 10; i++)
count[i] = 0;
/*统计各个筒要装入数据的个数*/
for (i = 0; i < num; i++)
{
j = getDigit(arr[i], radix);
count[j]++;
}
/*count[i]表示第i个筒的右边界索引*/
for (i = 1; i < 10; i++)
count[i] = count[i] + count[i - 1]; //前面每多一个,所放置的位置就要往后移动一位
/*从后往前,将数据依次装入筒中*/
for (i = num - 1; i >= 0; i--)
{
j = getDigit(arr[i], radix); //取数
bucket[count[j] - 1] = arr[i]; //放入桶内
count[j]--; //后被遍历到的数先进桶
}
for (i = 0; i < num; i++)
if (arr[i] != bucket[i])
{
arr[i] = bucket[i];
change_times++;
}
radix *= 10;
}
}
总结
- 插入排序:
- 最好情况:O(n),比较n-1次,交换0次
- 最坏情况:O(n2),比较n(n-1)/2次,交换(n+4)(n-1)/2次
- 平均情况:O(n2)
2.折半插入- 比较次数与序列初始状态无关,为log2(n!)次
- 移动次数与直接插入排序相同
3.希尔排序- 不稳定,对规模较大的序列效率高
- 全部正序比较n(log2n - 1)+1次
- 全部逆序比较n(3log2n - 4)/2+2次,移动105nlog2n次
4.冒泡排序- 最好情况:O(n),比较n-1次,交换0次
- 最坏情况:O(n2),比较n(n-1)/2次,交换3n(n-1)/2次
- 平均情况:O(n2)
5.快速排序- 不稳定
- 最好情况:O(nlog2n),空间复杂度O(log2n)
- 最坏情况:O(n2),比较n(n-1)/2次,移动2(n-1)次,空间复杂度O(n)
- 平均情况:O(nlog2n)
- 递归次数与每次划分后得到的分区的处理顺序无关
- 每次分区后先处理较短部分可以减少递归深度
6.选择排序- 选择排序的比较次数与元素的初始排列无关,为n(n-1)/2次
- 最坏情况下移动次数为3(n-1)
- O(n2)
7.堆排序- 从根结点到叶结点最多筛选⌊log2n⌋次
- O(nlog2n)
- 高度为n的堆最多容纳2h-1个元素,最少容纳2h-1个元素
8.锦标赛排序- O(nlog2n),空间复杂度O(n),稳定
9.归并排序- 比较次数和移动次数为O(nlog2n)
- 空间复杂度O(n)
- n个元素做m路归并排序需要⌈log2n⌉趟
- 两个长度为n的有序表合并时最少比较n次,最多比较2n-1次
- 对n个互异的数进行排序,需要比较log2(n!)次
- 最好情况下:直接插入和冒泡排序最快
- 最坏情况下:锦标赛排序,堆排序和归并排序最快
九:外排序
1.败者树
例
调整败者树
十:查找
1.折半查找
int Bsearch(int arr[], int low, int high, int k)
{
int mid;
while(low <= high)
{
mid = (low + high) / 2;
if(arr[mid] == k)
return mid;
else if (arr[mid] > k)
high = mid - 1;
else
low = mid + 1;
}
return 0;
}
2.B树