知识点总结
——算法、线性表——概念明晰:随机存取、顺序存取、随机存储和顺序存储1、存取结构分为`随机存取`和`非随机存取`(又称顺序存取)2、存储结构分为`顺序存储`和`随机存储`3、顺序存储结构4、随机存储结构
一、数据结构的概念1、基本概念:2、算法(1)概念(2)重要特性:
3、算法的评价标准(“好”的算法应该考虑达到以下目标)4、算法的时空效率(1)时间复杂度根据算法写成的程序在执行时耗费时间的长度,记为T(n) = O(n)(2)空间复杂度根据算法写成的程序在执行时占用存储单元的长度记为S(n)(3)语句频度一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)(4)一般O(n)的计算方法:(5)常见的时间复杂度有以下七种:
二、线性表1、顺序存储(1)结构体的定义(2)顺序表的初始化(3)顺序表的查找(时间复杂度为O(n))(4)顺序表的插入 (时间复杂度为O(n))(5)顺序表的删除(时间复杂度为O(n))
2、链表存储(1)结构体的定义(时间复杂度为O(n))(2)求表长(时间复杂度为O(n))(3)判空(4)查找(时间复杂度为O(n))①按序号查找 FindKth(时间复杂度为O(n))②按值查找,即定位 Find(时间复杂度为O(n))
(5)链表的插入(时间复杂度为O(n))(6)创建链表(时间复杂度为O(n))1、带头结点的【头插法】(时间复杂度为O(n))2、带尾结点的插入【尾插法】(时间复杂度为O(n))
(7)删除(时间复杂度为O(n))
3、二者时间复杂度和优缺点的比较1、两者复杂度比较2、两者优缺点比较
三、栈1、栈的顺序存储实现(1)顺序栈结构体的定义(2)顺序栈的创建(3)判满(4)判空(5)入栈(6)出栈
2、栈的顺序存储实现(1)顺序栈结构体的定义(2)顺序栈的创建(3)判空(4)判满 注意:链栈,不必判断堆栈是否满(5)入栈(6)出栈
3、栈的应用
四、队列1、队列的顺序存储实现(1) 循环队列的结构体定义(2)生成空队列(3)判空(4)判满(5)入队(6)出队
2、队列的链式存储实现(1)队列的链式存储结构体定义(2)生成空队列(3)判空(4)判满 链式队列,不必判断堆栈是否满(5)入队(6)出队
五、栈和队列操作的特点六、数组存储地址的计算
———————树———————一、二叉树1、定义2、结点的度、孩子、双亲、深度、有序树、无序树、树的高度a.结点、叶子、树的度b.孩子、双亲、兄弟、子孙、祖先c.无序树、有序树、森林d.层次、高度
2、性质3、满二叉树、完全二叉树和二叉排序树a.满二叉树b.完全二叉树c.二叉查找树
二、静态查找1、顺序存储结构2、顺序查找3、二分查找(也称“折半查找”,是一棵“二叉排序树”)4、二分查找判定树ASL计算(1)查找成功的ASL(2)查找不成功的ASL
三、动态查找1、二叉树链表结构描述如下:2、二叉搜索(排序、查找)树的构造过程(1)构造过程(2)插入过程算法及其代码(2)删除过程算法及其代码(3)查找过程算法及其代码a.二叉搜索树的递归查找函数b.迭代查找算法
(4)查找最大值和最小值a.最小元素的递归函数b.查找最大元素的迭代函数
四、二叉树的遍历1、先序遍历2、层序遍历(队列实现)3、由遍历序列还原二叉树b.已知后序遍历和中序遍历还原二叉树
五、递归遍历算法的应用1、求二叉树的深度2、求二叉树的叶子树3、交互(换)左、右子树
六、静态查找和动态查找的根本区别七、树/森林与二叉树的转换1、树、森林与二叉树的转换2、森林转换为二叉树3、二叉树转换为树4、二叉树转换为森林5、转换以后的特点:
八、线索二叉树1、存储结构2、如何判断是孩子还是线索3、三种遍历
九、哈夫曼树1、带权路径长度WPL2、哈夫曼树的构造(算法)3、哈夫曼树的性质4、哈夫曼编码
———散列查找———一、散列查找1、基本概念2、散列函数(1、关键词为数字时:a.直接定址法b.除留余数法(常用)c.数字分析法d.平方取中法
(2、关键词为字符时:a、ASCII码加和法b、前3个字符移位法
二、处理冲突的方法1、开放定址法a.线性探测法b、平方探测(二次探测)c.在散列法
2、分离链接法
———————————————图————————————————一、图的基本概念1、简单图2、完全图3、连通分量4、强连通分量5.顶点的度、入度和出度
二、图的存储1、数组(邻接矩阵)表示法a.无向图的邻接矩阵表示法b.有向图的邻接矩阵表示法c.有权图(网)的邻接矩阵表示法
2.邻接表(顺序存储与链式存储结合)a.无向图的邻接表b.有向图的邻接表与逆邻接表c.带权值的网图
三、图的遍历1、深度优先遍历算法2、广度优先遍历算法
二、最小生成树1、性质2、Prim算法3、Kruskal算法
三、拓扑排序四、最短路径迪杰斯特拉算法具体过程
———排序———一、排序的类别1、插入排序基本思想:【1】直接插入排序(1、基本思想:(2、执行过程(3、时空效率及稳定性
【2】希尔排序(1、基本思想:(2、执行过程(3、时空效率及稳定性
2、交换排序基本思想:【1】冒泡排序(1、基本思想:(2、执行过程(3、时空效率及稳定性
【2】快速排序(1、基本思想:(2、执行过程(3、时空效率及稳定性
3、选择排序基本思想:【1】简单选择排序(1、基本思想:(2、执行过程(3、时空效率及稳定性
【2】堆排序(1、基本思想:(2、执行过程要点<1>初始化堆的过程
(3、时空效率及稳定性
4、归并排序二、各种排序的比较口诀:快选堆希不稳,选堆归基不变
每天进步一点点 整理不易。如果我的试错经历有帮到你,可以支持一下我哦
——算法、线性表——
概念明晰:随机存取、顺序存取、随机存储和顺序存储
随机存取、顺序存取、随机存储和顺序存储这四个概念是完全不一样的,切不可将之混淆
很多人包括我可能认为随机存取就是随机存储,顺序存取就是顺序存取,其实不是这样。
下面完整的介绍一下这4个概念
1、存取结构
分为随机存取和非随机存取(又称顺序存取)
1、随机存取就是直接存取,可以通过下标直接访问的那种数据结构,与存储位置无关。例如数组。
非随机存取就是顺序存取,不能通过下标访问了,只能按照存储顺序存取,与存储位置有关,例如链表。
2、顺序存取就是存取第N个数据时,必须先访问前(N-1)个数据 (list);
随机存取就是存取第N个数据时,不需要访问前(N-1)个数据,直接就可以对第N个数据操作 (array)。
2、存储结构
分为顺序存储和随机存储
3、顺序存储结构
在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。
- 顺序存储结构是存储结构类型中的一种,该结构是把**逻辑上相邻的节点**存储在**物理位置上相邻的存储单元**中,结点之间的逻辑关系由存储单元的邻接关系来体现。
- 由此得到的储结构为顺序存储结构,通常顺序存储结构是借助于计算机程序设计语言(例如c/c++)的数组来描述的。
– 主要优点:节省存储空间。
因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。
– 主要缺点:不便于修改,对结点的插入、删除运算时可能要移动一系列的结点。
4、随机存储结构
在计算机中用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。它不要求逻辑上相邻的元素在物理位置上也相邻。因此它没有顺序存储结构所具有的弱点,但也同时失去了顺序表可随机存取的优点。
--随机存储最典型的代表为链式存储:
链式存储结构特点
1、比顺序存储结构的存储密度小 (每个节点都由数据域和指针域组成,所以相同空间内假设全存满的话顺序比链式存储更多)。
2、逻辑上相邻的节点物理上不必相邻。
3、插入、删除灵活 (不必移动节点,只要改变节点中的指针)。
4、查找结点时链式存储要比顺序存储慢。
5、每个结点是由数据域和指针域组成
一、数据结构的概念
1、基本概念:
数据:描述客观事实的符号,是计算机中可以操作的对象,能被计算机识别,并输给计算机处理的符号集合。数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理,也被成为记录。数据对象:是性质相同数据元素的集合,是数据的一个子集。数据项:一个数据元素可以由若干个数据项组成,数据项是数据不可分割的最小单位。数据结构:相互之间存在一种或者多种特定关系的数据元素的集合。可分为逻辑结构和物理结构。
2、算法
(1)概念
解决特定问题的求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。
(2)重要特性:
①输入:有零个输入或者多个输
②输出:只有一个或者多个输出
③有穷性:算法在执行有限个步骤时,会自动结束而不会陷入无限循环里面
④确定性:算法的每一步都有确定的含义而不会出现二义性
⑤可行性:算法的每一步都可以通过有限次数完成。
3、算法的评价标准(“好”的算法应该考虑达到以下目标)
①正确性。算法能够正确地求解问题。
②可读性。算法能具有良好的可读性,以帮助人们理解。
③健壮性。输入非法数据时,算法能适当地做出反应或进行处理。而不会产生莫名其妙的输出结果。
④效率与低存储量需求。效率指算法执行的时间,存储量需求是指算法执行过程中所需的最大存储空间。
4、算法的时空效率
(1)时间复杂度
根据算法写成的程序在执行时耗费时间的长度,记为T(n) = O(n)
(2)空间复杂度
根据算法写成的程序在执行时占用存储单元的长度记为S(n)
(3)语句频度
一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)
时间复杂度:时间复杂度实际上是一个函数,代表基本操作重复执行的次数,进而分析函数虽变量的变化来确定数量级,数量级用O表示,所以算法的时间复杂度为: T(n)=O(f(n))
在一个算法存在最好、平均、最坏三种情况,我们一般关注的是最坏情况,原因是,最坏情况是任何输入实例在运行时间的上界,对于某些算法来说,最坏情况出现的比较频繁,从大体上来看,平均情况和最坏情况一样差。
(4)一般O(n)的计算方法:
①用 1代替所有运行时间中出现的加法常数;
②在修改后的运行函数中**保留最高阶的项;
③如果最高阶的项系数不是1,则去除这个项系数。
④ 递归算法的时间复杂度为:递归总次数每次递归中基本操作执行的次数。
(5)常见的时间复杂度有以下七种:
① O(1)常数型;② O(log2N)对数型;③ O(N)线性型;④ O(Nlog2N)二维型;⑤ O(N^2)平方型;⑥ O(N^3)立方型;⑦ O(2^N)指数型。
例如:
i=1;①
while (i<=n)
{
i=i*2; ②
}
解:语句1的频度是1,
设语句2的频度是f(n),则:2^f(n)<=n;f(n)<=log2n
取最大值f(n)= log2n, T(n)=O(log2n )
二、线性表
1、顺序存储
(1)结构体的定义
typedef int Position;
typedef struct LNode * PtrToLNode;
struct LNode
{
ElmenetType Data[ MAXSIZE ];
Position Last;
};
typedef PtrToLNode List;
(2)顺序表的初始化
1、构造一个空表
2、动态分配表结构所需的存储空间,然后将表中Last指针置为-1 表示表中没有数据。
List MakeEmpty()
{
List L;
L = (List)malloc(sizeof(struct LNode));
L->Last = -1; //Last 置为-1 表示表中没有数据元素
Return L;
}
通过L我们可以访问相应线性表的内容。比如:下标为i 的元素:L->Data[i]
查询线性表的长度:L->Last+1;
(3)顺序表的查找(时间复杂度为O(n))
在线性表中查找与给定值 X 相等的数据元素。
由于线性表的元素都存储在数组Data中,所以这个查找的过程实际上就是在数组里顺序查找:
从第 1 个元素 a1 起依次和 X 比较, 直到找到一个与 X 相等的数据元素,返回它在顺序表中的存储下标;或者查遍整个表都没有找到与 X 相等的元素,则返回错误信息 ERROR。
#define ERROR -1 /* 将错误信息 ERROR 的值定义为任一负数都可以 */
Position Find( List L, ElementType X )
{
Position i = 0;
While( i <= L->Last && L->Data[i] != X)
i++;
if( i > L->Last)
return ERROR; /* 如果没有找到,则返回错误信息 */
else
return i; /* 找到后返回的是存储位置 */
}
(4)顺序表的插入 (时间复杂度为O(n))
在表的插入是指在表的第 i(1≤ i ≤ n + 1)个位序上插入一个值为 X 的新元素(也可以理解为在第 i 个元素之前插入新的元素)
插入后使得原来长度为 n 的序列,变为长度为 n+1的序列(i = 1时插入序列的最前端,i = n+1 时插入序列的最后)
将ai~an顺序向后移动(移动次序是从 an 到ai),为新元素让出位置;将 X 放入空出的第 i 个位序;修改 Last 指针(相当于修改表长),使之指向最后一个元素。
bool Insert( List L, ElementType X, int i)
{ /* 在 L 的指定位序 i 前插入一个新元素 X; 位序 i 元素数组位置下标为 i-1 */
Postion j;
if(L->Last == MAXSIZE-1)
{/* 表空间已满,不能插入 */
printf("表满!\n");
return false;
}
if( i<1 || i > L->Last+2)
{/* 检查插入位序的合法性:是否在 1~n+1; n为当前元素个数,即Last+1 */
printf("位序不合法!\n");
return false;
}
for( j = L->Last; j >= i-1; j--) /*Last 指向序列最后元素an */
L->Data[j+1] = L->Data[j]; /* 将位序为 i 及以后的元素顺序向后移动 */
L->Data[i-1] = X; /* Last 仍指向最后一个元素 */
L->Last++;
return true;
}
(5)顺序表的删除(时间复杂度为O(n))
将表中的位序为 i(1≤ i ≤ n + 1)的元素从线性表中去掉,删除后使原长度为 n 的数组元素序列,变为长度为 n-1 的序列
将a[i+1]~a[n] 顺序向前移动 ,a[i] 元素被a[i+1]覆盖;
修改 Last 指针(相当于修改表长)使之仍指向最后一个元素。
bool Delete(List L, int i)
{ /*从 L 中删除指定位序 i 的元素,该元素数组下标为 i-1*/
Position j;
if(i < 1 || i > L->Last + 1)/* 检查空表及删除位序的合法性*/
{
printf("位序%d不存在元素",i);
return false;
}
for( j = i; i <= L->Last; j++)
L->Data[j-1] = L->Data[j];/*将位序 i+1 及以后的元素顺序向前移动*/
L->Last--;/*Last 仍指向最后元素*/
return true;
}
2、链表存储
(1)结构体的定义(时间复杂度为O(n))
typedef struct LNode * PtrToLNode;
struct LNode
{
ElementType Data;
PtrToLNode Next;
};
typedef PtrToLNode Position; /*这里的位置是结点的地址 */
typedef PreToLNode List;
(2)求表长(时间复杂度为O(n))
在顺序存储中求表长是很容易的,直接返回 Last+1 就可以了。但在链式存储中,需要将链表从头到尾遍历一遍
设一个移动指针p和计数器cnt,初始化后,p从表的第 1 个结点开始逐步往后移,同时计数器 cnt+1.当后面不再有结点时,cnt 的值就是结点个数,即 表长。
int Length(List L)
{//默认该链表是有头结点的
Position p;
int i=0; /* 初始化计数器 */
//单向链表的遍历(三部曲)
p = L->next; /* p指向表的第 1 个结点 */
while(p)
{ /* 遍历单链表,统计结点数 */
p=p->next;
i++;
}
return i;
}
(3)判空
int ListEmpty(LinkList L)
{ //若 L 为空,则返回1,否侧返回 0
if(L->Next) //非空
return 0;
else
return 1;
}
(4)查找(时间复杂度为O(n))
有两种 按序号查找(FindKth)和 按值查找(Find)
①按序号查找 FindKth(时间复杂度为O(n))
对于顺序存储,按序号查找是很直接的事情,要得到第 K 个元素的值,直接取L->Data[K-1]即可。
但是对于链式存储则需要采用跟求表长类似的思路:
从链表的第 1 个元素结点起,判断当前结点是否是第 K 个;若是,则返回该结点的值,否则继续对比后一个,直到表结束为止。如果没有第 K 个结点则返回错误信息。
#define ERROR -1 /* 一般定义为表中元素不可能取到的值 */
ElementType FindKth(List L, int K)
{ /* 根据指定的位序 K, 返回 L 中相应的元素 */
Position P;
int cnt = 1; /* 位序从 1 开始 */
p = L; /* p 指向 L的第 1 个结点 */
while(p && cnt < K)
{
p = p->next;
cnt++
}
if((cnt == K) && p)
return p->Data; /* 返回第 K 个 */
else
return ERROR; /* 否则返回错误信息 */
}
②按值查找,即定位 Find(时间复杂度为O(n))
基本方法:也是从头到尾遍历,直到找到为止:
从链表的第 1 个元素结点起,判断当前结点的值是否等于 X;若是,返回该结点的位置,否则继续对比后一个,直到表结束位置为止;找不到时返回错误信息。
#define ERROR NULL /*空地址表示错误 */
Position Find( List L, ElementType X)
{
Position p = L;/* p指向 L 的第 1 个元素 */
while(p && p->Data != x)
{
p = p->Next;
}
if(p)
return p;
else
return ERROR;
}
(5)链表的插入(时间复杂度为O(n))
int ListInsert_L(LinkList &L, int i,ElementType e)
{
p = L;
j = 0;
while(p&& j {//寻找第 i-1 个结点 p = p->next; ++=j; } if(!p || j > i-1) return ERROR;// s = (LinkList)malloc(sizeof(LNode));//生成新结点s s->data = e; //将结点s 的数据域的值 更新为 e s->next = p->next; //将结点s 插入 L 中 p->next = s; return OK; } (6)创建链表(时间复杂度为O(n)) 1、带头结点的【头插法】(时间复杂度为O(n)) /* 带头结点的插入创建 */ void createListHead( Linklist L, int n ) { //建立头结点 L = (LNode*)malloc(sizeof(struct LNode)); L->Next = NULL; //建立单链表(头插法) LNode *temp = NULL; //申请空间,写入数据 for(int i = 0; i < n; i++) { tmp = (LNode*)malloc(sizeof(struct LNode)); /* 申请、填装结点 */ scanf("%d",&tmp->Data);//输入元素值 //插入到头结点的后面 tmp->Next = L->Next; L->Next = tmp; } } 2、带尾结点的插入【尾插法】(时间复杂度为O(n)) /*带尾结点的插入*/ void CreateList_L( Listlist &L, int n ) { //正位序数输入 n 个元素的值,建立带表头结点的单链表L //建立头结点 L = (LNode*)malloc(sizeof(struct LNode)); L->Next = NULL; //建立单链表(尾插法) LNode r = L; //尾指针指向头结点 //申请空间,写入数据 for(int i = 0;i < n; i++) { LNode *tmp = (LNode*)malloc(sizeof(struct LNode)); /* 申请新结点 */ scanf("%d",&tmp->Data); //输入元素 tmp->Next = NULL; //插入到尾结点后面 r->next = temp; r = tmp; //r指向新的尾结点 } } (7)删除(时间复杂度为O(n)) //将线性表L 中第 i 个数据元素删除 int ListDelete_L(LinkList &L, int i, ElementType &e) { p=L; int j=0; while(p->next && j < i-1) {//寻找第 i 个结点,并令p指向其前驱 p = p>next; ++j; } if(!(p->next)||j < i-1) return ERROR;//删除位置不合理 q = p->next; //临时保存被删除结点的地址以备释放 p->next = q->next; //改变被除结点的驱结点的指针域 e = q->data; //保存被删除结点的数据域 free(q); //释放被删除结点的空间 return OK; } 3、二者时间复杂度和优缺点的比较 1、两者复杂度比较 查找插入删除顺序表O(1)O(1)O(n)通过下标直接找到待操作元素,主要时间花在移动元素上。链表O(n)O(n)主要时间用于找到插入元素的位置O(n)主要时间用于找到待删除元素的位置 2、两者优缺点比较 数组优点缺点随机访问性强;查找速度快插入和删除效率低;可能浪费内存;内存空间要求高,必须有足够的连续内存空间;数组大小固定,不能动态拓展 链表优点缺点插入删除速度快;内存利用率高,不会浪费内存;大小没有固定,拓展很灵活。不能随机查找,必须从第一个开始遍历,查找效率低 两者的区别在于顺序结构的要求一片连续的存储空间,而链式结构的不要求存储空间连续。 三、栈 1、栈的顺序存储实现 通常由一个一维数组和一个记录栈顶元素位置的变量组成。 (1)顺序栈结构体的定义 当 Top = -1时,表示栈空;当Top = MaxSize -1 时,栈满! typedef int Position; typedef int ElementType; typedef struct SNode *PtrToNode; struct SNode { ElementType * Data; /*存储元素的数组*/ Position Top; /*栈顶指针*/ int MaxSize; /*堆栈最大容量*/ }; typedef PtrToNode Stack; (2)顺序栈的创建 Stack CreateStack(int MaxSize) /*顺序栈的创建*/ { Stack S = (Stack)malloc(sizeof(struct SNode)); S->Data = (ElementType *)malloc(MaxSize * sizeof(ElementType)); S->Top = -1; /*"-1"表示空栈 "MaxSize-1"表示满栈*/ S->MaxSize = MaxSize; /*指定栈的最大容量*/ return S; } (3)判满 bool IsFull(Stack S) /*判断栈是否满了*/ { return(S->Top == S->MaxSize-1); } (4)判空 bool IsEmpty(Stack S) /*判断堆栈是否为空*/ { return(S->Top == -1); } (5)入栈 在执行堆栈 Push 操作时,先判断栈是否满; 若不满,Top 加1,并将新元素放入 Data数组的Top位置上若满,则返回错误标志 bool Push(Stack S, ElementType X) /*顺序栈的 入栈 操作*/ { if(IsFull(S)) { printf("堆栈满!"); return false; } else { S->Data[++(S->Top)] = X; /*若是栈不满,则Top加 1,并将新元素放入Data数组的Top位置中*/ return true; } } (6)出栈 执行Pop操作时,首先判别栈是否为空; 若不为空,返回Data[Top],同时将Top-1;否则要返回错误标志 ElementType Pop(Stack S) /*顺序栈 的 出栈 操作*/ { if(IsEmpty(S)) { printf("堆栈空!"); return ERROR; /*ERROR 是 ElementType 类型的特殊值,标志错误。必须是正常栈元素数据不可能取到的值 */ } else return(S->Data[(S->Top)--]); /*若不空,返回Data[Top],同时将Top减 1*/ } 2、栈的顺序存储实现 链栈与单链表类似,但其操作受限制,插入和删除操作只能在链栈的栈顶进行。 (1)顺序栈结构体的定义 typedef struct SNode *PtrToSNode; typedef int ElementType; struct SNode { ElementType Data; PtrToSNode Next; }; typedef PtrToSNode Stack; (2)顺序栈的创建 Stack CreateStack() { /*构建一个堆栈的头结点,返回该结点指针*/ Stack S; S = (Stack)malloc(sizeof(struct SNode)); S->Next = NULL; return S; } (3)判空 bool IsEmpty(Stack S) { /*判断堆栈 S 是否为空,若是返回 true,否则返回 false*/ return(S->Next == NULL); } (4)判满 注意:链栈,不必判断堆栈是否满 (5)入栈 链栈,不必判断堆栈是否满 bool Push(Stack S, ElementType X) { /*将元素 X 压入堆栈 S */ PtrToSNode TmpCell; TmpCell = (PtrToSNode)malloc(sizeof(struct SNode)); TmpCell->Data = X; //头插法 TmpCell->Next = S->Next; S->Next =TmpCell; return true; } (6)出栈 ElementType Pop(Stack S) ElementType Pop(Stack S) { /*删除并返回堆栈 S 的栈顶元素*/ PtrToSNode FirstCell; ElementType TopElem; if(IsEmpty(S)) { printf("堆栈空!"); return ERROR; } else { FirstCell = S->Next; TopElem = FirstCell->Data; S->Next = FirstCell->Next; free(FirstCell); return TopElem; } }/*顺序栈 的 出栈 操作*/ { if(IsEmpty(S)) { printf("堆栈空!"); return ERROR; /*ERROR 是 ElementType 类型的特殊值,标志错误。必须是正常栈元素数据不可能取到的值 */ } else return(S->Data[(S->Top)--]); /*若不空,返回Data[Top],同时将Top减 1*/ } 3、栈的应用 四、队列 1、队列的顺序存储实现 (1) 循环队列的结构体定义 typedef int Status; typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */ /* 循环队列的顺序存储结构 */ typedef struct QNode { QElemType data[MAXSIZE]; int front; /* 头指针 */ int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */ }SqQueue; (2)生成空队列 /* 初始化一个空队列Q */ Status CreateQueue(SqQueue *Q) { SqQueue *Q = (SqQueue)malloc(sizeof(struct QNode)); Q->data = (ElementType*)malloc(MaxSize * sizeof(ElementType)); Q->front = Q->rear = 0; return OK; } (3)判空 队空的条件是:rear=front bool IsEmpty(SqQueue *Q) { return(Q->front == Q->rear); } (4)判满 队满的条件是:(rear+1)%数组的长度等于 front bool IsFull(SqQueue *Q) { return((Q->rear+1)% MaxSize == Q->front); } (5)入队 /* 若队列未满,则插入元素e为Q新的队尾元素 */ Status EnQueue(SqQueue *Q,QElemType e) { if ((Q->rear+1)%MAXSIZE == Q->front) /* 队列满的判断 */ return ERROR; Q->data[Q->rear]=e; /* 将元素e赋值给队尾 */ Q->rear=(Q->rear+1)%MAXSIZE;/* rear指针向后移一位置, */ /* 若到最后则转到数组头部 */ return OK; } (6)出队 /* 若队列不空,则删除Q中队头元素,用e返回其值 */ Status DeQueue(SqQueue *Q,QElemType *e) { if (Q->front == Q->rear) /* 队列空的判断 */ return ERROR; *e=Q->data[Q->front]; /* 将队头元素赋值给e */ Q->front=(Q->front+1)%MAXSIZE; /* front指针向后移一位置, */ /* 若到最后则转到数组头部 */ return OK; } 2、队列的链式存储实现 队列与堆栈一样,也可以采用链式存储结构,但队列的头(front)必须指向链表的头结点,队列的尾(rear)指向链表的尾结点。 (1)队列的链式存储结构体定义 typedef int Status; typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */ typedef struct QNode /* 结点结构 */ { QElemType data; struct QNode *next; }QNode,*QueuePtr; typedef struct /* 队列的链表结构 */ { QueuePtr front,rear; /* 队头、队尾指针 */ }LinkQueue; (2)生成空队列 /* 构造一个空队列Q */ Status InitQueue(LinkQueue *Q) { Q->front=Q->rear=(QueuePtr)malloc(sizeof(QNode)); if(!Q->front) exit(OVERFLOW); Q->front->next=NULL; return OK; } (3)判空 队空的条件是:rear=front Status QueueEmpty(LinkQueue Q) { if(Q.front==Q.rear) return TRUE; else return FALSE; } (4)判满 链式队列,不必判断堆栈是否满 (5)入队 /* 插入元素e为Q的新的队尾元素 */ Status EnQueue(LinkQueue *Q,QElemType e) { QueuePtr s=(QueuePtr)malloc(sizeof(QNode)); if(!s) /* 存储分配失败 */ exit(OVERFLOW); s->data=e; s->next=NULL; Q->rear->next=s; /* 把拥有元素e的新结点s赋值给原队尾结点的后继,见图中① */ Q->rear=s; /* 把当前的s设置为队尾结点,rear指向s,见图中② */ return OK; } (6)出队 /* 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR */ Status DeQueue(LinkQueue *Q,QElemType *e) { QueuePtr p; if(Q->front==Q->rear) return ERROR; p=Q->front->next; /* 将欲删除的队头结点暂存给p,见图中① */ *e=p->data; /* 将欲删除的队头结点的值赋值给e */ Q->front->next=p->next;/* 将原队头结点的后继p->next赋值给头结点后继,见图中② */ if(Q->rear==p) /* 若队头就是队尾,则删除后将rear指向头结点,见图中③ */ Q->rear=Q->front; free(p); return OK; } 五、栈和队列操作的特点 相同点不同点堆栈(FILO)只允许在端点处插入和删除元素;栈是先进后出或者后进先出;栈是只能在表的一端进行插入和删除操作的线性表队列(FIFO)只允许在端点处插入和删除元素;队列是先进先出;队列是只能在表的一端进行插入,然后在另外一端进行删除操作的线性表 六、数组存储地址的计算 数组类型存储地址的计算(a是数组首地址,len是每个数组元素所占长度)一维数组a[i]的存储地址:a+i*len二维数组:a[m] [n]按行存储:a+(i * n+j) * len;按列存储:a+(j * m+i) * len 例子:数组存储地址的计算示例: 1)已知一维数组a中每个元素占用2个字节,求a[10]的存储地址? 答:a[10]的存储地址为:a+10*2=a+20 2)已知二维数组a[4][5]中, 每个元素占用2个字节,求元素a[3][2]按行为主序存储的存储地址和按列为主序存储的存储地址? 答: 按行存储:a+(35+2) *2 = a+34 按列存储:a+(24+3) *2 = a+22 ———————树——————— 一、二叉树 1、定义 二叉树是每个节点最多有两个子树的树结构。 它有五种基本形态: 二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。 2、结点的度、孩子、双亲、深度、有序树、无序树、树的高度 a.结点、叶子、树的度 结点的度:结点拥有的子树的数目。叶子:度为零的结点。树的度:树中结点的最大的度 b.孩子、双亲、兄弟、子孙、祖先 双亲:若一个结点有子树,该结点称为子树根的"双亲"。孩子:子树的根是该结点的"孩子"。兄弟:有相同双亲的结点互为"兄弟"。子孙:一个结点的所有子树上的任何结点都是该结点的子孙。祖先:从根结点到某个结点的路径上的所有结点都是该结点的祖先。 c.无序树、有序树、森林 无序树:如果树中结点的各子树之间的次序是无次序的,可以交换位置。有序树:如果树中结点的各子树之间的次序是有次序的, 不可以交换位置。森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林。 d.层次、高度 层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1。 树的深度和高度:二叉树中节点的最大层次称为二叉树的深度或高度。 2、性质 性质1:二叉树第 i 层上最多为 2^(i-1) (i≥1)个结点。 性质2:深度为k的二叉树至多有2^k - 1个结点(k≥1)。 性质3:具有n个结点的【完全二叉树】的高度k为(log<2>n) +1)([log2n]表示不大于与其的整数) 性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。 性质5:如果对一棵有 n个结点的完全二叉树(其深度为(log<2>n) +1)的结点按 【层序】编号(从第1层到第(log<2>n) +1) 层,每层从左到右),对任一结点 i (1≤ i ≤ n)有: 如果 i = 1,则结点 i是二叉树的根,无双亲;如果 i > 1,则其双亲是结点 [i/2];如果2i >n,则结点 i 无左孩子(即结点 i 为叶子结点);否则其左孩子是结点 2i;如果 2i+1 >n,则结点 i 无右孩子;否则其右孩子是结点 2i+1。 3、满二叉树、完全二叉树和二叉排序树 a.满二叉树 定义:高度为h,并且由2{h} –1个结点的二叉树,被称为满二叉树。 b.完全二叉树 定义:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下一层的叶结点集中在靠左的若干位置上。这样的二叉树称为完全二叉树。 特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。 c.二叉查找树 定义:二叉查找树(Binary Search Tree),又被称为二叉搜索树。左小右大,任意结点的左、右子树也是二叉查找树 在二叉查找树中: (01) 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值; (02) 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; (03) 任意节点的左、右子树也分别为二叉查找树。 (04) 没有键值相等的节点(no duplicate nodes)。 二、静态查找 1、顺序存储结构 指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组下标i-1的分量中。 2、顺序查找 从表的一端开始,逐个将记录的关键字和给定值比较,若找到一个记录的关键字与给定值相等,则查找成功;若整个表中记录均比较过,仍未找到关键字等于给定值的记录,则查找失败。 缺点:查找表的长度越长,查找效率越低。 优点:简单、适应面广,对查找表结构没有要求,对顺序存储和链式存储都适用。 3、二分查找(也称“折半查找”,是一棵“二叉排序树”) 设查找表元素存储在一维数组r[1,…,n]中,在表中的元素已经按关键字递增方式排序的情况下, 进行[折半查找]的方法是:首先将待查元素的关键字(key)值与表r中间位置上(下标为mid)记录的关键字关键字进行比较, 若相等,则查找成功;若key>r[mid].key,则说明待查记录只可能在后半个子表r[mid+1,…,n]中;若key 这样逐步缩小范围,直到查找成功或子表为空时失败为止。 注意:每次缩小范围后,改变的下标是哪个 //递增的方式排序,则折半查找的算法为 //在数组r[low...high],在数组r中找值为key的元素 int Bsearch(int r[],int low,int high,int key) { int mid; while(low <= high) { mid = (low + high)/2; if(key == r[mid]) return mid; else if(key < r[mid]) high = mid-1; else low = mid+1; } return -1; } //折半查找,递归算法 int Bsearch_rec(int r[],int low,int high,int key) { int mid; if(low <= high) { mid = (low + high)/2; if(key == r[mid]) return mid; else if(key < r[mid]) return Bsearch_rec(r,low,mid-1,key); else return Bsearch_rec(r,mid+1,high,key); } return -1; } 折半查找的过程可以用一颗二叉树来描述,以当前查找区域间的中间位置序号作为根,左半个子表和右半个子表中的记录序号分别分别作为根的左子树和右子树上的结点,这样构造的二叉树称为折半查找判定树,从树上可以看出: 查找成功时,折半查找的过程恰好走了一条从根结点到被查找结点的路径,与关键字进行比较的次数即为被查找结点在树中的层数。因此,折半查找判定树在查找成功时进行比较的关键字个数最多不超过树的深度,而具有n个结点的判定树的深度为;所以折半查找在查找成功时和给定值进行比较的关键字个数最多为。 优点:查找效率更高,但它要求查找表进行顺序存储并按关键字进行排序。 缺点:对表进行插入或删除时,需要移动大量元素。 适用:表不易变动,且又经常进行查找的情况 4、二分查找判定树ASL计算 折半查找的过程看,可用二叉树来描述,二叉树中的每个结点对应有序表中的一个记录,结点中的值为该记录在表中的位置。通常称这个描述折半查找二叉树的过程称为折半查找判定树。 例如:顺序存储的序列{1,2,3,4,5,6,7,8,9,10} 来构建二叉判定树,计算其ASL 例如:长度为10的折半查找判定树的具体生成过程: 都遵循这个规律,左孩子结点<根结点<右孩子结点 【左小右大】 (1)在长度为10的有序表中进行折半查找,不论查找哪个记录,都必须和中间记录进行比较,而中间记录为 (1+10)/2 =5 (注意要取整) 即判定数的的根结点为5,如图7-2(a)所示。 (2)考虑判定树的左子树,即将查找区域调整到左半区,此时的查找区间为[1,4],那么中间值为(1+4)/2 =2 (注意要取整) ,所以做孩子根结点为2,如图7-2(b)所示。 (3)考虑判定树的右子树,即将查找区域调整到右半区,此时的查找区间为[6,10],那么中间值为(6+10)/2 =8 (注意要取整) ,所以做孩子根结点为8,如图7-2(c)所示。 (4)重复以上步骤,依次去确定左右孩子、 特点: 1.折半查找是一棵二叉排序树,每个根结点的值都大于左子树的所有结点的值,小于右子树所有结点的值。 2.折半查找判定数中的结点都是查找成功的情况,将每个结点的空指针指向一个实际上不存在的结点————外结点,所有外界点都是查找不成功的情况,如图7-2(e)所示。如果有序表的长度为n,则外结点一定有n+1个。 (1)查找成功的ASL 折半查找判定数中,某结点所在的层数就是即将要比较的次数,整个判定树代表的有序表的平均查找长度即为查找每个结点的比较次数之和除以有序表的 长度。 ASL成功 = 每层结点所在高度×每层结点数 之和 除以 总结点数 例如:长度为10的有序表的平均查找长度为 ASL=(1×1+2×2+3×4+4×3)/10=29/10; (2)查找不成功的ASL 折半查找判定数中,查找不成功的次数即为查找相应外结点(定义在上方)与内结点的比较次数。整个判定树代表的有序表的平均查找长度。查找失败时的有序表的平均查找长度即为查找每个外结点的比较次数之和除以外结点的个数。 ASL失败 = (每层【补上的】结点所在高度-1)×每层【补上的】结点数 之和 除以 【补上的】总结点数 例如:查找失败时,长度为10的有序表的平均查找长度为: ASL=(3×5+4×6)/11=39/11; 三、动态查找 1、二叉树链表结构描述如下: typedef struct TNode *Position; typedef Position BinTree; /* 二叉树类型 */ struct TNode {/*树结点定义 */ ElementType Data; /* 结点数据*/ BinTree Left; /*指向左子树*/ BinTree Right;/*指向右子树*/ }; 二叉链表至少包含3个域:数据域 data、左指针域 lchild和右指针域 rchild 指针域: n个结点有2n个指针域。 空指针域:n 个结点的二叉链表中含有 n+1 个空指针域。 2、二叉搜索(排序、查找)树的构造过程 (1)构造过程 构造二叉排序树的过程,就是从空二叉树开始,逐个向树中插入节点的过程。 设记录的关键码序列为:63,90,70,55,67,42,98,83,10,45,58 (2)插入过程算法及其代码 设待插入节点关键码值为 X : (1)先在树中查找值为 X 的节点,若查找成功,说明节点已存在,无需插入; (2)若查找失败,说明节点不存在,则将其插入到树中 因此,新插入节点一定是作为叶子节点插入的。 BinTree Insert(Bintree BST, ElmentType X) { if(!BST) {/*若原来树为空,生成并返回一个结点的二叉搜索树*/ BST = (BinTree)malloc(sizeof(struct TNode)); BST->Data = X; BST->Left = BST->Right = NULL; } else {/*开始查找插入元素的位置*/ if(X < BST->Data) BST->Left = Insert(BST->Left, X);/*递归插入左子树*/ else if(X > BST->Data) BST->Right = Insert(BST->Right, X);/*递归插入右子树*/ } return BST; } (2)删除过程算法及其代码 二叉搜索树的删除操作比其它操作更为复杂,要删除结点在树中的位置决定了操作所采用的策略。 a.若要删除的结点是叶子结点 可以直接删除,然后再修改其父结点的指针。 b.若要删除的结点只有一个孩子结点(该结点不一定是叶结点,可以是子树的根) 删除之前需要改变父结点的指针,指向要删除结点的孩子结点。 c.若要删除的结点有左、右两棵子树,有两种选择: 基本原则:保持二叉搜索树的有序性 1、取其右子树中的最小元素; 2、取其左子树中的最大元素。 (3)查找过程算法及其代码 BST树的查找思想: 首先将给定的K值与二叉排序树的根节点的关键字进行比较: 若相等,则查找成功; 若给定的K值小于BST树的根节点的关键字:继续在该节点的左子树上进行查找; 若给定的K值大于BST树的根节点的关键字:继续在该节点的右子树上进行查找。 a.二叉搜索树的递归查找函数 在二叉排序树上进行查找,则是从根结点出发走了一条从根到待查结点的路径; 若查找不成功,则是从根结点出发走了一条从跟到某一叶结点的路径。 Position Find(BinTree BST,ElementType X) { if(!BST->Data) return NULL;/* 查找失败 */ if(X > BST->Data) return Find(BST->Right, X);/* 在 右子树 中递归查找 */ else if(X < BST->Data) return Find(BST->Left, X);/* 在 左子树 中递归查找 */ else return BST;/* 在当前结点查找成功,返回当前结点的地址*/ } b.迭代查找算法 由于非递归函数的执行效率高,一般采用非递归的迭代来实现查找。很容易将递归函数改为迭代函数 while循环 代替 Find递归调用即可 Position Find(BinTree BST,ElementType X) { while(BST) { if(X > BST->Data) BST = BST->Right;/* 向 右子树 中移动,继续查找 */ else if(X < BST->Data) BST = BST->Left; /* 向 右子树 中移动,继续查找 */ else /* X == BST->Data;*/ break;/* 在当前结点查找成功,跳出循环 */ } return BST;/* 返回找到的结点地址,或是NULL */ } (4)查找最大值和最小值 根据二叉搜索树的性质,最小元素一定是在树的最左分支的端点上。最左分支的端点:最左分支上无左孩子的结点。 最大元素一定在最右分支的端结点上。 从根结点开始,当其不为空时,沿左分支或者右分支逐个判断各结点的指针,直到遇到空指针为止。当左分支逐层推下来查找到的是最小元素。反之,当右分支逐层推下来查找到的是最大元素。 a.最小元素的递归函数 Position FindMin(BinTree BST) { /* 最小元素在最左端点 */ if(!BST) return NULL;/* 空的二叉搜素树,返回NULL */ else if(!BST->Left) return BST; /* 找到最左端点并返回 */ else return FindMin(BST->Left); /*沿左分支递归查找 */ } b.查找最大元素的迭代函数 Position FindMax(BinTree BST) { if(BST) while(BST->Right); BST = BST->Right; /*沿右分支一直向下,直到最右端点 */ return BST; } 四、二叉树的遍历 指按照某种次序访问二叉树的所有结点,并且每个结点仅访问一次,得到一个线性序列。 1、先序遍历 (1)访问根结点 (2)先序遍历左子树 (3)先序遍历右子树 -中序、后序遍历相似 先序遍历:A → B → D → C 中序遍历:B → D → A → C 后续遍历:D → B → C → A 层序遍历:A → B → C → D void PreOrderTraverse(BiTree T) //链式二叉树先序遍历递归算法 { if (T != NULL) { printf_s("%d ", T->data); //访问根结点 PreOrderTraverse(T->lchild); //先序遍历左子树 PreOrderTraverse(T->rchild); //先序遍历右子树 } } //链式二叉树中序遍历递归算法 void InOrderTraverse(BiTree T) { if (T != NULL) { InOrderTraverse(T->lchild); printf_s("%d ", T->data); InOrderTraverse(T->rchild); } } //链式二叉树后序遍历递归算法 void PostOrderTraverse(BiTree T) { if (T != NULL) { PostOrderTraverse(T->lchild); PostOrderTraverse(T->rchild); printf_s("%d ", T->data); } } 2、层序遍历(队列实现) 仔细看看层序遍历过程,其实就是从上到下,从左到右依次将每个数放入到队列中,然后按顺序依次打印就是想要的结果。 实现过程 从队列中取出一个元素;访问该元素所指结点;若该元素所指结点的左、右孩子结点非空,则将其左、右孩子的指针顺序入队。 不断执行这三步操作,直到队列为空,再无元素可取,二叉树的程序遍历就完成了。 void LevelorDerTraversal(BinTree BT) { Queue Q; BinTree T; if(!BT) return;/* 若是空树则直接返回 */ Q = CreatQueue(); /* 创建空队列 */ AddQ(Q, BT); while(!IsEmpty(Q)) { T = DeteleQ(Q); printf("%d",T->Data); /* 访问取出队列的结点 */ if(T->Left) AddQ(Q, T->Left); if(T->Right) AddQ(Q, T->Right); } } 3、由遍历序列还原二叉树 已知先序遍历和中序遍历,可以还原二叉树; 已知中序遍历和后序遍历,可以还原二叉树; 已知先序遍历和后序遍历,不可以还原二叉树. a.已知先序遍历和中序遍历还原二叉树 算法思路: 1、根据先序遍历结果确定根节点。先序遍历的第一个节点为根节点。 2、 在中序遍历结果中找到根节点,根节点左侧的部分为左子树节点,根节点右侧的部分为右子树节点。 3、 将中序遍历的结果按根节点分为两部分,迭代的执行第一步和第二步,直到还原整个二叉树。 例如:已知先序遍历的结果为:ABDHIEJKCFLMGNO,中序遍历的结果为:HDIBJEKALFMCNGO 则二叉树为以下结构: 其后序遍历结果为:HIDJKEBLMFNOGCA b.已知后序遍历和中序遍历还原二叉树 算法思路: 1、根据后序遍历结果确定根节点。 后序遍历的最后一个节点为根节点。 2、在中序遍历结果中找到根节点,根节点左侧的部分为左子树节点,根节点右侧的部分为右子树节点。 3、将中序遍历的结果按根节点分为两部分,迭代的执行第一步和第二步,直到还原整个二叉树。 例如:已知后序遍历的结果为:HIDJKEBLMFNOGCA,中序遍历的结果为:HDIBJEKALFMCNGO 则二叉树为以下结构: 其先序遍历结果为:ABDHIEJKCFLMGNO 五、递归遍历算法的应用 1、求二叉树的深度 //求树的深度 int TreeDeep(BiTree T) { int deep = 0; if (T != NULL) { int leftDeep = TreeDeep(T->lchild); int rightDeep = TreeDeep(T->rchild); deep = leftDeep >= rightDeep ? leftDeep + 1 : rightDeep + 1; } return deep; } 2、求二叉树的叶子树 //求叶子树 int LeafCount(BinTree T,int num) { if(T) { if(!T->Left && !T->Right) { nm++; } TreeDeep(T->lchild, num); TreeDeep(T->rchild, num); } return num; } 3、交互(换)左、右子树 void Swap(BiTree *&right,BiTree *&left) { BiTree *temp=right; right=left; left=temp; } void SwapSubtrees(BiTree *T) { if(!T) return ; SwapSubtrees(T->rchild); SwapSubtrees(T->lchild); Swap(T->rchild,T->lchild); } 六、静态查找和动态查找的根本区别 上述基于二叉排序树的动态查找,它的基本原理和基于线性表的静态二分查找很相似,都是利用有序性不断缩小查找空间。 而之所以有静态和动态之分,主要是为了适应不同的应用需求。 适合用于静态查找数据一旦建立好,不需要或者很少进行 删除 和 插入 操作动态查找频繁的数据变化,插入 和 删除 是基本操作 七、树/森林与二叉树的转换 1、树、森林与二叉树的转换 由于二叉树是有序的,为了避免混淆,对于无序树,我们约定树中的每个结点的孩子结点按从左到右的顺序进行编号。 将树转换成二叉树的步骤是: (1)加线。就是在所有兄弟结点之间加一条连线; (2)抹线。就是对树中的每个结点,只保留他与第一个孩子结点之间的连线,删除它与其它孩子结点之间的连线; (3)旋转。就是以树的根结点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。 2、森林转换为二叉树 森林是由若干棵树组成,可以将森林中的每棵树的根结点看作是兄弟,由于每棵树都可以转换为二叉树,所以森林也可以转换为二叉树。 将森林转换为二叉树的步骤是: (1)先把每棵树转换为二叉树; (2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子结点,用线连接起来。当所有的二叉树连接起来后得到的二叉树就是由森林转换得到的二叉树。 3、二叉树转换为树 二叉树转换为树是树转换为二叉树的逆过程,其步骤是: (1)若某结点的左孩子结点存在,将左孩子结点的右孩子结点、右孩子结点的右孩子结点……都作为该结点的孩子结点,将该结点与这些右孩子结点用线连接起来; (2)删除原二叉树中所有结点与其右孩子结点的连线; (3)整理(1)和(2)两步得到的树,使之结构层次分明。 4、二叉树转换为森林 二叉树转换为森林比较简单,其步骤如下: (1)先把每个结点与右孩子结点的连线删除,得到分离的二叉树; (2)把分离后的每棵二叉树转换为树; (3)整理第(2)步得到的树,使之规范,这样得到森林。 5、转换以后的特点: (1、 根据树与二叉树的转换关系以及二叉树的遍历定义可以推知: 树的先序遍历与其转换的相应的二叉树的先序遍历的结果序列相同; 树的后序遍历与其转换的二叉树的中序遍历的结果序列相同; 树的层序遍历与其转换的二叉树的后序遍历的结果序列相同。 (2、 由森林与二叉树的转换关系以及森林与二叉树的遍历定义可知: 森林的先序遍历和中序遍历与所转换得到的二叉树的先序遍历和中序遍历的结果序列相同。 八、线索二叉树 传统的二叉链表仅能体现出一种父子关系,不能直接得到结点在遍历中的前驱或后继。引入【线索二叉树】正是为了加快查找结点前驱和后继的速度。 (1、定义: 前驱与后继:在二叉树的先序、中序或后序遍历序列中的两个相邻的结点;线索:指向前驱或后继的结点的指针;线索二叉树:加上线索的二叉链表的二叉树;线索化:对二叉树按某个遍历次序使其变为线索二叉树的过程。 (2、规定:【口诀:左前右后,0孩1前后】 若无左子树,令lchild指向其前驱结点;若无右子树,令rchild执行指向其后继结点增加两个标志域标识是指左/右孩子还是指向前驱/后继。 1、存储结构 //线索二叉树存储结构 typedef struct ThreadNode{ char data; struct ThreadNode *lchild, *rchild; // 左右孩子指针 int ltag, rtag; // 左右线索标志 }ThreadNode, *ThreadTree; 2、如何判断是孩子还是线索 其标志位含义如下: 【口诀:左前右后,0孩1前后】 这种加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。 根据线索性质的不同, 线索二叉树可分为前序线索二叉树、 中序线索二叉树和后序线索二叉树三种。 3、三种遍历 因为线索化后, 各个结点指向有变化, 因此原来的遍历方式不能使用, 需要使用新的方式遍历线索化二叉树。 中序线索二叉树的结点中隐含了线索二叉树的前驱和后继信息。 在对其遍历时,需要找到第一个具有前驱结点的左结点,然后依次找结点的后继。 在中序线索二叉树中找结点后继的规律是: 若其右标志为1,则右链为线索,指示其后继;否则遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继。 void InOrderTraverse(BiThrTree T){ // 中序输出 if(T) { InOrderTraverse(T->lchild); //中序遍历左子树 cout<< T->data; InOrderTraverse(T->rchild); //中序遍历右子树 } } 九、哈夫曼树 1、带权路径长度WPL 2、哈夫曼树的构造(算法) 构造 Huffman 树的基本思想:权值大的结点用短路径,权值小的结点用长路径。 构造过程 3、哈夫曼树的性质 4、哈夫曼编码 ———散列查找——— 一、散列查找 1、基本概念 散列函数 在进行查找时,在记录的存储位置与它的关键字之间建立一个确定的对应关系h,以线性表中每个元素的关键字K为自变量,通过函数h(K)计算出该元素的存储位置,我们将h函数称为散列函数或哈希函数。h(K)的值称为散列地址或哈希地址。 冲突 在实际应用中,通常可能出现一个待插入元素的散列地址单元已被占用情况,使得该元素无法直接存入此单元,这种情况称为冲突。 同义词 具有不同关键字而具有相同散列地址的元素称为同义词,即key1≠key2,但h(key1)=h(key2)。由同义词引起的冲突称作同义词冲突。 装填因子(α) 指散列表中已存入的元素数n与散列表空间大小m的比值,即:α=n/m。当α越小时,冲突可能性就越小,但同时,存储空间利用率就越低。 散列表:根据设定的哈希函数及处理冲突的方法将一组关键字映象到一个有限的连续的地址集上,即把记录存放在表中映象的位置上,这种表便称为散列表(哈希表)。 一个散列表的好坏与三个因素有关:1.装填因子 2、所采用的散列函数 3、解决冲突的方法 假定一个线性表为A=(18,75,60,43,54,90,46),选取散列函数为:h(K)=K%m 取m=13 则得每个元素散列地址: h(18)=18 % 13=5 h(75)=75 % 13=10 h(60)=60 % 13=8 h(43)=43 % 13=4 h(54)=54 % 13=2 h(90)=90 % 13=12 h(46)=46 % 13=7 根据散列地址,实现元素的存储映象H[m]: 0123456789101112H54431846607590 例:如向下表中再插入元素70时,70%13=5,则出现了冲突 0123456789101112H54431846607590 2、散列函数 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OtlYI1uv-1641217649135)(myReviewPicture/散列函数.png)] 构造散列函数的目标是使散列地址尽可能均匀分布在散列空间上,同时使计算尽可能简单,以节省计算时间。 (1、关键词为数字时: a.直接定址法 b.除留余数法(常用) c.数字分析法 分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址,如电话号码、身份证号码某几位会比较随机; **例:**有一组关键字如下: 92326875 92739628 92343634 92706816 92774638 92381262 92394220 通过分析:每个关键字从左到右第1、2、3位和第6位取值较集中,不宜作散列地址,其余的第4、5、7、8位取值分散,可以选择,若取最后两位作散列地址,得:(2,75,28,34,16,38,62,20) d.平方取中法 key取平方再取中间几位 (2、关键词为字符时: a、ASCII码加和法 h(key)=(求和key[i])mod TableSize b、前3个字符移位法 h(key)=(key[0]*27*27+key[1]*27+key[2])mod TableSize 二、处理冲突的方法 1、开放定址法 a.线性探测法 注意:查找某个值时,用散列函数计算完后,如果那个结果位置上的数字与关键词不一样时,并不能断定关键词不存在,还应该按照冲突解决策略继续找,直到找到空位置了还没找到,才能断定该关键词不存在。 b、平方探测(二次探测) 举例:h(key)=key mod 11; **注意:**取素数是为了减少公因子(减少冲突) c.在散列法 2、分离链接法 ———————————————图———————————————— 一、图的基本概念 集合只有同属于一个集合;线性结构存在一对一的关系;树形结构存在一对多的关系;图状结构存在多对多的关系。 1、简单图 简单图满足以下两条内容: 1)不存在重复边 2)不存在顶点到自身的边 2、完全图 任意两顶点之间都存在边 3、连通分量 在无向图中,两顶点有路径存在,就称为连通的。若图中任意两顶点都连通,同此图为连通图。无向图中的极大连通子图称为连通分量。 4、强连通分量 在有向图中,两顶点两个方向都有路径,两顶点称为强连通。 若任一顶点都是强连通的,称为强连通图。有向图中极大强连通子图为有向图的强连通分量。 5.顶点的度、入度和出度 顶点的度为以该顶点为一个端点的边的数目。 对于无向图,顶点的边数为度,度数之和是顶点边数的 2 倍。 对于有向图,入度是以顶点为终点,出度相反。有向图的全部顶点入度之和等于出度之和且等于边数。顶点的度等于入度与出度之和 注意:入度与出度是针对有向图来说的 二、图的存储 1、数组(邻接矩阵)表示法 建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间关系)。 设图A=(V,E)有n个顶点,则 图的邻接矩阵是一个二位数组A.arcs[n] [n],定义为: 在这里æ’å ¥å›¾ç‰‡æè¿° a.无向图的邻接矩阵表示法 分析1:无向图的邻接矩阵是对称的; 分析2:顶点i的度=第i行(列)中1的个数; 特别:完全图的邻接矩阵中,对角元素为0,其余1。 b.有向图的邻接矩阵表示法 注:在有向图的邻接矩阵中, 第 i 行含义:以结点vi为尾的弧(即出度边) 第 i 列含义:以结点vi为头的弧(即入度边) 分析1:有向图的邻接矩阵可能是不对称的; 分析2:顶点的出度 = 第 i 行元素之和 顶点的入度 = 第 i 列元素之和 顶点的度 = 第 i 行元素之和 + 第 i 列元素之和 c.有权图(网)的邻接矩阵表示法 2.邻接表(顺序存储与链式存储结合) a.无向图的邻接表 b.有向图的邻接表与逆邻接表 c.带权值的网图 三、图的遍历 1、深度优先遍历算法 深度优先搜索类似于树的先序遍历。 其基本思想是: 首先访问起始顶点v,然后由v出发,访问与v 邻接且未被访问的任一顶点w1,再访问与w1 邻接且未被访问的任一顶点W2……重复上述操作。 当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。 从顶点a 出发,进行深度优先遍历,可以得到的一种顶点序列为:a e d f c b 2、广度优先遍历算法 广度优先搜索类似于二叉树的层序遍历算法。 其基本思想是: 首先访问起始顶点v,接着由ν出发,依次访问v 的各个未访问过的邻接顶点W1,W2,…,Wi,然后依次访问W1,W2,…,Wi的所有未被访问过的邻接顶点;再从这些访问过的顶点出发,访问它们所有未被访问过的邻接顶点,直至图中的所有顶点都被访问过为止。若此时图中尚有顶点未被访问,则另选图中的一个未被访问的顶点作为始点,重复上述过程,直至图中所有顶点都被访问到为止。 从顶点1 出发,按照广度优先规则遍历,可以得到的一种顶点序列是: 1234576 二、最小生成树 1、性质 2、Prim算法 3、Kruskal算法 三、拓扑排序 四、最短路径 迪杰斯特拉算法 通过迪杰斯特拉算法计算图G中的最短路径时,需要指定起点s。 此外,需要引进两个集合S和U。 S的作用:记录已求出最短路径的顶点(以及相应的最短路径长度),U的作用:记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。初始时,S中只有起点s;U中是除s之外的顶点,并且U中顶点的路径是“起点s到该顶点的路径”。然后,从U中找到路径最短的顶点,并将其加入到S中; 接着,更新U中的顶点和顶点对应的路径。然后,再从U中找到路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。重复上述操作,直到遍历完所有顶点。 具体过程 1、初始化,所有顶点的距离初始化为无穷大(INFINITY) 2、选定点A,更新(A-A距离设为0) 3、S集合为{A,B},考察B的所有邻接点 为什么选定B加入集合S? 因为不可能还有其他路径比2还短,我不管经过C到B还是D到B都不可能是路径小于2,所以我们得到了A->B的最短路径 做完这一步,下一步加入集合S的是D 因为目前A->D的路径长度最短,为3(我已经知道了A直接到D和A经过B到D的路径长度) 如果A->B->X->D小于min{A->D,A->B->D},那么A->B->X小于min{A->D,A->B->D},那么加入集合的应该是X,这是矛盾的(接下来的操作都是一样的道理 4、S集合为{A,B,D},在U中没有D的邻接点,不操作 5、S集合为{A,B,D,C},在U中没有C的邻接点,不操作 6、S集合为{A,B,D,C,F},更新 7、S集合为{A,B,D,C,F,E},在U中没有E的邻接点,不操作 8、S集合为{A,B,D,C,F,E,G},在U中没有G的邻接点,不操作 9、最终结果如上图。 ———排序——— 一、排序的类别 1、插入排序 基本思想: 【1】直接插入排序 (1、基本思想: 1)、将待排序的一组序列(有N个数)分为已排好的和未排好的 2个部分; 2)、初始状态时,已排序序列仅包含第1 个元素,未排序序列中的元素为除去第1 个元素意外的N-1 个元素; 3)、此后,将未排序序列中的元素逐一插入到已排序的序列中; 4)、如此往复,经过N-1 次插入后,未排序序列中元素个数为0 ,则排序完成。 (2、执行过程 (3、时空效率及稳定性 【2】希尔排序 (1、基本思想: 1)、将带排序序列的一组元素按一定间隔分为若干序列分别进行插入排序; 2)、开始时设置的“间隔”较大,在每轮排序中,将**”间隔“逐步缩小** 3)、直到“间隔”为 1,也就到了最后一步,做简单插入排序。 (2、执行过程 (3、时空效率及稳定性 2、交换排序 基本思想: 【1】冒泡排序 (1、基本思想: (2、执行过程 (3、时空效率及稳定性 【2】快速排序 (1、基本思想: 1)、将未排序元素根据一个作为基准的“主元(pivot)分为两个子序列; 2)、其中一个子序列的记录均大于“主元”,另一个序列则均小于“主元; 3)、递归地对两个子序列用类似的方法进行排序。 (2、执行过程 (3、时空效率及稳定性 3、选择排序 基本思想: 【1】简单选择排序 (1、基本思想: 1)、在未排序的序列中选出最小元素和序列的首位元素交换, 2)、再在剩下的排序序列中再选出最小元素与序列的第2 个位置元素交换 3)、以此类推,最后形参从小到大的已排序序列。 (2、执行过程 (3、时空效率及稳定性 【2】堆排序 (1、基本思想: 1)、利用最大堆(或最小堆)输出堆顶元素,即最大值(或最小值); 2)、将剩余元素重新生成最大堆(或最小堆),继续输出堆顶元素; 3)、重复此过程,知道全部元素都已输出,得到的输出元素序列即为有序序列 (2、执行过程要点 <1>初始化堆的过程 下面是构建初始堆的过程 下面是堆排序的过程 (3、时空效率及稳定性 4、归并排序 二、各种排序的比较 口诀:快选堆希不稳,选堆归基不变 不稳:说的是 算法不稳定 不变:说的是 关于移动次数和关键字顺序无关的排序 每天进步一点点 整理不易。如果我的试错经历有帮到你,可以支持一下我哦