一)什么是树(tree)

树是n(n≥0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点:

  1. 有且仅有一个特定的称为根的节点。

  2. 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。

术语:根节点、子树、叶子节点、父节点、孩子节点、兄弟节点、树的高度

二)二叉树

二叉树(binary tree)是树的一种特殊形式。这种树的每个节点最多有2个孩子节点。注意,这里是最多有2个,也可能只有1个,或者没有孩子节点

二叉树的存储

1.链式存储

  • 存储数据的data变量

  • 指向左孩子的left指针

  • 指向右孩子的right指针

2.数组存储

使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来,这样可以更方便地在数组中定位二叉树的孩子节点和父节点:

父节点下标 parent,那么左孩子下标为 leftChild = 2×parent + 1;右孩子下标为 rightChild = 2×parent + 2

思考:什么二叉树适合用数组存储?

二叉树的遍历

线性结构(数组、链表)的遍历很简单,那么非线性结构树是如何遍历的呢?

主要分为:深度优先遍历(先序遍历、中序遍历、后序遍历)广度优先遍历(层序遍历)

深度优先遍历

先序(根)遍历

先序遍历输出顺序是根节点、左子树、右子树

public void preTraversal(TreeNode(parent,left,right,data) node) {if (null == node) {return;}// 输出节点值print(node.data);// 递归preTraversal(node.left);preTraversal(node.right);
}

中序(根)遍历

中序遍历输出顺序是左子树、根节点、右子树

public void middleTraversal(TreeNode(parent,left,right,data) node) {if (null == node) {return;}// 递归middleTraversal(node.left);// 输出节点值print(node.data);// 递归middleTraversal(node.right);
}

后序(根)遍历

后序遍历输出顺序是左子树、右子树、根节点

public void afterTraversal(TreeNode(parent,left,right,data) node) {    if (null == node) {return;}    // 递归    afterTraversal(node.left);    afterTraversal(node.right);    // 输出节点值    print(node.data);
}

广度优先遍历

层序遍历

由于二叉树同一层次的节点之间是没有直接关联的,所以层序遍历需要利用队列的方式来实现

  
public void levelTraversal(TreeNode(parent,left,right,data) root) {Queue<TreeNode> queue = new LinkedList<>();// 放入根节点queue.offer(root);while(!queue.isEmpty()) {// 出队TreedNode node = queue.pull();// 输出元素值print(node.data);if (node.left != null) {queue.offer(node.left);}if (node.right != null) {queue.offer(node.right);}}}

二叉树的应用

二叉树包含许多特殊的形式,每一种形式都有自己的作用,但是其最主要的应用还在于进行查找操作维持相对顺序

三)二叉查找树

二叉查找树在二叉树的基础上增加了以下几个条件:

  1. 如果左子树不为空,则左子树上所有节点的值均小于根节点的值

  2. 如果右子树不为空,则右子树上所有节点的值均大于根节点的值

  3. 左、右子树也都是二叉查找树

总结:左子树 < 根节点 < 右子树

如果要查找为4的节点:

  1. 访问根节点,4 < 6 ;

  2. 访问6的左孩子节点3,4 > 3;

  3. 访问3的右孩子节点4,4 = 4;

对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就是O(logn),和树的深度是一样。

二叉查找树存在的问题:

这里涉及到二叉树的自平衡,自平衡的方式有很多,比如平衡二叉树红黑树、树堆等。

四)平衡二叉树(AVL树)

平衡二叉树也称为AVL树,在二叉搜索树的基础上,平衡二叉树还需要满足如下条件:

  • 左右两个子树的高度差(平衡因子)的绝对值不超过1

  • 左右两个子树都是一棵平衡二叉树

平衡二叉树的目的是为了减少二叉查找树层次,提高查找速度。平衡二叉树是如何在插入的时候保持平衡呢?

自旋

树的自旋单旋转双旋转,分别作用于解决不同的情况。

LL情况(右旋或LL旋转)

在左子树的左节点上插入了新的数据,导致左右子树高度差绝对值 > 1;这种情况称为 左左(LL) 的情况,解决办法:

动图:

RR情况(左旋或RR旋转)

在右子树的右节点上插入了新的数据,导致左右子树高度差绝对值 > 1;这种情况称为 右右(RR) 的情况,解决办法:

动图:

LR情况(LR旋转-对左孩子RR旋,对根节点LL旋)

在左子树的右节点上插入了新的数据,导致左右子树高度差绝对值 > 1;这种情况称为 左右(LR) 的情况,解决办法:

RL情况(RL旋转-对右孩子LL旋,对根节点RR旋)

在右子树的左节点上插入了新的数据,导致左右子树高度差绝对值 > 1;这种情况称为 右左(RL) 的情况,解决办法:

实现代码:

    // 节点对象AvlNode root;
​public void insert(int data){root = insert(root,data);}// 插入节点public AvlNode insert(AvlNode n, int data) {if (n == null) {n = new AvlNode(data);}// 大于插入右子树if (data > n.data) {n.right = insert(n.right, data);
​// 检查并调整平衡,由于是右边插入,右子树的高度肯定大于等于左子树的高度if (height(n.right) - height(n.left) == 2) {// 判断data是插入点的左孩子还是右孩子if (data < n.right.data) {// RL情况,RL旋转n = doubleRightLeftRotate(n);} else {// RR情况,RR旋转n = singleRightRotate(n);}}// 小于插入左子树} else if (data < n.data) {n.left = insert(n.left, data);
​// 检查并调整平衡,由于是左边插入,左子树的高度肯定大于等于右子树的高度if (height(n.left) - height(n.right) == 2) {// 判断data是插入点的左孩子还是右孩子if (data < n.left.data) {// LL情况,LL旋转n = singleLeftRotate(n);} else {// LR情况,LR旋转n = doubleLeftRightRotate(n);}}}// 重新计算高度n.height = height(n);return n;}
​// 计算高度public int height(AvlNode n) {int left = n.left == null ? 0 : height(n.left);int right = n.right == null ? 0 : height(n.right);return Math.max(left,right) + 1;}
​// LL旋转private AvlNode singleLeftRotate(AvlNode root){// 把 root 的左节点 root_left 旋转为根结点AvlNode root_left = root.left;// 同时 root_left 的右子树变为 root 的左子树root.left = root_left.right;// root 变为 root_left 的右子树root_left.right = root;// 重新计算 root/root_left 的高度root.height = height(root);root_left.height = height(root_left);return root_left;//返回新的根结点}
​// RR旋转private AvlNode singleRightRotate(AvlNode root){// 把 root 的左节点 root_right 旋转为根结点AvlNode root_right = root.right;// 同时 root_right 的左子树变为 root 的右子树root.right = root_right.left;// root 变为 root_right 的右子树root_right.left = root;// 重新计算 root/root_right 的高度root.height = height(root);root_right.height = height(root_right);return root_right;//返回新的根结点}
​// RL情况(LR旋转-对右孩子LL旋,对根节点RR旋)private AvlNode doubleRightLeftRotate(AvlNode root){// 对 root 的右节点 root_right 进行LL旋转root.right = singleLeftRotate(root.right);// 对 root 进行 RR旋转return singleRightRotate(root);//返回新的根结点}
​// LR情况(LR旋转-对左孩子RR旋,对根节点LL旋)private AvlNode doubleLeftRightRotate(AvlNode root){// 对 root 的右节点 root_left 进行RR旋转root.left = singleRightRotate(root.left);// 对 root 进行 LL旋转return singleLeftRotate(root);//返回新的根结点}
​static class AvlNode {int data; AvlNode left; AvlNode right;// 当前节点高度int height;public AvlNode(int data) {this.data = data;}}

五)红黑树(Red-Black Tree,简称RBTree)

红黑树是一种含有红黑结点并能自平衡的二叉查找树,实际应用非常广泛,比如Linux内核中的完全公平调度器、高精度计时器、ext3文件系统等等,各种语言的函数库如Java的TreeMap和TreeSet,C++ STL的map、multimap、multiset等。

在二叉查找树的条件上,红黑树还必须满足下面性质:

  • 任何节点是红色或黑色。

  • 根节点是黑色。

  • 父子节点之间不能出现两个连续的红节点。

  • 任何一个节点向下遍历到其子孙的叶子节点,所经过的黑节点个数必须相等。

  • NIL节点被认为是黑色的(叶子节点)。

class  Node<T>{public  T value;public   Node<T> parent;public   boolean isRed;public   Node<T> left;public   Node<T> right;
}

RBTree的插入操作

插入新节点过后,可能会导致树的不平衡,这时就需要对树进行旋转操作和颜色修复,使得它符合红黑树的定义

新插入的节点是红色的,插入修复操作如果遇到父节点的颜色为黑则修复操作结束。也就是说,只有在父节点为红色节点的时候是需要插入修复操作的

插入修复操作分为以下的三种情况,而且新插入的节点的父节点都是红色的:

  1. 叔叔节点也为红色。

  2. 叔叔节点为空或黑色,且祖父节点、父节点和新节点处于一条斜线上。

  3. 叔叔节点为空或黑色,且祖父节点、父节点和新节点不处于一条斜线上。

插入操作-Case 1 - 叔叔节点也为红色。

case 1的操作是将父节点和叔叔节点与祖父节点的颜色互换,这样就符合了RBTRee的定义。即维持了高度的平衡,修复后颜色也符合RBTree定义的第三条和第四条。下图中,操作完成后 “90” 节点变成了新的节点。如果 “90” 节点的父节点不是黑色的话,则继续做修复操作。

插入操作-Case 2 - 叔叔节点为空或黑色,且祖父节点、父节点和新节点处于一条斜线上。(LL或RR)

case 2(LL或RR)的操作是将父节点进行右旋(左旋)操作,并且和祖父节点互换颜色。通过该修复操作RBTRee的高度和颜色都符合红黑树的定义。

插入操作-Case 3 - 叔叔节点为空或黑色,且祖父节点、父节点和新节点不处于一条斜线上。(LR或RL)

case 3的操作是将新节点进行左旋,这样就从case 3转换成case 2了。

总结

树结构是一个非常重要而且应用非常广的数据结构,作为研发人员也必须要了解的数据结构,了解了树结构你才能读懂很多组件的原理。树结构的延伸也非常的广,不止包含平衡二叉树、红黑树、还包含B树、B+树、2-3树、哈夫曼树、二叉堆等等,每一种都有相应的应用场景。

基础结构之树结构详解相关推荐

  1. 数据结构之树结构详解

    树的定义 树是一种很特别的数据结构,树这种数据结构叫做"树"就是因为它长得像一棵树.但是这棵树画成的图长得却是一棵倒着的树,根在上,叶在下. 树是图的一种,树和图的区别就在于:树是 ...

  2. axure树形表格_Axure教程:Axure中继器实现动态树结构控件详解

    树结构是原型设计中常用的控件之一,常用于组织架构等功能.本文作者参照钉钉后台组织架构样式,使用Axure中继器实现动态树结构控件. 一.交互样式 初始样式(以钉钉后台-通讯录-组织架构为例): 初始载 ...

  3. ORB-SLAM2代码/流程详解

    ORB-SLAM2代码详解 文章目录 ORB-SLAM2代码详解 1. ORB-SLAM2代码详解01_ORB-SLAM2代码运行流程 1 运行官方Demo 1.2. 阅读代码之前你应该知道的事情 1 ...

  4. JAVAC 命令详解 -d表示目录

    JAVAC 命令详解 结构 javac [ options ] [ sourcefiles ] [ @files ] 参数可按任意次序排列. options 命令行选项. sourcefiles 一个 ...

  5. 详解Java解析XML的四种方法

    http://developer.51cto.com  2009-03-31 13:12  cnlw1985  javaeye  我要评论(8) XML现在已经成为一种通用的数据交换格式,平台的无关性 ...

  6. Ext.Net学习笔记22:Ext.Net Tree 用法详解

    上面的图片是一个简单的树,使用Ext.Net来创建这样的树结构非常简单,代码如下: <ext:TreePanel runat="server"><Root> ...

  7. logback的使用和logback.xml详解

    一.logback的介绍 Logback是由log4j创始人设计的另一个开源日志组件,官方网站: http://logback.qos.ch.它当前分为下面下个模块: logback-core:其它两 ...

  8. WPD:Page Download Time Breakdown选项详解

    WPD:Page Download Time Breakdown选项详解 "页面下载时间细分"图显示每个页面组件下载时间的细分,可以根据它确定在网页下载期间事务响应时间缓慢是由网络 ...

  9. bootloader详解(转载)

    一.bootloader介绍 bootloader是硬件在加电开机后,除BIOS固化程序外最先运行的软件,负责载入真正的操作系统,可以理解为一个超小型的os.目前在Linux平台中主要有lilo.gr ...

最新文章

  1. python面试常用问题_Python面试中最常见的25个问题-结束
  2. 周昌印:忘记技术 从用户与市场考虑问题
  3. 2019年网络规划设计师上午真题及答案解析
  4. 图片裁剪(cropperjs)
  5. 开源大数据:Iceberg新一代数据湖技术实践
  6. centos虚拟机克隆
  7. Android AR场景拍照技术实现(有关键源代码)
  8. 携程Apollo 启动失败遇到的坑
  9. 详解基于机器学习的恶意代码检测技术
  10. 蓝牙核心规范(V5.2)7.8-深入详解之SMP(安全管理协议)|LE配对过程(1)
  11. 运行kettle-8.2源码
  12. Spiceworks数据统计:Win10发布半年使用情况
  13. VS2017 插件的安装目录
  14. WPS 关闭热点推送通知
  15. JPEG 原理分析及 JPEG 解码器的调试
  16. html计时加速,HTML-加速、再加速
  17. 左右滑屏设置_王者荣耀怎么滑屏操作 滑屏手法详解[多图]
  18. Deepin安装dotnet
  19. 【Spring】入门
  20. CG 函数之选手得分

热门文章

  1. Do you know the coding rules for administrative divisions in China
  2. 位运算(按位与 、按位或、按位异或、取反、左移右移)
  3. 二进制位运算(按位与、按位或、异或、左移、右移、非)
  4. Eclipse 报Problems During Content Assist 错误解决方法
  5. 面向对象——必敲项目(简化版qq炫舞)
  6. Syntax Error: Error: No ESLint configuration found
  7. 2021-2027全球及中国激光投影键盘行业研究及十四五规划分析报告
  8. 浩辰CAD机械软件中定制化行业工具技巧大盘点!
  9. 如何将实时网页添加到PowerPoint演示文稿
  10. Juniper BGP选路原则