数据结构与算法
- 解决问题的效率与数据的组织方式有关
- 解决问题的效率与空间的利用效率有关
- 解决问题的效率与算法的巧妙程度有关
数据结构:数据对象在计算机中的组织方式
数据对象必定与一系列操作相关联
完成操作所用的方法就是算法
数据对象的结构:逻辑结构(线性结构,树,图),物理存储结构(链式,数组)
使用抽象数据类型描述数据结构
抽象数据类型包括:数据对象集 和 相关操作集
- 测试算法的执行时间模板
#include<stdio.h>
#include<time.h>
#define MAXN 1000
void MyFunction();
int main()
{
double duration;
clock_t start,stop;
int i;
start=clock();
for(i=0;i<MAXN;i++){
MyFunction();
}
stop=clock();
duration=(double)(stop-start)/CLK_TCK/MAXN;
printf("%6.2e\n",duration);
return 0;
}
void MyFunction()
{
};
线性结构
线性表
- 线性表:由同类型数据元素构成有序序列的线性结构
- 数据对象集:线性表L、元素X,表示位置的整数 i
- 操作集:
//初始化空线性表
List MakeEmpty()
//返回为序为K的元素
ElementType FindKth(int K,List L)
//查找元素X第一次出现的位置
int Find(ElementType X,List L)
//在位序i 前插入新元素X
void Insert(ElementType X,int i,List L)
//删除指定位序 i 的元素
void Delete(int i,List L)
//返回线性表的长度
int Length(List L)
- 存储类型:数组(简单数据、结构数组)、链表
- 数组存储的两种初始化形式:
/*------------------方式一---------------*/
typedef struct LNode *List;
struct LNode {
ElementType Data[MAXSIZE];
Position Last;
};
List MakeEmpty()
{
List L;
L = (List)malloc(sizeof(struct LNode));
L->Last = -1;
return L;
}
/*------------------方式二---------------*/
typedef int Position;
struct SNode {
ElementType *Data; /* 存储元素的数组 */
Position Top; /* 栈顶指针 */
int MaxSize; /* 堆栈最大容量 */
};
typedef struct SNode *Stack;
Stack MakeEmpty( int MaxSize )
{
Stack S = (Stack)malloc(sizeof(struct SNode));
S->Data = (ElementType *)malloc(MaxSize * sizeof(ElementType));
S->Top = -1;
S->MaxSize = MaxSize;
return S;
}
存储形式的不同,操作集的实现方式不同
堆栈(栈)
- 堆栈(Stack):具有一定操作约束的线性表(只在一端插入、删除)
- 特点:先进后出
- 对象集:一个有限元素的线性表
- 操作集:长度为MaxSize 的堆栈S,堆栈元素item
//初始化一个最大长度为MaxSize的空堆栈
Stack MakeStack(int MaxSize)
//判断堆栈是否已满(数组存储时)
bool IsFull(Stack S)
//将元素item压入堆栈
void Push(Stack S,ElementType iten)
//判断堆栈是否已空
bool IsEmpty(Stack S)
//删除并返回栈顶元素
ElementType Pop(Stack S)
- 回溯算法?
队列
- 队列(Queue):具有一定操作约束的线性表(只在一端插入、在另一端删除)
- 特点:先进先出
- 对象集:一个有限元素的线性表
- 操作集:长度为MaxSize 的队列Q,队列元素item
//初始化一个最大长度为MaxSize的空队列
Queue MakeEmpty(int MaxSize)
//判断队列是否已满
bool IsFullQ(Queue Q)
//将元素item加入队列Q中
void AddQ(Queue Q,ElementType item)
//判断队列是否为空
bool IsEmptyQ(Queue Q)
//将对头元素删除
ElementType DeleteQ (Queue Q)
树
- 查找:静态查找(只有查找操作)、动态查找(除了查找操作还有插入和删除操作)
顺序静态查找时,常在0位置设置哨兵防止数组溢出
二分静态查找时,元素必须有序 且 必须用数组存储
- 二分查找可以表示为判定树结构。根节点所在层数为第一层;树的深度为最大层数;每个结点的查找次数为结点所在的层数;判定树的深度为 log n(向下取整)+1
辨析:层数、深度和高度
- 树的特点:子数不相交;除根结点外,每个结点有且仅有一个父节点,所以一棵n个结点的树有n-1条边
- 树中路径长度为:路径所包含边的个数
- 满二叉树 (完美二叉树)
二叉树的重要性质
- 一个二叉树第 i 层的最大结点数为:2^(i-1)
- 深度为k的二叉树有最大结点总数为:2^k-1,k>=1
- 任何二叉树都有 n0=n2+1
二叉树的存储
- 顺序存储(完全二叉树)
- 链表存储(一般二叉树)
二叉树的遍历(链表存储)
void InorderTraversal( BinTree BT )
{
if( BT ) {
InorderTraversal( BT->Left );
/* 此处假设对BT结点的访问就是打印数据 */
printf("%d ", BT->Data); /* 假设数据为整型 */
InorderTraversal( BT->Right );
}
}
void PreorderTraversal( BinTree BT )
{
if( BT ) {
printf("%d ", BT->Data );
PreorderTraversal( BT->Left );
PreorderTraversal( BT->Right );
}
}
void PostorderTraversal( BinTree BT )
{
if( BT ) {
PostorderTraversal( BT->Left );
PostorderTraversal( BT->Right );
printf("%d ", BT->Data);
}
}
void LevelorderTraversal ( BinTree BT )
{
Queue Q;
BinTree T;
if ( !BT ) return; /* 若是空树则直接返回 */
Q = CreatQueue(); /* 创建空队列Q */
AddQ( Q, BT );
while ( !IsEmpty(Q) ) {
T = DeleteQ( Q );
printf("%d ", T->Data); /* 访问取出队列的结点 */
if ( T->Left ) AddQ( Q, T->Left );
if ( T->Right ) AddQ( Q, T->Right );
}
}
堆
- 堆(heap):特殊的队列,元素取出顺序依照关键字的大小
- 特点:结构性(数组表示的完全二叉树),有序性(任意结点元素值都比子树所有结点大)
- 对象集:完全二叉树
- 操作集:最大堆H,元素item
typedef struct HNode *Heap; /* 堆的类型定义 */
struct HNode {
ElementType *Data; /* 存储元素的数组 */
int Size; /* 堆中当前元素个数 */
int Capacity; /* 堆的容量 */
};
typedef Heap MaxHeap; /* 最大堆 */
typedef Heap MinHeap; /* 最小堆 */
#define MAXDATA 1000 /* 该值应根据具体情况定义为大于堆中所有可能元素的值 */
MaxHeap CreateHeap( int MaxSize )
{ /* 创建容量为MaxSize的空的最大堆 */
MaxHeap H = (MaxHeap)malloc(sizeof(struct HNode));
H->Data = (ElementType *)malloc((MaxSize+1)*sizeof(ElementType));
H->Size = 0;
H->Capacity = MaxSize;
H->Data[0] = MAXDATA; /* 定义"哨兵"为大于堆中所有可能元素的值*/
return H;
}
bool IsFull( MaxHeap H )
{
return (H->Size == H->Capacity);
}
bool Insert( MaxHeap H, ElementType X )
{
/* 将元素X插入最大堆H,其中H->Data[0]已经定义为哨兵 。元素X插入到堆的最后一个位置,然后不断与父节点比较确定最终位置*/
int i;
if ( IsFull(H) ) {
printf("最大堆已满");
return false;
}
i = ++H->Size; /* i指向插入后堆中的最后一个元素的位置 */
for ( ; H->Data[i/2] < X; i/=2 )
H->Data[i] = H->Data[i/2]; /* 上滤X */
H->Data[i] = X; /* 将X插入 */
return true;
}
bool IsEmpty( MaxHeap H )
{
return (H->Size == 0);
}
/*----------- 向下过滤函数 -----------*/
void FilterDown( MaxHeap H, int p )
{ /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */
int Parent, Child;
ElementType X;
X = H->Data[p]; /* 取出根结点存放的值 */
for( Parent=p; Parent*2<=H->Size; Parent=Child ) {
Child = Parent * 2;
if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
Child++; /* Child指向左右子结点的较大者 */
if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
else /* 下滤X */
H->Data[Parent] = H->Data[Child];
}
H->Data[Parent] = X;
}
#define ERROR -1 /* 错误标识应根据具体情况定义为堆中不可能出现的元素值 */
ElementType DeleteMax( MaxHeap H )
{ /* 从最大堆H中取出键值为最大的元素,并删除一个结点 */
int Parent, Child;
ElementType MaxItem, X;
if ( IsEmpty(H) ) {
printf("最大堆已为空");
return ERROR;
}
MaxItem = H->Data[1]; /* 取出根结点存放的最大值 */
/* 用最大堆中最后一个元素从根结点开始向下过滤下层结点 */
H->Data[1] = H->Data[H->Size--]; /* 注意当前堆的规模要减小 */
FilterDown(H,1);
return MaxItem;
}
/*----------- 建造最大堆 -----------*/
void BuildHeap( MaxHeap H )
{ /* 调整H->Data[]中的元素,使满足最大堆的有序性 */
/* 这里假设所有H->Size个元素已经存在H->Data[]中 */
int i;
/* 从最后一个结点的父节点开始,到根结点1 */
for( i = H->Size/2; i>0; i-- )
FilterDown( H, i );
}
Huffman Tree
- 哈夫曼树:
- 特点:没有度为1的结点,n个叶结点的哈夫曼树共有2n-1个结点,任意结点的左右子树交换后仍然是哈夫曼树,同一组权值存在同构的哈夫曼树。
构造方法:每次把权值最小的两棵二叉树合并。
- 操作对象:最小堆(数组存储)、二叉树(链表存储)
/*----------Huffman树的构造------------*/
typedef struct TreeNode *HuffmanTree;
struct TreeNode{
int Weight;
HuffmanTree Left,Right;
}
/*-----------在最小堆的基础上进行操作------*/
HuffmanTree Huffman(MinHeap H)
{
int i;
HuffmanTree T;
BuildMinHeap(H);
for(i=1;i<H->Size:i++){
T=malloc(sizeof(struct TreeNode));
T->Left=DeleteMin(H);
T->Right=DeleteMin(H);
T->Weight=T->Left->Weight+T->Right->Weight;
Insert(H,T);
}
T=DeleteMin(H);
return T;
}
- 哈夫曼编码解决了给定的字符串使用最少的存储空间存储
- 特点:不等长编码,前缀码(所有编码都在叶结点)
前缀码:任何字符的编码都不是另一字符编码的前缀
集合
- 并查集:合并集合,查找某元素属于什么集合
- 存储方式:结构数组
typedef sruct {
ElementType Data;
int Parent;
}SetType;
SetName Find(SetType s[],ElementType item)
{
int i;
/*在数组中找到元素所在位置 i */
for(i=0;i<MaxSize&&S[i].Data!=item;i++);
if(i>=MaxSize)return -1;
/*找到元素的根结点*/
for(;S[i].Parent>=0;i=S[i].Parent);
return i;
}
void Union(SetType S[],ElementType item1,ElementType item2)
{
int Root1,Root2;
Root1=Find(S,item1);
Root2=Find(S,item2);
/*不属于同一个集合才需要合并*/
if(Root1!=Root2){
S[Root2].Parent=Root1;
}
}
图
- 特点:反映多对多的关系
- 术语:无向图、有向图、网络,邻结点(和当前顶点直接相邻的点),顶点的度(从该点发出的边数为“出度”,指向该点的边数为“入度”)
- 数据对象集:G(V,E) 非空有向顶点结合V和有限边集合E
- 操作集:图G ,顶点v,边e
//建图
Graph Create();
//插入边
Graph InsertVertex(Graph G,Vertex v);
//插入顶点
Graph InserEdge(Graph G,Edge e);
//顶点v出发的深度优先遍历
void DFS(Graph G,Vertex v)
//顶点v出发的宽度优先遍历
void BFS(Graph G,Vertex v);
//计算图G中顶点v到任意顶点的最短距离
void ShortestPath(Graph G,Vertex v,int Dist[]);
//计算图G的最小生成树
void MST(Graph G);
- 图的存储:
- 邻接矩阵G [ N ][ N ] (无向图的邻接矩阵可以使用一维数组存储,下标之间的关系为 i*(i+1)/2+j
- 优点:方便检查任意一对顶点间是否存在边,方便找任一顶点的所有“邻结点”,方便计算任一顶点的“度”
- 缺点:浪费空间(对于稀疏图而言),浪费时间 (统计稀疏图中一共有多少条边时)
/* 图的邻接矩阵表示法 */
#define MaxVertexNum 100 /* 最大顶点数设为100 */
#define INFINITY 65535 /* ∞设为双字节无符号整数的最大65535*/
typedef int Vertex; /* 用顶点下标表示顶点,为整型 */
typedef int WeightType; /* 边的权值设为整型 */
typedef char DataType; /* 顶点存储的数据类型设为字符型 */
/* 边的定义 */
typedef struct ENode *PtrToENode;
struct ENode{
Vertex V1, V2; /* 有向边<V1, V2> */
WeightType Weight; /* 权重 */
};
typedef PtrToENode Edge;
/* 图结点的定义 */
typedef struct GNode *PtrToGNode;
struct GNode{
int Nv; /* 顶点数 */
int Ne; /* 边数 */
WeightType G[MaxVertexNum][MaxVertexNum]; /* 邻接矩阵 */
DataType Data[MaxVertexNum]; /* 存顶点的数据 */
/* 注意:很多情况下,顶点无数据,此时Data[]可以不用出现 */
};
typedef PtrToGNode MGraph; /* 以邻接矩阵存储的图类型 */
MGraph CreateGraph( int VertexNum )
{ /* 初始化一个有VertexNum个顶点但没有边的图 */
Vertex V, W;
MGraph Graph;
Graph = (MGraph)malloc(sizeof(struct GNode)); /* 建立图 */
Graph->Nv = VertexNum;
Graph->Ne = 0;
/* 初始化邻接矩阵 */
/* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
for (V=0; V<Graph->Nv; V++)
for (W=0; W<Graph->Nv; W++)
Graph->G[V][W] = INFINITY;
return Graph;
}
void InsertEdge( MGraph Graph, Edge E )
{
/* 插入边 <V1, V2> */
Graph->G[E->V1][E->V2] = E->Weight;
/* 若是无向图,还要插入边<V2, V1> */
Graph->G[E->V2][E->V1] = E->Weight;
}
MGraph BuildGraph()
{
MGraph Graph;
Edge E;
Vertex V;
int Nv, i;
scanf("%d", &Nv); /* 读入顶点个数 */
Graph = CreateGraph(Nv); /* 初始化有Nv个顶点但没有边的图 */
scanf("%d", &(Graph->Ne)); /* 读入边数 */
if ( Graph->Ne != 0 ) { /* 如果有边 */
E = (Edge)malloc(sizeof(struct ENode)); /* 建立边结点 */
/* 读入边,格式为"起点 终点 权重",插入邻接矩阵 */
for (i=0; i<Graph->Ne; i++) {
scanf("%d %d %d", &E->V1, &E->V2, &E->Weight);
/* 注意:如果权重不是整型,Weight的读入格式要改 */
InsertEdge( Graph, E );
}
}
/* 如果顶点有数据的话,读入数据 */
for (V=0; V<Graph->Nv; V++)
scanf(" %c", &(Graph->Data[V]));
return Graph;
}
- 邻接表G [ N ](指针数组,每行一个链表,只存非0 元素
- 优点:方便找任一顶点的所有“邻结点”(无向图),节约空间(相对于稀疏图而言),方便计算无向图的度
- 缺点:不方便检查任意一对顶点间是否存在边
/* 图的邻接表表示法 */
#define MaxVertexNum 100 /* 最大顶点数设为100 */
typedef int Vertex; /* 用顶点下标表示顶点,为整型 */
typedef int WeightType; /* 边的权值设为整型 */
typedef char DataType; /* 顶点存储的数据类型设为字符型 */
/* 边的定义 */
typedef struct ENode *PtrToENode;
struct ENode{
Vertex V1, V2; /* 有向边<V1, V2> */
WeightType Weight; /* 权重 */
};
typedef PtrToENode Edge;
/* 邻接点的定义 */
typedef struct AdjVNode *PtrToAdjVNode;
struct AdjVNode{
Vertex AdjV; /* 邻接点下标 */
WeightType Weight; /* 边权重 */
PtrToAdjVNode Next; /* 指向下一个邻接点的指针 */
};
/* 顶点表头结点的定义 */
typedef struct Vnode{
PtrToAdjVNode FirstEdge;/* 边表头指针 */
DataType Data; /* 存顶点的数据 */
/* 注意:很多情况下,顶点无数据,此时Data可以不用出现 */
} AdjList[MaxVertexNum]; /* AdjList是邻接表类型 */
/* 图结点的定义 */
typedef struct GNode *PtrToGNode;
struct GNode{
int Nv; /* 顶点数 */
int Ne; /* 边数 */
AdjList G; /* 邻接表 */
};
typedef PtrToGNode LGraph; /* 以邻接表方式存储的图类型 */
LGraph CreateGraph( int VertexNum )
{ /* 初始化一个有VertexNum个顶点但没有边的图 */
Vertex V;
LGraph Graph;
Graph = (LGraph)malloc( sizeof(struct GNode) ); /* 建立图 */
Graph->Nv = VertexNum;
Graph->Ne = 0;
/* 初始化邻接表头指针 */
/* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
for (V=0; V<Graph->Nv; V++)
Graph->G[V].FirstEdge = NULL;
return Graph;
}
void InsertEdge( LGraph Graph, Edge E )
{
PtrToAdjVNode NewNode;
/* 插入边 <V1, V2> */
/* 为V2建立新的邻接点 */
NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
NewNode->AdjV = E->V2;
NewNode->Weight = E->Weight;
/* 将V2插入V1的表头 */
NewNode->Next = Graph->G[E->V1].FirstEdge;
Graph->G[E->V1].FirstEdge = NewNode;
/* 若是无向图,还要插入边 <V2, V1> */
/* 为V1建立新的邻接点 */
NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
NewNode->AdjV = E->V1;
NewNode->Weight = E->Weight;
/* 将V1插入V2的表头 */
NewNode->Next = Graph->G[E->V2].FirstEdge;
Graph->G[E->V2].FirstEdge = NewNode;
}
LGraph BuildGraph()
{
LGraph Graph;
Edge E;
Vertex V;
int Nv, i;
scanf("%d", &Nv); /* 读入顶点个数 */
Graph = CreateGraph(Nv); /* 初始化有Nv个顶点但没有边的图 */
scanf("%d", &(Graph->Ne)); /* 读入边数 */
if ( Graph->Ne != 0 ) { /* 如果有边 */
E = (Edge)malloc( sizeof(struct ENode) ); /* 建立边结点 */
/* 读入边,格式为"起点 终点 权重",插入邻接矩阵 */
for (i=0; i<Graph->Ne; i++) {
scanf("%d %d %d", &E->V1, &E->V2, &E->Weight);
/* 注意:如果权重不是整型,Weight的读入格式要改 */
InsertEdge( Graph, E );
}
}
/* 如果顶点有数据的话,读入数据 */
for (V=0; V<Graph->Nv; V++)
scanf(" %c", &(Graph->G[V].Data));
return Graph;
}
- 图的遍历:
- 深度优先搜索(Depth First Search)
/*----------类似于树的先序遍历------------------*/
void DFS(Vertex V)
{
visited [ V ]=true;
for( V 的邻结点W){
if( !visited[ W ]){
DFS( W );
}
}
}
- 广度优先搜索(Breadth First Search)
/*---------类似于树的层序遍历-----------*/
void BFS(Vertex v)
{
visited[v]=true;
Enqueue(v,Queue);
while(!IsEmpty(Queue)){
v=Dequeue(Queue);
for(v的每个邻接点w){
if(!visited[w]){
visited[w]=true;
Enqueue(w,Queue);
}
}
}
}
连通:从v到w存在一条路径,则称v和w是连通的
简单路径:路径中所有顶点都不同
回路:起点等于终点的路径
连通图:图中任意两顶点均连通
连通分量:无向图的极大连通子图(满足极大顶点数,再加一个顶点就不连通;满足极大边数,包含子图中所有顶点相连的所有边)
强连通:有向图中顶点v和w之间存在双向路径,则称v和w是强连通的
强连通图:有向图中任意两顶点均强连通
强连通分量:有向图的极大强连通子图
- 访问图中的每个连通分量
void ListComponents(Graph G)
{
for(图G 中的每个顶点v){
if(!visited[v]){
DFS(v);//or BFS(v);
}
}
最短路径问题
-
最短路径(Shortext Path):在网络中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径
-
源点(Source):起点
-
终点(Destination):终点
-
单源最短路径问题:从某个固定源点出发,求其到所有其他顶点的最短路径
-
无权图(类似于BFS)
/* 邻接表存储 - 无权图的单源最短路算法 */
/* dist[]和path[]全部初始化为-1 */
void Unweighted ( LGraph Graph, int dist[], int path[], Vertex S )
{
Queue Q;
Vertex V;
PtrToAdjVNode W;
Q = CreateQueue( Graph->Nv ); /* 创建空队列, MaxSize为外部定义的常数 */
dist[S] = 0; /* 初始化源点 */
AddQ (Q, S);
while( !IsEmpty(Q) ){
V = DeleteQ(Q);
for ( W=Graph->G[V].FirstEdge; W; W=W->Next ) /* 对V的每个邻接点W->AdjV */
if ( dist[W->AdjV]==-1 ) { /* 若W->AdjV未被访问过 */
dist[W->AdjV] = dist[V]+1; /* W->AdjV到S的距离更新 */
path[W->AdjV] = V; /* 将V记录在S到W->AdjV的路径上 */
AddQ(Q, W->AdjV);
}
} /* while结束*/
}
- 有权图(Dijkstra算法)
/* 邻接矩阵存储 - 有权图的单源最短路算法 */
Vertex FindMinDist( MGraph Graph, int dist[], int collected[] )
{ /* 返回未被收录顶点中dist最小者 */
Vertex MinV, V;
int MinDist = INFINITY;
for (V=0; V<Graph->Nv; V++) {
if ( collected[V]==false && dist[V]<MinDist) {
/* 若V未被收录,且dist[V]更小 */
MinDist = dist[V]; /* 更新最小距离 */
MinV = V; /* 更新对应顶点 */
}
}
if (MinDist < INFINITY) /* 若找到最小dist */
return MinV; /* 返回对应的顶点下标 */
else return ERROR; /* 若这样的顶点不存在,返回错误标记 */
}
bool Dijkstra( MGraph Graph, int dist[], int path[], Vertex S )
{
int collected[MaxVertexNum];
Vertex V, W;
/* 初始化:此处默认邻接矩阵中不存在的边用INFINITY表示 */
for ( V=0; V<Graph->Nv; V++ ) {
dist[V] = Graph->G[S][V];
if ( dist[V]<INFINITY )
path[V] = S;
else
path[V] = -1;
collected[V] = false;
}
/* 先将起点收入集合 */
dist[S] = 0;
collected[S] = true;
while (1) {
/* V = 未被收录顶点中dist最小者 */
V = FindMinDist( Graph, dist, collected );
if ( V==ERROR ) /* 若这样的V不存在 */
break; /* 算法结束 */
collected[V] = true; /* 收录V */
for( W=0; W<Graph->Nv; W++ ) /* 对图中的每个顶点W */
/* 若W是V的邻接点并且未被收录 */
if ( collected[W]==false && Graph->G[V][W]<INFINITY ) {
if ( Graph->G[V][W]<0 ) /* 若有负边 */
return false; /* 不能正确解决,返回错误标记 */
/* 若收录V使得dist[W]变小 */
if ( dist[V]+Graph->G[V][W] < dist[W] ) {
dist[W] = dist[V]+Graph->G[V][W]; /* 更新dist[W] */
path[W] = V; /* 更新S到W的路径 */
}
}
} /* while结束*/
return true; /* 算法执行完毕,返回正确标记 */
}
- 多源最短路径问题:求任意两顶点间的最短路径
/* 邻接矩阵存储 - 多源最短路算法 */
bool Floyd( MGraph Graph, WeightType D[][MaxVertexNum], Vertex path[][MaxVertexNum] )
{
Vertex i, j, k;
/* 初始化 */
for ( i=0; i<Graph->Nv; i++ )
for( j=0; j<Graph->Nv; j++ ) {
D[i][j] = Graph->G[i][j];
path[i][j] = -1;
}
for( k=0; k<Graph->Nv; k++ )
for( i=0; i<Graph->Nv; i++ )
for( j=0; j<Graph->Nv; j++ )
if( D[i][k] + D[k][j] < D[i][j] ) {
D[i][j] = D[i][k] + D[k][j];
if ( i==j && D[i][j]<0 ) /* 若发现负值圈 */
return false; /* 不能正确解决,返回错误标记 */
path[i][j] = k;
}
return true; /* 算法执行完毕,返回正确标记 */
}
最小生成树
- 最小生成树(Minimum Spanning Tree):(最小)边的权重和最小;(生成树)包含全部顶点,V-1条边都在图里;(树)无回路,V个顶点一定有V-1条边。
- 特点:有V-1条图里的边,且没有回路。
- Prim算法:让小树长大
/* 邻接矩阵存储 - Prim最小生成树算法 */
Vertex FindMinDist( MGraph Graph, WeightType dist[] )
{ /* 返回未被收录顶点中dist最小者 */
Vertex MinV, V;
WeightType MinDist = INFINITY;
for (V=0; V<Graph->Nv; V++) {
if ( dist[V]!=0 && dist[V]<MinDist) {
/* 若V未被收录,且dist[V]更小 */
MinDist = dist[V]; /* 更新最小距离 */
MinV = V; /* 更新对应顶点 */
}
}
if (MinDist < INFINITY) /* 若找到最小dist */
return MinV; /* 返回对应的顶点下标 */
else return ERROR; /* 若这样的顶点不存在,返回-1作为标记 */
}
int Prim( MGraph Graph, LGraph MST )
{ /* 将最小生成树保存为邻接表存储的图MST,返回最小权重和 */
WeightType dist[MaxVertexNum], TotalWeight;
Vertex parent[MaxVertexNum], V, W;
int VCount;
Edge E;
/* 初始化。默认初始点下标是0 */
for (V=0; V<Graph->Nv; V++) {
/* 这里假设若V到W没有直接的边,则Graph->G[V][W]定义为INFINITY */
dist[V] = Graph->G[0][V];
parent[V] = 0; /* 暂且定义所有顶点的父结点都是初始点0 */
}
TotalWeight = 0; /* 初始化权重和 */
VCount = 0; /* 初始化收录的顶点数 */
/* 创建包含所有顶点但没有边的图。注意用邻接表版本 */
MST = CreateGraph(Graph->Nv);
E = (Edge)malloc( sizeof(struct ENode) ); /* 建立空的边结点 */
/* 将初始点0收录进MST */
dist[0] = 0;
VCount ++;
parent[0] = -1; /* 当前树根是0 */
while (1) {
V = FindMinDist( Graph, dist );
/* V = 未被收录顶点中dist最小者 */
if ( V==ERROR ) /* 若这样的V不存在 */
break; /* 算法结束 */
/* 将V及相应的边<parent[V], V>收录进MST */
E->V1 = parent[V];
E->V2 = V;
E->Weight = dist[V];
InsertEdge( MST, E );
TotalWeight += dist[V];
dist[V] = 0;
VCount++;
for( W=0; W<Graph->Nv; W++ ) /* 对图中的每个顶点W */
if ( dist[W]!=0 && Graph->G[V][W]<INFINITY ) {
/* 若W是V的邻接点并且未被收录 */
if ( Graph->G[V][W] < dist[W] ) {
/* 若收录V使得dist[W]变小 */
dist[W] = Graph->G[V][W]; /* 更新dist[W] */
parent[W] = V; /* 更新树 */
}
}
} /* while结束*/
if ( VCount < Graph->Nv ) /* MST中收的顶点不到|V|个 */
TotalWeight = ERROR;
return TotalWeight; /* 算法执行完毕,返回最小权重和或错误标记 */
}
- Kruskal算法:将森林合并成树
/* 邻接表存储 - Kruskal最小生成树算法 */
/*-------------------- 顶点并查集定义 --------------------*/
typedef Vertex ElementType; /* 默认元素可以用非负整数表示 */
typedef Vertex SetName; /* 默认用根结点的下标作为集合名称 */
typedef ElementType SetType[MaxVertexNum]; /* 假设集合元素下标从0开始 */
void InitializeVSet( SetType S, int N )
{ /* 初始化并查集 */
ElementType X;
for ( X=0; X<N; X++ ) S[X] = -1;
}
void Union( SetType S, SetName Root1, SetName Root2 )
{ /* 这里默认Root1和Root2是不同集合的根结点 */
/* 保证小集合并入大集合 */
if ( S[Root2] < S[Root1] ) { /* 如果集合2比较大 */
S[Root2] += S[Root1]; /* 集合1并入集合2 */
S[Root1] = Root2;
}
else { /* 如果集合1比较大 */
S[Root1] += S[Root2]; /* 集合2并入集合1 */
S[Root2] = Root1;
}
}
SetName Find( SetType S, ElementType X )
{ /* 默认集合元素全部初始化为-1 */
if ( S[X] < 0 ) /* 找到集合的根 */
return X;
else
return S[X] = Find( S, S[X] ); /* 路径压缩 */
}
bool CheckCycle( SetType VSet, Vertex V1, Vertex V2 )
{ /* 检查连接V1和V2的边是否在现有的最小生成树子集中构成回路 */
Vertex Root1, Root2;
Root1 = Find( VSet, V1 ); /* 得到V1所属的连通集名称 */
Root2 = Find( VSet, V2 ); /* 得到V2所属的连通集名称 */
if( Root1==Root2 ) /* 若V1和V2已经连通,则该边不能要 */
return false;
else { /* 否则该边可以被收集,同时将V1和V2并入同一连通集 */
Union( VSet, Root1, Root2 );
return true;
}
}
/*-------------------- 并查集定义结束 --------------------*/
/*-------------------- 边的最小堆定义 --------------------*/
void PercDown( Edge ESet, int p, int N )
{ /* 改编代码4.24的PercDown( MaxHeap H, int p ) */
/* 将N个元素的边数组中以ESet[p]为根的子堆调整为关于Weight的最小堆 */
int Parent, Child;
struct ENode X;
X = ESet[p]; /* 取出根结点存放的值 */
for( Parent=p; (Parent*2+1)<N; Parent=Child ) {
Child = Parent * 2 + 1;
if( (Child!=N-1) && (ESet[Child].Weight>ESet[Child+1].Weight) )
Child++; /* Child指向左右子结点的较小者 */
if( X.Weight <= ESet[Child].Weight ) break; /* 找到了合适位置 */
else /* 下滤X */
ESet[Parent] = ESet[Child];
}
ESet[Parent] = X;
}
void InitializeESet( LGraph Graph, Edge ESet )
{ /* 将图的边存入数组ESet,并且初始化为最小堆 */
Vertex V;
PtrToAdjVNode W;
int ECount;
/* 将图的边存入数组ESet */
ECount = 0;
for ( V=0; V<Graph->Nv; V++ )
for ( W=Graph->G[V].FirstEdge; W; W=W->Next )
if ( V < W->AdjV ) { /* 避免重复录入无向图的边,只收V1<V2的边 */
ESet[ECount].V1 = V;
ESet[ECount].V2 = W->AdjV;
ESet[ECount++].Weight = W->Weight;
}
/* 初始化为最小堆 */
for ( ECount=Graph->Ne/2; ECount>=0; ECount-- )
PercDown( ESet, ECount, Graph->Ne );
}
int GetEdge( Edge ESet, int CurrentSize )
{ /* 给定当前堆的大小CurrentSize,将当前最小边位置弹出并调整堆 */
/* 将最小边与当前堆的最后一个位置的边交换 */
Swap( &ESet[0], &ESet[CurrentSize-1]);
/* 将剩下的边继续调整成最小堆 */
PercDown( ESet, 0, CurrentSize-1 );
return CurrentSize-1; /* 返回最小边所在位置 */
}
/*-------------------- 最小堆定义结束 --------------------*/
int Kruskal( LGraph Graph, LGraph MST )
{ /* 将最小生成树保存为邻接表存储的图MST,返回最小权重和 */
WeightType TotalWeight;
int ECount, NextEdge;
SetType VSet; /* 顶点数组 */
Edge ESet; /* 边数组 */
InitializeVSet( VSet, Graph->Nv ); /* 初始化顶点并查集 */
ESet = (Edge)malloc( sizeof(struct ENode)*Graph->Ne );
InitializeESet( Graph, ESet ); /* 初始化边的最小堆 */
/* 创建包含所有顶点但没有边的图。注意用邻接表版本 */
MST = CreateGraph(Graph->Nv);
TotalWeight = 0; /* 初始化权重和 */
ECount = 0; /* 初始化收录的边数 */
NextEdge = Graph->Ne; /* 原始边集的规模 */
while ( ECount < Graph->Nv-1 ) { /* 当收集的边不足以构成树时 */
NextEdge = GetEdge( ESet, NextEdge ); /* 从边集中得到最小边的位置 */
if (NextEdge < 0) /* 边集已空 */
break;
/* 如果该边的加入不构成回路,即两端结点不属于同一连通集 */
if ( CheckCycle( VSet, ESet[NextEdge].V1, ESet[NextEdge].V2 )==true ) {
/* 将该边插入MST */
InsertEdge( MST, ESet+NextEdge );
TotalWeight += ESet[NextEdge].Weight; /* 累计权重 */
ECount++; /* 生成树中边数加1 */
}
}
if ( ECount < Graph->Nv-1 )
TotalWeight = -1; /* 设置错误标记,表示生成树不存在 */
return TotalWeight;
}
拓扑排序
- 拓扑序:图中从v到w有一条有向路径,则v一定排在w之前。满足此条件的顶点序列称为一个拓扑序
- 拓扑排序:获得一个拓扑序的过程
- 作用:排课(预修课程关系)
-AOV(Activity On Edge):顶点表示活动
AOV如果有合理的拓扑序,则必定是有向无环图
- 算法描述:
- 改进算法:(该算法的另一个作用是用来检测有向图中是否DAG(有向无环))
关键路径
- 关键路径:由绝对不允许延误的活动组成的路径
- AOE网络(Activity On Edge):边表示活动
- 作用:
- 举例:
排序
- 内部排序(内存足够大)
- 稳定性:任意两个相等的数据,排序前后的相对位置不发生改变
冒泡排序
- 冒泡排序:稳定,可以对单向链表进行排序
插入排序
- 插入排序:稳定
- 时间复杂度下界
希尔排序
- 原始希尔排序代码:
- 改进的增量序列
选择排序
堆排序(选择排序的演变)
归并排序
常用于外排
- 核心Merge
- Merge代码:
- 归并排序(递归实现:稳定)
- 函数接口
- 归并排序(非递归实现:稳定)
- Merge_pass
- 接口函数
快速排序
- 核心思想:分而治之
最好情况每次中分
- 选主元
- 子集划分(元素等于pivot时停下来交换)
- 快速排序代码:
/* 快速排序 - 直接调用库函数 */
#include <stdlib.h>
/*---------------简单整数排序--------------------*/
int compare(const void *a, const void *b)
{ /* 比较两整数。非降序排列 */
return (*(int*)a - *(int*)b);
}
/* 调用接口 */
qsort(A, N, sizeof(int), compare);
/*---------------简单整数排序--------------------*/
/*--------------- 一般情况下,对结构体Node中的某键值key排序 ---------------*/
struct Node {
int key1, key2;
} A[MAXN];
int compare2keys(const void *a, const void *b)
{ /* 比较两种键值:按key1非升序排列;如果key1相等,则按key2非降序排列 */
int k;
if ( ((const struct Node*)a)->key1 < ((const struct Node*)b)->key1 )
k = 1;
else if ( ((const struct Node*)a)->key1 > ((const struct Node*)b)->key1 )
k = -1;
else { /* 如果key1相等 */
if ( ((const struct Node*)a)->key2 < ((const struct Node*)b)->key2 )
k = -1;
else
k = 1;
}
return k;
}
/* 调用接口 */
qsort(A, N, sizeof(struct Node), compare2keys);
/*--------------- 一般情况下,对结构体Node中的某键值key排序 ---------------*/
/* 快速排序 */
ElementType Median3( ElementType A[], int Left, int Right )
{
int Center = (Left+Right) / 2;
if ( A[Left] > A[Center] )
Swap( &A[Left], &A[Center] );
if ( A[Left] > A[Right] )
Swap( &A[Left], &A[Right] );
if ( A[Center] > A[Right] )
Swap( &A[Center], &A[Right] );
/* 此时A[Left] <= A[Center] <= A[Right] */
Swap( &A[Center], &A[Right-1] ); /* 将基准Pivot藏到右边*/
/* 只需要考虑A[Left+1] … A[Right-2] */
return A[Right-1]; /* 返回基准Pivot */
}
void Qsort( ElementType A[], int Left, int Right )
{ /* 核心递归函数 */
int Pivot, Cutoff, Low, High;
if ( Cutoff <= Right-Left ) { /* 如果序列元素充分多,进入快排 */
Pivot = Median3( A, Left, Right ); /* 选基准 */
Low = Left; High = Right-1;
while (1) { /*将序列中比基准小的移到基准左边,大的移到右边*/
while ( A[++Low] < Pivot ) ;
while ( A[--High] > Pivot ) ;
if ( Low < High ) Swap( &A[Low], &A[High] );
else break;
}
Swap( &A[Low], &A[Right-1] ); /* 将基准换到正确的位置 */
Qsort( A, Left, Low-1 ); /* 递归解决左边 */
Qsort( A, Low+1, Right ); /* 递归解决右边 */
}
else InsertionSort( A+Left, Right-Left+1 ); /* 元素太少,用简单排序 */
}
void QuickSort( ElementType A[], int N )
{ /* 统一接口 */
Qsort( A, 0, N-1 );
}
- 小规模数据建议设置阈值
表排序
- 问题来源:当元素的信息很大,移动的时间不可以忽略时。采用表排序可以不直接移动元素,而只是对相应指针进行排序。
- 当元素之间必须进行交换时
最坏的情况是:
桶排序
- 问题的提出:
基数排序
- 问题描述