(一).算法和数据结构
一. 引言
1.算法
(1).性质
特性:
- 输入:有零个或者多个由外部提供的量作为算法的输入。
- 输出:算法至少产生一个量作为输出。
- 确定性:组成的算法的每条指令是清晰的,无歧义的。
- 有穷性:算法中的每一条指令的执行次数是有限的,执行每一条指令的时间也是有限的。
- *可行性:算法的每一步必须是可行的。
概念:
- 数据:描述客观事物的数和字符的集合。
- 数据元素:数据的基本单位。
- 数据项:具有独立含义的数据最小单位,也称为字段或者域。
(2)设计三要素
- 数据
- 运算
- 控制
(3).控制转移
串行计算中 --------顺序、分支、循环、递归、无条件转移
2.算法设计要求
- 正确性:算法的正确性是只算法至少应该具有输入、输出和加工处理的无歧义性能够正确反映问题的需求,至少能够得到问题的正确答案。
- 算法程序没有语法错误;
- 算法程序对于合法输入能够产生满足要求的结果;
- 算法程序对于非法输入能够产生满足规格说明;
- 算法程序对于刁难的测试输入有满足要求的输出结果。
- 可读性:算法设计方便为了阅读、理解和交流,另一方面让计算机执行。
- 健壮性:可以处理一些不合理的输入,返回一些正确的输出结果。
- 时间效率高和存储量低:好算法具备时间效率高和存储量低,尽量考虑这方面问题。
- 可使用性:要求算法能够方便使用。
3.结构
(1)数据结构:是数据的存在方式。
-
逻辑数据结构:反映数据之间的逻辑的关系。
-
存储数据结构:反映成分数据在计算机内的存储安排。
- 顺序存储结构
- 链式存储结构
- 索引存储结构
- 哈希(散列)存储结构
-
数据类型:同一种类型数据的全体称为一个数据类型。
-
抽象数据类型:算法的一种数据模型连同定义在该模型上并作为该算法构件的一组模型;
(2)最常用的数据结构是数组数据结构和记录结构
二.线性表
1.线性表概念
线性表是一种非常灵便的结构,可以根据长度需要改变表的长度,也可以在表中任何位置对元素进行访问、插入和删除等操作,
由零个或多个数据元素组成的有限序列。
运算原则:随机存取
2.顺序存储结构的前驱、后继
前驱:称元素a(k)在元素之前,或a(k)是a(k+1)的前驱,k = 1,2,3,4······,n-1
后继:称元素a(k+1)在a(k)元素之后,或a(k+1)是a(k)的后继。
3.链式存储结构的定义
数据域:存储数据元素信息的域。
指针域:存储直接后继位置的域。指针域中存储的信息称为指针或者链。
两部分信息组成数据元素称为存储映像。
4.线性表的类别
(1)数组的单链表
(2)指针的单链表
(3)游标类型的单链表
用游标代替指针
(4)循环链表
当 rear = rear->next时,循环链表为空链表
(5)双链表
prior 为前驱指针;
next 为后继指针;
p->prior->next = p.next.prior;
5.顺序表和链表的的比较
链式存储结构的优点:
1.结点空间可以动态申请和释放;
2.数据元素的逻辑次序靠结点的指针来指示,插入和删除不需要移动元素位置;
链式存储结构的缺点:
1.存储密度小,每个结点的指针域需要额外占据存储空间。(存储密度 == 结点数据本身占用的空间/结点占用的空间总量; 存储密度越高,存储空间的利用率越高)
2.链式存储结构是非随机存储结构
顺序存储结构的优点:
顺序存储结构的缺点:
三-1.栈
1.概念
栈是一种特殊的线性表。这种表只在表首进行插入和删除操作。因此,表首对于栈来说具有特殊意义,为栈顶。相应地,表尾为栈底。不含任何空元素为空栈。
栈的修改是后进先出的原则。
表头(栈顶)Top 表尾(栈底)Base。
插入为入栈 删除为出栈(抛栈)。
插入和删除限定:
最后插入的元素会被最先删除。
逻辑结构:与线性表差不多的(一对一)
运算规则:后进先出
存储结构:顺序栈、链栈,
2.哪些算法需要使用栈。
数制转换、表达式求值、括号匹配的检验、八皇后问题、行编辑程序、函数调用、迷宫求解、递归调用的实现
3.顺序栈
特点:简单、方便、但容易产生溢出(数组大小固定)
上溢:栈已经满了,又要压元素。是一种错误,使问题的处理没办法进行。
下溢:栈已经空了,还要弹出元素。是一种结束,即问题处理结束。
4.链栈
(1)
- 链栈是运算受限的单链表,只能在链栈头部进行操作。
- 栈的头指针就是栈顶;
- 不需要头结点;
- 基本不存在栈满的情况;
- 空栈相当于头指针指向空;
- 插入和删除仅在栈顶处操作;
(2)递归
定义:
- 若一个对象部分地包含自己,或用它自己给自己定义,则称为这个对象是递归。
- 若一个过程直接地或间接地调用自己,则称这个过程为递归过程。
递归问题
- 分治法求解:对一个较为复杂的问题,能分解为几个相对简单的问题且解法相似
递归优缺点:
优点:结构清晰,程序易读。
缺点:每次调用要生成工作记录,保存状态信息,入栈;返回时要出栈,回复状态信息。时间开销大。
递归-----> 非递归
方法1:尾递归、单向递归——>循环结构
方法2:自用栈模拟系统的运行时栈
三-2.队列
1.概念
队列是另一种特殊的线性表。这种表只在表首(队首)进行删除操作,只在表尾(队尾)进行插入操作。由于队列的修改是按照先进先出规则进行的,所以队列又称为先进先出表
,简称FIFO
表。
插入和删除限定:
只能在表尾插入。
只能在表首删除。
逻辑结构:与线性表一样,一对一
运算规则:只能在队首和队尾运算,访问结点时依照先进先出原则
存储结构:顺序队、链队,以循环顺序队列更常见
实现方式:入队和出队操作。
2.解决问题
- 脱机打印输出;
- 按申请先后顺序依次输出;
- 多用户系统中,多个用户排成队,分时地循环使用CPU内存…
3.顺序队列
1) 顺序队列
A. 溢出问题
设数组大小为MAXSIZE
rear == MAXSIZE时,发生溢出
- 若front = 0:
rear == MAXSIZE时再入队为:真溢出
- 若front != 0 :
rear == MAXSIZE时再入队为:假溢出
B. 解决假上溢方法:
-
将队中元素依次向队头方向移动。缺点:浪费时间每移动一次,队中元素都要移动。
-
将队空间设想成一个循环的表,即分配给队列的m个存储单元可以循环使用,当rear为maxqsize时。
若向量的开始端空着,又可从头使用空着的空间。当front为maxqsize时,也是一样。
3)判断-队空-队满的方法
-
另外设置一个标志以区别 队空、队满
-
另设一个变量,记录元素的个数
-
少用一个元素空间
少用一个元素空间—循环队列解决队列满时问题判断方法:
循环队列中,头指针(front)和尾指针(rear)表示 , MAXSIZE表示数组空间的大小。
如果用以表示循环队列的话,出队时,头指针的移动可以这样表示:front = (front+1) % MAXSIZE
;入队时,尾指针的移动可以这样表示:rear = (rear+1) % MAXSIZE
。
队空判断为:front == rear。
队满判断:front == (rear+1)% MAXSIZE (保留一个空间,用以判断队满)。
4.链队
链队列操作
四-1.排序与选择算法
1.基本概念和排序方法的概述
(1).基本概念
- 排序:将一组杂乱无章的数据按一定的规律顺次排列起来。即,将无序序列排成一个有序序列(由小到大或由大到小)的运算
- 如果参加排序的数据结点包含多个数据域,那么排序往往针对其中某个域而言。
- 排序应用非常广泛
- 软件中直接应用
- 程序中间接应用
- 二分法
- 最短路径,最小生成树
- ········
- 排序方法分类
按数据存储介质:内部排序和外部排序
按比较器 :串行排序和并行排序
按主要操作 :比较排序和基数排序
- 按辅助空间 :原地排序和非原地排序
- 按稳定性 :稳定排序和非稳定排序
- 按自然性 :自然排序和非自然排序
(2).排序方法分类
- 按数据存储介质
- 内部排序:数据量不大、数据在内存,无需内外存交换数据
- 外部排序:数据量较大、数据在外存(文件排序)
- 外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多
- 按比较器的个数来分
- 串行排序:单处理机(同一时刻比较一对元素)
- 并行排序:多处理机(同一时刻比较多对元素)
- 按主要操作来分
- 比较排序:用比较的方法
- 插入排序、交换排序、选择排序、归并排序
- 基数排序:不比较元素大小,仅仅根据元素本身的取值确定其有序位置。
- 比较排序:用比较的方法
- 按辅助空间来分
- 原地排序:辅助空间用量O(1)的排序方法(所占的辅助存储空间与参与排序的数据量大小无关)
- 非原地排序:辅助空间用量超过O(1)的排序方法
- 按稳定性来分
- 稳定排序:能够使任何数值相等的元素,排序以后相对次序不变。
- 非稳定排序:不是稳定排序的方法。
- 排序的稳定性对于结构数据排序有意义。
- 按自然性来分
- 自然排序:输入数据越有序,排序的速度越快的排序方法
- 非自然排序:不是自然排序的方法
(3).按排序依据原则
- 插入排序:直接插入排序、折半插入排序、希尔排序
- 交换排序:冒泡排序、快速排序
- 选择排序:简单选择排序、堆排序
- 归并排序:2-路归并排序
- 基数排序
(4).按排序所需的工作量
- 简单的排序方法:T(n)=O(n²)
- 先进的排序方法:T(n)=O(n*㏒n)
- 基数排序:T(n)=O(d.n)
(5).存储结构
- 记录序列以顺序表存储
2.插入排序
(1).基本思想
- 每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对像全部都插入为止。
- 即边插入边排序,保证子序列中随时都是排好序的
- 基本操作
- 有序插入方法
- 在插入ai前,数组a的前半段(a[0]~a[i—1] )是有序段,后半段a[i]~a[n-1])是停留于輸入次序的"无序段"
- 插入a[i]使a[0]~a[ i - 1 ]有序,也就是要为a[ i ]找到有序位置 j (0 ≤ j ≤ i)将a[ i ] 插入在 a[ j ] 的位置上。
(2).插入排序的种类
- 顺序法(定位插入位置)—————————— 直接插入排序
- 二分法(定位插入位置)—————————— 二分插入排序
- 缩小增量(多变插入排序)———————— 希尔排序
(3).直接插入排序
#include <stdio.h>
#include <stdlib.h>
typedef int Status;
#define OK 1
#define ERROR 0
#define true 1
#define false 0
#define MAXSIZE 20 //设记录数不超过20个
typedef int KeyType; //设关键字为整数类型·
typedef char *InfoType;
typedef struct redtype //定义每一个记录(数据元素)的结构
{
KeyType key; //关键字
InfoType info; //其他数据项
} RedType;
typedef struct sqlist *slink;
typedef struct sqlist //定义顺序表的结构
{
RedType r[MAXSIZE - 1]; //存储顺序表的向量
//r[0]一般作哨兵或缓冲区
int length; //顺序表的长度
} SqList;
-
直接插入排序——————————采用顺序查找法查找插入位置
- 复制插入元素 x=a[i]
- 记录后移,查找插入位置 for(j = i-1 ; j>=0 && x<a[j] ;j–) a[j+1] = a[j];
- 插入到正确位置 a[j+1] = x;
-
直接插入排序(使用哨兵)
-
复制哨兵 L.r[0] = L.r[i];
-
记录后移,查找插入位置 for(j = i-1 ; L.r[0].key < L.r[j].ley ;–j) L.r[j+1] = L.r[j];
-
插入到正确位置 L.r[j+1] = L.r[0 ];
-
/* 直接插入排序 */ void Insert(SqList L) { int i, j; for (i = 2; i < L.length; ++i) { if (L.r[i].key < L.r[i - 1].key) { L.r[0] = L.r[i]; //复制为哨兵 for (j = i - 1; L.r[0].key < L.r[j].key; --j) { L.r[j + 1] = L.r[j]; //记录后移 } L.r[j + 1] = L.r[0]; //插入到正确位置 } } }
-
-
性能分析
实现排序的基本操作有两个
- "比较"序列中两个关键字大小
- "移动"记录
-
最好情况(关键字在记录序列中是顺序有序的):
-
11,25,32,47,56,70,
81,85,92,96
(记录序列) -
"比较"的次数:
-
"移动"的次数:0
-
-
最坏情况(关键字在记录序列中是逆序有序的):
-
85,92,96,
81,70,56,47,32,25,11
(记录序列) -
"比较"的次数:
-
"移动"的次数:
-
-
平均的情况
-
"比较"的次数:
-
"移动"的次数:
-
-
时间复杂度结论
- 原始数据越接近有序,排序速度越快
- 最坏情况下(输入数据是逆有序的) Tw(n)=0(n2)
- 平均情况下,耗时差不多是最坏情况的一 Te(n)=O(n2)要提高查找速度
- 减少元素的比较次数
- 减少元素的移动次数
(4).折半插入排序
-
查找插入位置时采用折半查找法
-
void InsertBin(SqList L) { int i, low = 1, hight, mid; for (i = 2; i <= L.length; ++i) { L.r[0] = L.r[i]; //复制为哨兵 hight = i - 1; low = 1; while (low <= hight) { mid = (low + hight) / 2; if (L.r[0].key < L.r[mid].key) { hight = mid - 1; } else { low = mid + 1; } } for (j = i - 1; j >= hight + 1; --j) { L.r[j + 1] = L.r[j]; //记录后移 } L.r[hight + 1] = L.r[0]; //插入到正确位置 } }
-
算法分析
- 折半查找比顺序
- 折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快;
- 它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。在插入第i个对象时,需要经过[ log₂i ]+1次关键码比较,才能确定它应插入的位置;
- 当n较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情况要差;
- 在对象的初始排列已经按关键码排好序或接近有皮时,直接插入排序比折半插入排序执行的关键码比较次数要少;
- 折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的仍贴排列
- 减少了比较次数,但没有减少移动次数
- 平均性能优于直接插入排序
- 时间复杂度为O(n2)
- 空间复杂度为0(1)
- 是一种稳定的排序方法
- 折半查找比顺序
(5).希尔排序
-
基本思想——————先整个待排记录序分割成若干子序列,分别进行直接插入排序,待整个序列中的记录”基本有序“时,再对全体记录进行一次直接插入排序
-
特点
- 缩小增量
- 多遍插入排序
-
思路:
- 定义增量序列Dk:Dm>Dm-1>···>D1=1
- 对每一个Dk进行”Dk-间隔“插入排序(k=M,M-1,…,1)
- 一次移动,移动位置较大,跳跃式地接近排序后的最终位置
- 最后一次只需要少量移动
- 增量序列必须是递减的,最后一个必须是1
- 增量序列应该是互质的
-
希尔排序算法效率与增量序列的取值有关
-
Hibbard增量序列
- D(k)=2^(k)-1 相邻元素互质
- 最坏情况: T(worst)=O(n^(3/2))
- 猜想:T(avg)=O(n^(5/4))
-
Sedgewick增量序列
-
{1,5,19,41,109,。。。}
————————9*4(i)-9*2(i)+1或4(i)-3*2(i)+1
-
猜想:T(avg)=O(n^(7/6)) T(worst)=(n^(4/3))
-
-
-
稳定性
- 时间复杂度式是n和d的函数
- O(n^(1.25)) ~ O(1.6*n^(1.25))-----经验公式
- 空间复杂度:O(1)
- 是一种不稳定的排序方法
- 如何选择最佳d的序列,目前尚未解决
- 最后一个增量值必须为1,无除了1之外的公因子
- 不宜在链式存储结构上实现
3.交换排序
(1).基本思想
两两比较,如果发生逆序则交换,直到所有记录都排好序为止
(2).冒泡排序
-
基本思想:每趟不断将记录两两比较,并按"前小后大"规则交换
-
升序
- 初始
- 如果有n个元素的话,需要n-1次排序
- 第 m 趟,需要比较 n-m 次
- 第一躺
- 位置0,1进行比较
- 位置1,2进行比较,大就交换 ,小就不用
- 以此类推。。。
- 第 n-1 趟
- 初始
-
/* 冒泡排序 */ void bubble_sort(SqList L) { int m, j, i; int n = L.length; RedType X;//交换临时存储单元 for (m = 1; m <= n; m++)//总共需要m趟 { for (j = 1; j <= n - m; i++) { if (L.r[j].key > L.r[j + 1].key)//发生逆序 { //交换 X = L.r[j]; L.r[j] = L.r[j + 1]; L.r[j + 1] = x; } } } }
-
优点:每趟结束时,不仅挤出一个最大值到最后面位置,还能同时部分理顺其他元素;
-
提高效率:
- 增加判断是否有进行交换顺序
-
时间复杂度
- 最好情况(正序):
- 比较次数:n-1
- 移动次数:0
- 最坏情况(逆序):
- 比较次数:
- 移动次数:
- 比较次数:
- 最好情况(正序):
-
算法评价
- 冒泡排序最好的时间复杂度是O(n)
- 冒泡排序最坏的时间复杂度是O(n²)
- 冒泡排序平均的时间复杂度是O(n²)
- 冒泡排序算法中增加一个辅助空间temp,辅助空间为S(n) = O(1)
- 冒泡排序是稳定的
(3).快速排序
-
基本思想:
- 任取一个元素为中心
- 所有比它小的元素的话一律放在前面,比它大的元素一律放后面
- 形成两个子表;
- 对各子表重新选择中心元素并依此规则调整(递归思想)
- 直到每一个子表的元素只剩下一个
- 通过一趟排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可以分别对真两部分记录仅需排序,以达到整个序列
-
具体实现:选定一个中间数值作为参考,所有元素与之比较,小的调到其左边,大的调到其右边。
-
枢轴(中间数):可以是第一个数,也可以是最后一个数,最中间的一个数,任选一个数等。
-
特点:
- 每一趟都是子表的形成都是采用从两头向中间交替式逼近法;
- 由于每趟中对各子表的操作都相似,可以采用递归算法。
-
/* 快速排序 */ void QSrot(SqList L, int low, int hight) { int pivotloc; if (low < hight) { pivotloc = Partiton(L, low, hight); //将L.r[low,hinght]一分为二,pivotloc为枢轴元素排好序的位置 QSrot(L, low, pivotloc + 1); ///对低子表的递归排序 QSrot(L, pivotloc - 1, hight); //对高子表的递归排序 } } int Partiton(SqList L, int low, int hight) { KeyType pivotkey; L.r[0] = L.r[low]; pivotkey = L.r[low].key; while (low < hight) { while (low<hight && L.r[hight].key >= pivotkey) { --hight; } L.r[low] = L.r[hight]; while (low<hight && L.r[hight].key <= pivotkey) { ++low; } L.r[hight] = L.r[low]; } L.r[low] = L.r[0]; return low; }
-
时间复杂度
- 证明,平均计算时间是O(nlog₂n);
- QSrot()😮(log₂n)
- Partiton()😮(n)
- 证明:就平均计算而言,快速排序是我们所谈论的所有内排序方法中最好的一个。
-
空间复杂度:
- 快速排序不是原地排序
- 由于程序中使用了递归,需要递归调用栈的支持,而栈的长度取决于递归调用的深度。(即使不用递归,也需要用用户栈)
- 平均情况:需要O(㏒n)的栈空间
- 最坏情况:栈空间可达O(n)。
-
稳定性:
- 快速排序是一种不稳定的排序方法
-
由于每一次枢轴记录的关键字都是大于其他所哟记录的关键字,致使一次划分后得到的子序列(1)的长度为0,这时已经退化为成为没有改进的冒泡排序
- 快速排序不适合对原本有序或基本有序的记录序列进行排序。
-
划分元素的选取是影响时间性能的关键
- 输入数据次序越乱,所选划分元素值的随机性越好,排序速度越快,快速排序不是自然排序方法。
- 改变划分元素的选取方法,至多只能改变算法平均情况的下的世界性能无法改变最坏情况下的时间性能。即最坏情况下,快速排序的时间复杂性总是O(n²)
4.选择排序
(1).简单选择排序
- 基本思想:在待排序的数据中选出最大(小)的元素放在其最终的位置。
- 基本操作:
- 首先通过n-1次关键字比较,从n各记录中找出关键字最小的记录,将它与第一个记录交换。
- 再通过n-2次比较,从剩余中的n-1次记录中找出关键字次小的记录,将它与第二个记录交换。
- 重复上述的操作,共进行n-1趟排序后,排序结束。
- 时间复杂度
- 记录移动次数
- 最好情况:0
- 最坏情况:3*(n-1)
- 比较次数:无论待排序处于什么状态,选择排序所需进行的“比较”次数都相同
- 记录移动次数
- 空间复杂度:O(1)。
- 算法稳定性
- 简单选择排序是不稳定排序
(2).堆排序
-
堆定义:
- 若有n个元素序列{a1,a2,…an}满足
- a(i) ≤ a(2i) 或 a(i) ≥ a(2i)
- a(i) ≤ a(2i+1) 或 a(i) ≥ a(2i+1)
- 则分别称该序列{a1,a2,…an}为小根堆和大根堆
- 从堆的定义可以看出,堆的实质是满足如下性质的完全二叉树:二叉树中任一非叶子结点均小于(大于)它的孩子结点
-
堆排序
- 若在输出堆顶的最小值(最大值)后,使得剩余n—1个元素的序列重又建成一个堆,
- 则得到n个元素的次小值(次大值.) …此反复,便能得到一个有序序列,这个过程称之为堆排序。
-
基本思想:
-
/* 堆排序的筛选 */ void HeapAdjust(elem R[], int s, int m) { /* 已知R[s。。m]中记录的关键字除R[s]之外均满足堆的定义,本函数调整R[s]的关键字,使R[s。m]成为一个大根堆 */ int j, i; rc = R[s]; for (j = 2 * s; j <= m; j *= 2) //沿着key较大的孩子结点向下筛选 { if (j < m && R[j] < R[j + 1]) //j为key较大的记录的下标 ++j; if (rc >= R[j]) break; R[s] = R[j]; s = j; /* code */ } R[s] = rc; } /* 建立小根堆 */ void HeadScort(elem R[]) { int i; for (i = n / 2; i >= 1; --i) { HeapAdjust(R, i, n); //建初始堆 } for (i = n; i > 1; --i) { Swap(R[1], R[i]); //根与最后一个元素进行交换 HeapAdjust(R, 1, i - 1); //对R[1]到R[i-1]重新建堆 } }
-
基本操作:
- 堆的调整
- 小根堆
- 输出堆顶元素之后,以堆中的最后一个元素替代之;
- 然后将根结点值与左右子树的根结点值进行比较,并与其中小者进行交换;
- 重复上述的操作,直至叶子结点,将得到的新的堆,称这个堆顶至叶子的调整过程为"筛选"
- 堆的建立
- 单结点的二叉树是堆;
- 在完全二叉树中所有以叶子结点(序号i> n/2)为根的子树是堆。
- 这样,我们只需依次将以序号为n/2, n/2-1, …1的结点为根的子树均调整为堆即可。即:对应由n个元素组成的无序序列, “筛选”只需从第n/2个元素开始。
- 从最后一个非叶子结点开始,以此向前调整:
- 调整从第n/2个元素开始,将以该元素为根的二叉树调整为堆
- 将以序号为n/2-1的结点为根的二叉树调整为堆;
- 再将以序号为n/2-2的结点为根的二叉树调整为堆;
- 再将以序号为n/2—3的结点为根的二叉树调整为堆;
- 实质上是一个线性表,可以用顺序存储一个堆
- 实质上,堆排序就是利用完全二叉树中父结点与孩子结点之间的内在联系来排序的。
-
算法性能分析
- 初始堆化所需时间不超过O(n)
- 排序阶段(不含初始堆化)
- 一次重新堆化所需时间不超过O(logn)
- n-1次循环所需时间不超过O(nlogn)
- Tw(n)=0(n)+ O(nlogn)= O(nlogn)
- 堆排序的时间主要耗费在建初始堆和调整建新堆时进行的反复,筛选上。堆排序在最坏情况下,其时间复杂度也为O(nlog₂n), 这是堆排序的最大优点。无论待排序列中的记录是正序还是逆序排列,都不会使堆排序处于“最好”或“最坏”的状态。
- 另外,堆排序仅需一个记录大小供交换用的辅助存储空间。
- 然而堆排序是一种不稳定的排序方法,它不适用于待排序记录个数n较少的情况,但对于n较大的文件还是很有效的。
5.归并排序
(1).基本思想
两个或两个以上的有序序列"归并"为一个有序序列
(2).2-路归并排序
-
在内部排序中,通常采用的是2-路归并排序
- 即:两个位置相邻的有序子序列R[1.m]和R[m+1…n]归并为一个有序序列R[1…n]
-
整个归并排序需要[log₂n]趟
-
归并排序算法分析
- 时间效率:O(nlog2n)
- 空间效率:O(n)
- 因为需要一个与原始序列同样大小的辅助序列(R1)。这正是此算法的缺点。
-
稳定性:稳定
6.基数排序
- 基本思想:分配+收集
- 也叫桶排序或箱排序:设置若干个箱子,将关键字为k的记录放入第k个箱子,然后在按序号将非空的连接
- 基数排序:数字是有范围的,均有0-9这十个数字组成,则需要设置十个箱子,相继按个,十,百…进行排序
- 算法分析
- 时间效率:O(k*(n+m))
- k:关键字
- m:关键字取值范围为m个值(桶的个数)
- n:往桶里扔的个数
- 空间复杂度:O(m+n)
- 时间效率:O(k*(n+m))
- 稳定性:稳定的
7.各种排序方法的综合比较
- 时间性能
- 按平均的时间性能来分,有三类排序方法:
- 时间复杂度为O(nlogn)的方法有:快速排序、堆排序和归并排序,其中以快速排序为最好;
- 时间复杂度为O(n)的有:直接插入排序、冒泡排序和简单选择排序,其中以直接插入为最好,特别是对那些对关键字近似有序的记录序列尤为如此;
- 时间复杂度为O(n)的排序方法只有:基数排序。
- 当待排记录序列按关键字顺序有序时,
- 直接插入排序和冒泡排序能达到O(n)的时间复杂度;
- 而对于快速排序而言,这是最不好的情况,此时的时间性能退化为O(n2),因此是应该尽量避免的情况。
- 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。
- 按平均的时间性能来分,有三类排序方法:
- 空间性能:指的是排序过程中所需的辅助空间大小
- 所有的简单排序方法(包括:直接插入、冒泡和简单选择)和堆排序的空间复杂度为O(1)
- 快速排序为O(logn),为栈所需的辅助空间
- 归并排序所需辅助空间最多,其空间复杂度为O(n)
- 链式基数排序需附设队列首尾指针,则空间复杂度为O(rd)
- 排序方法的稳定性能
- 稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。
- 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
- 对于不稳定的排序方法,只要能举出一个实例说明即可。
- 快速排序和堆排序是不稳定的排序方法。
- 关于“排序方法的时间复杂度的下限”
- 本章讨论的各种排序方法,除基数排序外,其它方法都是基于"比较关键字"进行排序的排序方法,可以证明,这类排序法可能达到的最快的时间复杂度为O(nlogn)
- 基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)。
- 可以用一棵判定树来描述这类基于“比较关键字”进行排序的排序方法。
8.各种排序方法比较
类别 | 排序方法 | 时间复杂度 | 时间复杂度 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
类别 | 排序方法 | 最好情况 | 最坏情况 | 平均情况 | 辅助存储 | 稳定性 |
插入排序 | 直接插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
插入排序 | 希尔排序 | O(n) | O(n²) | ~O(n^(1.3)) | O(1) | 不稳定 |
交换排序 | 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
交换排序 | 快速排序 | O(n㏒n) | O(n²) | O(n㏒n) | O(n㏒n) | 不稳定 |
选择排序 | 直接选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
选择排序 | 堆排序 | O(n㏒n) | O(n㏒n) | O(n㏒n) | O(n²) | 不稳定 |
归并排序 | 归并排序 | O(n㏒n) | O(n㏒n) | O(n㏒n) | O(n) | 稳定 |
基数排序 | k:待排元素的个数,m为基数的个数 | O(n+m) | O(k*(n+m)) | O(k*(n+m)) | O(n+m) | 稳定 |
四-2.串、数组和广义表
(一).串
1.定义
零个或者多个任意字符组成的有限序列。
S = "a1 a2 a3 ...... an" // (n>=0)
串名:S
串值:“a(1) a(2) a(3) … a(n)”
串长:n
空串:n = 0;
子串:一串中任意个连续字符组成的子序列(含空串)称为该串的子串。
主串:包含字串的串相应地称为主串
字符位置:字符在序列中的序号为该字符在串中的位置
子串位置:子串第一个字符在主串中的位置
空格串:由一个或多个空格组成的串 与空串不同
串相等:当且仅当两个串长度相等,并且各个对应位置上的字符都要相等,这两个串才是相等的。
2.案例引入
-
计算机非数值处理对象(文字编辑、符号处理)
-
病毒感染检测
3.存储结构
顺序存储结构:顺序串
链式存储结构:链串
优点:操作简单
缺点:存储密度低
4.模式匹配算法
确定主串中所含子串(模式串)第一次出现的位置
算法种类:
BF算法:
#define MAXLEN 255
typedef struct
{
char ch[MAXLEN + 1]; //存储串的一维数组
int length; //串当前的长度
} Sstring;
int index(Sstring S, Sstring T,int pos)
{
//i为主串下标 j为模式串下标
int j = 1, i = pos;
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 //匹配位置
}
else
{
return 0;
}
}
模式串的长度m,主串长度为n
时间复杂度:最快为O(m),最慢为O((n-m)m+m)或者 O((n-m+1)m) 若模式串的长度远远小于主串此时时间复杂度为 O(m*n)
KMP算法(特点:速度快):
//KMP算法
int index_KMP(Sstring S, Sstring T, int pos)
{
int i = pos, 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;
}
}
//较为完善
void get_nextVal(Sstring T, int nextVal[])
{
i = 1;
nextVal[1] = 0;
j = 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];
}
}
}
//较为简单
void get_next(Sstring T; int next[])
{
i = 1;
next[1] = 0;
j = 0;
while (i < T.length)
{
if (j == 0 || T.ch[i] == T.ch[j])
{
++i;
++j;
next[i] = j;
}
else
{
j = next[j];
}
}
}
时间复杂度:为O(m+n),
(二).数组
1.定义
- 数组:按照一定格式排列起来的,具有相同类型的数据元素的集合
- 一维数组:若线性表中数据元素为非结构简单的元素,则称为一维数组
- 一维数组的逻辑结构:线性结构。定长的线性表
- 声明格式:数据类型 变量名称[长度]
- 二维数组:若一维数组中的数据元素又是一维数组结构,则称为二维数组。
- 二维数组的逻辑结构:
- 非线性结构:每一个数据元素既在一个行表中,又在一个列表中。
- 线性结构:该线性表的每一个数据元素也是一个定长的线性表。
- 声明格式:数据类型 变量名称[行数] [列数];
- 三维数组:若二维数组中的元素又是一个一维数组,则称为三维数组。
- n维数组:若n-1维数组中的元素又是一个一维数组,则称为n维数组。
- 结论:
- 线性表结构是一个数组结构的一个特例,
- 而数组结构又是线性表结构的扩展。
- 数组特点:结构固定----定义后,维数和维界不变。
- 数组的基本操作:初始化、销毁、取元素、修改元素。(一般不做的插入和删除操作)
2.数组的抽象数据类型
- n维数组的抽象数据类型,n为数组的维数
- bi为数组的第i维长度
- 数据对象:J(i) = 0,…b(i-1),i = 1,2…,n J(i)为数组元素第i维的下标
- 二维数组的抽象数据类型的数据对象和数据定义关系的定义
- b(1)为第一维的长度 b(2):第二维的长度。
- a[j(1)] [j(2)]:第1维的下标为j(1),第二维的下标为j(2)。
3.数组顺序存储结构
-
数组特点:结构固定----定义后,维数和维界不变。
- 数组的基本操作:初始化、销毁、取元素、修改元素。(一般不做的插入和删除操作)
- 数组可以多维的,但是存储数据元素的内存单元是一维的,需要解决将多维数组映射到一维关系的
-
二维数组 :
-
存储单元是一维结构,而数组是个多维结构,则用连续存储单元存放数组的数据元素就有次序约定问题
-
以行序为主序
-
算存储地址单元:例:A[n][m]
-
a[i][j]的存储单元为:a[0][0]的地址单元 + (n*i+j)*size
-
-
以列序为主序
-
-
三维数组:
-
按页/行/列存放,页优先的顺序存储
-
a[m1][m2][m3]各维元素的个数为m1,m2,m3 下标为i1,i2, i3的数组元素的存储位置 LOC(i1,i2,i3) = a + i1*m2*m3 //前i1页的总元素数 + i2*m3 //第i1页的前i2行总元素个数 + i3 //第i2行前i3列元素个数
-
-
-
n维数组
-
- 各维元素个数为 m1,m2,m3,m4,......,m(n) - 下标为i1,i2,i3,i4,......,i(n)的数组元素的存储位置: - LOC(i1,i2,i3,i4,......,i(n)) = a + i1*m2*m3*m4*.....*m(n)+i2*m3*m4*.....*m(n)+.......+i(n-1)*m(n) +i(n) = a + n-1 n ( ∑ i(j) * ∏ m(k) ) + i(n) j = 1 k = j+1
-
-
特殊矩阵的存储压缩
- 矩阵:一个由m*n行个元素排成的m行n列的表。
- 矩阵的常规存储:
- 将矩阵描述成一个二维数组
- 矩阵的常规存储特点:
- 可以对元素进行随机存取;
- 矩阵运算非常简单;存储密度为1.
- 不适宜常规存储的矩阵:值相同的元素很多且呈现某种规律分布;零元素多
- 矩阵的压缩存储:为相同的非零元素只分配一个存储空间;对零元素不分配空间。
- 什么是压缩存储?
- 若多个数据元素值相同,为相同的非零元素只分配一个存储空间;对零元素不分配空间。
- 什么样的矩阵能够压缩?
- 对称矩阵
- 对角矩阵
- 三角矩阵
- 稀疏矩阵
-
对称矩阵
-
在n*n的矩阵a中,满足如下性质:
a(i)(j) = a(j)(i)(1<=i,j<=n)
-
存储方法
只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1)/2个元素空间。
-
存储结构
- 以行序存储在一个一维数组Sa[n(n+1)/2]中
-
-
三角矩阵
-
特点:对角线以下(或者以上)的数据元素(不包括对角线)全部为常数c。
-
存储方法:
重复元素 c 共享一个元素存储空间
,共占用n(n+1)/2+1
个元素 -
空间:Sa[1…n(n+1)/2+1]
-
上三角公式 下三角公式 k = { (i-1)*(2n-i+2)/2+j-i+1 i<=j; k = { i*(i-1)/2+j i>=j n*(n+1)/2+1 i>j n*(n+1)/2+1 i<j } }
-
-
对角矩阵(带状矩阵)
-
特点:在n*n的方阵中,所有非零元素都集中在以主对角线为中心的带状区域中,区域外的值都是0,则称为对角矩阵。常见的有三对矩阵角,五对角矩阵,七对角矩阵等。
-
存储方法:以对角线的顺序存储
-
{1,2,3,0,0,0}, {1,2,0,4,0,0}, {1,0,3,0,5,0}, {0,2,0,4,5,6}, {0,0,3,4,5,6}, {0,0,0,4,5,6},
-
-
-
稀疏矩阵
-
顺序存储结构
-
什么是稀疏矩阵:矩阵中非零元素的个数较少(一般少于5%)。
-
设在 m*n 的矩阵中有t个非零元素。
- 令 δ = t/(m * n)
- 当 δ <= 0.05时称为稀疏矩阵。
-
0,12, 9, 0, 0, 0 i代表第几行,j代表第几列,aij是当前元素的值 0, 0, 0, 0, 0, 0 三元组(i,j,aij)惟一确定矩阵的一个非零元 -3, 0, 0, 0,14, 0 M = 0, 0, 0, 0, 0, 0 M = {(1,2,12),(1,3,9),(3,1,-3),(3,5,14),(5,3,24),(6,2,18),(7,1,15),(7,4,-7)}和矩阵维数(7,6)唯一确定 0, 0,24, 0, 0, 0 0,18, 0, 0, 0, 0 15, 0, 0,-7, 0, 0
-
压缩存储原则:存非零元的值、行列位置和矩阵的行列数。
-
三元组:
- 又称为:有序的双下标法
- 三元组的不同表示方法可以解决稀疏矩阵不同压缩存储方法。
- 注意:为更可靠描述,通常再加上一个"总体"信息,即:总行数,总列数,非零元素总个数。(最好加在第一个行(或者数组下标为0的位置))
- 三元组顺序表的优点:非零元素在表中按行序有序存储,因此便于进行依次顺序处理的矩阵运算。
- 三元组顺序表的缺点:不能随机存取。若按行号存取某一行中的非零元素,则需从头开始进行查找。
-
-
链式存储结构
-
十字链表:
-
优点:它能够灵活地插入因运算而产生的新的非零元素,删除因运算而产生的新元素,实现矩阵的各种运算。
-
在十字链表中,矩阵的每一个非零元素用一个结点表示,该结点除了(row,col,value)以外,还要有两个域:
-
right:用于链接同一行中的下一个非零元素;
-
down :用以链接同一列中的下一个非零元素。
-
row col value down(列) right(行) -
0, 2 N= 1, 0 -2, 4 0, 0 [ 1 ] [ 2 ] M.chead(列指针) M.rhead(行指针) [1][2][2] [1]——————————————————----------[][^] | [2][1][1] | [2]-----------[][^] | | [3][1][-2] [3][2][4] [3]-------------[^][]----------[^][^] [4]
-
-
-
(三).广义表
1.定义
-
广义表(又称为列表Lists)是 n >= 0 个元素 a(0),a(1),a(3),…,a(n-1)的有限序列,其中每一个a(i)或者是原子,或者是一个广义表。
-
通常记作 Ls = {a(1),a(2),a(3),a(4),…,a(n)} 其中 Ls 是表名 ,n为表的长度,每一个 a(i) 为表的元素
-
一般用
大写字母
表示广义表
,小写字母
表示原子
-
表头:若非空(n>=1),则第一个元素a(1)就是表头
记作 head(LS) = a(1)。
注意表头:可以是原子,也可以是子表
-
表尾:除表头以外的其他元素组成的表。
记作 tail(LS) = (a(2),a(3),a(4),…,a(n)}。
注:表尾不是最后一个元素,而是一个子表。
2.性质
- 广义表中的数据元素有相对次序;一个直接前驱,一个直接后继
- 广义表的长度定义为最外层的所包含的元素个数;
- 如 C = (a,(b,c)) 是长度为2的广义表
- 广义表的深度定义为该广义表展开后所包含的括号的重数;
- 如 A = (b,c) A的深度为 1。B = (A,d) B的深度为2。 C=(f,B,h) C的深度为3。
注意:"原子"的深度为0,"空表"的的深度为1
。
- 广义表可以和其他的广义表
共享
; - 广义表可以是一个递归的表。如F=(a,F)=(a,(a,(a,(…))))
- 注意:递归表的深度是无穷的值,长度是有限值。
- 广义表是多层次的结构,广义表的元素可以是单元素,也可以是一个子表,而子表中还可以是一个子表。
3.与线性表的区别
- 广义表可以看成是线性表的推广,线性表是广义表的特例。
- 广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构。
当二维数组的每一行(每一列)作为子表处理时,二维数组即为一个广义表。
- 另外,树和有向图也可以用广义表表示。
- 由于广义表不仅集中了线性表、数组、树和有向图等常见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。
(四).案例
病毒检测:把病毒序列(长度m)和患者DNA序列都转换成字符串,进行模式匹配。
其中由于病毒的RNA是环状结构,可以把病毒序列串复制然后加在一起(长度2m),一起进行模式匹配。
病毒序列可以不断的往后移一位,循环m次。
五.树和二叉树
1.树
(1).概念
- 线性表
- 栈(特殊线性表)
- 线性结构 队列(特殊线性表) 1:1关系
- 字符串、数组、广义表
- 数据的逻辑结构
- 树形结构
- 非线性结构
- 图形结构
- 树形结构(非线性结构)
- 结点之间有分支
- 具有层次关系
(2).定义
树的定义:
- 树是一个n(n>=0)个结点的有限集合(树的定义是递归的定义),
- 若n = 0,称为空树;
- 若n > 0 ,则它满足如下的条件:
- 有且仅有一个特定的称为根的结点;
- 其余结点可以分为m(m>=0)个互不相交的有限集T1,T2,T3…,Tm,其中每一个集合本身又是一棵树,并成称为根的子树。
(3).基本术语
- 结点:数据元素以及指向子树的分支
根结点:非空树中无前驱结点的结点(只有一个)
- 结点的度:结点拥有的子树数。(分支的个数或后继元素的个数)
- 树的度:树内各结点的度的最大值。
度为 0
(无分支)的结点(叶子)
:终端结点
度不为 0 的分支结点(根结点以外的分支结点)
:内部结点(非终端结点)
树的深度
:树中结点的最大层次。树的高度:
从该结点到各叶结点的最长路径的长度。(指的是根结点的高度)
- 结点的子树的根结点称为该结点的
孩子
,该结点称为孩子的双亲
- 同一双亲的结点:兄弟结点
- 双亲在同一层的孩子结点:堂兄弟结点
- 结点的祖先:从根到该结点所经过分支上的所有结点
- 真祖先:将树中一个结点的非自身的祖先称为该结点的真祖先(树根没有真祖先)
- 结点的子孙:以某结点为根的子树中的任一结点。
- 真子孙:将树中一个结点的非自身的子孙称为该结点的真子孙(叶结点没有真子孙)
- 有序树:树中结点的各个子树从左到右有次序
(最左边为第一个孩子)
- 无序树:树中的结点各子树无序次。
- 森林:是m(m>=0) 棵互不相交的树的集合
- 把根节点删,树就变成森林。
- 一棵树可以看成一个特殊的森林。
- 给森林中的各子树加上一个双亲结点,森林就变成了树。
- 树一定是森林
- 森林不一定是树
(4).树结构和线性结构的区别
线性结构 | 线性结构 | 树结构 | 树结构 |
---|---|---|---|
第一个数据元素 | 无前驱 | 根节点(只有一个) | 无双亲 |
最后一个数据元素 | 无后继 | 叶子结点(可以有多个) | 无孩子 |
其他数据元素 | 一个前驱,一个后继 | 其他结点–中间结点 | 一个双亲,一个孩子 |
2.二叉树
(1).功能
- 二叉树结构简单,规律性最强;
- 所有的树可以转化为唯一对应的二叉树,不失一般性。
(2).定义
- 二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0)或者由一个根结点以及两棵不相交的分别称为这个根的左子树和右子树的二叉树组成。
- 特点:
- 每一个结点最多只有两个孩子(二叉树中不存在度大于2的结点)。
- 子树有左右之分,其次序不能颠倒
- 二叉树可以是一个空集,根可以有空的左子树或空的右子树
- 注:
二叉树不是树的特殊情况,它们是两个概念
- 二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树,还是右子树。
- 树当结点只有一个孩子时,就无须区分它是左还是右的次序。因此二者是不同的。就是二叉树和树的主要差别。
- 就是二叉树每一个结点位置或者说次序都是固定的,可以是空,但是不能说它没有位置,而树的结点位置是相对于别的结点来说的,没有别的结点时,它就无所谓了。
(3).基本形态
- 空二叉树
- 根和空的左右子树
- 根和左子树
- 根和右子树
- 根和左右子树
- 虽然二叉树和树的概念不一样,但是有关树的基本术语对二叉树都适用。
(4).抽象数据类型定义
(5).性质和存储结构
性质:
- 在二叉树的第i层上
至多
有2^(i-1)个结点(i>=1)。- 第i层上至少有1个结点
- 深度为 k 的二叉树至多有2^(k)-1个结点(k>=1)。
- 深度为 k 至少有 k 个结点。
- 对任何一棵二叉树 T,如果其叶子数为n(0),度为 2 的结点数为n(2),则 n(0) = n(2) + 1
- 总边数(B)
- B = n(所有结点) - 1
- B = n2(度为2的结点)*2 + n1(度为1的结点) * 1
- n0(度为0的结点 或 叶子结点) = n2(度为2的结点) +1
- 总边数(B)
- 具有n个结点的完全二叉树的深度为[log₂n]+1.
- :称作x的底,表示不大于x的最大整数([3.5] == 3)。
- 表明完全二叉树的结点数n与完全二叉数的深度k之间的关系
- 如果对一棵二叉树有n个结点的完全二叉树(深度为[log₂n]+1)的结点按层序编号(从第1层到第[log₂n]+1层。每层从左到右),则对任一结点i(1 <= i <= n),有:
- 如果i = 1,则结点i是二叉树的根,无双亲;如果i > 1,则双亲是结点[i/2],
- 如果2i > n,则结点i为叶子节点,无左孩子;否则,其左孩子结点是2i;
- 如果2i+1 > m,则结点i无右孩子结点;否则,其右孩子结点为2i+1。
- 表明完全二叉树中双亲结点和完全二叉树孩子结点编号之间的关系
存储结构:
- 顺序存储结构
- 按满二叉树的结点层次编号,依次存放在二叉树中的数据元素。
- 缺点:
- 大小固定,占用太多空间,
- 最坏情况:深度为k的且只有k个结点的单支树需要长度为2^k-1的一堆数组。
- 特点:
- 结点间关系蕴含在其存储位置中,浪费空间,适合存满二叉树和完全二叉树
- 链式存储结构
-
- 三叉链表:
- rchild;//右孩子
- data;//数据元素
- lchild;//左孩子
- parent;//双亲
- 在n个结点的二叉链中,空指针域数目:= 2n - (n-1) = n+1
-
特殊二叉树
- 满二叉树
- 一棵深度为k且有2^(k)-1个结点的二叉树称为满二叉树。
- 特点
- 每一层上的结点数都是最大结点数 (每层即满)
- 叶子节点都在最底层
- 编号:从根结点开始,自上而下,自左到右。
- 每一结点都有元素
- 完全二叉树
(6).遍历二叉树
- 定义:
- 顺着某一条搜索路径巡访问二叉树中的结点,使得每一个结点均被访问一次,而且仅被访问一次(周游)
- 目的:
- 得到树中所有结点的一个线性排列
- 用途:
- 它是得到树结构插入,删除,修改,查找和排序运算的前提,是二叉树一切运算的基础和核心。
- 遍历方法:
- 若规定先左后右
- DLR ———— 先(根)序遍历;根-左-右
- LDR ———— 中(根)序遍历;左-根-右
- LRD ———— 后(根)序遍历;左-右-根
- 若规定先左后右
- 算法描述
- 层次遍历
- 对一棵二叉树,从根结点开始,按从上到下、从左到右的顺序访问每一个结点。
- 每一个结点仅访问一次
- 先序遍历
(从最上面的根节点开始遍历)
- 判断是否为空
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
- 中序遍历
(从最低的左子树开始遍历)
- 判断是否为空
- 中序遍历左子树
- 访问根节点
- 中序遍历右子树
- 后序遍历
(从最低的左子树开始遍历)
- 判断是否为空
- 后序遍历左子树
- 后序遍历右子树
- 访问根节点
- 遍历左子树和右子树都是递归进行
- 层次遍历
- 表达式遍历结果
- 先序的:表达式在最前面的
- 前缀表达式(波兰式)
- 中序的:表达式在中间的
- 中缀表达式
- 后序的:表达式在后面的
- 后缀表达式(逆波兰式 )
- 先序的:表达式在最前面的
(7).根据遍历序列确定二叉树
-
若二叉树中各个结点的值均不同,则二叉树结点的先序遍历、中序遍历和后序遍历都是唯一的。
-
由二叉树的先序遍历和中序遍历,或由二叉树的后序遍历和中序遍历可以确定唯一一棵二叉树(先序和后序是不可以获取而二叉树)。
-
递归算法:
#include <stdio.h> #include <stdlib.h> /* 二叉链表式存储结构 */ typedef struct BiTree *btlink; typedef struct BiTree { btlink rchild; //右孩子 TElemType data; // btlink lchild; //左孩子 } BiNode; //先序遍历 int PreOrderTravel(btlink T) { if (T == NULL) { return 1; } else { printf("%d\t", T->data); // // visit(T); //根结点 PreOrderTravel(T->lchild); //左子树 PreOrderTravel(T->rchild); //右子树 } } //中序遍历 int InOrderTravel(btlink T) { if (T == NULL) { return 1; } else { modelOrderTravel(T->lchild); printf("%d\t", T->data); // visit(T); //根结点 modelOrderTravel(T->rchild); } } //后序遍历 int PostOrderTravel(btlink T) { if (T == NULL) { return 1; } else { OrOrderTravel(T->lchild); OrOrderTravel(T->rchild); printf("%d\t", T->data); // visit(T); //根结点 } }
-
算法分析
- 如果去点输出语句,从递归的角度来看,三种算法时完全相同的,或者说这三种算法的访问路径是相同的,只是访问点的时机不同
- 从虚线的出发点到终点的路径上,每一个结点经过3次
- 第
一
次经过时访问 =先序
遍历 - 第
二
次经过时访问 =中序
遍历 - 第
三
次经过时访问 =后序
遍历
- 第
-
-
非递归算法:
-
中序遍历
-
基本思想:
- 建立一个栈
- 根结点进栈,遍历子树
- 根结点出栈,输出根结点,遍历右子树
-
//中序遍历 void InOrderTraverse(btlink T) { LinkStack S = NewStack();//栈的开辟空间 btlink p;//新建一个空树 InitStack(S);//初始化 btlink q;//出栈的数据元素; p = T; while (p || !StackEmpty(S)) { if (p) { push(S, p);//入栈 p = p->lchild;//遍历左子树 } else { push(S, q);//出栈 printf("%d\t", q->data);//遍历根结点 p = q->rchild;//遍历=右子树 } /* code */ } }
-
(8).层次遍历
-
定义
- 对一棵二叉树,从根结点开始,按从上到下、从左到右的顺序访问每一个结点。
- 每一个结点仅访问一次
-
思路
-
使用一个队列
- 将根结点入队
- 队列不为空时循环,从队列中出列一个结点*p,访问它
- 若它有左孩子结点,将左孩子结点进队。
- 若它有右孩子结点,将右孩子结点进对。
-
#include <stdio.h> #include <stdlib.h> #define MAXSIZE 100 /* 二叉链表式存储结构 */ typedef struct BiTree *btlink; typedef struct BiTree { btlink rchild; //右孩子 TElemType data; // btlink lchild; //左孩子 } BiNode; typedef SqQueue *qlink; typedef struct { BiNode data[MAXSIZE]; //存放元素 int front, rear; //队头队尾 } SqQueue; //顺序循环队列类型 //初始化 btlink NewBNode() { return (btlink)malloc(sizeof(BiNode)); } /* 层次遍历 */ void LevelOrder(btlink T) { btlink p;//新的空的树 qlink qu;//一个空队列 InitQueue(qu);//初始化队列 enQueue(qu, T); //根结点入队 while (!QueueEmpty(qu))//队列是否为空 { deQueue(qu, p);//出队根结点 if (p->lchild != NULL) { enQueue(qu, p->lchild);//左子树入队 } if (p->rchild != NULL) { enQueue(qu, p->rchild);//右子树入队 } } }
-
(9).线索二叉树
- 定义:
- 利用二叉链表中的空指针域:
- 如果某一个结点的左孩子为空,则将空的左孩子指针域为
指向其前驱
;如果某一结点的右孩子为空,则将空的右孩子指针域为指向其后继
。 - ————————线索
- 对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化
- 如果某一个结点的左孩子为空,则将空的左孩子指针域为
- 为了区分lchild和rchild指针到底指向孩子的指针,还是指向前驱或者后继的指针,对二叉链表中每一个结点增设两个标志域ltag,rtag,并约定
ltag = 0
lchild指向该结点的左孩子
- ltag = 1 lchild指向该结点的前驱
rtag = 0
rchild指向该结点的右孩子
- rtag = 1 rchild指向该结点的后继
- 增设一个头结点
- ltag = 0,lchild指向根结点
- rtag = 1,rchild指向遍历序列中最后一个结点
- 遍历序列中第一个结点的lc域和最后一个结点的rc域都指向头结点
- 利用二叉链表中的空指针域:
3.森林
(1).定义
森林:是m(m>=0) 棵互不相交的树的集合。
(2).树的存储结构
-
双亲表示法
- 定义结构数组存放树的结点,每一个结点含两个域:
- 数据域:存放结点本身信息;
- 双亲域:指示本结点的双亲结点在数组中所在的位置;
- 特点:找双亲容易,找孩子难。
- 定义结构数组存放树的结点,每一个结点含两个域:
-
孩子链表
-
把每一个结点排列起来,看成是一个线性表,用单链表存储,则n个结点有n个孩子链表(叶子的孩子链表为空表)。
而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
-
特点:找孩子容易,找双亲难
-
带双亲的孩子链表
-
-
孩子兄弟表示法(二叉树表示法、二叉链表表示法)
- 用二叉链表作树的存储结构,链表中每一个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点
(3).树与二叉树的转换
由于树和二叉树都可以用二叉链表作存储结构,则以二叉链表作媒介可以导出树与二叉树之间的对应关系
a.树转换成二叉树
加线:在兄弟结点之间加一连线
抹线:对每一个结点,除了其左孩子外,去除其与其余孩子之间的关系
旋转:以树的根结点为轴心,将整树顺时针转45°
兄弟相连留长子
b.二叉树转换成树
加线:若p结点是双亲结点的左孩子,则将p结点的右孩子,右孩子,右孩子。。。。。沿分支找到的所有的右孩子,都与p的双亲用线连起来
抹线:抹掉原二叉树中双亲与右孩子之间的连线
调整:将结点按层次排列,形成树状结构
左孩右右练双亲,去掉原来孩子线。
(4).森林和二叉树的转换
a.森林转换成二叉树(二叉树与多棵树的关系)
- 将各棵树分别转换成二叉树
- 将每棵树的根结点用线相连
- 以第一棵树根结点为二叉树根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
- 树变二叉根相连
b.二叉树转换成森林
- 抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间的连线全部抹掉,使之变成孤立的二叉树。
- 还原:将孤立的二叉树还原成树。
- 去掉全部右孩线,孤立二叉再还原。
(5).树和森林的遍历
a.树的遍历(三种方式)
先(根)序遍历
- 若树不为空,则访问根结点,然后依次先根遍历各棵子树
后(根)序遍历
- 若树不为空,则先依次遍历各个子树,然后访问根结点
层次遍历
- 若树不为空,则自上而下,自左到右访问树中每一个结点
b.森林的遍历
- 将森林看成由三部分构成
- 森林中第一棵树的根结点;
- 森林中第一棵树的子树森林;
- 森林中其他树构成的森林。
- 先序遍历
- 若森林不空,则
- 访问森林中第一棵树的根结点
- 先序遍历森林中第一棵树的子树森林
- 先序遍历森林中(除第一棵树之外)其余构成的森林。
- 即依次从左到右对森林中的每一棵树进行先序遍历。
- 若森林不空,则
- 中序遍历
- 若森林不空,则
- 中序遍历森林中第一棵树的子树森林
- 访问森林中第一棵树的根结点
- 中序遍历森林中(除第一棵树之外)其余构成的森林。
- 即依次从左到右对森林中的每一棵树进行先序遍历。
- 若森林不空,则
(6).哈夫曼树及其应用
1.定义
路径:从树中的一个节点到另一个结点之间的分支构成这两个结点间的路径。
结点的路径长度:两结点间路径上的分支数。
树的路径长度:从树到每一个结点的路径长度之和。记作:TL
- 结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树。
- 权(weight):将树中的结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
- 结点的带权路径长度:从
根结点
到该结点
之间的路径长度
与该结点的权
的乘积
。 - 树的带权路径长度:树中所有
叶子结点
的带权路径长度之和
。 - 哈夫曼树:带权路径最短的树(最优树)
- “带权路径长度最短”:是在"度相同"的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等。
- 满二叉树不一定是哈夫曼树,具有相同带权结点的哈夫曼树不唯一。
2.构造哈夫曼树
-
贪心算法:构造哈夫曼树时首先选择权值小的叶子结点。
- 根据n个给定的权值{W1,W2,W3,W4,…,Wn}构成n棵二叉树的森林F = {T1,T2,T3,。。。,Tn},其中Ti只有一个带权为Wi的根结点(
构造森林全是根
)。 - 从F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点权值为其左右子树上根结点的权值之和。(
选用两小造新树
) - 在F中删除这两棵树,同时将新得到的二叉树加入到森林中。(
删除两小添新人
) - 重复2、3步骤,直到森林中只有一棵树为止,这棵树就是哈夫曼树。(
重复2、3剩单根
)
- 根据n个给定的权值{W1,W2,W3,W4,…,Wn}构成n棵二叉树的森林F = {T1,T2,T3,。。。,Tn},其中Ti只有一个带权为Wi的根结点(
-
哈夫曼树中的结点只有度为 0 或 2 ,没有度为 1 的结点。
-
包含n棵树的森林要经过 n-1 次合并才能形成哈夫曼树,共产生 n-1 个新结点
(包含 n 个叶子结点的哈夫曼树中共有 2n-1 个结点)。
-
算法实现
-
采取顺序存储结构------一维结构数组
-
结点类型定义
-
typedef struct htnode { int weight; int parent,lch,rch; /* data */ }HTNode,; //初始化
-
void initTreeNode(HuffmanTree HT, int count)
{
int m, i, cin = 1;
if (count <= 1)
{
return;
}
m = 2 * count - 1; //总的数组长度
HT[m];
//初始化为全部根结点的森林
for (i = 1; i <= m; i++)
{
HT[i].lch = 0;
HT[i].rch = 0;
HT[i].parent = 0;
/* code */
}
for (i = 1; i <= m; i++)
{
cin >> HT[i].weight; //注入权重
// HT[i].weight = cin;
}
for (i = n + 1; i < = m; i++)
{
int s1, s2;
Select(HT, i - 1, s1, s2);
HT[s1].parent = i;
HT[s2].parent = i; //从表中删除s1,s2
HT[i].lch = s1;
HT[i].rch = s2; //s1,s2分别为左右孩子
HT[i].weight = HT[s1].weight + HT[s2].weight; //i的权值为左右孩子之和
/* code */
}
}
```
3.哈夫曼树编码
-
*在远程通讯中,要将待传字符转换成由二进制的字符串:
*设要传送的字符为:
ABACCDA 若编码为:A—00 00010010101100
B—01
C—10
D—11
若将编码设计为长度不等的二进制编码,即让待传字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。
-
*设要传送的字符为:
ABACCDA *若编码为: A—0 000011010
B—00
C—1
D—01
重码
0000
={ AAAA,ABA,BB}关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀——————前缀编码
-
哈夫曼编码:
- 统计字符集中每一个字符在电文中出现的平均概率(概率越大,要求的编码越短)。
- 利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。
- 在哈夫曼树的每个分支上标上0或1:
- 结点的左分支标 0,右分支标 1
- 把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
- 为什么哈夫曼编码能够保证是前缀编码?
因为
没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀。
- 为什么哈夫曼编码能够保证字符编码总长最短?
因为
哈夫曼树的带权路径长度最短,故字符编码的总长最短。
- 性质1:哈夫曼编码是前缀码;
- 性质2:哈夫曼编码是最优前缀码。
-
文件的编码和解码
- 1.编码
- 输入各个字符及其权值
- 构造哈夫曼树 ----- HT[i]
- 进行哈夫曼树编码 -------HC[i]
- 查HC[i],得到各字符的哈夫曼树
- 2.解码
- 构造哈夫曼树
- 依次读入二进制码
- 读入0,则走向左孩子;读入1,则走向右孩子。
- 一旦到达某叶子时,即可译出字符
- 然后再从根出发继续译码,直到借宿。
- 1.编码
4.案例应用
(1)树
(2)二叉树
-
二叉树算法的应用
-
按先序遍历序列建立二叉树
-
从键盘输入二叉树的结点信息,建立二叉树存储结构
-
在建立二叉树的过程中按二叉树先序方式建立
-
#include <stdio.h> #include <stdlib.h> #define MAXSIZE 100 /* 二叉树顺序存储结构 */ typedef int TElemType; typedef TElemType SqBitree[MAXSIZE]; SqBitree bt; /* 二叉链表式存储结构 */ typedef struct BiTree *btlink; typedef struct BiTree { btlink rchild; //右孩子 TElemType data; // btlink lchild; //左孩子 } BiNode; //初始化 btlink NewBNode() { return (btlink)malloc(sizeof(BiNode)); } /* 创建中序二叉树*/ void CreateInBritree(btlink T) { char ch; scanf("%c", &ch); if (ch == '#') { T = NULL; } else { if (!(T = NewBNode())) { exit(1); } else { T->data = ch; //生成根结点 CreateBritree(T->lchild); //构建左子树 CreateBritree(T->rchild); //构建右子树 } } }
-
-
复制二叉树
-
遍历的树是否为空,如果为空,递归结束
-
otherwise ,申请新的结点空间,复制根结点
- 递归复制左子树
- 递归复制右子树
-
#include <stdio.h> #include <stdlib.h> #define MAXSIZE 100 /* 二叉树顺序存储结构 */ typedef int TElemType; typedef TElemType SqBitree[MAXSIZE]; SqBitree bt; /* 二叉链表式存储结构 */ typedef struct BiTree *btlink; typedef struct BiTree { btlink rchild; //右孩子 TElemType data; // btlink lchild; //左孩子 } BiNode; //初始化 btlink NewBNode() { return (btlink)malloc(sizeof(BiNode)); } /* 复制二叉树 */ int CopyBiTree(btlink T,btlink NewT) { if(T == NULL) { NewT = NULL; return 0; } else { NewT = NewBNode(); NewT->data = T->data; CopyBiTree(T->lchild,NewT->lchild); CopyBiTree(T->rchild,NewT->rchild); } }
-
-
计算二叉树的深度
-
如果是否为空,是则深度为0;
-
否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1.
-
/* 计算二叉树的深度 */ int Depth(btlink T) { int m, n; if (T == NULL) { return 0; } else { m = Depth(T->lchild); n = Depth(T->rchild); if (m > n) { return (m + 1); } else { return (n + 1); } } }
-
-
计算二叉树结点的总数
-
如果是否为空,则结点个数为0;
-
否则,结点个数等于左子树的结点加上右子树的结点再+1
-
/* 计算二叉树结点的总数 */ int NodeCount(btlink T) { if (T == NULL) { return 0; } else { return NodeCount(T->lchild) + NodeCount(T->rchild) + 1; } }
-
-
计算叶子结点的总数
-
如果为空,则叶子结点个数为0;
-
否则,为左子树的叶子结点个数+右子树的叶子结点个数
-
/* 叶子结点树 */ int LeadCount(btlink T) { if (T == NULL) { return 0; } if (T->lchild == NULL && T->rchild == NULL) { return 1; } else { return LeadCount(T->lchild)+LeadCount(T->rchild);//统计左子树和右子树的叶子节点的总数 } }
-
-
六.图
1.基本概念和术语
(1).基本概念
- 定义
- 图:G = (V,E)
- V:顶点(数据元素)的有穷非空集合
- E:边的有穷集合
- 逻辑结构:多对多
(2).术语
- 无向图:每条边都是无方向的。
- 有向图:每条边都是有方向的。
- 完全图:任意两点都有一条边相连
- 有向完全图:n个顶点,n(n-1)/2条边
- 无向完全图:n个顶点,n(n-1)条边
- 稀疏图:有很少边或者弧的图(e<nlog(n)).
- 稠密图:有较多的边或者弧的图
- 网 :边或弧带权的图
- 邻接 :有边或弧相连的两个顶点直之间的关系。
- 存在(Vi,Vj),则称Vi和Vj互为邻接点。
- 存在<Vi,Vj>,则称Vi邻接到Vj,Vj邻接于Vi。
- 关联(依附):边或者弧与顶点之间的关系。
- 存在(Vi,Vj)/<Vi,Vj>,则称该边或者弧关联于Vi和Vj。
- 顶点的度:与该顶点相关联的边的数目。记作TD(v)
- 在有向图中,顶点的度等于该顶点的入度与出度之和。
- 顶点v的入度:是以v为终点的有向边的条数,记作ID(v)
- 顶点v的出度:是以v为始点的有向边的条数,记作OD(v)
- 路径:接续的边构成的顶点序列
- 路径长度:路径上边或者弧的数目(权值)之和。
- 回路(环):第一个顶点和最后一个顶点相同的路径
- 简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
- 简单回路(简单环):除了起点路径和终点相同外,其余顶点均不相同的路径。
- 连通图(强连通图):在无(有)向图 G = (V,{E})中,若对任何两个顶点v、u 都存在从 v 到 u 的路径,则称G是连通图(强连通图)。
- 权与网:图中边或弧所具有的相关数称为权。表明从一个顶点到另一个顶点的距离或耗费。
- 带权的图就是网。
- 子图:设有两个图G = (V,{E})、G1 = (V1,{E1}) ,若V1∈V,E1∈E。
- 则称G1是G的子图。
- 连通分量(强连通分量)
无向图G的极大连通子图称为G的连通分量
极大连通子图意思是:该子图是G连通子图,将G的任何不在该子图中的顶点加入,子图不再连通、
- 有向图G的极大强连通子图称为G的强连通分量。
极大强连通子图意思是:该子图是G的强连通子图,将D的任何不在该子图中的顶点加入,子图不再是强连通的。 - 极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边,使子图不再连通。
- 生成树:包含无向图G所有顶点的极小连通子图。
- - 一个图可以有许多棵不同的生成树 - 所有生成树具有以下共同特点 - 生成树的顶点个数与图的顶点个数相同; - 生成树是图的极小连通子图,去掉一条边则非连通 - 一个有n个顶点的连通图的生成树有n-1条边 - 在生成树中再加一条边必然形成回路 - 生成树中任意两个顶点间的路径是唯一的 - 含有n个顶点的n-1条边的图不一定是生成树
- 生成森林:对非连通图,由各个连通分量的生成树的集合
2.类型定义
(1).图的抽象数据类型定义如下:
- ADT Graph{
数据对象V:具有相同特性的数据元素的集合,称为顶点集
数据关系R : R={VR}- VR = {<V,W> | <V,W> |V,W ∈V ^ p(V,W),
- <V,w>表示从v到w的弧,P(vw)定义了弧 <v,w> 的信息
- }
- 基本操作P:
- Create_Graph:图的创建操作。
- 初始条件:无。
- 操作结果:生成一个没有顶点的空图G。
- GetVex(G,v):求图中的顶点v的值。
- 初始条件:图G存在,v是图中的一个顶点。
- 操作结果:生成一个没有顶点的空图G
- CreateGraph(&G,V,VR){
- 初始条件:V是图的顶点集,VR是图中弧的集合。
- 操作结果:按V和VR的定义构造图G 。
- DFSTraverse(G)
- 初始条件:图G存在。
- 操作结果:对图进行深度优先遍历.
- BFSTraverse(G)
- 初始条件:图G存在。
- 操作结果:对图进行广度优先遍历。
- }ADT Graph
- Create_Graph:图的创建操作。
3.存储结构
(1).图没有顺序存储结构,但是可以借助二维数组来表示元素之间的关系
- 数组表示法(
邻接矩阵
) - 多重链表
邻接表
- 邻接多重表
- 十字链表
(2).使用方式
a.邻接矩阵
-
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间的关系),
-
设图A = (V,E)有n个顶点,则:
-
顶点表Vesx[n] 代表图中的每一个顶点,下标表示顶点在数组的位置
-
图的邻接矩阵是一个二维数组A.arcs[n] [n],定义为如果存在<i,j>∈E 或者(i,j)数以E
-
则A.arcs[i] [j] = 1,否则 A.arcs[i] [j] = 0 (i,j 体现为顶点表中顶点的下标位置,表明哪两个顶点有连接)。
-
无向图,则两个顶点下标都要标上 A.arcs[i] [j] = 1
- ————————————
无向图的邻接矩阵是对称的;
- 顶点 i 的度 = 第 i 行(列)中1的个数;
- 完全图的邻接矩阵中,对角元素为0,其余为1。
- ————————————
-
有向图
-
以行为始发点的顶点,如果存在与其他顶点存在关系,就为1.
{以第i行含义:以结点Vi为尾的弧(即出度边)}(发出弧)
-
列记录终点边的顶点,如果存在与其他顶点存在关系,就为1。
{第i列的含义:以结点Vi为头的弧(即入度边)}(接收弧)
-
有向图的邻接矩阵可能是不对称的
-
顶点的出度 = 第i行元素之和
顶点的入度 = 第i列元素之和
顶点的度 = 顶点的出度 + 顶点的出度
-
-
-
网(有权图)
- 定义 A.arcs[n] [n],定义为如果存在<i,j>∈E 或者(i,j)数以E则A.arcs[i] [j] = Wij(弧的权值),否则 A.arcs[i] [j] = ∞(无边弧) (i,j 体现为顶点表中顶点的下标位置,表明哪两个顶点有连接)。
-
-
存储形式
-
用两个数组分别表示存储顶点表和邻接矩阵
-
采用邻接矩阵表示法创建无向网
- 无向网
- 无向图
- 有向图
- 有向网
-
输入总的顶点数和边数
-
依次输入点的信息存入顶点表中。
-
初始化邻接矩阵,使每一个权值初始化为最大值。
-
构造邻接矩阵
-
typedef int Status; #define OK 1 #define ERROR 0 #define MAXInt 32767 //初始化表示权值的最大值 #define MVNnum 100 //最大顶点数 typedef char VerTexType; //设顶点的数据类型为字符型 typedef int ArcType; //假设边的权值为整数类型 int LocateVex(AMGrapth G,VerTexType u); /* 邻接矩阵 */ typedef struct amgrapth *amglink; typedef struct amgrapth { VerTexType vexs[MVNnum]; //顶点表 ArcType arcs[MVNnum][MVNnum]; //邻接矩阵 int vertexnum, arcnum; //图当前的顶点数和边数 } AMGrapth; //初始化 Status CtreatUDN(AMGrapth G, int size) { //无向网, int i, j,k; G.arcnum = G.vertexnum = size; //定义总顶点数和边数 for (i = 0; i < G.vertexnum; ++i) { G.vexs[i] = size >> i; //依次输入顶点的信息 } /* i 为行 j 为列 G.arcs[行][列] */ for (i = 0; i < G.vertexnum; i++) { for (j = 0; j < G.vertexnum; ++j) { G.arcs[i][j] = MAXInt; //定义边的权值为最大值 如果是无向图,权值为都0 } } for ( k = 0; k < G.arcnum; ++k) { size = size >> k; v1 = v2 = w = size;//输入一条边所依附的顶点及边的权值 如果是无向图, 权值都为 1 i = LocateVex(G,V1); j = LocateVex(G,v2);//确定v1和v2在G中的位置 G.arcs[i][j] = w;//边<v1,v2>的权值为w G.arcs[j][i] = G.arcs[i][j];//置<v1,v2>的对称边<v2,v1>的权值为w 这一条代码去掉就是有向图的初始化 } return OK; } int LocateVex(AMGrapth G,VerTexType u) { int i; for ( i = 0; i < G.vertexnum; ++i) { if(u == G.vexs[i]) return i; } return -1; }
-
-
优点:
- 直观、简单、好理解。
- 方便检查任意一对顶点间是否存在边。
- 方便找任一顶点的所有"邻接点"(有边直接相连的顶点)。
- 方便计算任一顶点的度。
-
缺点
- 不便于删除和插入
- 浪费空间——————存稀疏图
- 稠密图是合算的
- 浪费时间——————存稀疏图
-
b.邻接表
-
顶点:按编号顺序将顶点存储在一维数组中[头结点:data(数据域);firstarc(指针边结点的地址)];
-
关联同一顶点的边(以顶点为尾的弧):用线性链表存储{表结点:adjvex(邻接点域,存放与Vi邻接的顶点在表头数组中的位置。);nextrac(链域,下一个边结点);info(存放权值或者其他信息)}
-
特点
- 邻接表不唯一
- 若无向图中有n个顶点、e条边,则其邻接表需n个头结点和2e个表结点。适宜存储稀疏矩阵
- 无向图中顶点vi的度为第i个单链表中的结点数。
-
有向图
- 邻接表
- 顶点Vi的出度为第i个单链表中的结点个数。
- 顶点Vi的入度为整个单链表中邻结点域值是i-1的结点数
- 找出度易,找入度难
- 逆邻接表
- 顶点Vi的入度为第i个单链表中的结点个数。
- 顶点Vi的出度为整个单链表中邻结点域值是i-1的结点数
- 找入度易,找出度难
- 邻接表
-
存储形式
-
输入总边数和总顶点数
-
建立顶点表
- 依次输入点的信息存入顶点表中,
- 使每一个表头结点的指针域初始化为null
-
创建链表
- 依次输入每条边依附的两个顶点
- 确定两个顶点的序号i和j,建立边结点
- 将此边结点分别插入到vi和vj对应的两个边链表的头部
-
/* 邻接表 */ //无向图 int GnLocateVex(ALGraph G, VerTexType u); typedef struct arcnode *arclink; typedef struct arcnode { int adjvex; //该边所指向的顶点的位置 arclink nextrac; //指向下一条边的指针 char info; //和边相关的信息 } ArcNode; typedef struct gnode { VerTexType data; //顶点表 顶点值 arclink firstarc; //指向第一条依附该顶点的边的指针 } GNode, AdjList[MVNnum]; //adjList表示邻接表的类型 //AdjList a ==== GNode v[MVNum] typedef struct algraph { AdjList vertices; //vertices -- VerTex的复数 int vertexnum, arcnum; //图当前的顶点数和边数 } ALGraph; //初始化 Status initALGraph(ALGraph G, int size) { int i, j, k; G.arcnum = G.vertexnum = size; //输入总边数和总顶点数 for (i = 0; i < G.vertexnum; ++i) { G.vertices[i].data = size + i; //输入顶点值 G.vertices->firstarc = NULL; //初始化表头结点的指针域为null } for (k = 0; k < G.arcnum; ++k) //输入各边,构造邻接表 { cin >> v1 >> v2; //输入一条边依附的两个顶点 i = GnLocateVex(G, v1); //获取对应的v1的下标 j = GnLocateVex(G, v2); //获取v2对应的下标 arclink p1; //生成一个新的边结点p1 p1->adjvex = j; //邻接点序号为j p1->nextrac = G.vertices[i].firstarc; G.vertices[i].firstarc = p1; //将新结点p1插入到vi的边表头部 arclink p2; //生成一个新的边结点p2 p2->adjvex = i; //邻接点序号为i p2->nextrac = G.vertices[j].firstarc; G.vertices[j].firstarc = p2; //将新结点p2插入到vj的边表头部 } return OK; } int GnLocateVex(ALGraph G, VerTexType u) { int i; for (i = 0; i < G.vertexnum; ++i) { if (u == G.vertices[i]) return i; } return -1; }
-
-
存储特点
- 方便找任一顶点的所有"邻接点"。
- 节约稀疏图空间。
- 不方便检查任意一对顶点间是否存在边
- 方便计算任一顶点的“度”。(针对于无向度)
c.十字链表
-
十字链表(Orthogonal List)是有向图的另一种链式存储结构。
我们也可以把它看成是将有向图的邻接表和逆邻接表结合起来形成的一种链表。
有向图中的每一条弧对应十字链表中的一个弧结点,同时有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点。
-
顶点结点:data(数据域);firstin(第一条入弧);firstout(第一条出弧)。
-
弧结点:tailvex(弧尾位置);headvex(弧头位置,下一个边结点);hlink(弧头相同的下一条弧);tlink(弧尾相同的下一个弧)。
d.邻接多重表(无向图的的另一种链式存储结构)
- 顶点结点:data(存与顶点有关的信息);firstedge(指向第一条依附于该顶点的边)。
- 边结点:mark(标志域);ivex(该边依附的两个顶点在表头数组中的位置);ilink(指向依附于ivex的下一条边);
- :jvex(该边依附的两个顶点在表头数组中的位置);jlink(指向依附于jvex的下一条边);info(存放权值或者其他信息)。
4.图的遍历
(1).定义
从已给的连通图中某一顶点出发,沿着一些边访问图中所有的顶点,且使每一个顶点仅被访问一次,就叫作图的遍历,它是图的基本运算
实质:找每一个顶点的邻接点的过程
(2).特点
- 图中可能存在回路,且图的任一顶点都可能与其他顶点相通,在访问完某个顶点之后可能会沿着某些边又回到曾经访问的顶点。
- 怎样避免重复访问:
- 设置辅助数组 Visited[i] 初始化每一个顶点代表数组的下标,数组值为0。如果哪一个顶点被遍历了,就修改为1,表明该顶点已经被访问过了
(3).图常用的遍历
-
深度优先搜索(Depth_First Search------DFS)(连通图的深度优先遍历类似于树的先序遍历)
-
步骤:
- 在访问图中某一起始顶点v后,由v出发,访问它的任一邻接顶点再从w出发,访问与w邻接但还未被访问过的顶点w2
- 然后再从w2出发,进行类似的访问,……
- 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点u为止。
- 接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。
- 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;
- 如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
-
邻接矩阵(实现图的深度遍历–无向图)
-
算法
-
//邻接矩阵图的深度优先遍历实现方法 void DFS(AMGrapth G, int v) { int visited[MAXInt]; int count, i; count << v; visited[v] = true; //访问第v个顶点 for (i = 0; i < G.vertexnum; ++i) //依次检查邻接矩阵v所在的行 { if ((G.arcs[v][i] != 0) && (!visited[i])) { DFS(G, i); //i是v的邻接点,如果i未访问,则递归调用dfs } } }
-
-
算法效率:
- 用邻接矩阵来表示图,遍历图只能每一个顶点都要从头扫描该顶点所在行,时间复杂度是O(n^2)
- 用邻接表来表示图,虽然有2e个结点,但是只需扫描e个结点即可完成遍历,加上访问n个头节点的时间,时间复杂度为O(n+e)
-
-
结论:
- 稠密图适合在邻接矩阵上进行深度遍历
- 稀疏图适合于在邻接表上进行深度遍历
-
非连通图的遍历
- 先遍历一个连通分量,等到第一个的连通分量无法在退的时候,就访问下一个连通分量,以此类推。
-
-
广度优先搜索(Breadth_First Search-------BFS)
-
从图的某一个顶点出发,首先依次访问该结点的所有邻接点。再按这些顶点被先后访问的先后次序依次访问与它们相邻接的所有未被访问的结点
-
邻接表(实现图的广度优先)
-
算法
-
void BFS(ALGraph G, int v) { int visited[MAXInt]; int cout, w; cout << v; visited[v] = true; //访问第v个顶点 InitQueue(Q); //辅助队列Q初始化,置空 EnQueue(Q, v); //v进队 while (!QueueEmpty(Q)) //队列非空 { DeQueue(Q, u); //队头元素出队并置为u for (w = FirstAdjVex(G, u); w >= 0; w = NexrAdjVex(G, u, w)) { if ((!visited[w])) //w为u的尚未访问的邻接顶点 { cout << w; visited[w] = true; EnQueue(Q, w); //w进队 } /* code */ } /* code */ } }
-
-
算法效率
- 如果使用邻接矩阵,则bfs对于每一个被访问的顶点,都要循环检查矩阵中整整一行(n个元素),总的时间代价为O(n^2)
- 用邻接表来表示的图,虽然有2e个表结点,但是只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为O(n+e)
-
-
-
DFS和BFS算法效率的比较
- 空间复杂度相同,都是O(n)借用栈或者队列;
- 时间复杂度只与存储结构(邻接矩阵或者邻接表)有关,而与搜索路径无关
5.图的应用
(1).无向图的生成树
- 生成树:包含无向图G所有顶点的极小连通子图。
- 一个图可以有许多棵不同的生成树
- 所有生成树具有以下共同特点
- 生成树的顶点个数与图的顶点个数相同;
- 生成树是图的极小连通子图,去掉一条边则非连通
- 一个有n个顶点的连通图的生成树有n-1条边
- 在生成树中再加一条边必然形成回路
- 生成树中任意两个顶点间的路径是唯一的
- 含有n个顶点的n-1条边的图不一定是生成树
- 生成森林:对非连通图,由各个连通分量的生成树的集合
- 设图G = (V,E)是一个连通图当从图任意顶点出发遍历图G时,将边集E(G)分成两个集合T(G)和B(G)。其中T(G)是遍历图经过时的边的集合,B(G)是遍历图时未经过的集合。显然,G1(V,T)是图的极小连通子图。即子图G1是连通图G的生成树。
(2).最小生成树
给定一个无向网络,在该网所有生成树中,使得各边权值之和最小的那棵生成树称为该网最小生成树,也叫最小代价生成树。
-
MST性质
:设N = (V,E)是一个连通网,U是顶点集V的一个非空子集。 若边(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树
- 解释:
- 在生成树的构造过程中,图中n个顶点分数两个集合:
- 已经落在生成树上的顶点集:U
- 尚未落在生成树上的顶点集:V-U
- 接下来则应在所有连通U中顶点和V-U中顶点的边中选取权值最小的边
- 在生成树的构造过程中,图中n个顶点分数两个集合:
- 解释:
-
普利姆算法:
- 设N—(V, E)是连通网, TE是N上最小生成树中边的集合。
- 初始令U={uo), (LoE V), TE={}。
- 在所有u∈ U, ve∈V-U的边(u, V)∈中,找一条代价最小的边(Uo, Vo)
- 将(Uo,Vo) 并入集合TE,同时Vo并入U
- 重复上述操作直至U=V为止,则T(V, TE)为N的最小生成树
-
克鲁斯卡尔算法:
- 设连通网N=(V,E),令最小生成树初始状态为只有n个顶点而无边的非连通图T=(V,{}), 每个顶点自成一个连通分量。
- 在E中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环)
- 则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
- 依此类推,直至T中所有顶点都在同一连通分量上为止。
-
最小生成树不是唯一的
-
两种算法比较
-
算法名 普利姆算法 克鲁斯卡尔算法 算法思想 选择点 选择边 时间复杂度 O(n^2) (n为顶点数) O(eloge) (e为边数) 适应范围 稠密图 稀疏图
-
(3).最短路径
- 典型应用:交通网络图
- 问题抽象:在
有向网
中A点(源点
)到达B点(终点
)的多条路径中,寻找一条各边权值之和最小
的路径,即最短路径
。- 最短路径与最小生成树不同,路径不一定包含n个顶点,也不一定包含n-1条边。
- 第一类问题:两点间最短路经。(单源最短路径)
- 迪杰斯特拉算法
- 初始化:向找从源点V0到各终点Vk的直达路径(v0,vk),即通过一条弧到达的路径
- 选择:从这些路径中选择一条长度最短的路径(v0,u)。
- 更新:然后对其余各条的路径进行适当调整:
- 若图中存在弧(U,Vk),且(V0,u)+(u,vk)< (V0,Vk),则以路径(V0,u,Vk)代替(V0,Vk);
- 在调整后的各条路径中再找长度最短的路径,依此类推。
- 迪杰斯特拉算法
- 第二类问题:某源点到其他各点的路径(所有顶点间最短路径)。
- 每一次以一个顶点源点,重复执行迪杰斯特拉算法n次 时间复杂度 O(n^3)
- 佛洛依德算法 时间复杂度:O(n^3)
- 逐个顶点试探
- 从Vi到Vj的所有可能存在的路径中
- 选出一条长度最短的路径
(4).拓扑排序
-
有向无环图:无环的有向图,简称DAG图
-
有向无环图通常用来描述一个工程或者一个系统的进行过程。(通常把计划、施工、生产、程序流程等当成是一个工程)
一个工程可以分为若干子工程,只要完成了这些子工程(活动),就可以导致整个工程的完成
-
AOV网(拓扑排序):
- 用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网
- 若从i到j有一条有向路径,则i是j的前驱,j是i的后继。
- 若<i,j>是网中有向边,则i是j的直接前驱;j是i的直接后继
- AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然是荒谬的
-
-
定义:
- 在AOV网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧<i,j>存在,则在这个序列中,i一定排在j的前面,具有这种性质的线性序列称为拓扑排序,相应的拓扑排序的算法称为拓扑排序。
-
方法:
- 在有向图中选一个没有前驱的顶点且输出之
- 从图中删除该顶点和所有以它的为尾的弧
- 重复上面的两步,直至全部顶点均已输出;
- 或者当图中不存在无前驱的顶点为止
- 一个AOV网的拓扑序列不是唯一的
-
应用:
- 检测AOV网中是否存在环方法:
- 对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。
- 检测AOV网中是否存在环方法:
(5).关键路径
-
AOE网(关键路径):
-
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以弧表示活动,以顶点表示活动之间的优先制约关系,称这种有向图为边表示活动的网,简称AOE网
-
关键路径
- 把工程计划表示为边表示活动的网络,即AOE网,用顶点表示事件,弧表示活动,弧瓜的权表示活动持续时间。
- 事件表示在它之前的活动已经完成,在它之后的活动可以开始
- 源点:入度为0的顶点(表示整个工程开始)
- 汇点:出度为0的顶点(表示整个工程结束)
- 关键路径:路径长度最长的长度
- 路径长度:路径上各活动持续时间之和
-
确定关键路径
- ve(vj):表示事件vj的最早发生时间
- vl(vj):表示事件vj的最迟发生时间
- e(i):表示活动ai的最早开始时间
- l(i):表示活动ai的最迟开始时间
- l(i) - e(i):表示完成活动ai的时间余量。
-
关建活动:关键路径上的活动,即l(i) == e(i),(即l(i)-e(i) == 0)的活动。
-
如何找(i) == e(i)的关键活动?
- 设活动ai用弧<j,k>表示,其持续时间记为:Wj,k
- 则有:
(1) e(i) = ve(j)
-
(2) l(i) =vl(k) -Wj,k
- 则有:
- 如何求ve(j)和vl(j) ?
- (1)从ve(1)=0开始向前递推
- ve(j) =Max{ve(i) + Wi,j},<i,j>∈T,2 <= j <= n
- 其中T是所有以j为头的弧的集合。
- (2)从vl(n) = ve(n)开始向后递推
- vl(i) =Min{vl(j)-wi,j},<i,j>∈S,1<=i<=n-1,
- 其中S是所有以i为尾的弧的集合
- (1)从ve(1)=0开始向前递推
讨论:
关键路径的讨论
1、若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。
2、如果一个活动处于所有的关键路径上,那么提高这个活动的速度,就能缩短整个工程的完成时间。如:a1、a4
3、处于所有的关键路径上的活动完成时间不能缩短太多否则会使原来的关键路径变成不是关键路径。这时,必须重新寻找关键路径。如:a1由6天变成3天,就会改变关键路径。 - 设活动ai用弧<j,k>表示,其持续时间记为:Wj,k
6.关系
(1).邻接矩阵和邻接表
- 联系:邻接表中每一个链表对应邻接矩阵中的每一行,链表中结点个数等于一行中非零元素的个数
- 区别:
- 对于任意确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。
- 邻接矩阵的空间复杂度O(n^2),而邻接表的空间复杂度为O(n+e).
- 用途:邻接表多用于稀疏图,而邻接矩阵多用于稠密图。
7.案例引入
(1).六度空间理论
七.查找
1.基本概念
- 查找:查找表是由同一类型的数据元素(或记录)构成的集合。由于集合中的数据元素之间存在着松散的关系,因此查找表是一种应用灵便的结构。
- 关键字:用来标识一个数据元素(或记录)的某个数据项的值。
- 主关键字:可唯一地识别一个记录的关键字是主关键字。
- 次关键字:反之,用来识别若干记录的关键字是次关键字。
- 查找成功:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素或(记录)
- 若查找表中存在这样一个记录,则称查找成功。
- 查找结果给出整个记录的信息,或指示该记录在查找表中的位置:
- 否则称“查找不成功”。
- 查找结果给出“空记录”或"空指针"。
- 若查找表中存在这样一个记录,则称查找成功。
- 查找目的
- 查询某一个"特定的"数据元素是否在查找表中;
- 检索某个"特定的"数据元素的各种属性;
- 在查找表中插入一个数据元素;
- 删除查找表中的某一个数据元素。
- 查找表分类
- 静态查找表:仅作"查询"(检索)操作的查找表
- 动态查找表:作"插入"和"删除"操作的查找表
- 评价查找算法指标:关键字的平均比较次数,也称平均查找长度ASL
- 查找过程中研究什么:查找的方法取决于查找表的结构,即表中数据元素是依何种关系组织在一起的。
2.线性表
(1).顺序查找
-
范围:
- 顺序表或链式表表示的静态查找表
- 表内元素之间无序
-
顺序表的表示:
-
typedef int KeyType; typedef struct elemtype *elink; typedef struct elemtype { KeyType key; //关键字域 /* data */ //其他字域 } ElemType; typedef struct stable { elink R; //表基址 int length; //表长 /* data */ } SSTable; SSTable ST; //定义顺序表 //查找关键字 Status Search_Sep(SSTable ST, KeyType key) { int i; for (i = ST.length; i >= 1; --i) { if (ST.R[i].key == key) return i; /* code */ } return ERROR; } //哨兵:监视哨 Status Saerch_seq(SSTable ST, KeyType key) { int i; ST.R[0].key = key; for (i = ST.length; ST.R[i].key != key; --i); return i; }
-
时间复杂度:O(n)
- 查找成功时的平均查找长度,ASL(n) = (1+2+3+4+。。。+n)/n = (n+1)/2
-
空间复杂度:一个辅助空间O(1);
-
优点:算法简单,逻辑次序无要求,且不同存储结构均可用
-
缺点:ASL太长,时间效率太低
(2).二分查找
-
折半查找:每次将待查找记录所在区间缩小一半
-
/* 二分法 */ //折半算法(非递归算法) Status Search_Bin(SSTable ST, KeyType key) { int low, hight, mid; low = 0; hight = ST.length; while (low <= hight) { mid = (low + hight) / 2; if (ST.R[mid].key == key) return mid; else if (ST.R[mid].key > key) { hight = mid - 1; } else { low = mid + 1; } } return 0; } //递归算法 Status Search_BinDgui(SSTable ST, KeyType key, int low, int hight, int mid) { if (low <= hight) return 0; mid = (low + hight) / 2; if (ST.R[mid].key == key) return mid; else if (ST.R[mid].key > key) { hight = mid + 1; Search_Bin(ST, key, low, hight, mid); } else { low = mid + 1; Search_Bin(ST, key, low, hight, mid); } }
-
时间复杂度O(n/2):
-
查找成功:
- 比较次数 = 路径上的结点数
- 比较次数 = 结点的层次
- 比较次数 <= 判定树的深度(log2(n)+1)
-
查找失败:
- 比较次数 = 路径上的内部结点数
- 比较次数 <= 判定树的深度(log2(n)+1)
-
空间复杂度:
-
平均查找长度ASL:
- 设表长n = 2^h-1,则h = log2(n+1)(此时判定树为深度 = h的满二叉树),且表中每一个记录的查找概率相等:Pi = 1/n;
-
优点:效率比顺序查找高
-
缺点:只适用于有序表,且限于顺序存储结构(对线性链式表无效)
(3).分块查找
- 分块查找:索引顺序查找
- 将表分成几块,且表或者有序,或者分块有序,若i < j,则将j块中所有记录的关键字均大于第i块中的最大关键字。
- 建立"索引表"(每一个结点含有最大关键字域和指向本块第一个结点的指针,且关键字有序)。
- 查找过程:先确定待查找记录所在块(顺序或折半查找),再在块中进行查找(顺序查找)。
- 查找效率:ASL = Lb + Lw;(Lb是对索引表查找的ASL,Lw是对快查找的ASL)
- 折半查找: ASL(bs)=log₂(n/s+1)+s/2; (log₂N <= ASL(bs) <= (n+1)/2)(S是每块内部的记录个数,n/s即块的数目)
- 优点:插入和删除比较容易,无需进行大量移动。
- 缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算。
- 适用情况:如果线性表既要快速查找又经常动态变化,则可采用分块查找。
(4).区别
顺序查找 | 折半查找 | 分块查找 | |
---|---|---|---|
ASL | 最大 | 最小 | 中间 |
表结构 | 有序表,序表 | 有序表 | 分块有序 |
存储结构 | 顺序表,线性链表 | 顺序表 | 顺序表,线性链表 |
3.树表
(1).概念
- 当表插入、删除操作频繁时,为维护表的有序性,需要移动衣中很多记录。
- 改用动态查找表——几种特殊的树
- 表结构在查找过程中动态生成
- 对于给定值key
- 若表中存在,则成功返回;
- 否则,插入关键字等于key的记录
(2).二叉排序树
-
定义:又称二叉搜索树,二叉查找树
-
二叉排序树或者是空树,或满足如下性质的二叉树:
- 若其左子树非空,则左子树所有的结点的值均小于根结点的值
- 若其右子树非空,则右子树所有的结点的值均大于等于根结点的值
- 其左右子树本身又各是一棵二叉排序树。
-
性质:
中序遍历非空二叉排序树所得到的数据元素序列是一个按关键字排序的递增有序序列。
-
查找
- 若查找的关键字等于根结点,成功
- 否则
- 若小于根结点,查其左子树
- 若大于根结点,查其右子树
- 左右子树上的操作类似
-
查找算法
#include <stdio.h> #include <stdlib.h> typedef int Status; #define OK 1 #define ERROR 0 #define true 1 #define false 0 typedef int KeyType; typedef struct InfoType { int info; /* data */ } InfoType; typedef struct elemtype { KeyType key; //关键字项 InfoType otherinfo; //其他数据域 /* data */ } ElemType; typedef struct bstnode *bstlink; typedef struct bstnode { ElemType data; //数据域 bstlink lchild; //左孩子指针 bstlink rchild; //右孩子指针 /* data */ } BSTNode; bstlink SearchBST(bstlink T, KeyType key) { if ((!T) || key == T->data.key) { return T; } else if (key < T->data.key) { return SearchBST(T->lchild, key); } else { return SearchBST(T->rchild, key); } }
-
【算法思想】
-
若二叉排序树为空,则查找失败,返回空指针。
-
若二叉排序树非空,将给定值key与根结点的关键字
T—>data.key进行比较:
- 若key等于T—>data.key,则查找成功,返回 结点地址;
- 若key小FT->data.key, 则进一步查找左子树;
- 若key大FT—>data.key,则进一步查找右子树。
-
-
二叉排序树上查找某关键字等于给定值的结点过程,其实就是走了一条从根到该结点的路径
- 比较的关键字的次数 = 此结点所在层次数
- 最多比较次数 = 树的深度
-
二叉排序树的平均查找长度
- 含有n个结点的二叉排序树的平均查找长度和树的形态有关
- 最好情况:O(log₂n) 与对半查找中的判定树相同
- 最坏情况:O(n) 与顺序查找情况相同
- 最好情况:ASL = log₂(n+1)-1
- 最坏情况:ASL = (n+1)/2
- 含有n个结点的二叉排序树的平均查找长度和树的形态有关
-
插入
若二叉排序树为空,则插入结点作为根结点插入到空树中否则,继续在其左、右子树上查找
- 树中已有,不再插入
- 树中没有
- 查找直至某个叶子结点的左子树或右子树为空为止,则插入结点应为该叶子结点的左孩子或右孩子
- 插入的元素在叶子结点上
-
生成
- 从空树出发,经过一系列的查找、插入操作之后,可以生成一棵二叉排序树
- 一个无序序列可以通过二叉排序树而变成一个有序序列。构造树的过程就是对无序序列进行排序的过程
- 插入都是在叶子结点上,故无需移动其他结点。相当于在有序序列插入记录而无需移动其他记录。
- 不同插入次序的序列生成不同形态的二叉排序树
-
删除
- 从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变
- 如果删除的是叶子结点:直接删除该结点
- 删除的结点只有左子树或者右子树时,用其右子树或者左子树替代它(结点替换)
- 被删除的结点既有左子树,也有右子树
- 以中序前趋值替换之(值替换),然后删除该前驱趋点,前趋结点是左子树中最大的结点。
- 也可以用后继替换之,然后再删除该结点后继结点。后继结点是右子树中最小的结点。
- 从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变
(3).平衡二叉树
-
定义
- 又称AVL树
- 一棵平衡二叉树或者是空树,或者是具有下列性质的二叉排序树;
- 左子树与右子树的高度之差的绝对值小于等于1;
- 左子树与右子树也是平衡二叉排序树。
-
平衡因子
为了方便起见,给每个结点附加一个数字,给出该结点左子树右子树的高度差。这个数字称为结点的平衡因子(BF)。
平衡因子 = 结点左子树的高度 — 结点右子树的高度
根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是 -1、0 和 1
对于一棵有n个结点的AVL树,其高度保持在O(log₂n)的数量级,ASL也保持在O(log₂n)量级
-
调整平衡二叉树
当我们在一棵平衡二叉排序树上插入一个结点时,可能导致失衡,即出现平衡因子绝对值大于1的结点
-
平衡调整四种类型
- LL型
- A 失衡结点 不止一个失衡结点时,为最小失衡子树的根结点
- B: A 结点的孩子,c结点的双亲
- C:插入新结点的子树
- LR 型
- RL 型
- RR 型
- 调整原则
- 降低高度
- 保持二叉排序树性质
- LL型
-
调整
- LL 型
- LR 型
- RL 型
- RR 型
4.散列表
(1).基本概念
记录的存储位置与关键字之间存在对应关系
对应关系 ------hash函数
Loc(i) = H(keyi)
- 优点:查找效率高
- 缺点:空间效率低!
- 术语
- 散列方法(杂凑方法):选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;
- 查找时,由用一个函数对给定的值k计算地址,将k与地址单元中元素关建码进行比,确定查找是否成功。
- 散列函数(杂凑函数):散列方法中使用的转换函数
- 散列表:按上诉思想构造表
- 冲突:不通关键码映射到同一个散列地址上
- 同义词:具有相同函数值的多个关键字
- 散列方法(杂凑方法):选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;
(2).构造方法
-
构造好散列函数
- 所选函数尽可能简单,以便提高转换速度
- 所选函数对关键码计算出的地址,应在散列表地址集中致均匀分布,以减少空间浪费。
- 因素:
- 执行速度
- 关键字长度
- 散列表大小
- 关键字的分布情况
- 查找频率。
-
制定一个好的解决冲突的方案
- 查找时,如果散列函数计算出的地址中查不到关键码,则应当根据解决冲突的规则,有规律地查询其他相关单元
-
根据元素集合的特性构造
- n个数据原仅占用n个地址,虽然散列查找是以空间换时间,但仍希望散列的地址空间尽量小
- 无论用什么方法存储,目的都是尽量均匀地存放元素,以避免冲突。
-
构造方法类型
- 直接定址法
- 优点:以关键码key的某个线性函数值为散列地址,不会产生冲突
- 缺点:要占用连续的地址空间,空间效率低
- 数字分析法
- 平方取中法
- 除留余数法
- Hash(key) = key %(mop) p(p是一个整数)
- 关建选取合适的p
- 技巧:
- 设表长为m,取
p <= m 且为质数
- 设表长为m,取
- 随机数法
- 直接定址法
(3).处理冲突方法
- 开放地址法(开地址法)
- 基本思想:有冲突时就去寻找下一个空的散列地址,只要散列地址足够大,空的散列地址总能找到,并将数据元素存入
- 常用方法
- 线性探测法
- Hi = (Hash(key)+Di) mod m (1 <= i <= m)
- 二次探测法
- Hi = (Hash(key)+Di) mod m (m 4k+3)
- 伪随机探测法
- 线性探测法
- 链地址法
- 基本思想:相同散列地址的记录链成一单链表
- m个散列地址就设m个单链表,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构。
- 链地址法建立散列表步骤
- 取数据元素的关键字key,计算其散列函数值(地址)。若该地址对应的链表为空,则将该元素插入此链表;否则执行Step2解决冲突。
- 根据选择的冲突处理方法,计算关键字key的下一个存储地址。若该地址对应的链表为不为空,则利用链表的前插法或后插法将该元素插入此链表。
- 优点:
- 非同义词不会冲突,无"聚集"现象
- 链表上的结点空间是动态申请,更适合于表长的不确定的情况
- 缺点:
- 再散列法
- 建立一个公共溢出区
(4).散列表查找及其性能分析
- 使用平均查找长度ASL来衡量查找算法,ASL取决于
- 散列函数
- 处理冲突的方法
- 散列表的装填因子α { α =(表中填入的记录数 / 哈希表的长度)}
- α越大,表中记录数越多,说明表装的越满,发生冲突可能性越大,查找时的比较次数就越多。
- ASL与装填因子α有关!既不是严格的O(1),也不是O(n)。
- ASL ≈ 1 + (a/2) (拉链法)
- ASL ≈ 1/2( 1 + 1/(1-a) ) (线性探测法)
- ASL ≈ -1/a * ln(1-a) (随机探测法)
(5).结论
-
散列表技术具有很好的平均性能,优于一些传统的技术
-
链地址法优于开地址法
-
除留余数法作散列表优于其它类型函数
序树为空,则插入结点作为根结点插入到空树中否则,继续在其左、右子树上查找- 树中已有,不再插入
- 树中没有
- 查找直至某个叶子结点的左子树或右子树为空为止,则插入结点应为该叶子结点的左孩子或右孩子
- 插入的元素在叶子结点上
-
生成
- 从空树出发,经过一系列的查找、插入操作之后,可以生成一棵二叉排序树
- 一个无序序列可以通过二叉排序树而变成一个有序序列。构造树的过程就是对无序序列进行排序的过程
- 插入都是在叶子结点上,故无需移动其他结点。相当于在有序序列插入记录而无需移动其他记录。
- 不同插入次序的序列生成不同形态的二叉排序树
-
删除
- 从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变
- 如果删除的是叶子结点:直接删除该结点
- 删除的结点只有左子树或者右子树时,用其右子树或者左子树替代它(结点替换)
- 被删除的结点既有左子树,也有右子树
- 以中序前趋值替换之(值替换),然后删除该前驱趋点,前趋结点是左子树中最大的结点。
- 也可以用后继替换之,然后再删除该结点后继结点。后继结点是右子树中最小的结点。
- 从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变
(3).平衡二叉树
-
定义
- 又称AVL树
- 一棵平衡二叉树或者是空树,或者是具有下列性质的二叉排序树;
- 左子树与右子树的高度之差的绝对值小于等于1;
- 左子树与右子树也是平衡二叉排序树。
-
平衡因子
为了方便起见,给每个结点附加一个数字,给出该结点左子树右子树的高度差。这个数字称为结点的平衡因子(BF)。
平衡因子 = 结点左子树的高度 — 结点右子树的高度
根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是 -1、0 和 1
对于一棵有n个结点的AVL树,其高度保持在O(log₂n)的数量级,ASL也保持在O(log₂n)量级
-
调整平衡二叉树
当我们在一棵平衡二叉排序树上插入一个结点时,可能导致失衡,即出现平衡因子绝对值大于1的结点
-
平衡调整四种类型
- LL型
- A 失衡结点 不止一个失衡结点时,为最小失衡子树的根结点
- B: A 结点的孩子,c结点的双亲
- C:插入新结点的子树
- LR 型
- RL 型
- RR 型
- 调整原则
- 降低高度
- 保持二叉排序树性质
- LL型
-
调整
- LL 型
- LR 型
- RL 型
- RR 型
4.散列表
(1).基本概念
记录的存储位置与关键字之间存在对应关系
对应关系 ------hash函数
Loc(i) = H(keyi)
- 优点:查找效率高
- 缺点:空间效率低!
- 术语
- 散列方法(杂凑方法):选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;
- 查找时,由用一个函数对给定的值k计算地址,将k与地址单元中元素关建码进行比,确定查找是否成功。
- 散列函数(杂凑函数):散列方法中使用的转换函数
- 散列表:按上诉思想构造表
- 冲突:不通关键码映射到同一个散列地址上
- 同义词:具有相同函数值的多个关键字
- 散列方法(杂凑方法):选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;
(2).构造方法
-
构造好散列函数
- 所选函数尽可能简单,以便提高转换速度
- 所选函数对关键码计算出的地址,应在散列表地址集中致均匀分布,以减少空间浪费。
- 因素:
- 执行速度
- 关键字长度
- 散列表大小
- 关键字的分布情况
- 查找频率。
-
制定一个好的解决冲突的方案
- 查找时,如果散列函数计算出的地址中查不到关键码,则应当根据解决冲突的规则,有规律地查询其他相关单元
-
根据元素集合的特性构造
- n个数据原仅占用n个地址,虽然散列查找是以空间换时间,但仍希望散列的地址空间尽量小
- 无论用什么方法存储,目的都是尽量均匀地存放元素,以避免冲突。
-
构造方法类型
- 直接定址法
- 优点:以关键码key的某个线性函数值为散列地址,不会产生冲突
- 缺点:要占用连续的地址空间,空间效率低
- 数字分析法
- 平方取中法
- 除留余数法
- Hash(key) = key %(mop) p(p是一个整数)
- 关建选取合适的p
- 技巧:
- 设表长为m,取
p <= m 且为质数
- 设表长为m,取
- 随机数法
- 直接定址法
(3).处理冲突方法
- 开放地址法(开地址法)
- 基本思想:有冲突时就去寻找下一个空的散列地址,只要散列地址足够大,空的散列地址总能找到,并将数据元素存入
- 常用方法
- 线性探测法
- Hi = (Hash(key)+Di) mod m (1 <= i <= m)
- 二次探测法
- Hi = (Hash(key)+Di) mod m (m 4k+3)
- 伪随机探测法
- 线性探测法
- 链地址法
- 基本思想:相同散列地址的记录链成一单链表
- m个散列地址就设m个单链表,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构。
- 链地址法建立散列表步骤
- 取数据元素的关键字key,计算其散列函数值(地址)。若该地址对应的链表为空,则将该元素插入此链表;否则执行Step2解决冲突。
- 根据选择的冲突处理方法,计算关键字key的下一个存储地址。若该地址对应的链表为不为空,则利用链表的前插法或后插法将该元素插入此链表。
- 优点:
- 非同义词不会冲突,无"聚集"现象
- 链表上的结点空间是动态申请,更适合于表长的不确定的情况
- 缺点:
- 再散列法
- 建立一个公共溢出区
(4).散列表查找及其性能分析
- 使用平均查找长度ASL来衡量查找算法,ASL取决于
- 散列函数
- 处理冲突的方法
- 散列表的装填因子α { α =(表中填入的记录数 / 哈希表的长度)}
- α越大,表中记录数越多,说明表装的越满,发生冲突可能性越大,查找时的比较次数就越多。
- ASL与装填因子α有关!既不是严格的O(1),也不是O(n)。
- ASL ≈ 1 + (a/2) (拉链法)
- ASL ≈ 1/2( 1 + 1/(1-a) ) (线性探测法)
- ASL ≈ -1/a * ln(1-a) (随机探测法)
(5).结论
- 散列表技术具有很好的平均性能,优于一些传统的技术
- 链地址法优于开地址法
- 除留余数法作散列表优于其它类型函数