zoukankan      html  css  js  c++  java
  • 数据结构与算法之美学习笔记(二叉树基础)

    目录:

    • 树的基础知识
    • 二叉树、满二叉树、完全二叉树
    • 如何遍历一棵树
    • 什么是二叉查找树
    • 二叉查找树的增删改的实现
    • 二叉查找树中的重复数据如何处理
    • 散列表已经这么强了,为什么还要有二叉查找树(二叉查找树相对于散列表的优势)

    笔记参考:

    https://time.geekbang.org/column/article/67856?utm_source=pinpaizhuanqu&utm_medium=geektime&utm_campaign=guanwang&utm_term=guanwang&utm_content=0511

    https://time.geekbang.org/column/article/68334?utm_source=pinpaizhuanqu&utm_medium=geektime&utm_campaign=guanwang&utm_term=guanwang&utm_content=0511

    树的基础知识

    1、什么是树

    2、树的基本概念

    节点、父子关系:

    • 节点:图中的圆点就是树的元素,我们把它叫做节点。
    • 父子关系:用来连接节点之间关系,叫做“父子关系”。

    树的根节点、父节点、子节点、叶子节点、兄弟节点:

    • 父节点:A节点是B、C、D的父节点。
    • 子节点:同理B、C、D是A节点的子节点。
    • 叶子节点:没有子节点的叫做叶子节点,也叫叶节点,如G、H、I、J、K、L。
    • 兄弟节点:拥有同一个父节点的节点们叫做兄弟节点,如B、C、D是父节点,K、L是父节点等。
    • 树的根节点:没有父节点的节点叫做,如图中E就是树的根节点。

    节点高度、节点深度、节点的层数、树的高度:还是用上图来理解。

    • 节点高度:该节点到叶子节点的最长路径(边数,从0开始计算),如F到叶子节点的最长路径也就是到G、H、I、J,所以F节点的高度为2。
    • 节点深度:根节点到该节点的所经历的边数(从0开始计算),如C就是2。
    • 节点的层数:节点深度加1。
    • 树的高度:根节点的高度。

    再来看个简单的例子:

    二叉树、满二叉树、完全二叉树

    满足树的条件,且一个节点最多只能有两个子节点的树就叫做二叉树,分别是左子树和右子树,如下图。

    这个图里面,有两个比较特殊的二叉树,分别是编号2和编号3这两个。

    编号2是满二叉树,编号3是完全二叉树。


    1、满二叉树:

    • 所有叶子节点都在最后一层,并且节点总数为2 ^ n - 1,其中n为树层数。
    • 除叶子节点之外,每个节点都有左右两个子节点。

    2、完全二叉树:

    • 叶子节点都在最底下两层。
    • 最后一层的叶子节点都靠左排列,并且除了最后一层,其它层的节点个数都要达到最大。

    从长相上来看完全二叉树也不具有特殊性啊,为什么要单独把这种树拎出来呢?

    首先要了解到,表示一棵树可以用数组,也可以用链表(链表更直观些),就长下面那样。

    基于数组的顺序存储法,我们把根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 * i = 2 的位置,右子节点存储在 2 * i + 1 = 3 的位置。以此类推,B 节点的左子节点存储在 2 * i = 2 * 2 = 4 的位置,右子节点存储在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。

    也就是说,我们舍弃下标为0的空间,如果节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。


    综上,完全二叉树只需要浪费下标为0的空间就可以很好的把树串联起来了,如果不是完全二叉树的话则会浪费调很多空间。

    所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树会单独拎出来的原因,也是为什么完全二叉树要求最后一层的子节点都靠左的原因。

    如何遍历一棵树

    经典的方法有三种,前序遍历、中序遍历和后序遍历。

    • 前序遍历是指,对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
    • 中序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
    • 后序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

     1 public class BinaryTree {
     2     
     3     /**
     4      * 前序遍历
     5      */
     6     public void preOrder(Node treeNode) {
     7         if (treeNode == null) {
     8             return;
     9         }
    10         System.out.print(treeNode.value + " ");
    11         preOrder(treeNode.left);
    12         preOrder(treeNode.right);
    13     }
    14 
    15     /**
    16      * 中序遍历
    17      */
    18     public void inOrder(Node treeNode) {
    19         if (treeNode == null) {
    20             return;
    21         }
    22         inOrder(treeNode.left);
    23         System.out.print(treeNode.value + " ");
    24         inOrder(treeNode.right);
    25     }
    26 
    27     /**
    28      * 后序遍历
    29      */
    30     public void postOrder(Node treeNode) {
    31         if (treeNode == null) {
    32             return;
    33         }
    34         postOrder(treeNode.left);
    35         postOrder(treeNode.right);
    36         System.out.print(treeNode.value + " ");
    37     }
    38 
    39     /**
    40      * 树节点
    41      */
    42     public static class Node {
    43         int value;
    44         Node left;
    45         Node right;
    46 
    47         public Node(int value) {
    48             this.value = value;
    49         }
    50 
    51         public Node(int value, Node left, Node right) {
    52             this.value = value;
    53             this.left = left;
    54             this.right = right;
    55         }
    56     }
    57 
    58 }

    什么是二叉查找树

    二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。它是怎么做到这些的呢?

    二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值(中序遍历二叉查找树可以输出有序的数据序列)。

    二叉查找树的增删查的实现

    1、查询数据:

    • 先取根节点,如果满足则返回根节点。
    • 如果小于根节点则在左子树递归查找,大于则在右子树递归查找。

    2、添加数据:

    • 若根节点无数据则将数据放入根节点。
    • 插入数据比节点大且右子树为空,则直接插入到右子节点;如果不为空,则递归遍历右子树。
    • 同理,插入数据比节点小且左子树为空,则直接插入到左子节点;如果不为空,则递归遍历左子树。

    3、删除数据:

    • 删除的是叶子节点,只需要将父节点中指向要删除节点的指针置为null。
    • 删除的节点只有一个子节点(左子节点或右子节点),我们只需要将其父节点的指向改为其子节点即可(需要根据大小判断放在左还是右)。
    • 删除的节点有两个子节点,找到该节点右子树中最小的节点,把它替换到删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了)。

    二叉查找树的查找、插入操作都比较简单易懂,但是它的删除操作就比较复杂了 。针对要删除节点的子节点个数的不同,我们需要分三种情况来处理。

    • 第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。
    • 第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。
    • 第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。

    实际上,关于二叉查找树的删除操作,还有个非常简单、取巧的方法,就是单纯将要删除的节点标记为“已删除”,但是并不真正从树中将这个节点去掉。

    这样原本删除的节点还需要存储在内存中,比较浪费内存空间,但是删除操作就变得简单了很多。而且,这种处理方法也并没有增加插入、查找操作代码实现的难度。

      1 public class BinaryTree {
      2 
      3     /**
      4      * 根节点
      5      */
      6     private Node root;
      7 
      8     /**
      9      * 1、先取根节点,如果满足则返回根节点。
     10      * 2、如果小于根节点则在左子树递归查找,大于则在右子树递归查找。
     11      *
     12      * @param value 查询的值
     13      */
     14     public Node find(int value) {
     15         Node node = root;
     16         while (node != null) {
     17             if (value > node.value) {
     18                 node = node.right;
     19             }
     20             else if (value < node.value) {
     21                 node = node.left;
     22             }
     23             else {
     24                 // value == node.value
     25                 return node;
     26             }
     27         }
     28         return null;
     29     }
     30 
     31     /**
     32      * 1、若根节点无数据则将数据放入根节点。
     33      * 2、插入数据比节点大且右子树为空,则直接插入到右子节点;如果不为空,则递归遍历右子树。
     34      * 3、同理,插入数据比节点小且左子树为空,则直接插入到左子节点;如果不为空,则递归遍历左子树。
     35      *
     36      * @param value 插入的值
     37      */
     38     public void insert(int value) {
     39         if (root == null) {
     40             root = new Node(value);
     41             return;
     42         }
     43         Node node = root;
     44         while (node != null) {
     45             if (value > node.value) {
     46                 if (node.right == null) {
     47                     node.right = new Node(value);
     48                     return;
     49                 }
     50                 node = node.right;
     51             }
     52             else {
     53                 // value <= node.value
     54                 if (node.left == null) {
     55                     node.left = new Node(value);
     56                     return;
     57                 }
     58                 node = node.left;
     59             }
     60         }
     61     }
     62 
     63     /**
     64      * 1、删除的是叶子节点,只需要将父节点中指向要删除节点的指针置为null。
     65      * 2、删除的节点只有一个子节点(左子节点或右子节点),我们只需要将其父节点的指向改为其子节点即可(需要根据大小判断放在左还是右)。
     66      * 3、删除的节点有两个子节点,找到该节点右子树中最小的节点,把它替换到删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了)。
     67      *
     68      * @param value 删除的值
     69      */
     70     public void delete(int value) {
     71         // p指向要删除的节点,初始化指向根节点
     72         Node p = root;
     73         // pp记录的是p的父节点
     74         Node pp = null;
     75         // 找到要删除的节点
     76         while (p != null && p.value != value) {
     77             pp = p;
     78             if (value > p.value) {
     79                 p = p.right;
     80             }
     81             else {
     82                 p = p.left;
     83             }
     84         }
     85         if (p == null) {
     86             // 没有找到要删除的节点
     87             return;
     88         }
     89 
     90         // 要删除的节点有两个子节点,找到右子树的最小节点
     91         if (p.left != null && p.right != null) {
     92             Node minp = p.right;
     93             Node minpp = p;
     94             while (minp.left != null) {
     95                 minpp = minp;
     96                 minp = minp.left;
     97             }
     98             // 将minp的数据替换到p中
     99             p.value = minp.value;
    100             p = minp;
    101             pp = minpp;
    102         }
    103 
    104         // 删除节点是叶子节点或仅有一个子节点
    105         Node child;
    106         if (p.left != null) {
    107             child = p.left;
    108         }
    109         else if (p.right != null) {
    110             child = p.right;
    111         }
    112         else {
    113             child = null;
    114         }
    115 
    116         if (pp == null) {
    117             // 删除的是根节点
    118             root = child;
    119         }
    120         else if (pp.left == p) {
    121             pp.left = child;
    122         }
    123         else {
    124             pp.right = child;
    125         }
    126     }
    127 
    128     /**
    129      * 前序遍历
    130      */
    131     public void preOrder(Node treeNode) {
    132         if (treeNode == null) {
    133             return;
    134         }
    135         System.out.print(treeNode.value + " ");
    136         preOrder(treeNode.left);
    137         preOrder(treeNode.right);
    138     }
    139 
    140     /**
    141      * 中序遍历
    142      */
    143     public void inOrder(Node treeNode) {
    144         if (treeNode == null) {
    145             return;
    146         }
    147         inOrder(treeNode.left);
    148         System.out.print(treeNode.value + " ");
    149         inOrder(treeNode.right);
    150     }
    151 
    152     /**
    153      * 后序遍历
    154      */
    155     public void postOrder(Node treeNode) {
    156         if (treeNode == null) {
    157             return;
    158         }
    159         postOrder(treeNode.left);
    160         postOrder(treeNode.right);
    161         System.out.print(treeNode.value + " ");
    162     }
    163 
    164     /**
    165      * 树节点
    166      */
    167     public static class Node {
    168         int value;
    169         Node left;
    170         Node right;
    171 
    172         public Node(int value) {
    173             this.value = value;
    174         }
    175 
    176         public Node(int value, Node left, Node right) {
    177             this.value = value;
    178             this.left = left;
    179             this.right = right;
    180         }
    181     }
    182 
    183 }
     1 public class BinaryTreeTest {
     2 
     3     public static void main(String[] args) {
     4         //    4
     5         //  2   6
     6         // 1 3 5 7
     7         BinaryTree binaryTree = new BinaryTree();
     8         binaryTree.insert(4);
     9         binaryTree.insert(2);
    10         binaryTree.insert(6);
    11         binaryTree.insert(1);
    12         binaryTree.insert(3);
    13         binaryTree.insert(5);
    14         binaryTree.insert(7);
    15 
    16         BinaryTree.Node node = binaryTree.find(4);
    17         System.out.println("----- 二叉树前序遍历 -----");
    18         binaryTree.preOrder(node);
    19         System.out.println("
    ----- 二叉树中序遍历 -----");
    20         binaryTree.inOrder(node);
    21         System.out.println("
    ----- 二叉树后序遍历 -----");
    22         binaryTree.postOrder(node);
    23 
    24         binaryTree.delete(4);
    25     }
    26 
    27 }

    二叉查找树中的重复数据如何处理

    1、节点扩展:比较容易。

    二叉查找树中每一个节点不仅会存储一个数据,因此我们通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。

    2、当成新节点处理:不好理解,不过更加优雅。

    每个节点仍然只存储一个数据。在查找插入位置的过程中,如果碰到一个节点的值,与要插入数据的值相同,我们就将这个要插入的数据放到这个节点的右子树,也就是说,把这个新插入的数据当作大于这个节点的值来处理

    当要查找数据的时候,遇到值相同的节点,我们并不停止查找操作,而是继续在右子树中查找,直到遇到叶子节点,才停止。这样就可以把键值等于要查找值的所有节点都找出来。

    对于删除操作,我们也需要先查找到每个要删除的节点,然后再按前面讲的删除操作的方法,依次删除。

    散列表已经这么强了,为什么还要有二叉查找树(二叉查找树相对于散列表的优势)

    1、第一,二叉树有先天的排序优势:散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。

    2、第二,散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O(logn)。

    3、第三,笼统地来说,尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。

    4、第四,散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。

    5、最后,为了避免过多的散列冲突,散列表装载因子不能太大,特别是基于开放寻址法解决冲突的散列表,不然会浪费一定的存储空间。综合这几点,平衡二叉查找树在某些方面还是优于散列表的,所以,这两者的存在并不冲突。我们在实际的开发过程中,需要结合具体的需求来选择使用哪一个。

  • 相关阅读:
    理解java的三大特性之封装
    Spring_事务-注解代码
    Spring_使用 NamedParameterJdbcTemplate
    C#多线程简单例子讲解
    C#多线程编程
    ASP.NET MVC 的URL路由介绍
    NHibernate二级缓存(第十一篇)
    NHibernate之配置文件属性说明
    NHibernate之映射文件配置说明
    NHibernate 延迟加载与立即加载 (第七篇)
  • 原文地址:https://www.cnblogs.com/bzfsdr/p/14536619.html
Copyright © 2011-2022 走看看