前言
数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这些联系可以通过定义相关的函数来给出。 ——Sartaj Sahni
数据结构是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最有效率的算法。 ——中文维基百科
解决问题的效率跟数据的组织方式是相关的。 ——陈越
-----------------------------------------------------------------------------------------------------------------------------
要学好这门课,你要有每周投入8小时(或者更多)的决心,其中听课只占一小部分 —— 每次讲课的时间一般只有1小时左右,重要的是课后的练习。光说不练嘴把势,只了解原理是远远不够的,你必须在实践中去深刻体会每一个概念的运用,才能真正知道经典的数据结构为什么存在、以及在什么情况下可以最好地解决什么样的问题。
每周至少花1天的时间:学习《数据结构》
-----------------------------------------------------------------------------------------------------------------------------
1.几道例题
例1:如何在书架上摆放图书?
随便放
简单,但找的时候累死
按照书名拼音字母顺序排放
如何找书?
二分法查找(十大经典算法之一,具体自行百度)
新书如何插入?
需要将此书后面的所有书均往后挪一个顺序,很麻烦。。。
将书架分成几个区域,每个区域指定摆放某一类的书;每个区域内部按照书名拼音字母顺序摆放‘
新书插入?
先定类别,二分查找确定位置,移出空位;
如何找书?
先定类别,再二分查找;
问题:空间如何分配?类别应分多细?
告诉我们:解决问题方法的效率,与数据的组织方式有关。
-----------------------------------------------------------------------------------------------------------------------------
例2:写程序实现一个函数PrintN,使得传入一个正整数为N的参数后,顺序打印从1到N的全部正整数?
![](https://i-blog.csdnimg.cn/blog_migrate/537cb9661507e2e08dd7dacf4bf9cb0f.png)
有以上两种实现方法,虽然递归看起来更简洁一点,但是递归的程序对空间的占用有时很恐怖,比如说输入1000000时,程序直接就非正常终止,stack overflow了
告诉我们:解决问题方法的效率,跟空间利用效率有关;
-----------------------------------------------------------------------------------------------------------------------------
例3:写程序计算给定多项式在定点x处的值
![](https://i-blog.csdnimg.cn/blog_migrate/85cdf98dff74bf2c3ca0be1116aee5ca.png)
第二种写法是秦九韶算法。比第一种函数快很多
使用clock()函数配合常数CLK_TCK来测试函数的运行效率,使用之前需要先include一个time.h头文件;pow()函数是计算次方的,使用前需要先include一个math.h头文件
![](https://i-blog.csdnimg.cn/blog_migrate/fd0c83a786d333f59221cbaa276cd1b1.png)
![](https://i-blog.csdnimg.cn/blog_migrate/32d2d62caff9d6edd7fe52b72be8a385.png)
但是由于任务太简单了,跑得太快,计算机捕捉不到。解决方法很简单,让程序多跑几次,最后求平均值。结果是:第一个算法的效率比第二种算法的效率差了一个数量级左右。
![](https://i-blog.csdnimg.cn/blog_migrate/631f5534ae6398bb799c72ce14bfc402.png)
告诉我们:解决问题方法的效率,跟算法的巧妙程度有关;
-----------------------------------------------------------------------------------------------------------------------------
2.什么是数据结构?
数据结构:数据对象在计算机中的组织方式
逻辑结构:就是像树,图,队列,栈这样形容结构的结构形式
物理存储结构:上面的这些逻辑结构在机器内存中具体是如何存储的,是用一个数组来存储呢(连续),还是用一个链表来存储呢(分隔开存储)
这些数据对象必定与一系列加在其上的操作相关联,完成这些操作所用的方法就叫做算法
描述数据结构,有一个很好的方法:抽象数据类型(Abstract Data Type,ADT)。一个数据结构,就是一个抽象数据类型。
数据类型(包含两种东西)
数据对象集:这种数据是什么东西
数据集合相关联的操作集:对这种数据的一系列操作。
在C语言(面向过程)中,上面这两个是独立处理的。在像C++、JAVA、Python这些面向对象的语言中,很好的为这些数据类型设计了一个机制:把数据集和与其相关的操作集封装在了一个类里面。类似于python中定义类是要同时定义类的属性和方法?
有点像C语言最后设计链表的感觉
抽象
描述数据类型的方法不依赖于具体实现
对一个数据类型的描述,应该与存放数据的机器无关、与数据存储的物理结构无关、与实现操作的算法和变成语言均无关
![](https://i-blog.csdnimg.cn/blog_migrate/ca45adcdb93f3082cf7cbb7588ce6f77.png)
抽象的表示,抽象在哪里呢?
在定义数据对象集中的元素值的时候,对它的类型我们是不关心的,在操作集中定义函数的时候我们也是不关心的,操作的是对应元素的类型,什么都可以(int\float\ double)。也可以定义一个结构体,令每一个元素都是这样一个结构体。
对于定义的这样一个矩阵,机器使用什么样的内存结构存它,我们也是不关心的。可以用二维数组、一维数组或十字链表等
定义的这些矩阵,对齐操作时是按行加还是按列加,还是用什么语言写,我们都是不管的
数组和链表是数据类型还是数据结构?
链表是一种数据结构(包含抽象的组织方式,也就是操作集)?
数据类型指的是int、long等,可以看作是数据对象集?
数组是C/C++中内置的数据类型,但不是基础数据类型而是构造数据类型?
-----------------------------------------------------------------------------------------------------------------------------
3.什么是算法?
伪码描述的一个特点就是抽象,虽然看起来很具体,但是有一些细节并没有说明;List用数组还是链表实现都可以,不影响算法;Swap用函数实现还是宏实现都可以;
像这些具体实现的细节,在我们描述算法的时候是不关心的
-----------------------------------------------------------------------------------------------------------------------------
4.评价算法好坏的指标
数据的规模是 n。如果空间复杂度S(n)太大,程序可能会非正常中断;时间复杂度T(n)太大的话,程序运行所需时间可能会很久很久。
递归与循环下的空间复杂度S(n):
递归程序每被调用一次,就需要在内存中开一块空间放置当前被调用程序的状态,输入一个数字N,该程序被调用了N次,就需要在内存中至少有N块空间用于放置临时的程序运行状态。 因此该程序运行一次占用空间的量是随着N线性增长的,所以该递归程序的空间复杂度是S(N)=C * N。
循环程序运行时,只用到了一个临时变量和一个for循环,没有涉及到任何程序调用的问题,因此不管N有多大,该程序占用的空间始终都是固定的,不会随着N的增长而增长,其空间占用始终是常量。
递归与循环下的时间复杂度T(n):
机器运算加减法的时候要比乘除法快很多,当程序中同时包含是,加减法所需时间可以忽略不记,只算乘除法的次数就可以。两种程序运行时,所需执行乘法的次数如下所示。当n很大时,肯定是第二种算法时间更短。
分析算法的效率时,最常用的还是最坏情况复杂度Tworst(n)
-----------------------------------------------------------------------------------------------------------------------------
5.复杂度的渐进表示法
在分析算法时,不需要对算法进行非常精确的分析,只需要知道它的增长趋势即可
对渐进表示法的解读:
T(n) = O(f(n)):表示对于充分大的n而言,f(n)是T(n)的某种上界。
T(n) = Ω(g(n)):表示对于充分大的n而言,g(n)是T(n)的某种下界。
T(n) = Θ(h(n)):表示T(n) = O(h(n))和T(n) = Ω(h(n))同时成立,也就是说h(n)既是上界也是下界,T(n)和h(n)基本上等价。
一个算法的上界或下界可以有很多个,但是我们希望在分析算法时上界或下界越贴近正常情况越好,因此在写上界或下界的时候,常常写在能力范围内能找到的最小上界和最大下界
// 计算机中logn默认以2为底,但是以哪个数字为底都无关紧要,因为在分析算法的时候他们只是差了一个常数倍而已,没有本质区别
复杂度对比和分析
复杂度分析窍门
把两个算法拼接在一起时,它们算法复杂度的上界是二者中比较大的那个;
把两个算法嵌套在一起时,它们的算法复杂度的上界是二者上界的乘积;
for循环的时间复杂度是循环次数乘以循环体内代码的时间复杂度;
if-else结构有三个地方决定复杂度:if条件判断复杂度、if语句内复杂度和else语句内复杂度,总体代码的复杂度取三者间最大一个;
当两个复杂度加在一起的时候,最后取的是比较大的那一项
-----------------------------------------------------------------------------------------------------------------------------
6.应用实例:最大子列和问题
暴力方法
因为有三层for循环,所以时间复杂度T(N) = O(N^3)
算法2:k的那个循环可以省去,对于相同的i不同的j,只需要在上一次的基础上累加一项即可,算法复杂度为O(N^3)
但是一个专业的程序员看到一个算法或程序的复杂度为O(N^2)时,就应该会想有没有可能将这个算法的复杂度改进成 O(N) 或 O(log(N))呢?
分治法:分而治之,
一个序列的最大子列和可以分成三种情况,左边最大,右边最大和跨越界限最大,三个中最大的值就是当前序列的最大子列和。将整个序列分到两两一对那种程度,然后递归的对每一段序列使用这种算法。求跨越界限的最大子列和时,需要从中间开始,往左走和往右走寻找两边的最大子列,再合并到一起。
算法复杂度可以由左边的复杂度加上右边的复杂度,最后加上跨越中间的算法复杂度组成
下面计算时间复杂度的公式可以一直展开,知道展开至T(1)为止。其中N/2^k可能不是正好为1,但是也差不多,这里使用了极限的思想。当两个复杂度加在一起的时候,取得是比较大的哪一项,所以最后等于O(NlogN)
副作用:牺牲内存来提高速度
在线(联机)算法
思想:如果当前的子列和为负,则ThisSum再从0开始计算,因为为负的子列和对后面的任何数都会使其减小。直到发现当前的子列和大于MaxSum时,才更新MaxSum。ThisSum相当于找出这串数字中的所有极大值,MaxSum相当于在所有的极大值中找最大值。
一般来讲,一个算法的效率越高,它都是有副作用的,这里的副作用就是算法的正确性不够明显,别人理解起来比较难
上面这个表是以上四个算法跑出来的结果,NA:not available
-----------------------------------------------------------------------------------------------------------------------------
第二章:线性表&堆栈&队列
1.引子:多项表达式
对多项式的表示(存储)可以有很多种形式
顺序存储结构(数组或其他形式)直接表示:
使用一个系数数组a[i]对其进行表示,优点是直接且实现起来比较简单
但是有个问题,比如下图中的问题,表达这样一个多项式,至少需要2001个分量,但是1999个都是0,而且在一个循环中,绝大部分都是没有意义
顺序存储结构表示非零项
不对所有项表示,只对非零项表示。也是用一个数组表示,但是每个成员是一个二元组的集合。用结构数组表示。
对这样存储方式的数组,若需要做运算,则需要按指数大小有序存储
链表结构存储非零项
排列的时候按照指数递降或递增的顺序进行排列
加法的实现算法与方法2中的方式相同,先比较第一项,谁指数大谁输出,剩下的这一个与另一个数组中的下一项进行比较,谁大谁输出,剩下的这一个与另一个数组中的下一项进行比较......直到遍历输出完两个数组中的所有元素。
2.线性表及其顺序存储(数组)
对于线性表这样的数据结构,可以用一种抽象数据类型List来表示
所谓抽象数据类型,实际包含了两个要素。一个是这个抽象数据类型所包含的每个元素的类型(数据对象集),另一个是在这个对象集上有什么操作(操作集)
假设有一个具体的线性表L属于抽象数据类型List,里面每个位置 需要 有一个整数i表示位置,每个元素X 需要 属于ElementType类型,这个类型可以是int、float或结构体类型。对于这样一个L,需要 有以下操作:(这里是抽象描述,像用什么语言实现都可以)
使用数组的连续存储空间来实现线性表
初始化:PtrL->Last是该线性表最后一个元素的位置,n。这个线性表总共有n个元素。
查找操作
插入:下面代码中for循环的平均移动次数为n/2,平均时间性能为O(n)。
这里的i是第i个元素(从1开始数),而下标是从0开始数的。
可以插入到n+1的位置,下面检查合法性的代码的条件可能有些问题,应该使>PtrL->Last+2,但是能够懂这种思想即可
删除:删除表的第 i(1<=i<=n)个位置上的元素
平均移动次数为(n-1)/2,平均时间性能为O(n) ?
3.线性表的链式存储实现
主要操作的实现
求表长(下图是一个单向链表)
查找
可以按序号查找和按值查找,返回的都是这个元素的指针
插入操作实现
当插入的位置是第一个时,可以直接返回s。因为s此时已经是链表的头指针,而在定义时PtrL是一个链表的地址,本质上也就是这个链表的头指针。
平均时间复杂度是O(n/2)
删除操作:删除链表第i(1<=i<=n)个位置上的结点
被删除的结点是通过malloc申请的,所以删除后需要对其进行free
删除头结点的情况,如果PtrL本身就是空的,直接 return NULL
平均时间复杂度也是O(n/2)
4.广义表
如何表示二元多项式?
一种做法是仍将其看作是一个一元多项式,只不过这时的系数不像以前那样是个常量,这里的系数也是一个一元多项式。也就是说对于主体仍有链表表示,各个系数的一元多项式也是用链表表示。
这种结构的表叫做广义表
在广义表中,元素不仅可以像线性表中都是单元素,也可以是另一个广义表
这在其构造的时候就会有一个问题:对于广义表中某个元素的一个域,它有可能是一个不能分解的比如说指针域,有可能是另一个广义表。
union可以把不同类型的元素组合起来,为了区分不同的类型,往往会再设置一个标志域。通过Tag来说明接下来的那块空间到底是Data还是SubList
多重链表:链表中的结点可能同时属于多个链
广义表就是一个多重链表
双向链表(每一个结点包含两个指针,一个往前指,一个往后指)的两条链表是同一个链表,不叫多重链表
例(十字链表):对于一个大学的选课系统,选课结果用一个数组表示,行代表课程,列代表学生。这种情况下0很多,是稀疏矩阵,造成大量的空间浪费
因此可以采用一种典型的多重链表——十字链表来存储稀疏矩阵
整个链表有两种类型,Head和Term。Head是行和列的头部,只有指针域。Term是原稀疏矩阵中的非零项,其数据域包含行坐标、列坐标和数值。特别的,左上角的那个Term是整个十字链表表示稀疏矩阵 的入口结点,4、5、7分别代表这个稀疏矩阵总共有4行5列,非零项个数有7项,并且这个Term的指针指向所有列的头结点和尾结点。
每一行每一列都设计成一个循环链表(尾部指针指向头部),每个节点属于某一行,也属于某一列,类似十字,因此叫十字链表。
Head和Term是两种类型的结点。但是它们有一个共性是都有两个指针:行方向(Right)和列方向(Down),只有中间的那个域不同,Head是指针域,Term是数据域。因此可以使用union将他们串起来。
5.堆栈
堆栈是一种线性结构,也是一种特殊的线性表
算术表达式的运算符具有优先级,用计算机求解时需考虑
下面的中缀表达式和后缀表达式的例子是等价的。对于计算机而言,后缀表达式的计算要比中缀表达式更容易。因为中缀表达式还要考虑这个数值是否可以被这个运算符操作,还要往后面看才知道,而后缀表达式可以直接进行计算。
后缀表达式的计算:042×+ 是4和2先乘,得到08+,然后0个8再相加。每次都是运算符前两位的数值做计算。从左往右扫描,遇到运算数则先记住,遇到运算符号的时候则将最近记住的两个运算数拿来计算,计算的结果仍然记住。
因此对于运算数的存储,需要有一种数据结构满足:先放进去的后拿出来,后放进去的先拿出来。这样的算法的时间复杂度是O(N)
堆栈的抽象数据类型(数据结构)描述
堆栈(Stack)是具有一定操作约束的线性表,只在一端(Top)做插入、删除
后入先出:LIFO
数据对象集:一个有0个或多个元素的有穷线性表
最重要的操作是Push(入栈)和Pop(出栈),做这两个操作的同时往往需要判别堆栈有没有满和空
堆栈的顺序存储实现
// 数组定义时没有使用malloc借用内存,所以出栈后不需要free空间。但是链表需要。
栈的顺序存储结构通常由一个一维数组和一个记录栈顶元素位置的变量组成。这个Top是一个整型变量,不是一个地址,而是代表栈顶元素在数组中的下标。
用数组表示stack的时候,Top=-1代表堆栈为空
++(Ptrs->Top)和(Ptrs->Top)++都会使(Ptrs->Top)的值加1,但是++(Ptrs->Top)返回的是加一后的值,后者是返回原来的值
入栈:需考虑stack是否已满
出栈:需考虑栈是否已空
用一个数组实现两个堆栈
方法:使这两个stack分别从数组的两头开始向中间生长,当两个栈的top指针相遇时,表示两个stack都满了。当top1 = -1 , top2 = MaxSize时,表示栈空。
用Tag表示是第一个堆栈还是第二个堆栈
DStack结构中有三个分量:一个数组,两个指针Top1 、Top2
插入(Push)和删除(Pop)
堆栈的链式存储实现
用链表来实现堆栈时,一定要用链表头部 作为Top指针。因为这样对stack的插入和删除都比较方便
建立空栈 :创建一个新的堆栈相当于创建一个堆栈的头结点,这个头结点不代表任何一个元素,将来有结点要插入就是插在这个结点的后面。堆栈的第二个节点开始才是真正的元素,第一个结点仅仅作为一个头结点使用。
判断堆栈S是否为空:S 是这个堆栈的地址也是其头结点的指针,如果 S-> NULL,则堆栈 S为空。
插入(Push)操作:在第一个结点以后插入
删除(Pop)操作:需要考虑堆栈是否为空,不需要考虑是否已满。因为数组大小是固定的,所以需要判断是否已满,但是链表不是。删除操作就是把第二个结点删除。
堆栈的应用:表达式求值
如何将中缀表达式转换为后缀表达式?
因为运算数相对顺序不变,所以碰到运算数直接输出
但运算符号顺序发生改变,所以碰到一个运算符时先存储起来,再碰到一个运算符时与目前已存储运算符的最后一个相比较,若当前运算符优先级比存储的最后一位运算符的优先级低,则已存储的最后一位运算符可以取出来输出,当前的运算符继续与栈内的最后一位比较,若当前的运算符优先级大,则进入栈内作为最后一位。否则继续。表达式内容没了之后,将栈内的元素依次输出。 // 相同优先级的符号碰上的时候,左边的优先
但有括号怎么办?将括号也当作一个运算符。左括号:进入堆栈之前,它的优先级最高,但是一旦被放进堆栈后,我们认为它的优先级最低。右括号:碰到右括号的时候,我们认为括号内的内容已经处理完了,所以将堆栈内的内容一个一个抛出来,直到碰到左括号为止。
堆栈除了求表达式外,还有其他很多用处
6.队列
队列
具有一定操作约束的线性表,插入(入队,AddQ)和删除(出队,DeleteQ)操作只能在一端插入,在另一端删除。
先进先出:FIFO
队列的抽象数据类型描述
队列的顺序存储实现
由一个一维数组和一个记录队列头元素位置的变量front,以及一个记录队列尾元素位置的变量rear组成。front、rear实际上是数组的下标
例子:假如说有一些工作需要按到来的顺序进行处理,那么对这些工作的处理就形成了一个队列(FIFO)。最开始Front、Rear都是指向-1,当加入一个元素时,Rear加1,当删除一个元素时Front加1。始终保持Rear指向最后一个元素,Front指向第一个元素的前一个位置。但是当Rear指向数组的最后一个位置时,再往里加元素就加不进去了,如何处理?
当Front == Rear 时,代表这个队列是空的。
顺环队列:将数组看作是一个环形,可以循环往里增加元素
但是有一个矛盾:目前一个队列中元素的状态有n+1种:空,1个元素、、、n个元素(满),而我们判别这种状态是根据front和rear的差距,而这种差距只有n种状态:0,1,2,,n-1。这显然是不可能的。
队列空和队列满的情况下,front和rear的距离都是0,无法判断
如何实现5的下一个位置是0呢?使用求余函数。(5+1)%6==0;(3+1)%6==4;
用数组来实现时,删除操作不需要要free()
一般来讲,我们会选取下面的第二种方案,不让一个数组放满,当只剩一个空间的时候,就认为这个队列已经满了。
队列的链式存储实现
可以用一个单链表实现。插入和删除操作分别在链表的两头进行。链表的末尾实施插入操作可以,做删除操作麻烦。所以只能前面做front,后面做rear
不带头结点的链式队列 出队操作:首先判断队列是否为空(PtrQ->是否为NULL),再判断队列是否只有一个元素(PtrQ->front与PtrQ->rear是否相等),若是的话,则删除后队列置为空;
7.多项式加减算法
多项式加法运算
基本思路
具体实现:定义一个结构,包含一个系数,一个指数,还有一个指向下一个结点的结构指针。
当P1的指数大的时候,将P1当前项存入结果多项式,并使P1指向下一项,P2不变。再比较P1和P2
代码实现:对于结果多项式,需要设置front、rear分别指向它的表头和表尾。并且,为了方便实现,申请了一块临时的结点指针,让front、rear都指向它。
compare()函数:如果第一个参数的值大,返回1,第二个大返回-1,两个值相等则返回0.
Attach函数将第一个、第二个参数作为拷贝的系数和指数形成一个新的项赋值给 *rear。
P1 = P1 -> link :P1向后挪一位
8.一元多项式的乘积与和(例题看的不是很懂)
两个一元多项式的乘积
输入第一个是有多少项,而且后面的每一对表示一项的系数和指数
多项式表示:对于这种比较简单的题,用动态数组比较合适。但是为了更好的理解,下面用链表来实现。每个节点的数据域包含两个信息,系数和指数。还有一个指针域指向下一个结点
程序框架及多项式读入:P1, P2, PP, PS 都是结点指针。先读取第一个数 scanf,代表几轮循环。
两个多项式的相加:
两个多项式的相乘:下面以逐项插入的方法来实现。
将多项式输出:
第三章:树&二叉树
1.引子
层次关系是树的一种特征。数据管理中三个典型的操作:插入、删除、查找。查找问题:静态查找和动态查找
静态查找是指该集合不会改变。动态查找是指该集合会发生变化。
静态查找
顺序查找
哨兵:在数组的边界或最后设一个值,在每次查找中不需要判断下标是否达到边界,只需要当碰到这个值时就退出来循环即可。将下标返回,如果下标为0则说明没找到,如果等于0则找到了。
但是时间复杂度为O(n),当数据量很大的时候,效率很低
二分查找
先决条件:①所有的元素是连续存放(数组,放链表里面不行);②所有的元素必须是有序存放(小到大,或大到小)
每次找两个边界的中间值,与所要找的值进行判断
下面第一个例子是成功找到,第二个例子是没找到
设计函数时,需要有两个参数:结构指针(包含一个数组分量和一个长度分量),和元素K。如果元素K小于中间元素mid时,调整右边界为mid-1;如果元素K大于中间元素mid时,调整左边界为mid+1;循环条件为左边界小于等于右边界。
二分查找的启示
(二分查找)判定树:使用二分查找找到每个元素的次数为该元素所在的层数
n个结点的判定树的深度为 log_{2}^{n}+1
ASL(平均查找次数):如果所找元素在判定树内,ASL = 3
由于我们在查找之前对数组中的元素进行了有序化的组织(二分查找),使得查找过程按照固定的顺序来进行,这个顺序就形成了上面的这种类似于树的结构。 那反过来讲,如果不将元素放在数组内,而是直接将数据放在上面的这样层次化的结构中来存储,是不是也可以达到二分查找的效果?这就是后面要讲的查找树,以树这样的层次结构来存储数据,使得查找更加的方便。
查找树和二分查找的效率相当,都是logn。但是查找树中插入和删除结点,比在二分查找数组中方便的多。因此以查找树的结构来存储可以很好的结构查找中的第二大类问题:动态查找。
2.树
树的定义
注意:树没有回路,它是无回路的连通图。是边数最多的无回路图,边数最少的连通图。
除了根节点外,每个结点有且仅有一个父节点
一颗N个结点的树有N-1条边。(每个节点都有一个往上的边,根节点没有)
结点的度:结点的子树个数。树的度:树所有节点中最大的度数
树的表示
数组实现:只能判断节点的顺序,无法得知一个节点的多个儿子和祖先节点等。所以一般不使用数组来实现。
链表实现:可以实现,但是每个结点的子节点个数不一样,所以导致链表中每个结点的指针域不同,实现起来还是比较麻烦。
为了避免麻烦,如果将所有结点的结构设置相同,比如说将每个结点的指针域设为3个,那么整个链表的指针域有3n个,但是实际上n个结点只需要n-1个指针,这就会导致有2n+1个指针域是空的,造成空间的浪费。
儿子-兄弟表示法(链表)
树上的每个结点的结构是统一的,每个结点的包含一个数据域和两个指针域,一个指向第一个子节点,另一个指针指向下一个兄弟节点。
将上面的这个树旋转45°,这样的树结构叫做二叉树,每个结点有两个指针,一个指向左边,一个指向右边。每个节点最多有两个儿子。度为2的树。 二叉树是本节课最重要的一种树结构。
一般的数据我们都可以用儿子兄弟表示法,把它用二叉树链表的结构来实现
3.二叉树
二叉树的定义
二叉树是一种度为2的树,与一般度为2的树的区别是二叉树有左右之分。
几种特殊的二叉树
斜二叉树:只有左儿子或只有右儿子。实际就是一个线性结构的链表
完美二叉树(满二叉树):除了叶节点,每个节点都有两个儿子。并且叶节点都在同一层。
完全二叉树:对完美二叉树和完全二叉树进行编号,完全二叉树中每个编号的位置与其在完美二叉树中的位置相同。右下角的那棵树不是完全二叉树。
二叉树的几个重要性质
二叉树的第i层的最大节点数为:2^{i-1}, i>=1。
深度为k的二叉树,最大节点数总数为:2^{k}-1
n0(叶节点总数) = n2(度为2的结点总数) +1
边的总数有两种表示:n0 + n1 +n2 -1;0n0 + 1*n1 + 2*n2;两式相等
二叉树的抽象数据类型定义
一个有穷的节点集合。最重要的操作是 遍历。
二叉树的存储结构
// 一般需要考虑两点:能否用数组实现;能否用链表实现;
顺序存储结构(数组)
有一种二叉树非常适合用数组实现:完全二叉树
把一颗树放在数组里面是比较简单的,关键是要看放进去之后找出他们之间的关系容易不容易
每个非根结点的父节点是:i/2
每个结点的左子结点是2i,右子节点是2i+1。前提是该元素存在
一般二叉树也可以采用这种结构,就是用空元素补齐。但会造成空间浪费。
链表存储
4.二叉树的遍历
// 下面的例子主要是以二叉树的链式存储结构来实现的
// 先、中、后的不同是根据访问根节点这一过程的位置所起的:递归的方式都一样,只是访问根节点这个过程在三个步骤中的位置不同。
先序遍历
过程:访问根节点——按先序遍历的原则访问左子树——按先序遍历的原则访问右子树。实际上就是一个递归的过程。
中序遍历
过程:按中序遍历的原则访问左子树——访问根节点——按中序遍历的原则访问右子树。
后序遍历
过程:按后序遍历的原则访问左子树——按后序遍历的原则访问右子树——访问根节点。
总结
先序、中序和后序遍历过程中,经过所有结点的路线都是一致的,只是打印(访问)各结点的时机不同。
对于每个度为0的结点(叶节点),在访问过程中都只经历1次。对于每个度为1的结点,在这个路线中都会被经历2次。对于每个度为2的结点,在这个路线中都会被经历三次。三种遍历方式的区别就在于在这个路线中,度为2结点的在被经历的这三次中,那一次被打印(访问)的区别。
5.二叉树的非递归遍历
// 前面三种遍历方式都是用递归来实现的,递归根本的实现方法还使用堆栈,那能否直接用堆栈实现
中序遍历非递归(堆栈)遍历算法
先序遍历非递归遍历算法
三种递归的路线都是一样的,区别就在于在第几次碰见的时刻打印出来。因此找到碰到结点的次序,然后修改 printf() 函数即可。
6. 层序遍历
二叉树遍历的本质:如何把一个二维结构,变成一个一维的线性序列的过程。不同的遍历方式将产生不同的遍历序列
二叉树遍历的核心问题:二维结构的线性化
访问左、右儿子的时候,至少保存自己这个结点或右、左儿子结点,否则以后找不到。
关键的就是如何保存这些结点:以中序遍历为例,使用堆栈实现,保存的本身结点;使用队列实现,保存的是右儿子结点。
使用队列来实现层序遍历
7.几个例子
// 对几种遍历的代码进行改造即可
输出二叉树中的叶子节点:
// 对任何一种遍历代码进行改进即可
求二叉树的高度:
// 左右子树最大高度加一即可,所以需先知道左右子树高度,因此改造后序遍历
PostOrderGetHeight()函数是求树高度的函数,递归调用它
二元运算表达式树及其遍历
叶节点是运算数,非叶节点是运算符号
三种遍历方式可以得到三种不同的访问结果
但是中缀表达式会受到运算符优先级的影响,可能不准确。可以通过加括号的方式解决这样的问题:当输出左子树的时候先输出一个左括号,结束时再输出一个右括号
由两种遍历序列确定二叉树:能否通过三种遍历中的任意两种遍历序列,来确定唯一确定的一颗二叉树呢?
必须要有中序遍历才行
先序+后序 不可以 ,比如下面这个例子
树的同构
给定两棵树T1和T2,如果T1可以通过若干次左右孩子互换就变成T2,则我们称两棵树是“同构”的。
输入规则:第一个是该节点的信息(带编号),第二个是左孩子编号,第二个是右孩子的编号。输入的顺序不做要求。0只是表示该节点的编号,不代表是根节点。
用结构数组表示二叉树:
拥有链表的灵活性,并且在数组中存储
注意:Left 和 Right 不是指针
判断根节点方法:不出现在左右儿子上的编号,就是根节点
程序框架搭建
建树
设计判断是否同构的函数