浙江大学陈越老师《数据结构与算法》课程笔记

目录

1.1 什么是数据结构

1.1 解决问题方法的效率

(1)图书管理:和数据组织方式有关

(2)printN:和空间利用率有关

(3)多项式加法:和算法的巧妙程度有关

  • 用clock来打点计时,clock_t型数据,除以CLOCKS_PER_SEC

1.2 数据结构的定义

  • 数据结构是关于数据对象在计算机中的组织方式

  • 包括逻辑结构和物理存储结构

  • 数据对象必定与一系列加在其上的操作相关,完成这些操作的方法就是算法

1.3 抽象数据类型

  • 数据类型:数据对象集和相关联的操作集

  • 抽象:描述数据类型的方法不依赖于具体实现

1.2 什么是算法

1.2.1 算法指标

  • 空间复杂度S(n):占用存储单元的长度

  • 时间复杂度T(n):耗费时间的长度

  • n是数据规模

1.2.2 复杂度的渐进表示法

  • O(f(n))是T(n)的最小上边界

  • T1(n)+T2(n)=max(O(f1(n), O(f2(n))

  • T1(n)*T2(n)=O(f1(n))* O(f2(n))

  • 若T(n)是关于n的k阶多项式,那T(n)=O(n^k)

  • for循环的复杂度为循环次数乘以循环体代码的复杂度

  • if-else复杂度为三者中最大

1.2.3 例子—求最大连续子列和

  • 分而治之: T(n)=O(nlogn)

  • 在线处理:每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能给出当前的正解

2.1 线性表及其实现

2.1.1 引子—多项式的表示

  • 顺序存储结构:结构数组

  • 链表结构

2.1.2 线性表

(1)线性表定义

  • 由同类型数据元素构成有序序列的线性结构

(2)线性表的抽象数据类型描述

  • 类型名称:线性表(List)
  • 数据对象集:n个元素构成的有序序列
  • 操作集:初始化、查找元素、查找位置、插入、删除

2.1.2 顺序存储

  • 利用数组连续存放
  • 下标从0开始
typedef struct LNode *List;
struct LNode{
    ElementType Data[MAXSIZE];
    int Last;
};
struct LNode L;
List PtrL;

2.1.3 链式存储

  • 下标从1开始
typedef struct LNode *List;
struct LNode{
    ElementType Data;  //节点对应的数据
    List Next;  //下一个节点的位置
};
struct Lnode L;
List PtrL;

2.1.4 广义表与多重链表

  • 二元多项式用广义表表示
  • 广义表的定义
typedef struct GNode *GList;
struct GNode{
int Tag; /*标志域:0表示结点是单元素,1表示结点是广义表 */
union { /*子表指针域Sublist与单元素数据域Data复用,即共用存储空间*/
ElementType Data;
GList SubList;
} URegion;
GList Next; /* 指向后继结点 */
};
  • 多重链表:链表中的节点可能同时隶属于多个链
  • 采用十字链表来存储稀疏矩阵

2.2 堆栈及实现

2.2.1 什么是堆栈

(1)计算机如何进行表达式求值

  • 利用栈求后缀表达式
  • 堆栈也是线性表,只在一端做插入、删除

2.2.2 堆栈的顺序存储实现

  • 结构体:Data数组和Top值

2.2.3 堆栈的链式存储实现

  • 结构体:Data值和Next指针
  • Top指针指向头结点

2.2.4 堆栈应用:表达式求值

(1)将中缀表达式转化为后缀表达式

  • 从头到尾读取 中缀表达式的每个对象 ,对不同对象按不同的情况处理
    ① 运算数:直接输出;
    ② 左括号:压入堆栈;
    ③ 右括号:将栈顶的运算符弹出并输出,直到遇到左括号( 出栈,不输出);
    ④ 运算符:

    • 若优先级大于栈顶运算符时,则把它压栈;
    • 若优先级小于等于栈顶运算符时,将栈顶运算符弹出并输出;再比较新的栈顶运算符,直到该运算符大于 栈顶运算符优先级为止,然后将该运算符压栈;

    ⑤ 若各对象 处理完毕,则把堆栈中存留的运算符一并输出。

(2)函数调用及递归实现

(3)回溯算法

2.3 队列及实现

2.3.1 队列及顺序存储实现

(1)队列的定义

  • 具有一定操作约束的线性表,只能在一端插入,另一端删除

(2)队列的顺序存储实现

  • 由一个一维数组和一个记录队列头位置的变量front和记录尾的变量rear组成

    • 插入rear+1,删除front+1
  • 循环队列:充分利用空间,front=rear时队列为空

2.3.2 队列的链式存储实现

  • 在链表头部做删除(front指向头),在链表尾部做插入(rear指向尾)
struct Node{
ElementType Data;
struct Node *Next;
};
struct QNode{ /* 链队列结构 */
struct Node *rear; /* 指向队尾结点 */
struct Node *front; /* 指向队头结点 */
};

2.4 应用实例:多项式加法运算

3.1 树与树的表示

3.1.1 引子

  • 树的分层管理在查找上效率更高

(1)查找

  • 根据给定关键字,从集合R中找出关键字与K相同的记录
  • 静态查找:集合中记录是固定的
  • 动态查找:集合中记录是动态变化的,可以插入和删除

(2)顺序查找

  • 建立哨兵,可以减少一个判断条件,不成功则返回0
  • O(N)

(3)二分查找

  • 时间负责度O(logN)

(4)二分查找判定树

  • 判定树上每个结点需要的查找次数刚好为所在的层数
  • 查找成功时次数不会超过树的深度
  • 以树的形式存储数据,可以方便插入和删除,解决动态查找问题

3.1.4 树的定义和术语

(1)定义

  • 根和子树
  • 子树不相交;除根结点外,每个结点有且仅有一个父结点;一颗N结点的树有N-1条边

(2)结点

  • 结点的度:结点的子树个数
  • 树的度:树的所有结点中最大的度数
  • 树的深度:所有结点中的最大层次是这颗树的深度
  • 兄弟结点

3.1.5 树的表示

  • 链表:儿子兄弟表示法
  • 旋转45度变为二叉树

3.2.1 二叉树的定义及性质

(1)定义

  • 由根节点和左子树、右子树组成
  • 二叉树有左右子树之分,普通二叉树没有

(2)特殊二叉树

  • 完美二叉树
  • 完全二叉树

(3)重要性质

  • 深度为k的二叉树最大结点总数为2^k-1
  • n_0=n_2+1

(4)遍历方法

  • 先序、中序、后续、层次遍历

3.2.2 二叉树的存储结构

(1)顺序存储结构

  • 完全二叉树,按顺序编号,父结点的序号是[i/2],左孩子结点是2*i,右孩子结点是2*i+1

  • 一般的补为完全二叉树,有空间浪费

(2)链表存储

  • 指向左右孩子

3.3.1 先序中序后序递归遍历

(1)先序遍历

  • 先访问根节点,先序遍历其左子树,先序遍历其右子树

(2)中序遍历

  • 中序遍历左子树,访问根节点,中序遍历右子树

(3)后序遍历

  • 后序遍历左子树,后序遍历右子树,访问根节点

3.3.2 中序非递归遍历

  • 遇到一个结点,压栈,遍历其左子树
  • 左子树遍历结束后,从栈顶弹出这个结点并访问
  • 再去中序遍历该结点的右子树

3.3.3 层序遍历

  • 从队列中取出一个元素
  • 访问该元素
  • 将左孩子右孩子入队列

3.3.4 树的同构

  • T1通过若干次左右孩子互换变成T2,就称同构

4.1 二叉搜索树

4.1.1 二叉搜索树及查找

(1)特点

  • 左子树上的所有结点小于根节点
  • 右子树上所有结点大于根节点

(2)相关函数

  • 查找特定值函数,查找最小值函数,查找最大值函数
  • 插入和删除函数

(3)Find函数

  • 尾递归(在函数返回时递归)函数改为迭代函数
Position IterFind( ElementType X, BinTree BST )
{
	while( BST ) {
		if( X > BST->Data )
			BST = BST->Right; /* 向右子树中移动 , 继续查找*/
		else if( X < BST->Data )
			BST = BST->Left; /* 向左子树中移动 , 继续查找*/
		else /* X == BST->Data */
			return BST; /* 查找成功 , 返回结点的找到结点的地址*/
	}
	return NULL; /* 查找失败*/
}
  • 查找的效率取决于树的高度->平衡二叉树

(4)查找最小值和最大值函数

  • 查找指针指向最左和最右结点

4.1.2 二叉搜索树的插入

BinTree Insert( ElementType X, BinTree BST )
{
    if( !BST ){
    /* 若原树为空 , 生成并返回一个结点的二叉搜索树*/
        BST = malloc(sizeof(struct TreeNode));
        BST->Data = X;
        BST->Left = BST->Right = NULL;
    }else /* 开始找要插入元素的位置*/
    	if( X < BST->Data )
    		BST->Left = Insert( X, BST->Left);
    					/* 递归插入左子树*/
    	else if( X > BST->Data )
    		BST->Right = Insert( X, BST->Right);
    					/* 递归插入右子树*/
    		/* else X 已经存在 , 什么都不做 */
    return BST;
}

4.1.3 二叉搜索树的删除

(1)分三种情况

  • 删除叶结点:直接删除,修改父节点指针为null
  • 删除结点只有一个孩子:将父节点的指针指向要删除的结点的孩子结点
  • 删除结点有两个孩子:用右子树的最小元素或者左子树的最大元素替代被删除结点

(2)代码

BinTree Delete( ElementType X, BinTree BST )
    { Position Tmp;
    if( !BST ) printf(" 要删除的元素未找到");
    else if( X < BST->Data )
    	BST->Left = Delete( X, BST->Left); /* 左子树递归删除 */
    else if( X > BST->Data )
    	BST->Right = Delete( X, BST->Right); /* 右子树递归删除 */
    else /* 找到要删除的结点 */
    	if( BST->Left && BST->Right ) { /* 被删除结点有左右两个子结点 */
    		Tmp = FindMin( BST->Right );
    		/* 在右子树中找最小的元素填充删除结点*/
   			BST->Data = Tmp->Data;
    		BST->Right = Delete( BST->Data, BST->Right);
    		/* 在删除结点的右子树中删除最小元素*/
    	} else { /* 被删除结点有一个或无子结点*/
    		Tmp = BST;
    		if( !BST->Left ) /* 有右孩子或无子结点*/
    			BST = BST->Right;
    		else if( !BST->Right ) /* 有左孩子或无子结点*/
    			BST = BST->Left;
    		free( Tmp );
    	}
    return BST;
}

4.1.4 什么是平衡二叉树

  • 平均查找长度ASL

  • 平衡因子BF:某一结点左右子树的高度差

  • 平衡二叉树(AVL树):对任一结点,BF<=1

4.1.5 平衡二叉树的调整

  • 破坏者和被破坏者
  • RR旋转

  • LL旋转

  • LR旋转

  • RL旋转

  • 下面平衡了上面就平衡了

5.1 堆

5.1.1 什么是堆

  • 结构性:用数组表示的完全二叉树
  • 有序性:任意结点的关键字是其子树所有结点的最大/最小值(大顶堆/小顶堆)

5.1.2 堆的插入

(1)最大堆的创建

  • 完全二叉树用数组存储

(2)最大堆的插入

  • 找到最后一个结点,和父节点进行比较,大的话往上挪
void Insert( MaxHeap H, ElementType item )
{ /* 将元素item 插入最大堆H , 其中H->Elements[0] 已经定义为哨兵 */
    int i;
    if ( IsFull(H) ) {
        printf(" 最大堆已满");
        return;
    }
    i = ++H->Size; /* i 指向插入后堆中的最后一个元素的位置 */
    for ( ; H->Elements[i/2] < item; i/=2 )
        H->Elements[i] = H->Elements[i/2]; /* 向下过滤结点 */
        H->Elements[i] = item; /* 将item 插入 */
}

5.1.3 堆的删除

(1)最大堆删除根节点

  • 最后一个元素补充到根节点处
  • 如果左右子树有大于根节点的,将根节点往下移

5.1.4 堆的建立

  • 将N个元素按输入顺序存入
  • 从低层开始,逐步向上,将根节点调整为堆
  • 时间复杂度为O(n)

5.2 哈夫曼树与哈夫曼编码

5.2.1 什么是哈夫曼树

  • 场景:找到一颗判定树,使得查找效率最高

  • 方法:根据结点不同的查找频率来构造有效的搜索树

  • 带权路径长度(WPL):每个叶子结点带有权值,算总和

  • 最优二叉树(哈夫曼树):WPL最小的二叉树

5.2.2 哈夫曼树的构造

  • 每次把权值最小的两颗二叉树合并
  • 算法,复杂度为O(N*logN),借助最小堆完成
typedef struct TreeNode *HuffmanTree;
struct TreeNode{
    int Weight;
    HuffmanTree Left, Right;
}
HuffmanTree Huffman( MinHeap H )
{ /* 假设H->Size 个权值已经存在H->Elements[]->Weight 里 */
    int i; HuffmanTree T;
    BuildMinHeap(H); /* 将H->Elements[] 按权值调整为最小堆*/
    for (i = 1; i < H->Size; i++) { /* 做H->Size-1 次合并*/
        T = malloc( sizeof( struct TreeNode) ); /* 建立新结点*/
        T->Left = DeleteMin(H);
        /* 从最小堆中删除一个结点 , 作为新T 的左子结点*/
        T->Right = DeleteMin(H);
        /* 从最小堆中删除一个结点 , 作为新T 的右子结点*/
        T->Weight = T->Left->Weight+T->Right->Weight;
        /* 计算新权值*/
        Insert( H, T ); /* 将新T 插入最小堆*/
    }
    T = DeleteMin(H);
    return T;
}

5.2.3 哈夫曼编码

  • 给定一段字符串,如何对字符编码,使得该字符串编码的存储空间最少
  • 不等长编码避免二义性:使用前缀码,将二叉树用于编码
  • 将字符频率作为权值,求哈夫曼树

5.3 集合及运算

5.3.1 集合的表示及查找

(1)并查集运算

  • 集合并、查某元素属于什么集合
  • 电脑联通性问题

(2)集合的存储

  • 用树结构表示集合,每个结点代表一个集合元素,根结点代表集合
  • 双亲表示法:孩子指向双亲
  • 采用数组存储形式

5.3.2 集合运算

  • 查找某个元素所在的集合,用根结点表示
  • 并运算:分别找到两个元素所在集合树的根节点,如果不同根,将其中一个根节点的Parent设置为另一个根节点的数组下标

6.1 什么是图

6.1.1什么是图

  • 表示多对多的关系
  • 包含一组顶点V和一组边E
  • 不考虑重边和自回路
  • 无向图、有向图、带权重的图(网络)

6.1.2 邻接矩阵表示法

  • 无向图只用存一半节省空间

  • 求邻接点和度方便

  • 存稀疏图浪费空间时间

6.1.3 邻接表表示法

6.2 图的遍历

6.2.1 DFS

  • 类似树的先序遍历,栈
  • 邻接矩阵O(N^2),邻接表O(N+E)

6.2.2 BFS

  • 类似树的层次遍历,队列
  • 邻接矩阵O(N^2),邻接表O(N+E)

6.2.3 图不连通怎么额办

  • 连通:从V到W存在一条路径

  • 路径:V到W的路径是顶点的集合,若集合中所有顶点不同,称简单路径

  • 回路:起点等于终点的路径

  • 连通图:图中任意两顶点均连通

  • 连通分量:无向图的极大连通子图(极大顶点数和极大边数)

  • 强连通:有向图中V和W之间存在双向路径

  • 强连通和强连通分量

  • 弱连通:不是强连通且变为无向图后连通

  • 对于不连通的图,每个连通分量都遍历一遍

7.1 有向图最短路径问题

7.1.1 概述

  • 最短路径的源点和终点
  • 单源最短路径问题:从某固定源点除法,求到所有其他顶点的最短路径
  • 多源最短路径问题:求任意两顶点间的最短路径

7.1.2 无权图的单源最短路径

  • 按照递增顺序通过BFS逐步找出到各个顶点的最短路径
void Unweighted ( Vertex S )
{ Enqueue(S, Q);
    while(!IsEmpty(Q)){
        V = Dequeue(Q);
        for ( V 点 的每个邻接点 W )
    		if ( dist[W]==-1 ) {
                dist[W] = dist[V]+1;
                path[W] = V;
                Enqueue(W, Q);
    	}
    }
}

7.1.3 有权图的单源最短路

  • Dijkstra算法,不考虑负值圈

(1)分析

  • S=
  • 对任一未收录的顶点v,dist[v]为s到v的最短路径长度,但该路径长度仅经过S中的顶点
  • 路径是按照递增长度的顺序生成的
    • 真正的最短路径必然只经过S中的顶点
    • 每次从未收录的顶点中选dist最小的收录(贪心)
    • 收录v进入s,只可能影响到同v相邻的未收录w的dist值

(2)代码

void Dijkstra(Vertex s) {
    while(1){
        v=未收录顶点中找dist最小者;
        if (v不存在)
            break;
        collected[v]=true;
        for(v的每一个邻接点w){
            if(collected[w]==false){
                if(dist[w]>dist[v]+E<v,w>){
                    dist[w]=dist[v]+E<v,w>;
                    path[w]=v;
                }
        }
    }
}
/*开始算法前,将所有dist置为无穷,s自己置为0,s的邻接点置为权重*/

(3)时间复杂度

  • 直接扫描所有未收录顶点:T=O(V^2+E),对于稠密图效果好
  • 将dist存在最小堆中:T=O(VlogV+ElogV),对于稀疏图效果好

7.1.4 多源最短路算法

(1)Floyd算法

  • Dk[i][j]=路径{i->{l<=k}->j}的最小长度
  • 最初的D-1是邻接矩阵,不连通的顶点为无穷
  • 从Dk-1到Dk:
    • 如果k不在i到j的最短路径上,Dk=Dk-1
    • 如果k在j到j的最短路径上,Dk[i][j]=Dk-1[i][k]+Dk-1[k][j]

(2)代码

void Floyd()
{ for ( i = 0; i < N; i++ )
    for( j = 0; j < N; j++ ) {
        D[i][j] = G[i][j];
        path[i][j] = -1;
    }
    for( k = 0; k < N; k++ )
    	for( i = 0; i < N; i++ )
    		for( j = 0; j < N; j++ )
            	if( D[i][k] + D[k][j] < D[i][j] ) {
            		D[i][j] = D[i][k] + D[k][j];
            		path[i][j] = k;
    			}
}

(3)输出路径

  • i到j的路径由三段组成,path[i][k]、k和path[k][j],递归输出

(4)时间复杂度

  • T=O(v^3)

8.1 最小生成树问题

8.1.1 Prim算法

(1)什么是最小生成树(MST)

  • 无回路,包含图中全部顶点,包含V-1条边

  • 任加一条边都会构成回路

  • 图连通 = 存在最小生成树

(2)贪心算法

  • 每一步都要最好的,即权重最小的边,不能有回路

(3)Prim算法

  • 让一颗小树不断长大

(4)代码

void Prim(){
	MST = {s};
	while(1){
        V = 未收录顶点中dist最小者;
        if (V不存在)
            break;
        dist[V]=0;	  //收录进来
		for(V的每个邻接点W){
			if(dist[W] != 0){
				if(dist[W] > E<V,W>){
					dist[W] = E<V,W>
					parent[W] = V
				}
			}
		}
	if (MST中的顶点不到V个)         //dist为无穷,图不连通
		Error(“生成树不存在”);
}

//初始化:dist[v] = E<s,V>或无穷, parent[s] = -1
//用父亲结点数组表示最小生成树

(5)时间复杂度

  • T=O(V^2)
  • 用在稠密图中

8.1.2 Krustal算法

  • 将森林合并成树
void Kruskal ( Graph G )
{ 	MST = { } ;
    while ( MST到中不到|V|-1条边 && E中还有边 ) {
        从E边中取一条权重最小的边E<v,w>;      //利用最小堆
        将E<v,w>从E中删除;
    	if(E<v,w>不在MST中构成回路)      //利用并查集,看v,w是否在两颗树上
    		将E<v,w>加入MST;
    	else
    		彻底无视E<v,w>;
    }
    if(MST中不到|V|-1条边 )
    	Error ( “ 生成树不存在” );
}
  • 时间复杂度O(ElogE),用在稀疏图中

8.1.3 拓扑排序

  • AOV (Activity On Vertex)网络:结点代表某一活动,边代表先后关系
  • 拓扑序 :如果图中从V 到W有一条有向路径,则V 一定排在W之前。满足此条件的顶点序列称为一个拓扑序
  • AOV 如果有合理的拓扑序,则必定是有向无环图(DAG)
  • 算法一
void TopSort()
{ for ( cnt = 0; cnt < |V|; cnt++ ) {
    V = 未输出的入度为0 的顶点;    //O(V)
    if(这样的V在不存在 ) {
    	Error(“图中有回路”);
    	break;
    }
    输出V,或者记录V 的输出序号;
    for (V的每个邻接点W)
    	Indegree[W]––;
    }
}
//T=O(V^2)
  • 算法二,随时将入度变为0的顶点放到队列中
void TopSort()
{ for (图中每个顶点V)
    if (Indegree[V]==0)
    	Enqueue(V,Q);
    while (!IsEmpty(Q)){
        V = Dequeue( Q );
        输出V ,或者记录V的输出序号; cnt++;
        for (V点的每个邻接点 W )
        	if (––Indegree[W]==0 )
        		Enqueue( W, Q );
    }
    if (cnt != |V|)
    	Error(“图中有回路”);
}
//T=O(V+E)

8.1.4 关键路径问题

  • 关键路径是由绝对不允许延误的活动组成的路径,即从头到尾不含机动时间的路径

9.1 简单排序

9.1.1 冒泡排序

(1)算法

void Bubble_sort(ElementType A[], int N){
	for (P=N-1; P>=0; P--){
		flag=0;
		for (i=0; i<P; i++){      //一趟冒泡下来,保证最大数沉底
			if(A[i]>A[i+1]){
				swap(A[i], A[i+1]);
				flag=1;
			}
		}
	if (flag==0)      //一趟全程无交换,已有序,退出
		break;
	}
}

(2)时间复杂度

  • O(N^2)

(3)优点

  • 可用于链表存储的数值排序

  • 稳定性:任意两个相等的数据,排序前后的相对位置不发生改变

9.1.2 插入排序

(1)算法

void insertion_sort(ElementType A[], int N){
	for (P=1; P<N; P++) {
		Tmp = A[P];
		for (i=P; i>0 && A[i-1]>Tmp; i--){
			A[i] = A[i-1];      //移出空位
		}
		A[i] = Tmp;      //插入
	}
}

(2)时间复杂度

  • O(N^2)

9.1.3 时间复杂度下限

  • 交换2个相邻元素正好消去1个逆序对

  • 任意N个不同元素组成的序列平均具有N(N-1)/4个逆序对

  • 任何以交换两元素来排序的算法,平均时间复杂度为Ω(N^2)

  • 提高算法效率的关键:每次消去多个逆序对;每次交换相隔较远的元素

9.2.1 希尔排序

  • 定义一个增量序列,D(M)>D(M-1)>...>D(1)=1,对序列进行D(k)间隔插入排序
void Shell_sort( ElementType A[], int N )
{ for ( D=N/2; D>0; D/=2 ) {    /* 希尔增量序列为D(M)=N/2*/
    for ( P=D; P<N; P++ ) {    /* 插入排序,把1替换为D*/
    	Tmp = A[P];
    	for ( i=P; i>=D && A[i-D]>Tmp; i-=D )
    		A[i] = A[i-D];
    		A[i] = Tmp;
    	}
    }
}
  • Sedgewick增量序列:

9.3.1 选择排序

void Selection_Sort ( ElementType A[], int N )
{ for ( i = 0; i < N; i ++ ) {
    MinPosition = ScanForMin( A, i, N–1 );
    /* 从A[i]到A[N–1]中找最小元,并将其位置赋给MinPosition */
    Swap( A[i], A[MinPosition] );
    /* 将未排序部分的最小元换到有序部分的最后位置*/
    }
}
  • 只用做N次交换
  • 关键在于快速找到最小元

9.3.2 堆排序

  • 算法一,需要额外O(N)空间
void Heap_Sort ( ElementType A[], int N )
{   BuildHeap(A);    /* O(N) */
    for ( i=0; i<N; i++ )
    	TmpA[i] = DeleteMin(A);    /* O(logN) */
    for ( i=0; i<N; i++ )    /* O(N) */
    	A[i] = TmpA[i];
}
  • 算法二,建立最大堆,将根节点放到最后位置
void Heap_Sort ( ElementType A[], int N )
{ for ( i=N/2; i>=0; i-- )     /* BuildHeap */
    PercDown( A, i, N );      //建立一个根节点位于i处,结点数量为N的最大堆
  for ( i=N-1; i>0; i-- ) {
    Swap( &A[0], &A[i] );    /* DeleteMax */
    PercDown( A, 0, i );
    }
}

9.4 归并排序

9.4.1 有序子列的归并

/*L = 左边起始位置, R = 右边起始位置, RightEnd = 右边终点位置*/
void Merge( ElementType A[], ElementType TmpA[],int L, int R, int RightEnd )
{ 
    LeftEnd = R - 1; /*左边终点位置。假设左右两列挨着*/
    Tmp = L; /*存放结果的数组的初始位置*/
    NumElements = RightEnd - L + 1;
    while( L <= LeftEnd && R <= RightEnd ) {
        if ( A[L] <= A[R] ) TmpA[Tmp++] = A[L++];
        else TmpA[Tmp++] = A[R++];
    }
    while( L <= LeftEnd ) /*直接复制左边剩下的 */
    	TmpA[Tmp++] = A[L++];
    while( R <= RightEnd ) /*直接复制右边剩下的 */
    	TmpA[Tmp++] = A[R++];
    for( i = 0; i < NumElements; i++, RightEnd -- )
    	A[RightEnd] = TmpA[RightEnd];
}

9.4.2 递归算法

  • 分而治之,将序列拆分为一半一半,再归并
void MSort( ElementType A[], ElementType TmpA[], int L, int RightEnd )
{ 
    int Center;
    if ( L < RightEnd ) {
        Center = ( L + RightEnd ) / 2;
        MSort( A, TmpA, L, Center );
        MSort( A, TmpA, Center+1, RightEnd );
        Merge( A, TmpA, L, Center+1, RightEnd );
    }
}
  • 时间复杂度T(N)=O(NlogN)

  • 统一函数接口

void Merge_sort( ElementType A[], int N )
{ 
    ElementType *TmpA;
    TmpA = malloc( N * sizeof( ElementType ) );   //在最外面开辟临时数组的空间
    if ( TmpA != NULL ) {
        MSort( A, TmpA, 0, N-1 );
        free( TmpA );
    }
    else Error( “ 空间不足" );
}

9.4.3 非递归算法

void Merge_pass( ElementType A[], ElementType TmpA[], int N, int length ) /*一趟归并,length = 当前有序子列的长度 */
{ 
    for ( i=0; i <= N–2*length; i += 2*length )
    	Merge1( A, TmpA, i, i+length, i+2*length–1 );
    if ( i+length < N ) /* 归并最后2个子列 */
    	Merge1( A, TmpA, i, i+length, N–1);
    else /* 最后只剩1列 个子列 */
    	for ( j = i; j < N; j++ ) TmpA[j] = A[j];
}

void Merge_sort( ElementType A[], int N )
{ 
    int length = 1; /* 初始化子序列长度 */
    ElementType *TmpA;
    TmpA = malloc( N * sizeof( ElementType ) );
    if ( TmpA != NULL ) {
    	while( length < N ) {
    		Merge_pass( A, TmpA, N, length );
    		length *= 2;
    		Merge_pass( TmpA, A, N, length );   //做两次,保证TmpA拷贝到A处
    		length *= 2;
    	}
    	free( TmpA );
    }
    else Error( “ 空间不足" );
}

10.1 快速排序

10.1.1 算法概述

  • 分而治之,选pivot,划分左右两边,递归

10.1.2 选主元

  • 最好情况:每次正好中分,O(NlogN)

  • 最坏情况:每次都选中最值,O(N^2)

  • 选头中尾的中位数

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 */
    //三者中最小数放到左边,最大放到右边,中位数放到R-1处
}

10.1.3 算法原理

  • 左右指针,pivot位于最右边,左边小或者右边大时移动指针,否则就停住,都停住后交换两个值
  • 当左右指针相等时,停止比较,将pivot放到指针处
  • 快速排序快在一次排序就将pivot放到了准确的位置,不会再改变

10.1.4 小规模数据的处理

  • 使用了递归,小规模数据还不如插入排序快
  • 程序中定义一个cutoff阈值,当数据规模小于它时,停止递归,使用插入排序

10.1.5 算法实现

void Quicksort( ElementType A[], int Left, int Right )
{ 
    if ( Cutoff <= Right-Left ) {
        Pivot = Median3( A, Left, Right );
        i = Left; j = Right – 1;
        for( ; ; ) {
        	while ( A[ ++i ] < Pivot ) { }
        	while ( A[ ––j ] > Pivot ) { }
        	if ( i < j )
        		Swap( &A[i], &A[j] );
        	else break;
        }
        Swap( &A[i], &A[ Right-1 ] );
        Quicksort( A, Left, i-1 );
        Quicksort( A, i+1, Right );
    }
    else
    	Insertion_Sort( A+Left, Right-Left+1 );
}

void Quick_Sort(ElementType A[],int N)
{
	Quicksort( A, 0, N-1 );
}

10.2 表排序

10.2.1 算法概述

  • 存放一本书/电影,直接移动数据代价过大,移动指向其的指针

  • 简介排序:定义一个指针数组作为“表”,指向序列,移动指针进行排序

10.2.2 物理排序

  • 需要移动序列位置

  • N个数字的排列由若干个独立的环组成

  • 挪出A[0],依次补上A[3]、A[1]、A[5],然后将A[0]放到A[5]处

  • 判断一个环的结束:if( table[i] == i)

10.3 基数排序

10.3.1 桶排序

  • 取值有限,N个学生,M=101个不同的成绩值,排序

void Bucket_Sort(ElementType A[], int N)
{ 
	count[] 初始化;
    while (读入1个学生成绩grade)
    	将该生插入count[grade] 链表;
    for ( i=0; i<M; i++ ) {
    	( if ( count[i] )
    		输出整个count[i]链表;
    }
}
  • 时间复杂度O(M+N)

10.3.2 基数排序

  • 假设N=10,M=1000>>N,桶排序效率低
  • 以10-基数为例,按照个位数、十位数、百位数的顺序,依次将序列挂入链表

10.3.3 多关键字的排序

10.4 排序算法的比较

11.1 散列表

11.1.1 引子

  • 查找的本质:已知对象找位置

    • 有序安排对象:二分查找、二叉搜索树
    • 直接计算出对象位置:散列
  • 散列查找的两项任务:

    • 计算位置:构造散列函数确定关键词存储位置
    • 解决冲突:应用某种策略解决多个关键词位置相同的问题

11.1.2 散列表

  • 装填因子:设散列表空间大小为m,填入表中元素个数是n,则n/m为散列表的装填因子
  • 散列(Hashing)基本思想:以关键字key为自变量,通过散列函数h,计算出对应的函数值h(key),作为数据对象的存储地址

11.2 散列函数的构造方法

11.2.1 数字关键词的散列函数构造

  • 一个好的散列函数要考虑以下两个因素

    • 计算简单
    • 地址空间分布均匀,减少冲突
  • 直接定址法:取关键词的线性函数值

  • 取留余数法:h(key)=key mod p, p一般取素数

  • 数字分析法:取关键字上比较随机的位作为散列地址

  • 折叠法:把关键词分割成位数相同的几部分,然后叠加

  • 平方取中法

11.2.2 字符串关键词的散列函数构造

  • 取ASCII码
  • 移位法

Index Hash ( const char *Key, int TableSize )
{ 
    unsigned int h = 0; /* 散列函数值, 初始化为0*/
    while ( *Key != ‘\0’) /* 位移映射 */
    	h = ( h << 5 ) + *Key++; 
    return h % TableSize; 
} 

11.3 冲突处理方法

11.3.1 开放地址法

  • 处理冲突的方法
    • 换个位置:开放定址法
    • 同一位置的冲突对象组织在一起:链地址法

(1)定义

  • 一旦产生冲突,就按规则寻找另一空地址

(2)方法

11.3.2 线性探测

  • 线性探测易形成聚集

  • 散列函数h(key)=key mod 11,因此,分为11类分析

11.3.3 平方探测

  • 定理显示,如果散列表长度是某个4k+3形式的素数时,平方探测法可以探查到整个散列表空间

11.3.5 平方探测的实现

  • 懒惰删除:增加一个删除标记(Deleted),而并不是真正删除它,以便查找时不会“ 断链 ”,其空间可以在下次插入时重用。
typedef struct HashTbl *HashTable;
struct HashTbl{
    int TableSize;
    Cell *TheCells;
}H;

Position Find( ElementType Key, HashTable H ) /* 平方探测*/
{ 
    Position CurrentPos, NewPos;
    int CNum; /* 记录冲突次数 */
    CNum = 0;
    NewPos = CurrentPos = Hash( Key, H->TableSize );
    while( H->TheCells[ NewPos ].Info != Empty && 
    	   H->TheCells[ NewPos ].Element != Key ) {
        /* 字符串类型的关键词需要 strcmp 函数!! */
        if(++CNum % 2){ /* 判断冲突的奇偶次 */
        	NewPos = CurrentPos + (CNum+1)/2*(CNum+1)/2;
        	while( NewPos >= H->TableSize )
        		NewPos -= H->TableSize;
        } else {
            NewPos = CurrentPos - CNum/2 * CNum/2;
            while( NewPos < 0 )
            	NewPos += H->TableSize;
        }
    }
    return NewPos;
}
  • 再散列,装填因子过大时,查找效率下降,需要加倍扩大散列表,该过程叫再散列

11.3.6 分离链接法

  • 将相应位置上冲突的所有关键词存储 在同一个单链表中

typedef struct ListNode *Position, *List;
struct ListNode {
    ElementType Element;
    Position Next;
};
typedef struct HashTbl *HashTable;
struct HashTbl {
    int TableSize;
    List TheLists;
};

Position Find( ElementType Key, HashTable H )
{ 
    Position P;
    int Pos;
    Pos = Hash( Key, H->TableSize );      /*初始散列位置*/
    P = H->TheLists[Pos]. Next;     /*获得链表头*/
    while( P != NULL && strcmp(P->Element, Key) )
    	P = P->Next;
    return P;
}

11.4 散列表的性能分析

  • 影响冲突的因素:散列函数是否均匀,处理冲突的方法,装填因子

  • 散列法的查找效率期望是常数O(1)

  • 散列表应用:文件中单词词频统计

12.1 串的模式匹配

(1)c的库函数strstr

  • char *strstr(char *string, char *pattern)

  • strstr返回子串头指针

  • 时间复杂度T=O(n*m)

(2)KMP算法

  • match函数的子列是可以交叉的

  • 当第j个匹配不上时,不用移动string指针,只用将pattern指针移动到match[j-1]处,从该处往后再比较

  • 时间复杂度T=O(n+m)

(3)KMP的代码

Position KMP( char *string, char *pattern )
{ 
    int n = strlen(string);
    int m = rlenrn t st e (patte );
    int s, p, *match;
    match = (int *)malloc(sizeof(int) * m);
    BuildMatch(pattern, match);
    s = p = 0;
    while (s<n && p<m) {
        if (string[s]==pattern[p]) { s++; p++; }
        else if (p>0) p = match[p-1]+1;
        else s++;
    }
    return (p == m)? (s-m) : NotFound;
}

(4)BuildMatch的实现

(5)BuildMatch的代码

void BuildMatch(char *pattern, int *match)
{
    int i, j;
    int m = strlen(pattern);
    match[0] = -1;
    for (j=1; j<m; j++) { 
    	i = match[j-1];
    	while ((i>=0) && (pattern[i+1]!=pattern[j]))
    		i = match[i];
    	if (pattern[i+1]==pattern[j])
    		match[j] = i+1;
    	else match[j] = -1;
    }
}
//时间复杂度T=O(m)
posted @ 2022-10-03 21:27  z5onk0  阅读(686)  评论(0编辑  收藏  举报