【数据结构】树

树的基本概念

树是n(n>=0)个有限数据元素的集合。

特点:

1、树的根结点没有前驱结点,除根结点之外的所有结点有且只有一个前驱结点。

2、树中所有结点可以有0或多个后继结点。

基本术语:

结点的度:结点的分支数

终端结点(叶子):度为0的结点

结点的层次:根结点层次为1,根结点子树的根为第2层,以此类推

树的度:树的所有结点度的最大值

树的深度:树的所有结点层次的最大值

有序树、无序树:树的每棵子树从左往右的排列有一定顺序,不得互换,为有序树,否则为无序树

森林:m(m>=0)棵互不相交的树的集合

二叉树

定义

1、每个结点最多有两颗子树

2、子树有左右之分

性质

1、在二叉树的第i层上最多有个结点(i>=1);

2、深度为k的二叉树最多有个结点(k>=1),深度为k的二叉树有个结点为满二叉树,

结点位置与对应满二叉树的结点一一对应为完全二叉树 (基于完全二叉树,增加一定的限制条件后即为***二叉堆***);

3、对于任一棵二叉树BT,如果度为0的结点个数为,度为2的结点个数为,则=+1;


假设树的结点数为n,度为0的结点个数为,度为1的结点个数为,度为2的结点个数为,则有,另外树的连线个数再加上根结点也是树的总结点数,而树的连线个数为,则有,综上得=+1。


4、具有n个结点的完全二叉树的深度为, 为不大于的最大整数;

5、对于有n个结点的完全二叉树中所有结点按照从上到下、从左到右顺序进行编号,则对其中任一结点i(1<=i<=n),都有:

  1. i=1时,该结点是这棵树的根,没有双亲,否则双亲结点编号为[i/2];
  2. 2*i>n时,该结点没有左孩子,否则其左孩子编号为2*i;
  3. 2*i+1>n时,该结点没有右孩子,否则其右孩子编号为2*i+1;

存储结构

1、顺序存储结构

  • 只适用于完全二叉树,按照每个结点的编号顺序存放结点内容
  • 空间利用率高,可以通过下标快速寻找孩子和双亲的位置

2、链式存储结构

  • 最常用
  • 二叉链式存储
  • 三叉链式存储

遍历

先序遍历

根结点->先序遍历根的左子树->先序遍历根的右子树

中序遍历

中序遍历根的左子树->根结点->中序遍历根的右子树

后序遍历

后序遍历根的左子树->后序遍历根的右子树->根结点

以下面的二叉树为例,它的先序遍历、中序遍历、后序遍历分别是:

6 8 11 12 3 2 4 1 7

12 11 8 3 6 4 1 2 7

12 11 3 8 1 4 7 2 6

仅通过先序遍历和后序遍历无法确定一棵树

二叉搜索树(BST)

1、非空左子树的所有键值小于其根结点的键值

2、非空右子树的所有键值大于其根结点的键值

3、左右子树都是二叉搜索树

二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1,2,3,4,5,6},构造二叉搜索树如图 1.1。依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为 O(n)。

二叉搜索树的查找效率取决于树的高度,当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高,因此引入下面的平衡二叉搜索树。

平衡二叉搜索树(AVL树,Balanced Binary Tree (BBT))

什么是平衡二叉树(AVL) - 知乎 (zhihu.com)

特点

1、非空左子树的所有键值小于其根结点的键值

2、非空右子树的所有键值大于其根结点的键值

3、左右子树都是二叉搜索树

4、任一结点的两个子树高度差(平衡因子BF)等于-1、0、1

结点结构:

typedef struct AVLNode *Tree;

typedef int ElementType;

struct AVLNode{

    int depth; //深度,这里计算每个结点的深度,通过深度的比较可得出是否平衡

    Tree parent; //该结点的父节点

    ElementType val; //结点值

    Tree lchild;

    Tree rchild;

    AVLNode(int val=0) {
        parent = NULL;
        depth = 0;
        lchild = rchild = NULL;
        this->val=val;
    }
};

插入

插入数据时,AVL树通过旋转最小失衡子树 来维持整棵树的平衡。在新插入的结点向上查找,以第一个平衡因子的绝对值 超过 1 的结点为根的子树称为最小失衡子树

左旋

(1)结点的右孩子替代此结点位置

(2)右孩子的左子树变为该结点的右子树

(3)结点本身变为右孩子的左子树

右旋

(1)结点的左孩子代表此结点

(2)结点的左孩子的右子树变为结点的左子树

(3)将此结点作为左孩子的右子树。

四种插入方式

  • 在结点左孩子的左子树上插入导致失衡(LL) ->对以结点为根的树执行右旋
  • 在结点右孩子的右子树上插入导致失衡(RR)->对以结点为根的树执行左旋
  • 在结点左孩子的右子树上插入导致失衡(LR)->先对结点的左子树执行左旋,再对以结点为根的树执行右旋
  • 在结点右孩子的左子树上插入导致失衡(RL)->先对结点的右子树执行右旋,再对以结点为根的树执行左旋

删除

(1)删除叶子结点->直接删除,然后依次向上调整为AVL树

(2)删除的结点只有左子树->将该结点的值替换为左孩子的值,然后删除左孩子结点【根据AVL树的特性,左孩子一定是叶子节点,转化为情况(1)】

(3)删除的结点只有右子树->将该结点的值替换为右孩子的值,然后删除右孩子结点【根据AVL树的特性,右孩子一定是叶子节点,转化为情况(1)】

(4)删除的结点既有左子树又有右子树->将该结点的值替换为中序遍历的前继结点或后继结点,然后删除前继结点或后继结点【根据中序遍历的特性,前继结点或后继节点会是(1)(2)(3)中的其中一种】

总结:对非叶子结点的删除最后都会转化成对叶子节点的删除。

AVL树的查找效率很高,但是由于插入、删除时需要通过旋转来维持平衡度,所以创建一个AVL树的成本其实不小,由此诞生了平衡度不那么严格的红黑树。

红黑树(Red Black Tree,RBT)

Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的

内容引自:

什么是红黑树,一篇文章解决所有疑惑~~ - 知乎 (zhihu.com)

特点

红黑树(Red Black Tree)是一颗自平衡(self-balancing)的二叉排序树(BST),树上的每一个结点都遵循下面的规则(特别提醒,这里的自平衡和平衡二叉树AVL的高度平衡有别):

  1. 每一个结点都有一个颜色,要么为红色,要么为黑色;
  2. 树的根结点、叶子节点(外部节点,空节点)都是**黑色,**这里的叶子节点指的是最底层的空节点(外部节点),下图中的那些null节点才是叶子节点,null节点的父节点在红黑树里不将其看作叶子节点
  3. 树中不存在两个相邻的红色结点(即红色结点的父结点和孩子结点均不能是红色);
  4. 任意一个结点 (包括根结点)到其任何后代 NULL 结点(默认是黑色的)的每条路径都具有相同数量的黑色结点。

黑高(Black Height)

在一颗红黑树中,从某个结点 x 出发(不包含该结点)到达一个叶结点的任意一条简单路径上包含的黑色结点的数目称为 黑高 ,记为 bh(x)

红黑树的黑高则为其根结点的黑高 。根据红黑树的性质 3、4,一颗红黑树的黑高bh >= h/2

引理:一棵有n个内部结点的红黑树的高度 h <= 2lg(n+1)。


现有如下图所示的一颗红黑树,将其中红色结点合入其黑色父结点可得到下右图

合并后的红黑树变成了一个2-3-4树,每个结点拥有2、3或4个子结点,假设该树有n+1个叶子结点,则有

对于具有N个结点的红黑树而言则有,根据可以得到,所以红黑树的插入、删除、查找的时间复杂度都是O(logn).


当涉及到频繁的插入和删除操作,优先选择红黑树;当涉及的插入和删除不频繁,而查找操作相对来说更频繁时,优先选择AVL树。

B树 (平衡多路查找树,B-树)

内容引自:

B树和B+树_b+树 顺序写-CSDN博客

B树和AVL树的区别是B树属于多叉树,一个结点的查找路径不止左右两个,而是多个。数据库索引技术里大量使用B树和B+树。

B树的阶数:M阶表示一个B树的结最多有M个查找路径。M=2是二叉树。

特点(以M阶B树为例)

1、每个结点的值(索引)按递增次序排列;

2、根结点的子结点个数为[2, M];

3、除根结点外的非叶子结点的子结点个数为[Math.ceil(M/2), M],Math.ceil()为向上取整;

4、每个非叶子结点的值(索引)个数=子结点个数-1。最小为Math.ceil(M/2)-1,最大为M-1;

5、B树的所有叶子结点位于同一层。

如下图一个3阶B树:

可以看出:

  1. 除根结点外,所有非叶子结点都至少有M/2=1.5取整=2个结点;
  2. 每个结点中的索引值都是从小到大排序的;
  3. 所有的叶子结点都在同一层;

查找

以查找结点5为例:

(1)第一次读IO,把结点9读入内存,再与目标数5比较,5小于9,往9的左边走;

(2)第二次读IO,把结点2、6读入内存,然后比较结点中的2和6与目标值5,5大于2小于6,所以往中间路径走;

(3)第三次读IO,把结点3、5读入内存,然后发现结点中有5,因此找到目标值。

1、在数据库查询中,以树存储数据,树有多少层,就意味着要读取多少次磁盘IO,而读取IO是很费时间的操作。当数据量非常大时,用AVL树存的话,树高肯定很高,那么读取IO的次数也会很多,而B树的出现就是为了压缩树的高度。B树的一个结点装多个值,对结点的处理在内存中,速度就快了很多;

2、B树的每一个结点都包含key(索引值)和value(对应数据),因此离根结点越近的元素查找起来会更快(相比于B+树)。

插入(结点分裂)

以5阶B树为例,在空树中插入39:

继续插入22,41,97:

此时,再插入一个53,超过了允许的最大索引个数4,以中心元素41分裂:

继续插入13,21:

此时再插入一个40,其中一个结点中有13,21,22,39,40五个元素,超过了4,以中心元素22进行分裂,分裂出的22进位到上一层的结点中:

继续插入30,27:

此时再插入一个33,其中一个结点中会有27,30,33,39,40五个元素,超过了4,以中心元素33进行分裂,分裂出的33进位到上一层结点中:

继续插入36,35:

此时插入一个34,会有一个结点中有34,35,36,39,40五个元素,以中心元素36进行分裂,分裂出的36进位到上一层结点中:

继续插入24,29:

此时插入一个26,其中一个结点中有24,26,27,29,30五个元素,以中心元素27进行分裂,分裂出的27进位到上一层结点中时,会导致上一层结点拥有22,27,33,36,41五个元素,继续以中心元素33进行分裂:

删除

原始状态:

删除21:

删除后的结点索引数仍然大于等于2(Math.ceil(5/2)-1=2),因此删除结束。

继续删除27,27是非叶子结点,所以删除27的话,要用27的后继28替代它,删除原来的28,

但是28删除后,它所在的结点索引个数只剩下1个,(此时可以向这个结点的右兄弟借一个索引值31,但不是直接拿,而是将父结点的索引30下移到本结点,然后将31上移到父结点,但这个时候右兄弟的索引个数又只剩下1个,需要父结点下沉31,并和本结点合并为一个结点:

另一种情况,此时可以向这个结点的左兄弟借一个索引值26,将父结点的索引28下移到本结点,然后将26上移到父结点:

继续删除32,删除32后结点只剩下一个索引值,并且左右兄弟结点都只有2个索引值,不能借,只能让父结点下移索引值30,并跟左兄弟合并成一个结点:

继续删除40,删除40后结点只剩下一个索引值,并且左右兄弟都只有2个索引值,只能让父结点下移索引值36,并跟左兄弟合并成一个结点,但是此时父结点只有一个索引值41,它的左兄弟也只有2个索引值,还需要它的父结点下移索引值,并跟它的左兄弟合并成一个结点:

B+树

B+树基于B树被提出,下图是一颗4阶B+树:

B树和B+树的区别:

  1. B+内有两种结点,一种是索引结点,一种是叶子结点;
  2. B+树的索引结点不会保存记录,只用于索引,所有数据保存在B+树的叶子结点中,而B树所有结点都会保存数据;
  3. B+树的叶子结点都会被连成一条链表,叶子本身按索引值从小到大排序,方便范围查找数据;
  4. B树的所有索引值不会重复,B+树非叶子结点的索引值最后一定会出现在叶子结点中。

为什么有B+树?

解释这个问题要从B树的优点和缺点说起:

B树的优点:

B树的每个结点都有key(索引值)和value(对应数据),因此方位离根结点近的元素查找起来会更快速(相对于B+树);

B树的缺点:

不利于范围查找(区间查找),如果要找0~100的索引值,B树需要多次从根结点逐个查找,而B+树由于叶子结点都有链表,且链表中按照索引值从小到大排列,可以直接通过遍历链表实现范围查找。

哈夫曼树

数据结构------哈夫曼树(Huffman Tree) - 知乎 (zhihu.com)

特点

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

基本术语

  • 路径:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路
  • 路径长度:通路中分支的数目,若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1
  • 结点的权:将树中结点赋给一个有着某种含义的数值
  • 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
  • 树的带权路径长度:所有叶子结点的带权路径长度之和,记为WPL。如上图:数的带权路径长度为:WPL = (2+3) * 3 + 4 * 2 + 6 * 1 = 29

构造

假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、...、wn,则哈夫曼树的构造规则为:

(1) 将w1、w2、...,wn看成是有n 棵树的森林(每棵树仅有一个结点);

(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;

(3)从森林中删除选取的两棵树,并将新树加入森林;

(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。

例如:对 2,3,4,6 这四个数进行构造:

相关推荐
努力学习编程的伍大侠几秒前
基础排序算法
数据结构·c++·算法
XiaoLeisj28 分钟前
【递归,搜索与回溯算法 & 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)
数据结构·算法·leetcode·决策树·深度优先·剪枝
Jasmine_llq1 小时前
《 火星人 》
算法·青少年编程·c#
闻缺陷则喜何志丹1 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
Lenyiin1 小时前
01.02、判定是否互为字符重排
算法·leetcode
鸽鸽程序猿2 小时前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd2 小时前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo6172 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v2 小时前
leetCode43.字符串相乘
java·数据结构·算法