一、树的介绍
树型结构是一种非线性的数据结构,它具有一个称为根节点(root node)的特殊节点,以及一些称为子节点(child nodes)的节点。每个节点可以有零个或多个子节点,但只能有一个父节点(parent node),除了根节点没有父节点。在树型结构中,节点之间的连接关系表示了它们之间的层次关系。
树型结构常用于表示具有层次关系的数据,例如文件系统、组织结构、目录结构等。它提供了一种便捷的方式来组织和访问数据。
树型结构的应用非常广泛,例如在计算机科学中,树型结构被用于实现搜索算法(如二叉搜索树)、存储和检索数据(如B树、堆)、表达抽象语法树等。在现实生活中,树型结构也有很多应用,比如家谱、图书分类、产品组织关系等。
二、树的定义和基本术语
树:是n个结点的有限集,当0==n时称为空树,我们不讨论空树。
根结点:树的最顶层的结点,一棵树有且仅有一个。
子树:一棵树除根结点外,剩余的是若干个互不相交的有限集,每一个集合本身又是棵树,称称为根的子树。
结点的度:树的结点包含一个数据元素及若干个指向其子树的分支,结点拥有的子树数量称为结点的度。
叶子结点:结构的度为0,被称为叶子结点或终端结点。
分支结点:结构的度不为0,被称为分支结点或非终端结点,也被称为内部结点。
树的度:是指树内各结点度的最大值。
密度:指的是一棵树中,所有结点的总数。
孩子、双亲、兄弟、祖先、子孙:结点的子树称为该结点的孩子,而该结点是孩子结点的双亲,拥有共同双亲的结点互为兄弟,从双亲结点往上,直到根结点都称为孩子结点的祖先结点,以某结点为根的子树中的任一结点都被称为该结点的子孙。
层数、深度、高度:从根结点开始定义,根为第一层、根的孩子为第二层依次类推,树中结点的最大层数被称为树的深度或高度,双亲在同一层的结点互为堂兄弟。
有序树和无序树:将树中结点的各子树看成从左到右是有序次,即不能交换(顺序有意义,表达一些含义),则称该树为有序树,否则称为无序树。
森林:若干个棵互不相交的树的集合称为森林,对树中每个结点而言,其子树集合就是森林。
就逻辑结构而言,任何一棵树都是一个二元组 Tree = (root,F),其中root是数据元素,称做树的根结点,F是若干棵子树构成的森林。
普通树的存储(了解)
树可以顺序存储、链式存储,也可以混合存储,由于存储的信息不同,有以下表示方式:
-
双亲表示法
-
顺序存储,记录双亲的下标,方便找双亲,不方便找孩子
-
-
孩子表示法
-
顺序存储:用数组存储孩子下标,浪费空间
-
链式存储:用链表存储孩子下标,节约空间
-
方便找孩子,不方便找双亲
-
-
兄弟表示法:
-
记录第一个孩子节点,链式存储所有兄弟节点
-
方便找兄弟,不方便找双亲
-
总结:普通树不常用、一般转换成二叉树使用。
三、二叉树的定义和性质
二叉树:
是一种特殊的树型结构,也就是每个结点最多有两棵子树(二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,顺序不能颠倒。
满二叉树:
若一棵树的层数为k,它总结点数是2^k-1,则这棵树就是满二叉树。
完全二叉树:
若一棵树的层数为k,它前k-1层的总结点数是2^(k-1)-1,第k层的结点按照从左往右的顺序排列,则这棵树就是完全二叉树。
二叉树的重要性质:
性质1:
在二叉树的第i层上,最多有2^(i-1)个结点。
性质2:
深夜为k的二叉树,最多有2^k-1个节点。
性质3:
对于任何一棵二叉树,如果叶子结点的数量为n0,度为2结点的数量为n2,则n0=n2+1;
总数n 叶子节点n0 求度1的节点数 n1 = n- n0-(n0-1) 度2节点n2 = n0-1
性质4:
具有n个结点的完全二叉树的高度为(log2n)+1。
性质5:
有一个n个结点的完全二叉树,结点按照从上到下从左到右的顺序排序为1~n。
1、i > 1时,i/2就是它的双亲结点。
2、i*2是i的左子树,当i*2>n时,则i没有左子树。
3、2*i+1是i的右子树,2*i+1>n时,则i没有右子树。
四、树与二叉树的转换
树与二叉树之间的转换有两种常见的操作:将一棵树转换为二叉树(树的编码),以及将一个二叉树还原为原始树(解码)。
将树转换为二叉树的一种常见方法是使用所谓的"左孩子-右兄弟"表示方法,也称为"二叉树表示法"。该方法通过对树的每个节点进行转换来构建二叉树:
-
对于树的每个节点,将它的第一个子节点作为二叉树的左孩子。
-
将该节点的其他子节点依次链接为左孩子的右兄弟节点。
-
对于树的每个节点进行递归处理,直到将整个树转换为二叉树。
这种转换方法可以保留原始树的结构和层次关系,在二叉树中的左子树仍然表示该节点的子节点,右子树表示同一层级的其他兄弟节点。
将二叉树还原为原始树的操作称为解码。解码过程与转换过程相反:
-
对于二叉树的每个节点,将它的左孩子作为解码后的节点的第一个子节点。
-
将该节点的右孩子链接为解码后的节点的其他兄弟节点。
-
对于二叉树的每个节点进行递归处理,直到将整个二叉树解码为原始树。
需要注意的是,树与二叉树之间的转换并不是唯一的,还可以使用其他方法进行编码和解码。这两种方法只是最常见的转换方式之一,具体的转换方式可能会根据具体需求和使用场景有所不同。
注意:参看树与二叉树的转换图解.pdf
五、二叉树的遍历
前序遍历:
1、判断二叉树是否为空,若二叉树为空,则不操作。
2、访问根结点
3、前序遍历左子树
4、前序遍历右子树
中序遍历:
1、判断二叉树是否为空,若二叉树为空,则不操作。
2、中序遍历左子树
3、访问根结点
4、中序遍历右子树
后序遍历:
1、判断二叉树是否为空,若二叉树为空,则不操作。
2、后序遍历左子树
3、后序遍历右子树
4、访问根结点
注意:前中后由根节点决定,并且左右子树的次序不会改变。
注意:根据 前序+中序 或者 中序+后序 还原一棵树,前序+后序无法还原(无法判断出根节点的左右子树)
层序遍历:
按照从上到下、从左到右的顺序遍历二叉树,需要与队列结构配合,普通的顺序队列即可,不需要链式队列或循环队列。
六、二叉树的顺序表示与实现
前提:由于顺序存储需要根据元素的相对位置确定关系,所有先把二叉树补成完全二叉树,之前空的点可以使用特殊的值表示,并把完全二叉树的层序遍历结果存储到数组中,只有补成了完全二叉树,才能根据性质5的公式对二叉树进行相关操作。
注意:性质5的公式是按照结点的序号设计的,在使用时要注意数组下标与序号的转换。
七、二叉树的链式表示与实现
有两种创建链式二叉树的方式:
方式一:需要给每个度不为2的结点补充一些空白子结点,使得树中除了空白结点其它结点的度全为2,然后以前序的方式遍历二叉树,然后以遍历的结果创建二叉树。
方式二:不需要对二叉做任何改变,以前序+中序或中序+后序的遍历结果创建二叉树。
八、二叉搜索树的表示与实现
二叉搜索树(Binary Search Tree,简称BST)是一种特殊的二叉树,它具有以下特点:
-
对于任意节点,其左子树中的值都小于该节点的值,右子树中的值都大于该节点的值。
-
左子树和右子树也都是二叉搜索树。
由于以上特点,二叉搜索树可以快速地进行插入、删除和查找等操作。
插入操作: 当向二叉搜索树中插入一个新的节点时,需要从根节点开始比较节点的值,并根据比较结果选择左子树或右子树继续进行比较,直到找到一个空的位置插入新节点。
删除操作: 删除节点时,首先需要找到待删除的节点。如果待删除的节点没有子节点,可以直接删除即可。如果待删除的节点有一个子节点,可以用子节点替换待删除的节点。如果待删除的节点有两个子节点,可以找到其右子树中的最小节点,即右子树中的最左节点,将其值复制到待删除节点,并删除该最小节点。
查找操作: 查找操作在二叉搜索树中非常高效。从根节点开始,若目标值等于当前节点的值,则找到了目标节点。若目标值小于当前节点的值,则继续在左子树中查找,若目标值大于当前节点的值,则继续在右子树中查找,直到找到目标节点或遍历到空节点。
二叉搜索树的缺点:
二叉搜索树的元素添加顺序会影响二叉搜索树的形状,二叉搜索树的形状会影响它的操作效率,在极端情况下,二叉搜索树可能会呈单枝状分布,使用速度接近单向链表,这种极端情况出现在的原因就添加的元素基本有序。
一、线索二叉树
-
规律:在有n个节点的链式二叉树中必定存在 n+1 个空指针
-
链式二叉树中有很多的空指针,可以让这些空指针指向前一个节点\后一个节点,从而在有序遍历(中序遍历)二叉树时,不需要使用递归而通过循环即可以完成,并且效率要比递归快得多
-
一定是搜索二叉树
线索二叉树的结构
typedef struct TreeNode { int data; // 数据域 struct TreeNode* left; bool lflag; // 左子树是否是线索 为真时,左子树是线索 指向前一个节点 struct TreeNode* right; bool rflag; // 右子树是否是线索 为真时,右子树是线索 指向下一个节点 }TreeNode;
构建线索二叉树
-
首先需要有一颗搜索二叉树,然后通过中序遍历并生成线索,通过检查右子树是否为空来决定是否生成线索,让右子树指向下一个节点。
-
当构成线索二叉树后 ,可以通过循环遍历的方式有序遍历二叉树,不需要中序递归遍历也可以
二、选择树(了解)
-
是一种完全二叉树,把待比较的数据存储在最后一层,根节点的值是左右子树中其中一个,是它们的最大值或最小值,选择树的功能是快速地找出最大值或最小值
三、堆
堆结构介绍
大顶堆(大根堆):根节点的值比左右子树都大,同时左右子树都满足该规则
小顶堆(小根堆):根节点的值比左右子树都小,同时左右子树都满足该规则
堆结构是一种特殊的完全二叉树,它与堆内存是两种概念
堆结构的根节点一定是整棵树中的最大值、最小值
堆结构如何存储:
首先堆结构是一种完全二叉树,并且需要在使用的时候频繁地找双亲节点进行比较,所以链式不好找双亲节点,因此堆结构非常适合用顺序存储,通过二叉树性质5来实现找双亲:
性质5:
有一个n个结点的完全二叉树,结点按照从上到下从左到右的顺序排序为1~n。
1、i > 1时,i/2就是它的双亲结点。
2、i*2是i的左子树,当i*2>n时,则i没有左子树。
3、2*i+1是i的右子树,2*i+1>n时,则i没有右子树
堆排序
-
借助堆结构,先把待排序数组先调整成大顶堆或者小顶堆结构,然后堆顶与末尾元素交换,从堆顶到末尾元素的前一个元素之间重新调整成堆结构,重复该步骤,最后当堆中只剩一个元素时,待排序数组就有序了。
-
可以循环实现、也可以递归实现
-
堆结构还是优先队列的底层逻辑
// 堆排序 循环实现 void heap_sort(int* arr,size_t len) { // 先把数组从下往上调成堆结构 for(int i=1; i<=len; i++) { // i j 是编号 int j = i; while(j > 1) { if(arr[j-1] > arr[j/2-1]) { SWAP(arr[j-1],arr[j/2-1]); j /= 2; continue; } break; } } // 在堆结构中有两个以上元素,需要进行堆排序 while(len > 1) { // 交换堆顶和末尾 SWAP(arr[0],arr[len-1]); // 删除末尾 len--; // 从堆顶编号1开始 自上而下重新判断调成堆结构 int i = 1; while(i-1 < len) { // 当右子树存在 左子树必定存在 if(i*2 < len) { // 右大于等于左 且比根大 交换根和右 if(arr[i*2] >= arr[i*2-1] && arr[i*2] > arr[i-1]) { SWAP(arr[i-1],arr[i*2]); // 更新编号 往右子树继续调整 i = i*2+1; } // 左比右大 且比根大 交换根和左 else if(arr[i*2-1] > arr[i*2] && arr[i*2-1] > arr[i-1]) { SWAP(arr[i-1],arr[i*2-1]); // 往左子树调整 i = i*2; } // 根是最大的 else break; } // 有左子树 没有右子树 else if(i*2-1 < len) { // 左比根大 交换 if(arr[i*2-1] > arr[i-1]) { SWAP(arr[i*2-1],arr[i-1]); } break; } // 没有左右 else break; } // 继续步骤1 直到个数剩下1个结束 } } // 从top下标到end下标 自顶向下调整成堆结构 void _heap_sort(int* arr,int top,int end) { if(top >= end) return; int max = top+1; // 先假设top最大 max最大值的编号 int l = max*2; // 左子树编号 int r = max*2+1; // 右子树编号 // 有左 且比最大大 更新最大节点的编号 if(l-1 <= end && arr[l-1] > arr[max-1]) { max = l; } // 有右 且比最大大 更新最大节点的编号 if(r-1 <= end && arr[r-1] > arr[max-1]) { max = r; } if(max != top+1) { // 最大值不是根 交换根与最大值 SWAP(arr[top],arr[max-1]); // 继续从max~end继续调整 _heap_sort(arr,max-1,end); } } // 递归堆排序 void heap_sort_recursion(int* arr,size_t len) { // 先自下而上调整成堆结构 for(int i=2; i<=len; i++) { int j=i; while(j > 1) { if(arr[j-1] > arr[j/2-1]) { SWAP(arr[j-1],arr[j/2-1]); j /= 2; } else break; } } // 堆顶与末尾交换 for(int i=len-1; i>0; i--) { SWAP(arr[0],arr[i]); // 交换结束后,重新从0~i-1调整 _heap_sort(arr,0,i-1); } }
平衡二叉树(AVL树)
-
前提一定是搜索二叉树,对于根节点的左右子树的高度差不能超过1,并且所有子树都要循序这个要求
-
如果一个搜索二叉树呈现或接近单支状,它的查找效率很低,很接近链表,因此如果能让它平衡时,查找效率最高
-
由于节点的位置要受到相互之间值的影响,并且在往平衡二叉树中添加节点或者删除节点前,二叉树本身是平衡的,所以只可能在最后操作的节点附近不满足平衡条件,因此需要在该过程中对该节点进行判断并调整。
-
因此一棵平衡二叉树因为添加操作导致不平衡的原因,总结就四种:
第一种: x y / \ / \ y t1 z x / \ / \ / \ z t2 t3 t4 t2 t1 / \ t3 t4 以y为轴 右旋转 第二种: x y / \ / \ t1 y x z / \ / \ / \ t2 z t1 t2 t3 t4 / \ t3 t4 以y为轴 左旋转 第三种: x x z / \ / \ / \ y t1 z t1 y x / \ / \ / \ / \ t2 z y t4 t2 t3 t4 t1 / \ / \ t3 t4 t2 t3 先以z为轴左旋转 再以z为轴右旋转 达到平衡 第四种: x x z / \ / \ / \ t1 y t1 z x y / \ / \ / \ / \ z t2 t3 y t1 t3 t4 t2 / \ / \ t3 t4 t4 t2 先以z为轴右旋转 再以z为轴左旋转 达到平衡
删除节点
-
待删除的节点是叶子节点,直接删除
-
待删除节点的左子树或者右子树为空,则使用非空节点替换
-
待删除节点左右子树非空,则根据左右子树的高度,选择高的一边子树,如果是左子树高,选择左子树中的最大节点赋值给待删除节点,然后再左子树中继续删除该最大节点,相当于继续处理情况1或情况2;如果是右子树高,则在右子树选择最小值节点继续同样处理。
-
删除后可能导致不平衡,需要重新调整平衡
平衡二叉树的优点:
避免了二叉搜索树呈现单支状,让其能以最佳的效率进行查找操作 O(log2n)
平衡二叉树的缺点:
在插入、删除操作时,为了达到平衡需要进行大量的左旋、右旋操作、计算高度,所以此时操作速度慢
因此AVL树适合在数据量大并且数据量比较稳定,没有太多的插入、删除操作,适合大量的查找操作。
红黑树
-
是一种自平衡的有序二叉树,不是根据树的高来调整平衡、而是树节点的颜色来调整
红黑树的特性: (1)每个节点或者是黑色,或者是红色。 (2)根节点是黑色。 (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!] (4)如果一个节点是红色的,则它的子节点必须是黑色的。 (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
红黑树的优点:
插入、删除操作的效率比AVL高
缺点:
没有AVL那么均匀,查找效率略低于AVL树,但是因为性质5决定了不至于太差,综合而言,综合性能优秀,所以应用场景很多
哈夫曼树
相关基础概念
路径长度:从一个节点到另一个节点之间的路径条数目
从根节点到第L层节点路径长度是L-1
树的路径长度: 从根节点出发到每个节点的路径长度之和
节点的权:如果给树中每个节点赋予一个某种含义的数值,该数值称为该节点的权值
节点的带权路径长度:从根节点到该节点的路径长度与该节点的权值的乘积
树的带权路径长度:所有叶子节点的带权路径长度之和 WPL
成绩: <60 60~69 70~79 80~89 90~100 等级 E D C B A 比例 5% 30% 40% 15% 10% 普通带权二叉树的WPL: WPL=1*5+2*30+3*40+4*15+4*10 = 285 哈夫曼树的WPL: WPL=40+2*30+3*15+4*5+4*10 = 205 WPL是评价一棵带权二叉树优劣重要标准
哈夫曼树:一定是一棵WPL最小的带权二叉树
构建哈夫曼树:
1、把n个带权节点先存储一个集合F中,每个节点的左右子树都为空
2、从F中选取根节点的权值最小的两个节点作为左右子树构成一棵新的二叉树,左小右大,这颗二叉树的根节点的权值等于两个子树权值之和
3、从F中删除刚刚取出来的两个节点,并把新形成的二叉树根节点放入F中
4、重复步骤2、3,直到F中只剩下一棵树,就是哈夫曼树
哈弗曼编码
目的:当年是为了解决远距离通信传输内容最优解问题
代发送文字:ABBCD EEAFD
方法1:转换成二进制 001 总共要发送30个二进制数
方法2:
1、根据字母出现频率,构建哈夫曼树
假设:A 28 B 5 C 7 D 18 E 30 F 12、
2、规定哈夫曼树中左分支为0,右分支为1,从根节点出发到叶子节点经过的路径分支组成的0和1的有序序列就是该叶子节点的哈弗曼编码
A 10 B 0110 C 0111 D 00 E11 F010
ABBCD EEAFD哈夫曼编码:100110011001110011111001000 总共27字符
作用:数据压缩、文件压缩也是一种解法