首页 > 编程语言 >浙江大学陈越老师《数据结构与算法》课程笔记

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

时间:2022-10-03 21:34:42浏览次数:50  
标签:结点 ElementType BST 陈越 int 算法 浙江大学 数据结构 查找

目录

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={源点s+已经确定了最短路径的顶点vi}
  • 对任一未收录的顶点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增量序列:{1, 5, 19, 41, 109, … }

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)

标签:结点,ElementType,BST,陈越,int,算法,浙江大学,数据结构,查找
From: https://www.cnblogs.com/z5onk0/p/16751304.html

相关文章

  • 数据结构与算法分析——C语言描述(第9章 图论算法)*
    目录9.1若干定义图的表示9.1若干定义一个图(graph)\(G=(V,E)\)由顶点(vertex)的集\(V\)和边(edge)/弧(arc)的集\(E\)组成。每一条边就是一幅点对\((v,w)\),其中\(v,......
  • 数据结构与算法分析——C语言描述(第5章 散列)
    目录5.1一般想法5.2散列函数5.3分离链接法(separatechaining)5.4开放定址法(openaddressing)本章讨论散列表(hashtable)ADT,不过它只支持二叉查找树所允许的一部分......
  • 数据结构应用题
    数据结构应用题考点数组数组的存储结构一维数组A[0...n-1]为例,存储关系\[LOC(ai)=LOC(a0)+(i)×L(0≤i<n)\]L是每个数组元素所占存储单元多维数组对于多维数组,有两......
  • 02 线性表 | 数据结构与算法
    1.线性表线性表的定义特点:存在唯一一个被称为第一个的数据元素存在唯一一个被称为最后一个的数据元素除了第一个元素之外,其他的数据元素都有唯一一个直接前驱除......
  • 01 入门 | 数据结构与算法
    1.数据数据:数据是指对客观事物进行记录并且可以可以鉴别的抽象符号数据元素:数据的基本单位,在计算机当中作为一个整体考虑数据对象:具有相同性质的数据元素的集合数据......
  • 数据结构相关基本概念和术语
    数据结构相关基本概念和术语目录数据结构相关基本概念和术语数据(Data)数据元素(DataElement)数据项(DataItem)数据对象(DataObject)数据结构(Datastructure)四类基本结构集合线......
  • 数据结构与算法【Java】09---多路查找树
    目录前言1、二叉树与B树1.1、二叉树的问题分析1.2、多叉树1.3、B树的基本介绍2、2-3树2.1、2-3树简介2.2、2-3树应用案例2.3、补充3、B树、B+树和B*树3.1、B树的简......
  • [ 数据结构 - C++]红黑树RBTree
    在上篇文章我们了解了第一种平衡二叉搜索树AVL树,我们知道AVL树是通过平衡因子来控制左右子树高度差,从而将二叉树变成一颗平衡二叉搜索树。本篇文章我们将要了解另外一种平衡......
  • 数据结构应用题
    数据结构应用题数组应用题数组的存储结构一维数组A[0...n-1]为例,存储关系\[LOC(ai)=LOC(a0)+(i)×L(0≤i<n)\]L是每个数组元素所占存储单元多维数组对于多维数组,有......
  • 高级算法/数据结构
    AhoCorasick自动机用于多模式字符串匹配。可持久化线段树利用前缀和思想求区间第k小等不易直接求出的值。后缀数组Manacher求最长回文串。......