数据结构复习其二
——算法、线性表——
概念明晰:随机存取、顺序存取、随机存储和顺序存储
随机存取、顺序存取、随机存储和顺序存储这四个概念是完全不一样的,切不可将之混淆
很多人包括我可能认为随机存取就是随机存储,顺序存取就是顺序存取,其实不是这样。
下面完整的介绍一下这4个概念
1、存取结构
分为随机存取
和非随机存取
(又称顺序存取)
1、
随机存取
就是直接存取
,可以通过下标直接访问的那种数据结构,与存储位置无关。例如数组。
非随机存取
就是顺序存取
,不能通过下标访问了,只能按照存储顺序存取,与存储位置有关,例如链表。2、
顺序存取
就是存取第N个数据时,必须先访问前(N-1)个数据 (list);
随机存取
就是存取第N个数据时,不需要访问前(N-1)个数据,直接就可以对第N个数据操作 (array)。
2、存储结构
分为顺序存储
和随机存储
3、顺序存储结构
在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。
1
2
3 - 顺序存储结构是存储结构类型中的一种,该结构是把**逻辑上相邻的节点**存储在**物理位置上相邻的存储单元**中,结点之间的逻辑关系由存储单元的邻接关系来体现。
- 由此得到的储结构为顺序存储结构,通常顺序存储结构是借助于计算机程序设计语言(例如c/c++)的数组来描述的。
12– 主要优点:节省存储空间。
因为分配给数据的存储单元全用存放结点的数据(不考虑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)指数型。
例如:
1 | i=1;① |
二、线性表
1、顺序存储
(1)结构体的定义
1 | typedef int Position; |
(2)顺序表的初始化
1、构造一个空表
2、动态分配表结构所需的存储空间,然后将表中Last指针置为-1 表示表中没有数据。
1 | List MakeEmpty() |
- 通过L我们可以访问相应线性表的内容。比如:下标为i 的元素:L->Data[i]
- 查询线性表的长度:L->Last+1;
(3)顺序表的查找(时间复杂度为O(n))
在线性表中查找与给定值 X 相等的数据元素。
由于线性表的元素都存储在数组Data中,所以这个查找的过程实际上就是在数组里顺序查找:
- 从第 1 个元素 a1 起依次和 X 比较, 直到找到一个与 X 相等的数据元素,返回它在顺序表中的存储下标;
- 或者查遍整个表都没有找到与 X 相等的元素,则返回错误信息 ERROR。
1 |
|
(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 指针(相当于修改表长),使之指向最后一个元素。
1 | bool Insert( List L, ElementType X, int i) |
(5)顺序表的删除(时间复杂度为O(n))
将表中的位序为 i(1≤ i ≤ n + 1)的元素从线性表中去掉,删除后使原长度为 n 的数组元素序列,变为长度为 n-1 的序列
- 将a[i+1]~a[n] 顺序向前移动 ,a[i] 元素被a[i+1]覆盖;
- 修改 Last 指针(相当于修改表长)使之仍指向最后一个元素。
1 | bool Delete(List L, int i) |
2、链表存储
(1)结构体的定义(时间复杂度为O(n))
1 | typedef struct LNode * PtrToLNode; |
(2)求表长(时间复杂度为O(n))
在顺序存储中求表长是很容易的,直接返回 Last+1 就可以了。但在链式存储中,需要将链表从头到尾遍历一遍
- 设一个移动指针p和计数器cnt,初始化后,p从表的第 1 个结点开始逐步往后移,同时计数器 cnt+1.
- 当后面不再有结点时,cnt 的值就是结点个数,即 表长。
1 | int Length(List L) |
(3)判空
1 | int ListEmpty(LinkList L) |
(4)查找(时间复杂度为O(n))
有两种 按序号查找(FindKth)和 按值查找(Find)
①按序号查找 FindKth(时间复杂度为O(n))
对于顺序存储,按序号查找是很直接的事情,要得到第 K 个元素的值,直接取L->Data[K-1]即可。
但是对于链式存储则需要采用跟求表长类似的思路:
- 从链表的第 1 个元素结点起,判断当前结点是否是第 K 个;
- 若是,则返回该结点的值,否则继续对比后一个,直到表结束为止。
- 如果没有第 K 个结点则返回错误信息。
1 |
|
②按值查找,即定位 Find(时间复杂度为O(n))
基本方法:也是从头到尾遍历,直到找到为止:
- 从链表的第 1 个元素结点起,判断当前结点的值是否等于 X;
- 若是,返回该结点的位置,否则继续对比后一个,直到表结束位置为止;
- 找不到时返回错误信息。
1 |
|
(5)链表的插入(时间复杂度为O(n))
1 | int ListInsert_L(LinkList &L, int i,ElementType e) |
(6)创建链表(时间复杂度为O(n))
1、带头结点的【头插法】(时间复杂度为O(n))
1 | /* 带头结点的插入创建 */ |
2、带尾结点的插入【尾插法】(时间复杂度为O(n))
1 | /*带尾结点的插入*/ |
(7)删除(时间复杂度为O(n))
1 | //将线性表L 中第 i 个数据元素删除 |
3、二者时间复杂度和优缺点的比较
1、两者复杂度比较
查找 | 插入 | 删除 | |
---|---|---|---|
顺序表 | O(1) | O(1) | O(n)通过下标直接找到待操作元素,主要时间花在移动元素上。 |
链表 | O(n) | O(n)主要时间用于找到插入元素的位置 | O(n)主要时间用于找到待删除元素的位置 |
2、两者优缺点比较
数组 | 优点 | 缺点 |
---|---|---|
随机访问性强;查找速度快 | 插入和删除效率低;可能浪费内存;内存空间要求高,必须有足够的连续内存空间;数组大小固定,不能动态拓展 |
链表 | 优点 | 缺点 |
---|---|---|
插入删除速度快;内存利用率高,不会浪费内存;大小没有固定,拓展很灵活。 | 不能随机查找,必须从第一个开始遍历,查找效率低 |
两者的区别在于顺序结构的要求一片连续的存储空间,而链式结构的不要求存储空间连续。
三、栈
1、栈的顺序存储实现
通常由一个一维数组和一个记录栈顶元素位置的变量组成。
(1)顺序栈结构体的定义
当 Top = -1时,表示栈空;当Top = MaxSize -1 时,栈满!
1 | typedef int Position; |
(2)顺序栈的创建
1 | Stack CreateStack(int MaxSize) /*顺序栈的创建*/ |
(3)判满
1 | bool IsFull(Stack S) /*判断栈是否满了*/ |
(4)判空
1 | bool IsEmpty(Stack S) /*判断堆栈是否为空*/ |
(5)入栈
在执行堆栈 Push 操作时,先判断栈是否满;
- 若不满,Top 加1,并将新元素放入 Data数组的Top位置上
- 若满,则返回错误标志
1 | bool Push(Stack S, ElementType X) /*顺序栈的 入栈 操作*/ |
(6)出栈
执行Pop操作时,首先判别栈是否为空;
- 若不为空,返回Data[Top],同时将Top-1;
- 否则要返回错误标志
1 | ElementType Pop(Stack S) /*顺序栈 的 出栈 操作*/ |
2、栈的顺序存储实现
链栈与单链表类似,但其操作受限制,插入和删除操作只能在链栈的栈顶进行。
(1)顺序栈结构体的定义
1 | typedef struct SNode *PtrToSNode; |
(2)顺序栈的创建
1 | Stack CreateStack() |
(3)判空
1 | bool IsEmpty(Stack S) |
(4)判满 注意:链栈,不必判断堆栈是否满
(5)入栈
链栈,不必判断堆栈是否满
1 | bool Push(Stack S, ElementType X) |
(6)出栈
1 | ElementType Pop(Stack S) ElementType Pop(Stack S) |
3、栈的应用
四、队列
1、队列的顺序存储实现
(1) 循环队列的结构体定义
1 | typedef int Status; |
(2)生成空队列
1 | /* 初始化一个空队列Q */ |
(3)判空
队空的条件是:rear=front
1 | bool IsEmpty(SqQueue *Q) |
(4)判满
队满的条件是:(rear+1)%数组的长度等于 front
1 | bool IsFull(SqQueue *Q) |
(5)入队
1 | /* 若队列未满,则插入元素e为Q新的队尾元素 */ |
(6)出队
1 | /* 若队列不空,则删除Q中队头元素,用e返回其值 */ |
2、队列的链式存储实现
队列与堆栈一样,也可以采用链式存储结构,但队列的头(front)必须指向链表的头结点,队列的尾(rear)指向链表的尾结点。
(1)队列的链式存储结构体定义
1 | typedef int Status; |
(2)生成空队列
1 | /* 构造一个空队列Q */ |
(3)判空
队空的条件是:rear=front
1 | Status QueueEmpty(LinkQueue Q) |
(4)判满 链式队列,不必判断堆栈是否满
(5)入队
1 | /* 插入元素e为Q的新的队尾元素 */ |
(6)出队
1 | /* 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR */ |
五、栈和队列操作的特点
相同点 | 不同点 | |
---|---|---|
堆栈(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[mid].key,则说明待查记录只可能在前半个子表r[1,…,mid-1]中;
这样逐步缩小范围,直到查找成功或子表为空时失败为止。
注意:每次缩小范围后,改变的下标是哪个
1 | //递增的方式排序,则折半查找的算法为 |
折半查找的过程可以用一颗二叉树来描述,以当前查找区域间的中间位置序号作为根,左半个子表和右半个子表中的记录序号分别分别作为根的左子树和右子树上的结点,这样构造的二叉树称为折半查找判定树,从树上可以看出:
查找成功时,折半查找的过程恰好走了一条从根结点到被查找结点的路径,与关键字进行比较的次数即为被查找结点在树中的层数。因此,折半查找判定树在查找成功时进行比较的关键字个数最多不超过树的深度,而具有n个结点的判定树的深度为;所以折半查找在查找成功时和给定值进行比较的关键字个数最多为。
优点:查找效率更高,但它要求查找表进行顺序存储并按关键字进行排序。
缺点:对表进行插入或删除时,需要移动大量元素。
适用:表不易变动,且又经常进行查找的情况
4、二分查找判定树ASL计算
折半查找的过程看,可用二叉树来描述,二叉树中的每个结点对应有序表中的一个记录,结点中的值为该记录在表中的位置。通常称这个描述折半查找二叉树的过程称为折半查找判定树。
例如:顺序存储的序列{1,2,3,4,5,6,7,8,9,10} 来构建二叉判定树,计算其ASL
1 | 例如:长度为10的折半查找判定树的具体生成过程: |
特点:
1.折半查找是一棵二叉排序树,每个根结点的值都大于左子树的所有结点的值,小于右子树所有结点的值。
2.折半查找判定数中的结点都是查找成功的情况,将每个结点的空指针指向一个实际上不存在的结点————外结点,所有外界点都是查找不成功的情况,如图7-2(e)所示。如果有序表的长度为n,则外结点一定有n+1个。
(1)查找成功的ASL
折半查找判定数中,某结点所在的层数就是即将要比较的次数,整个判定树代表的有序表的平均查找长度即为查找每个结点的比较次数之和除以有序表的 长度。
ASL成功 = 每层结点所在高度×每层结点数 之和 除以 总结点数
1 | 例如:长度为10的有序表的平均查找长度为 |
(2)查找不成功的ASL
折半查找判定数中,查找不成功的次数即为查找相应外结点(定义在上方)与内结点的比较次数。整个判定树代表的有序表的平均查找长度。查找失败时的有序表的平均查找长度即为查找每个外结点的比较次数之和除以外结点的个数。
ASL失败 = (每层【补上的】结点所在高度-1)×每层【补上的】结点数 之和 除以 【补上的】总结点数
1 | 例如:查找失败时,长度为10的有序表的平均查找长度为: |
三、动态查找
1、二叉树链表结构描述如下:
1 | typedef struct TNode *Position; |
二叉链表至少包含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)若查找失败,说明节点不存在,则将其插入到树中
因此,新插入节点一定是作为叶子节点插入的。
1 | BinTree Insert(Bintree BST, ElmentType X) |
(2)删除过程算法及其代码
二叉搜索树的删除操作比其它操作更为复杂,要删除结点在树中的位置决定了操作所采用的策略。
a.若要删除的结点是叶子结点
可以直接删除,然后再修改其父结点的指针。
b.若要删除的结点只有一个孩子结点(该结点不一定是叶结点,可以是子树的根)
删除之前需要改变父结点的指针,指向要删除结点的孩子结点。
c.若要删除的结点有左、右两棵子树,有两种选择:
基本原则:保持二叉搜索树的有序性
1、取其右子树中的最小元素;
2、取其左子树中的最大元素。
(3)查找过程算法及其代码
BST树的查找思想:
首先将给定的K值与二叉排序树的根节点的关键字进行比较:
- 若相等,则查找成功;
- 若给定的K值小于BST树的根节点的关键字:继续在该节点的左子树上进行查找;
- 若给定的K值大于BST树的根节点的关键字:继续在该节点的右子树上进行查找。
a.二叉搜索树的递归查找函数
在二叉排序树上进行查找,则是从根结点出发走了一条从根到待查结点的路径;
若查找不成功,则是从根结点出发走了一条从跟到某一叶结点的路径。
1 | Position Find(BinTree BST,ElementType X) |
b.迭代查找算法
由于非递归函数的执行效率高,一般采用非递归的迭代来实现查找。很容易将递归函数改为迭代函数
while循环 代替 Find递归调用即可
1 | Position Find(BinTree BST,ElementType X) |
(4)查找最大值和最小值
根据二叉搜索树的性质,最小元素一定是在树的最左分支的端点上。最左分支的端点:最左分支上无左孩子的结点。
最大元素一定在最右分支的端结点上。
- 从根结点开始,当其不为空时,沿左分支或者右分支逐个判断各结点的指针,直到遇到空指针为止。
- 当左分支逐层推下来查找到的是最小元素。
- 反之,当右分支逐层推下来查找到的是最大元素。
a.最小元素的递归函数
1 | Position FindMin(BinTree BST) |
b.查找最大元素的迭代函数
1 | Position FindMax(BinTree BST) |
四、二叉树的遍历
指按照某种次序访问二叉树的所有结点,并且每个结点仅访问一次,得到一个线性序列。
1、先序遍历
(1)访问根结点
(2)先序遍历左子树
(3)先序遍历右子树-中序、后序遍历相似
先序遍历:A → B → D → C
中序遍历:B → D → A → C
后续遍历:D → B → C → A
层序遍历:A → B → C → D
1 | void PreOrderTraverse(BiTree T) //链式二叉树先序遍历递归算法 |
2、层序遍历(队列实现)
仔细看看层序遍历过程,其实就是从上到下,从左到右依次将每个数放入到队列中,然后按顺序依次打印就是想要的结果。
实现过程
- 从队列中取出一个元素;
- 访问该元素所指结点;
- 若该元素所指结点的左、右孩子结点非空,则将其左、右孩子的指针顺序入队。
不断执行这三步操作,直到队列为空,再无元素可取,二叉树的程序遍历就完成了。
1 | void LevelorDerTraversal(BinTree BT) |
3、由遍历序列还原二叉树
已知先序遍历和中序遍历,可以还原二叉树;
已知中序遍历和后序遍历,可以还原二叉树;
已知先序遍历和后序遍历,不可以还原二叉树.
a.已知先序遍历和中序遍历还原二叉树
算法思路:
1、根据先序遍历结果确定根节点。先序遍历的第一个节点为根节点。
2、 在中序遍历结果中找到根节点,根节点左侧的部分为左子树节点,根节点右侧的部分为右子树节点。
3、 将中序遍历的结果按根节点分为两部分,迭代的执行第一步和第二步,直到还原整个二叉树。
例如:已知先序遍历的结果为:ABDHIEJKCFLMGNO,中序遍历的结果为:HDIBJEKALFMCNGO
则二叉树为以下结构:
其后序遍历结果为:HIDJKEBLMFNOGCA
b.已知后序遍历和中序遍历还原二叉树
算法思路:
1、根据后序遍历结果确定根节点。
后序遍历的最后一个节点为根节点。
2、在中序遍历结果中找到根节点,根节点左侧的部分为左子树节点,根节点右侧的部分为右子树节点。
3、将中序遍历的结果按根节点分为两部分,迭代的执行第一步和第二步,直到还原整个二叉树。
例如:已知后序遍历的结果为:HIDJKEBLMFNOGCA,中序遍历的结果为:HDIBJEKALFMCNGO
则二叉树为以下结构:
其先序遍历结果为:ABDHIEJKCFLMGNO
五、递归遍历算法的应用
1、求二叉树的深度
1 | //求树的深度 |
2、求二叉树的叶子树
1 | //求叶子树 |
3、交互(换)左、右子树
1 | void Swap(BiTree *&right,BiTree *&left) |
六、静态查找和动态查找的根本区别
- 上述基于二叉排序树的动态查找,它的基本原理和基于线性表的静态二分查找很相似,都是利用有序性不断缩小查找空间。
- 而之所以有静态和动态之分,主要是为了适应不同的应用需求。
适合用于 | |
---|---|
静态查找 | 数据一旦建立好,不需要或者很少进行 删除 和 插入 操作 |
动态查找 | 频繁的数据变化,插入 和 删除 是基本操作 |
七、树/森林与二叉树的转换
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、存储结构
1 | //线索二叉树存储结构 |
2、如何判断是孩子还是线索
其标志位含义如下: 【口诀:左前右后,0孩1前后】
- 这种加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。
- 根据线索性质的不同, 线索二叉树可分为前序线索二叉树、 中序线索二叉树和后序线索二叉树三种。
3、三种遍历
因为线索化后, 各个结点指向有变化, 因此原来的遍历方式不能使用, 需要使用新的方式遍历线索化二叉树。
中序线索二叉树的结点中隐含了线索二叉树的前驱和后继信息。
在对其遍历时,需要找到第一个具有前驱结点的左结点,然后依次找结点的后继。
在中序线索二叉树中找结点后继的规律是:
- 若其右标志为1,则右链为线索,指示其后继;
- 否则遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继。
1 | void InOrderTraverse(BiThrTree T){ // 中序输出 |
九、哈夫曼树
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]:
0 1 2 3 4 5 6 7 8 9 10 11 12 H 54 43 18 46 60 75 90
例:如向下表中再插入元素70时,70%13=5,则出现了冲突
0 1 2 3 4 5 6 7 8 9 10 11 12 H 54 43 18 46 60 75 90
2、散列函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OtlYI1uv-1641217649135)(myReviewPicture/散列函数.png)]
1 | 构造散列函数的目标是使散列地址尽可能均匀分布在散列空间上,同时使计算尽可能简单,以节省计算时间。 |
(1、关键词为数字时:
a.直接定址法
b.除留余数法(常用)
c.数字分析法
1 | 分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址,如电话号码、身份证号码某几位会比较随机; |
**例:**有一组关键字如下:
92326875
92739628
92343634
92706816
92774638
92381262
92394220
通过分析:每个关键字从左到右第1、2、3位和第6位取值较集中,不宜作散列地址,其余的第4、5、7、8位取值分散,可以选择,若取最后两位作散列地址,得:(2,75,28,34,16,38,62,20)
d.平方取中法
1 | key取平方再取中间几位 |
(2、关键词为字符时:
a、ASCII码加和法
1 | h(key)=(求和key[i])mod TableSize |
b、前3个字符移位法
1 | 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、归并排序
二、各种排序的比较
口诀:快选堆希不稳,选堆归基不变
不稳:说的是 算法不稳定
不变:说的是 关于移动次数和关键字顺序无关的排序
end