【从蛋壳到满天飞】JAVA 数据结构解析和算法实现-二分搜索树

思惟导图

前言

【从蛋壳到满天飞】JAVA 数据结构解析和算法实现,所有文章大概的内容以下:
Arrays(数组)、Stacks(栈)、Queues(队列)、LinkedList(链表)、Recursion(递归思想)、BinarySearchTree(二分搜索树)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(优先队列)、SegmentTree(线段树)、Trie(字典树)、UnionFind(并查集)、AVLTree(AVL 平衡树)、RedBlackTree(红黑平衡树)、HashTable(哈希表)php

源代码有三个:ES6(单个单个的 class 类型的 js 文件) | JS + HTML(一个 js 配合一个 html)| JAVA (一个一个的工程)html

所有源代码已上传 github,点击我吧,光看文章可以掌握两成,动手敲代码、动脑思考、画图才能够掌握八成。前端

本文章适合 对数据结构想了解而且感兴趣的人群,文章风格一如既往如此,就以为手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,但愿对想学习数据结构的人或者正在学习数据结构的人群有帮助。java

树结构

  1. 线性数据结构是把全部的数据排成一排node

    1. 树结构是倒立的树,由一个根节点延伸出不少新的分支节点。
  2. 树结构自己是一个种自然的组织结构python

    1. 如 电脑中文件夹目录结构就是树结构
    2. 这种结构来源于生活,
    3. 好比 图书馆总体分红几个大馆,
    4. 如 数理馆、文史馆等等,
    5. 到了数理馆还要分红 不少的子类,
    6. 如 数学类的图书、物理类的图书、化学类的图书,计算机类的图书,
    7. 到了计算机类的图书还要再分红各类不一样的子类,
    8. 如 按语言分类 c++、java、c#、php、python 等等,
    9. 如 按领域分类 网站编程、app 开发、游戏开发、前端、后端等等,
    10. 每个子领域可能又要分红不少领域,
    11. 一直到最后索引到一本一本的书,
    12. 这就是一个典型的树结构。
    13. 还有 一个公司的组织架构也是这样的一种树结构,
    14. 从 CEO 开始下面可能有不一样的部门,
    15. 如财务部门(Marketing Head)、人事部门(HR Head)、
    16. 技术部门(Finance Head)、市场部门(Audit Officer)等等,
    17. 每一个部门下面还有不一样的职能分工,最后才到具体的一个一我的。
    18. 还有家谱,他自己也是一个树结构,
    19. 其实树结构并不抽象,在生活中随处可见。
  3. 树结构很是的高效c++

    1. 好比文件管理,
    2. 不可能将全部的文件放到一个文件夹中,
    3. 而后用一个线性的结构进行存储,
    4. 那样的话查找文件太麻烦了,
    5. 可是若是给它作成树机构的话,
    6. 那么就能够很容易的检索到目标文件,
    7. 好比说我想检索到个人照片,
    8. 直接找到我的文件夹,而后找到图片文件夹,
    9. 最后找到本身的照片,这样就很快速很高效的找到了目标文件。
    10. 在公司使用这种树形的组织架构也是这个缘由,
    11. CEO 想就技术开发的一些问题进行一些讨论,
    12. 他确定要找相应职能的一些人,
    13. 他不须要去市场部门、营销部门、人事部门、财务部门、行政部门找人,
    14. 他直接去技术部这样的开发部门去找人就行了,
    15. 一会儿就把查询的范围缩小了。
    16. 在数据结构领域设计树结构的本质也是如此。
  4. 在计算机科学领域不少问题的处理git

    1. 当你将数据使用树结构进行存储后,出奇的高效。
  5. 二分搜索树(Binary Search Tree)github

    1. 二分搜索树有它的局限性
  6. 平衡二叉树:AVL;红黑树,面试

    1. 平衡二叉树还有不少种
  7. 算法须要使用一些特殊的操做的时候将数据组织成树结构

    1. 会针对某一类特殊的操做产生很是高效的结果,
    2. 使用以及并查集
    3. 都是为了知足对数据某一个类特殊的操做进行高效的处理,
    4. 同时对于某些特殊的数据,不少时候能够另辟蹊径,
    5. 将他们以某种形式存储成树结构,
    6. 结果就是会对这类特殊的数据
    7. 它们所在的那个领域的问题
    8. 相应的解决方案提供极其高效的结果。
  8. 线段树、Trie(字典树、前缀树)

    1. 线段树主要用来处理线段这种特殊的数据,
    2. Trie 主要用于处理字符串这类特殊的数据,
    3. 要想实现快速搜索的算法,
    4. 它的本质依然是须要使用树结构的,
    5. 树结构不见得是显式的展现在你面前,
    6. 它同时也能够用来处理不少抽象的问题,
    7. 这就像栈的应用同样,
    8. 从用户的角度看只看撤销这个操做或者只看括号匹配的操做,
    9. 用户根本想不到这背后使用了一个栈的数据结构,
    10. 可是为了组建出这样的功能是须要使用这种数据结构的,
    11. 同理树也是如此,不少看起来很是高效的运算结果,
    12. 它的背后实际上是由于有树这种数据结构做为支撑的,
    13. 这也是数据结构、包括数据结构在计算机科学领域很是重要的意义,
    14. 数据结构虽然解决的是数据存储的问题,
    15. 可是在使用的层面上不只仅是由于要存储数据,
    16. 更重要的是在你使用某些特殊的数据结构存储数据后,
    17. 能够帮助你辅助你更加高效的解决某些算法问题
    18. 甚至对于某些问题来讲若是没有这些数据结构,
    19. 那么根本无从解决。

二分搜索树(Binary Search Tree)

  1. 二叉树

    1. 和链表同样,也属于动态数据结构,
    2. 不须要建立这个数据结构的时候就定好存储的容量,
    3. 若是要添加元素,直接 new 一个新的空间,
    4. 而后把它添加到这个数据结构中,删除也是同理,
    5. 每个元素也是存到一个节点中,
    6. 这个节点和链表不一样,它除了要存放这个元素 e,
    7. 它还有两个指向其它节点的变量,分别叫作 left、right,
    class Node {
          E e;
          Node left;
          Node right;
       }
  2. 二叉树也叫多叉树,

    1. 它每个节点最多只能分红两个叉,
    2. 根据这个定义也能定义出多叉树,
    3. 若是每一个节点能够分出十个叉,
    4. 那就能够叫它十叉树,能分多少叉就叫多少叉树,
    5. Trie 字典书自己就是一个多叉树。
  3. 在数据结构领域对应树结构来讲

    1. 二叉树是最经常使用的一种树结构,
    2. 二叉树具备一个惟一的根节点,
    3. 也就是最上面的节点。
    4. 每个节点最多有两个子节点,
    5. 这两个子节点分别叫作这个节点的左孩子和右孩子,
    6. 子节点指向左边的那个节点就是左孩子,
    7. 子节点指向右边的那个节点就是右孩子。
    8. 二叉树每一个节点最多有两个孩子,
    9. 一个孩子都没有的节点一般称之为叶子节点,
    10. 二叉树每一个节点最多有一个父亲,
    11. 根节点是没有父亲节点的。
  4. 二叉树和链表同样具备自然递归的结构

    1. 链表自己是线性的,
    2. 它的操做既可使用循环也可使用递归。
    3. 和树相关的不少操做,
    4. 使用递归的方式去写要比使用非递归的方式简单不少。
    5. 二叉树每个节点的左孩子同时也是一个二叉树的根节点,
    6. 一般叫管这棵二叉树作左子树。
    7. 二叉树每个节点的右孩子同时也是一个二叉树的根节点,
    8. 一般叫管这棵二叉树作右子树。
    9. 也就是说每个二叉树它的左侧和右侧右分别链接了两个二叉树,
    10. 这两个二叉树都是节点个数更小的二叉树,
    11. 这就是二叉树所具备的自然的递归结构。
  5. 二叉树不必定是“满”的

    1. 满二叉树就是除了叶子节点以外,
    2. 每个节点都有两个孩子。
    3. 就算你整个二叉树上只有一个节点,
    4. 它也是一个二叉树,只不过它的左右孩子都是空,
    5. 这棵二叉树只有一个根节点,
    6. 甚至 NULL(空)也是一棵二叉树。
    7. 就像链表中,只有一个节点它也是一个链表,
    8. 也能够把 NULL(空)看做是一个链表。
  6. 二分搜索树是一棵二叉树

    1. 在二叉树定义下全部其它的术语在二分搜索树中也适用,
    2. 如 根节点、叶子节点、左孩子右孩子、左子树、右子树、
    3. 父亲节点等等,这些在二分搜索树中也同样。
  7. 二分搜索树的每个节点的值

    1. 都要大于其左子树的全部节点的值,
    2. 都要小于其右子树的全部节点的值。
    3. 在叶子节点上没有左右孩子,
    4. 那就至关于也知足这个条件。
  8. 二分搜索树的每一棵子树也是二分搜索树

    1. 对于每个节点来讲,
    2. 它的左子树全部的节点都比这个节点小,
    3. 它的右子树全部的节点都比这个节点大,
    4. 那么用二分搜索树来存储数据的话,
    5. 那么再来查找一个数据就会变得很是简单,
    6. 能够很快的知道从左侧找仍是右侧找,
    7. 甚至能够不用看另一侧,
    8. 因此就大大的加快了查询速度。
    9. 在生活中使用树结构,本质也是如此,
    10. 例如我要找一本 java 编程的书,
    11. 那么进入图书馆我直接进入计算机科学这个区域找这本书,
    12. 其它的类的图书我根本不用去管,
    13. 这也是树这种结构存储数据以后再对数据进行操做时
    14. 才可以很是高效的核心缘由。
  9. 为了可以达到二分搜索树的性质

    1. 必须让存储的元素具备可比较性,
    2. 你要定义好 元素之间如何进行比较,
    3. 由于比较的方式是具备多种的,
    4. 必须保证元素之间能够进行比较。
    5. 在链表和数组中则没有这个要求,
    6. 这个就是二分搜索树存储数据的一个局限性,
    7. 也说明了凡事都是有代价的,
    8. 若是想加快搜索的话就必须对数据有必定的要求。

代码示例

  1. 二分搜索树其实不是支持全部的类型

    1. 因此应该对元素的类型有所限制,
    2. 这个限制就是 这个类型必须拥有可比较性,
    3. 因此在 java 里面的表示就是 对泛型进行约束,
    4. 泛型 E 必须知足 Comparable<E>,
    5. 也就是这个类型 E 必须具备可比较性。
  2. 代码实现

    public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
       }

向二分搜索树中添加元素

  1. 若是二分搜索树的根节点为空的话

    1. 第一个添加的元素就会成为根节点,
    2. 若是再添加一个元素,那么就因该从根节点出发,
    3. 根据二分搜索树的定义,
    4. 每一个节点的值要比它的左子树上全部节点的值大,
    5. 假设第二个添加的元素的值小于第一个添加的元素的值,
    6. 那么很显然第二个添加的元素要被添加到根节点的左子树上去,
    7. 根节点的左子树上只有一个节点,
    8. 那么这个节点就是左子树上的根节点,
    9. 这个左子树上的根节点就是顶层根节点的左孩子。
  2. 按照这样的规则,每来一个新元素从根节点开始,

    1. 若是小于根节点,那么就插入到根节点的左子树上去,
    2. 若是大于根节点,那么就插入到根节点的右子树上去,
    3. 因为无论是左子树仍是右子树,它们又是一棵二分搜索树,
    4. 那么这个过程就是依此类推下去,
    5. 一层一层向下比较新添加的节点的值,
    6. 大的向右,小的向左,不停的向下比较,
    7. 若是这个位置没有被占住,那么就能够在这个位置上添加进去,
    8. 若是这个位置被占了,那就不停的向下比较,
    9. 直到找到一个合适的位置添加进去。
  3. 若是遇到两个元素的值相同,那暂时先不去管,

    1. 也就是不添加进去,由于已经有了,
    2. 自定义二分搜索树不包含重复元素,
    3. 若是想包含重复元素,
    4. 只须要定义左子树小于等于节点、或者右子树大于等于节点,
    5. 只要把“等于”这种关系放进定义里就能够了。
  4. 二分搜索树添加元素的非递归写法,和链表很像

    1. 可是在二分搜索树方面的实现尽可能使用递归来实现,
    2. 就是要锻炼递归算法的书写,
    3. 由于递归算法的不少细节和内容须要不断去体会,
    4. 可是非递归的写法也很实用的,
    5. 由于递归自己是具备更高的开销的,
    6. 虽然在现代计算机上这些开销并不明显,
    7. 可是在一些极端的状况下仍是能够看出很大的区别,
    8. 尤为是对于二分搜索树来讲,
    9. 在最坏的状况下它有可能会退化成一个链表,
    10. 那么在这种状况下使用递归的方式很容易形成系统栈的溢出,
    11. 二分搜索树一些非递归的实现你能够本身练习一下。
  5. 在二分搜索树方面,递归比非递归实现起来更加简单。

代码示例

  1. 代码

    public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
    
             // 向二分搜索树中添加一个元素 e
             public void add (E e) {
                   if (root == null) {
                         root = new Node(e);
                         size ++;
                   } else {
                         add(root, e);
                   }
             }
    
             // 向以node为根的二分搜索树种插入元素E,递归算法
             private void add (Node node, E e) {
                   // node 是对用户屏蔽的,用户不用知道二分搜索树中有怎样一个节点结构
    
                   // 若是出现相同的元素就不进行操做了
                   if (e.equals(node.e)) {
                         return;
                   } else if (e.compareTo(node.e) < 0 && node.left == null) {
                         // 给左孩子赋值
                         node.left = new Node(e);
                         size ++;
                         return;
                   } else if (e.compareTo(node.e) > 0 && node.right == null) {
                         // 给右海子赋值
                         node.right = new Node(e);
                         size ++;
                         return;
                   }
    
                   // 这里是处理节点被占了,那就进入下一个层的二叉树中
                   if (e.compareTo(node.e) < 0) {
                         // 去左子树
                         add(node.left, e);
                   } else { // e.compareTo(node.e) > 0
                         // 去右子树
                         add(node.right, e);
                   }
             }
       }
  2. 对于二分搜索的插入操做

    1. 上面的代码是相对比较复杂的,
    2. 能够进行改进一下,
    3. 让代码总体简洁一些,
    4. 由于递归算法是有不少不一样的写法的,
    5. 并且递归的终止条件也是有不一样的考量。

深刻理解递归终止条件

  1. 改进添加操做

    1. 递归算法有不少不一样的写法,
    2. 递归的终止条件也有不一样的考量。
  2. 以前的算法

    1. 向以 node 为根的二分搜索树中插入元素 e,
    2. 其实将新的元素插入至 node 的左孩子或者右孩子,
    3. 若是 node 的左或右孩子为空,那能够进行相应的赋值操做,
    4. 若是是 node 的左右孩子都不为空的话,
    5. 那就只能递归的插入到相应 node 的左或右孩子中,
    6. 由于这一层节点已经满了,只能考虑下一层了,
    7. 下一层符合要求而且节点没有满,就能够进行相应的赋值操做了。
    8. 可是有对根节点作出了特殊的处理,要防止根节点为空的状况发生,
    9. 若是根节点为空,那么就将第一个元素赋值为根节点,
    10. 可是除了根节点之外,其它节点不须要作这种特殊处理,
    11. 因此致使逻辑上并不统一,而且递归的终止条件很是的臃肿,

代码示例

  1. 代码

    public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
    
             // 向二分搜索树中添加一个元素 e
             // 改进:直接调用add
             public void add (E e) {
                   root = add(root, e);
             }
    
             // 向以node为根的二分搜索树种插入元素E,递归算法
             // 改进:返回插入的新节点后二分搜索树的根
             private Node add (Node node, E e) {
    
                   // 处理最基本的问题
                   if (node == null) {
                         size ++;
                         return new Node(e);
                   }
    
                   // 空的二叉树也是叉树。
                   if (e.compareTo(node.e) < 0) {
                         // 将处理后的结果赋值给node的左子树
                         node.left =  add(node.left, e);
                   } else if (e.compareTo(node.e) > 0) {
                         // 将处理后的结果赋值给node的右子树
                         node.right =  add(node.right, e);
                   } // 若是相同 就什么都不作
    
                   // 最后返回这个node
                   return node;
             }
    
       //    // 向二分搜索树中添加一个元素 e
       //    public void add (E e) {
       //        if (root == null) {
       //            root = new Node(e);
       //            size ++;
       //        } else {
       //            add(root, e);
       //        }
       //    }
    
       //    // 向以node为根的二分搜索树种插入元素E,递归算法
       //    private void add (Node node, E e) {
       //        // node 是对用户屏蔽的,用户不用知道二分搜索树中有怎样一个节点结构
       //
       //        // 若是出现相同的元素就不进行操做了
       //        if (e.equals(node.e)) {
       //            return;
       //        } else if (e.compareTo(node.e) < 0 && node.left == null) {
       //            // 给左孩子赋值
       //            node.left = new Node(e);
       //            return;
       //        } else if (e.compareTo(node.e) > 0 && node.right == null) {
       //            // 给右海子赋值
       //            node.right = new Node(e);
       //            return;
       //        }
       //
       //        // 这里是处理节点被占了,那就进入下一个层的二叉树中
       //        if (e.compareTo(node.e) < 0) {
       //            // 去左子树
       //            add(node.left, e);
       //        } else { // e.compareTo(node.e) > 0
       //            // 去右子树
       //            add(node.right, e);
       //        }
       //    }
       }
  2. 虽然代码量更少了,可是也更难理解的了一些

    1. 首先从宏观的语意的角度去理解定义这个函数的语意后
    2. 整个递归函数处理的逻辑如何成立的,
    3. 其次从微观的角度上能够写一些辅助代码来帮助你一点一点的查看,
    4. 从一个空的二分搜索树开始,往里添加三五个元素,
    5. 看看每一个元素是如何逐步的添加进去。
    6. 能够尝试一些链表这个程序插入操做的递归算法,
    7. 其实这两者之间是拥有很是高的类似度的,
    8. 只不过在二分搜索树中须要判断一下是须要插入到左子树仍是右子树而已,
    9. 对于链表来讲直接插入到 next 就行了,
    10. 经过两者的比较就能够更加深刻的理解这个程序。

二分搜索树的查询操做

  1. 查询操做很是的容易

    1. 只须要不停的看每个 node 里面存的元素,
    2. 不会牵扯到整个二分搜索树的添加操做
  2. 和添加元素同样须要使用递归的进行实现

    1. 在递归的过程当中就须要从二分搜索树的根开始,
    2. 逐渐的转移在二分搜索树的子树中缩小问题的规模,
    3. 缩小查询的树的规模,直到找到这个元素 e 或者发现找不到这个元素 e。
  3. 在数组和链表中有索引这个概念,

    1. 可是在二分搜索树中没有索引这个概念。

代码示例

  1. 代码

    public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
    
             // 向二分搜索树中添加一个元素 e
             // 改进:直接调用add
             public void add (E e) {
                   root = add(root, e);
             }
    
             // 向以node为根的二分搜索树中插入元素E,递归算法
             // 改进:返回插入的新节点后二分搜索树的根
             private Node add (Node node, E e) {
    
                   // 处理最基本的问题
                   if (node == null) {
                         size ++;
                         return new Node(e);
                   }
    
                   // 空的二叉树也是叉树。
                   if (e.compareTo(node.e) < 0) {
                         // 将处理后的结果赋值给node的左子树
                         node.left =  add(node.left, e);
                   } else if (e.compareTo(node.e) > 0) {
                         // 将处理后的结果赋值给node的右子树
                         node.right =  add(node.right, e);
                   } // 若是相同 就什么都不作
    
                   // 最后返回这个node
                   return node;
             }
    
             // 查询二分搜索数中是否包含某个元素
             public boolean contains (E e) {
                   return contains(root, e);
             }
    
             // 向以node为根的二分搜索树 进行查找  递归算法
             public boolean contains (Node node, E e) {
    
                   // 解决最基本的问题 也就是遍历完全部节点都没有找到
                   if (node == null) {
                         return false;
                   }
    
                   // 若是 e 小于当前节点的e 则向左子树进发
                   if (e.compareTo(node.e) < 0) {
                       return contains(node.left, e);
                   } else if (e.compareTo(node.e) > 0) { // 若是 e 大于当前节点的e 则向右子树进发
                       return contains(node.right, e);
                   } else { // 若是e 等于 当前节点 e 则直接返回true
                         return true;
                   }
             }
       }

二分搜索树的遍历-前序遍历

  1. 遍历操做就是把这个数据结构中全部的元素都访问一遍

    1. 在二分搜索树中就是把全部节点都访问一遍,
  2. 访问数据结构中存储的全部元素是由于与业务相关,

    1. 例如 给全部的同窗加两分,给全部的员工发补贴等等,
    2. 因为你的数据结构是用来存储数据的,
    3. 不只能够查询某些特定的数据,
    4. 还应该有相关的方式将全部的数据都进行访问。
  3. 在线性结构下,遍历是极其容易的

    1. 不管是数组仍是链表只要使用一下循环就行了,
    2. 可是这件事在树结构下没有那么简单,
    3. 可是也没有那么难:)。
  4. 在树结构下遍历操做并无那么难

    1. 若是你对树结构不熟悉,那么可能就有点难,
    2. 可是若是你熟悉了树结构,那么并不是是那么难的操做,
    3. 尤为是你在掌握递归操做以后,遍历树就更加不难了。
  5. 对于遍历操做,两个子树都要顾及

    1. 即要访问左子树中全部的节点又要访问右子树中全部的节点,
    2. 下面的代码中的遍历方式也称为二叉树的前序遍历,
    3. 先访问这个节点,再访问左右子树,
    4. 访问这个节点放在了访问左右子树的前面因此就叫前序遍历。
    5. 要从宏观与微观的角度去理解这个代码,
    6. 从宏观的角度来看,
    7. 定义好了遍历的这个语意后整个逻辑是怎么组建的,
    8. 从微观的角度来看,真正的有一个棵二叉树的时候,
    9. 这个代码是怎样怎样一行一行去执行的。
    10. 当你熟练的掌握递归的时候,
    11. 有的时候你能够不用遵照 那种先写递归终止的条件,
    12. 再写递归组成的的逻辑 这样的一个过程,如写法二,
    13. 虽然什么都不干,可是也是 return 了,
    14. 和写法一中写的逻辑实际上是等价的,
    15. 也就是在递归终止条件这部分能够灵活处理。
    16. 写法一看起来逻辑比较清晰,递归终止在前,递归组成的逻辑在后。
    // 遍历以node为根的二分搜索树 递归算法
    function traverse(node) {
       if (node === null) {
          return;
       }
    
       // ... 要作的事情
    
       // 访问该节点 两边都要顾及
       // 访问该节点的时候就去作该作的事情,
       // 如 给全部学生加两分
       traverse(node.left);
       traverse(node.right);
    }
    
    // 写法二 这种逻辑也是能够的
    function traverse(node) {
       if (node !== null) {
          // ... 要作的事情
    
          // 访问该节点 两边都要顾及
          // 访问该节点的时候就去作该作的事情,
          // 如 给全部学生加两分
          traverse(node.left);
          traverse(node.right);
       }
    }

代码示例(class: MyBinarySearchTree, class: Main)

  1. MyBinarySearchTree

    public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
    
             // 向二分搜索树中添加一个元素 e
             // 改进:直接调用add
             public void add (E e) {
                   root = add(root, e);
             }
    
             // 向以node为根的二分搜索树中插入元素E,递归算法
             // 改进:返回插入的新节点后二分搜索树的根
             private Node add (Node node, E e) {
    
                   // 处理最基本的问题
                   if (node == null) {
                         size ++;
                         return new Node(e);
                   }
    
                   // 空的二叉树也是叉树。
                   if (e.compareTo(node.e) < 0) {
                         // 将处理后的结果赋值给node的左子树
                         node.left =  add(node.left, e);
                   } else if (e.compareTo(node.e) > 0) {
                         // 将处理后的结果赋值给node的右子树
                         node.right =  add(node.right, e);
                   } // 若是相同 就什么都不作
    
                   // 最后返回这个node
                   return node;
             }
    
             // 查询二分搜索数中是否包含某个元素
             public boolean contains (E e) {
                   return contains(root, e);
             }
    
             // 向以node为根的二分搜索树 进行查找  递归算法
             public boolean contains (Node node, E e) {
    
                   // 解决最基本的问题 也就是遍历完全部节点都没有找到
                   if (node == null) {
                         return false;
                   }
    
                   // 若是 e 小于当前节点的e 则向左子树进发
                   if (e.compareTo(node.e) < 0) {
                       return contains(node.left, e);
                   } else if (e.compareTo(node.e) > 0) { // 若是 e 大于当前节点的e 则向右子树进发
                       return contains(node.right, e);
                   } else { // 若是e 等于 当前节点 e 则直接返回true
                         return true;
                   }
             }
    
             // 二分搜索树的前序遍历
             public void preOrder () {
                   preOrder(root);
             }
    
             // 前序遍历以node为根的二分搜索树 递归算法
             public void preOrder (Node node) {
                   if (node == null) {
                         return;
                   }
    
                   // 输出
                   System.out.println(node.e);
    
                   preOrder(node.left);
                   preOrder(node.right);
    
       //        // 这种逻辑也是能够的
       //        if (node != null) {
       //            // 输出
       //            System.out.println(node.e);
       //
       //            preOrder(node.left);
       //            preOrder(node.right);
       //        }
             }
       }
  2. Main

    public class Main {
    
             public static void main(String[] args) {
    
                   MyBinarySearchTree<Integer> mbst = new MyBinarySearchTree<Integer>();
    
                   int [] nums = {5, 3, 6, 8, 4, 2};
                   for (int i = 0; i < nums.length ; i++) {
                         mbst.add(nums[i]);
                   }
    
                   /////////////////
                   //      5      //
                   //    /   \    //
                   //   3    6    //
                   //  / \    \   //
                   // 2  4     8  //
                   /////////////////
                   mbst.preOrder();
             }
       }

二分搜索树的遍历调试-前序遍历

  1. 遍历输出二分搜索树

    1. 能够写一个辅助函数自动遍历全部节点生成字符串,
    2. 辅助函数叫作 generateBSTString,
    3. 这个函数的做用是,生成以 node 为根节点,
    4. 深度为 depth 的描述二叉树的字符串,
    5. 这样一来要新增一个辅助函数,
    6. 这个函数的做用是,根据递归深度生成字符串,
    7. 这个辅助函数叫作 generateDepthString。

代码示例(class: MyBinarySearchTree, class: Main)

  1. MyBinarySearchTree

    public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
    
             // 向二分搜索树中添加一个元素 e
             // 改进:直接调用add
             public void add (E e) {
                   root = add(root, e);
             }
    
             // 向以node为根的二分搜索树中插入元素E,递归算法
             // 改进:返回插入的新节点后二分搜索树的根
             private Node add (Node node, E e) {
    
                   // 处理最基本的问题
                   if (node == null) {
                         size ++;
                         return new Node(e);
                   }
    
                   // 空的二叉树也是叉树。
                   if (e.compareTo(node.e) < 0) {
                         // 将处理后的结果赋值给node的左子树
                         node.left =  add(node.left, e);
                   } else if (e.compareTo(node.e) > 0) {
                         // 将处理后的结果赋值给node的右子树
                         node.right =  add(node.right, e);
                   } // 若是相同 就什么都不作
    
                   // 最后返回这个node
                   return node;
             }
    
             // 查询二分搜索数中是否包含某个元素
             public boolean contains (E e) {
                   return contains(root, e);
             }
    
             // 向以node为根的二分搜索树 进行查找  递归算法
             public boolean contains (Node node, E e) {
    
                   // 解决最基本的问题 也就是遍历完全部节点都没有找到
                   if (node == null) {
                         return false;
                   }
    
                   // 若是 e 小于当前节点的e 则向左子树进发
                   if (e.compareTo(node.e) < 0) {
                       return contains(node.left, e);
                   } else if (e.compareTo(node.e) > 0) { // 若是 e 大于当前节点的e 则向右子树进发
                       return contains(node.right, e);
                   } else { // 若是e 等于 当前节点 e 则直接返回true
                         return true;
                   }
             }
    
             // 二分搜索树的前序遍历
             public void preOrder () {
                   preOrder(root);
             }
    
             // 前序遍历以node为根的二分搜索树 递归算法
             public void preOrder (Node node) {
                   if (node == null) {
                         return;
                   }
    
                   // 输出
                   System.out.println(node.e);
    
                   preOrder(node.left);
                   preOrder(node.right);
    
       //        // 这种逻辑也是能够的
       //        if (node != null) {
       //            // 输出
       //            System.out.println(node.e);
       //
       //            preOrder(node.left);
       //            preOrder(node.right);
       //        }
             }
    
             @Override
             public String toString () {
                   StringBuilder sb = new StringBuilder();
                   generateBSTString(root, 0, sb);
                   return sb.toString();
             }
    
             // 生成以node为根节点,深度为depth的描述二叉树的字符串
             private void generateBSTString (Node node, int depath, StringBuilder sb) {
                   if (node == null) {
                         sb.append(generateDepthString(depath) + "null\n");
                         return;
                   }
    
                   sb.append(generateDepthString(depath) + node.e + "\n");
    
                   generateBSTString(node.left, depath + 1, sb);
                   generateBSTString(node.right, depath + 1, sb);
    
             }
    
             // 生成路径字符串
             private String generateDepthString (int depth) {
                   StringBuilder sb = new StringBuilder();
                   for (int i = 0; i < depth; i++) {
                         sb.append("-- ");
                   }
                   return  sb.toString();
             }
       }
  2. Main

    public class Main {
    
             public static void main(String[] args) {
    
                   MyBinarySearchTree<Integer> mbst = new MyBinarySearchTree<Integer>();
    
                   int [] nums = {5, 3, 6, 8, 4, 2};
                   for (int i = 0; i < nums.length ; i++) {
                         mbst.add(nums[i]);
                   }
    
                   /////////////////
                   //      5      //
                   //    /   \    //
                   //   3    6    //
                   //  / \    \   //
                   // 2  4     8  //
                   /////////////////
                   mbst.preOrder();
    
                   System.out.println();
    
                   // 输出 调试字符串
                   System.out.println(mbst.toString());
             }
       }

二分搜索树的遍历-中序、后序遍历

  1. 前序遍历

    1. 前序遍历是最天然的一种遍历方式,
    2. 同时也是最经常使用的一种遍历方式,
    3. 若是没有特殊状况的话,
    4. 在大多数状况下都会使用前序遍历。
    5. 先访问这个节点,
    6. 而后访问这个节点的左子树,
    7. 再访问这个节点的右子树,
    8. 整个过程循环往复。
    9. 前序遍历的表示先访问的这个节点。
    function preOrder(node) {
       if (node == null) return;
    
       // ... 要作的事情
       // 访问该节点
    
       // 先一直往左,而后不断返回上一层 再向左、终止,
       // 最后整个操做循环往复,直到所有终止。
       preOrder(node.left);
       preOrder(node.right);
    }
  2. 中序遍历

    1. 先访问左子树,再访问这个节点,
    2. 最后访问右子树,整个过程循环往复。
    3. 中序遍历的表示先访问左子树,
    4. 而后再访问这个节点,最后访问右子树,
    5. 访问这个节点的操做放到了访问左子树和右子树的中间。
    function inOrder(node) {
       if (node == null) return;
    
       inOrder(node.left);
    
       // ... 要作的事情
       // 访问该节点
    
       inOrder(node.right);
    }
  3. 中序遍历后输出的结果是排序后的结果。

    1. 中序遍历的结果是二分搜索树中
    2. 存储的全部的元素从小到大进行排序后的结果,
    3. 这是二分搜索树一个很重要的一个性质。
    4. 二分搜索树任何一个节点的左子树上全部的节点值都比当前节点的小,
    5. 二分搜索树任何一个节点的右子树上全部的节点值都比当前节点的大,
    6. 每个节点的遍历都是从左往本身再往右,
    7. 先遍历这个节点的左子树,先把比本身节点小的全部元素都遍历了,
    8. 再遍历这个节点,而后再遍历比这个节点大的全部元素,这个过程是递归完成的,
    9. 以 小于、等于、大于的顺序遍历获得的结果天然就是一个从小到大的排序的,
    10. 你也能够 使用大于 等于 小于的顺序遍历,那样结果就是从大到小排序了。
    11. 也正是由于这个缘由,二分搜索树有的时候也叫作排序树,
    12. 这是二分搜索树额外的效能,
    13. 当你使用数组、链表时若是想让你的元素是顺序的话,
    14. 必须作额外的工做,不然没有办法保证一次遍历获得的元素都是顺序排列的,
    15. 可是对于二分搜索树来讲,你只要听从他的定义,
    16. 而后使用中序遍历的方式遍历整棵二分搜索树就可以获得顺序排列的结果。
  4. 后序遍历

    1. 先访问左子树,再访问右子树,
    2. 最后访问这个节点,整个过程循环往复。
    3. 后序遍历的表示先访问左子树,
    4. 而后再访问右子树,最后访问这个节点,
    5. 访问这个节点的操做放到了访问左子树和右子树的后边。
    function inOrder(node) {
       if (node == null) return;
    
       inOrder(node.left);
       inOrder(node.right);
       // ... 要作的事情
       // 访问该节点
    }
  5. 二分搜索树的前序遍历和后序遍历并不像中序遍历那样进行了排序

    1. 后续遍历的应用场景是那些必须先处理完左子树的全部节点,
    2. 而后再处理完右子树的全部节点,最后再处理当前的节点,
    3. 也就是处理完这个节点的孩子节点以后再去处理当前这个节点。
    4. 一个典型的应用是在内存释放方面,若是须要你手动的释放内存,
    5. 那么就须要先把这个节点的孩子节点全都释放完而后再来释放这个节点自己,
    6. 这种状况使用二叉树的后序遍历的方式,
    7. 先处理左子树、再处理右子树、最后处理本身。
    8. 可是例如javac#这样的语言都有垃圾回收机制,
    9. 因此不须要你对内存管理进行手动的控制,
    10. c++ 语言中须要手动的控制内存,
    11. 那么在二分搜索树内存释放这方面就须要使用后序遍历。
    12. 对于一些树结构的问题,
    13. 不少时候也是须要先针对一个节点的孩子节点求解出答案,
    14. 最终再由这些答案组合成针对这个节点的答案,
    15. 树形问题有分治算法、回溯算法、动态规划算法等等。
  6. 二分搜索树的前中后序遍历

    1. 主要从程序的角度进行分析,
    2. 不少时候对一些问题的分析,若是直接给你一个树结构,
    3. 而后你可以直接看出来对于这棵树来讲它的前中后序遍历的结果是怎样的,
    4. 那就能够大大加快解决问题的速度,
    5. 同时这样的一个问题也是和计算机相关的考试的题目,
    6. 对于这样的一个问题的更加深刻的理解
    7. 也能够帮助你理解二分搜索树这种数据结构。

代码示例(class: MyBinarySearchTree, class: Main)

  1. MyBinarySearchTree

    public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
    
             // 向二分搜索树中添加一个元素 e
             // 改进:直接调用add
             public void add (E e) {
                   root = add(root, e);
             }
    
             // 向以node为根的二分搜索树中插入元素E,递归算法
             // 改进:返回插入的新节点后二分搜索树的根
             private Node add (Node node, E e) {
    
                   // 处理最基本的问题
                   if (node == null) {
                         size ++;
                         return new Node(e);
                   }
    
                   // 空的二叉树也是叉树。
                   if (e.compareTo(node.e) < 0) {
                         // 将处理后的结果赋值给node的左子树
                         node.left =  add(node.left, e);
                   } else if (e.compareTo(node.e) > 0) {
                         // 将处理后的结果赋值给node的右子树
                         node.right =  add(node.right, e);
                   } // 若是相同 就什么都不作
    
                   // 最后返回这个node
                   return node;
             }
    
             // 查询二分搜索数中是否包含某个元素
             public boolean contains (E e) {
                   return contains(root, e);
             }
    
             // 向以node为根的二分搜索树 进行查找  递归算法
             private boolean contains (Node node, E e) {
    
                   // 解决最基本的问题 也就是遍历完全部节点都没有找到
                   if (node == null) {
                         return false;
                   }
    
                   // 若是 e 小于当前节点的e 则向左子树进发
                   if (e.compareTo(node.e) < 0) {
                       return contains(node.left, e);
                   } else if (e.compareTo(node.e) > 0) { // 若是 e 大于当前节点的e 则向右子树进发
                       return contains(node.right, e);
                   } else { // 若是e 等于 当前节点 e 则直接返回true
                         return true;
                   }
             }
    
             // 二分搜索树的前序遍历
             public void preOrder () {
                   preOrder(root);
             }
    
             // 前序遍历以node为根的二分搜索树 递归算法
             private void preOrder (Node node) {
                   if (node == null) {
                         return;
                   }
    
                   // 输出
                   System.out.println(node.e);
    
                   preOrder(node.left);
                   preOrder(node.right);
    
       //        // 这种逻辑也是能够的
       //        if (node != null) {
       //            // 输出
       //            System.out.println(node.e);
       //
       //            preOrder(node.left);
       //            preOrder(node.right);
       //        }
             }
    
             // 二分搜索树的中序遍历
             public void inOrder () {
                   inOrder(root);
             }
    
             // 中序遍历以node为根的二分搜索树 递归算法
             private void inOrder (Node node) {
                   if (node == null) return;
    
                   inOrder(node.left);
                   System.out.println(node.e);
                   inOrder(node.right);
    
             }
    
             // 二分搜索树的后序遍历
             public void postOrder () {
                   postOrder(root);
             }
    
             // 后续遍历以node为根的二分搜索树 递归算法
             private void postOrder (Node node) {
                   if (node == null) return;
    
                   postOrder(node.left);
                   postOrder(node.right);
                   System.out.println(node.e);
             }
    
             @Override
             public String toString () {
                   StringBuilder sb = new StringBuilder();
                   generateBSTString(root, 0, sb);
                   return sb.toString();
             }
    
             // 生成以node为根节点,深度为depth的描述二叉树的字符串
             private void generateBSTString (Node node, int depath, StringBuilder sb) {
                   if (node == null) {
                         sb.append(generateDepthString(depath) + "null\n");
                         return;
                   }
    
                   sb.append(generateDepthString(depath) + node.e + "\n");
    
                   generateBSTString(node.left, depath + 1, sb);
                   generateBSTString(node.right, depath + 1, sb);
    
             }
    
             // 生成路径字符串
             private String generateDepthString (int depth) {
                   StringBuilder sb = new StringBuilder();
                   for (int i = 0; i < depth; i++) {
                         sb.append("-- ");
                   }
                   return  sb.toString();
             }
       }
  2. Main

    public class Main {
    
             public static void main(String[] args) {
    
                   MyBinarySearchTree<Integer> mbst = new MyBinarySearchTree<Integer>();
    
                   int [] nums = {5, 3, 6, 8, 4, 2};
                   for (int i = 0; i < nums.length ; i++) {
                         mbst.add(nums[i]);
                   }
    
                   /////////////////
                   //      5      //
                   //    /   \    //
                   //   3    6    //
                   //  / \    \   //
                   // 2  4     8  //
                   /////////////////
    
                   // 前序遍历
                   mbst.preOrder(); // 5 3 2 4 6 8
                   System.out.println();
    
                   // 中序遍历
                   mbst.inOrder(); // 2 3 4 5 6 8
                   System.out.println();
    
                   // 后序遍历
                   mbst.postOrder(); // 2 4 3 8 6 5
                   System.out.println();
    
       //        // 输出 调试字符串
       //        System.out.println(mbst.toString());
             }
       }

二分搜索树的遍历-深刻理解前中后序遍历

  1. 再看二分搜索树的遍历

    1. 对每个节点都有三次的访问机会,
    2. 在遍历左子树以前会去访问一下这个节点而后才能遍历它的左子树,
    3. 在遍历完左子树以后才可以回到这个节点,以后才会去遍历它的右子树,
    4. 在遍历右子树以后又回到了这个节点。
    5. 这就是每个节点使用这种递归遍历的方式其实会访问它三次,
  2. 对二分搜索树前中后这三种顺序的遍历

    1. 其实就对应于这三个访问机会是在哪里进行真正的那个访问操做,
    2. 在哪里输出访问的这个节点的值,
    3. 是先访问这个节点后再遍历它的左右子树,
    4. 仍是先遍历左子树而后访问这个节点最后遍历右子树,
    5. 再或者是 先遍历左右子树再访问这个节点。
    function traverse(node) {
       if (node === null) return;
    
       // 1. 第一个访问的机会   前
    
       traverse(node.left);
    
       // 2. 第二个访问的机会   中
    
       traverse(node.right);
    
       // 3. 第三个访问的机会   后
    }
  3. 二叉树前中后序遍历访问节点的不一样

    1. 前序遍历访问节点都是在第一个访问机会的位置才去访问节点,
    2. 中序遍历访问节点都是在第二个访问机会的位置才去访问节点,
    3. 后序遍历访问节点都是在第三个访问机会的位置才去访问节点,

二分搜索树的遍历-非递归写法

  1. 前序遍历的递归写法

    1. 前序遍历是最天然的一种遍历方式,
    2. 同时也是最经常使用的一种遍历方式,
    3. 若是没有特殊状况的话,
    4. 在大多数状况下都会使用前序遍历。
    5. 先访问这个节点,
    6. 而后访问这个节点的左子树,
    7. 再访问这个节点的右子树,
    8. 整个过程循环往复。
    9. 前序遍历的表示先访问的这个节点。
    function preOrder(node) {
       if (node == null) return;
    
       // ... 要作的事情
       // 访问该节点
    
       // 先一直往左,而后不断返回上一层 再向左、终止,
       // 最后整个操做循环往复,直到所有终止。
       preOrder(node.left);
       preOrder(node.right);
    }
  2. 前序遍历的非递归写法

    1. 使用另一个数据结构来模拟递归调用时的系统栈。
    2. 先访问根节点,将根节点压入栈,
    3. 而后把栈顶元素拿出来,对这个节点进行操做,
    4. 这个节点操做完毕以后,再访问这个节点的两个子树,
    5. 也就是把这个节点的左右两个孩子压入栈中,
    6. 压入栈的顺序是先压入右孩子、再压入左孩子,
    7. 这是由于栈是后入先出的,因此要先压入后续要访问的那个节点,
    8. 再让栈顶的元素出栈,对这个节点进行操做,
    9. 这个节点操做完毕以后,再访问这个节点的两个子树,
    10. 可是这个节点是叶子节点,它的两个孩子都为空,
    11. 那么什么都不用压入了, 再去取栈顶的元素,
    12. 对这个节点进行操做,这个节点操做完毕以后,
    13. 再访问这个节点的两个子树,可是这个节点也是叶子节点,
    14. 那么什么都不用压入了,栈中也为空了,整个访问操做结束。
  3. 不管是非递归仍是递归的写法,结果都是一致的

    1. 非递归的写法中,栈的应用是帮助你记录你下面要访问的哪些节点,
    2. 这个过程很是像使用栈模拟了一下在系统栈中相应的一个调用,
    3. 至关于在系统栈中记录下一步依次要访问哪些节点。
  4. 将递归算法转换为非递归算法

    1. 是栈这种数据结构很是重要的一种应用。
  5. 二分搜索树遍历的非递归实现比递归实现复杂不少

    1. 由于你使用了一个辅助的数据结构才能完成这个过程,
    2. 使用了栈这种数据结构模拟了系统调用栈,
    3. 在算法语意解读上远远比递归实现的算法语意解读要难不少。
  6. 二分搜索树的中序遍历和后序遍历的非递归实现更复杂

    1. 尤为是对于后序遍从来说难度更大,
    2. 可是中序遍历和后序遍历的非递归实现,实际应用并不普遍。
    3. 可是你能够尝试实现中序、后序遍历的非递归实现,
    4. 主要是锻炼你算法实现、思惟逻辑实现思路,
    5. 在解决这个问题的过程当中可能会遇到一些困难,
    6. 能够经过查看网上的资料来解决这个问题,
    7. 这样的问题有可能会在面试题及考试中出现,
    8. 也就是中序和后序遍历相应的非递归实现。
    9. 在经典的教科书中通常都会有这三种遍历的非递归实现,
    10. 经过二分搜索树的前序遍历非递归的实现方式中能够看出,
    11. 彻底可使用模拟系统的栈来完成递归转成非递归这样的操做,
    12. 在慕课上 有一门课《玩转算法面试》中彻底模拟了系统栈的写法,
    13. 也就是将前中后序的遍历都转成了非递归的算法,
    14. 这与经典的教科书上的实现不同,
    15. 可是这种方式对你进一步理解栈这种数据结构仍是二分搜索树的遍历
    16. 甚至是系统调用的过程都是颇有意义的。
  7. 对于前序遍从来说不管是递归写法仍是非递归写法

    1. 对于这棵树来讲都是在遍历的过程当中一直到底,
    2. 这样的一种遍历方式也叫深度优先遍历,
    3. 最终的遍历结果都会先来到整颗树最深的地方,
    4. 直到不能再深了才会开始返回到上一层,
    5. 因此这种遍历就叫作深度优先遍历。
    6. 与深度优先遍历相对应的就是广度优先遍历,
    7. 广度优先遍历遍历出来的结果它的顺序实际上是
    8. 整个二分搜索树的一个层序遍历的顺序。

代码示例(class: MyBinarySearchTree, class: Main)

  1. MyBinarySearchTree

    import java.util.Stack;
    
          public class MyBinarySearchTree<E extends Comparable<E>> {
    
                private class Node {
                    public E e;
                    public Node left, right;
    
                    public Node (E e) {
                          this.e = e;
                          left = null;
                          right = null;
                    }
                }
    
                private Node root;
                private int size;
    
                public MyBinarySearchTree () {
                      root = null;
                      size = 0;
                }
    
                public int getSize() {
                      return size;
                }
    
                public boolean isEmpty () {
                      return size == 0;
                }
    
                // 向二分搜索树中添加一个元素 e
                // 改进:直接调用add
                public void add (E e) {
                      root = add(root, e);
                }
    
                // 向以node为根的二分搜索树中插入元素E,递归算法
                // 改进:返回插入的新节点后二分搜索树的根
                private Node add (Node node, E e) {
    
                      // 处理最基本的问题
                      if (node == null) {
                            size ++;
                            return new Node(e);
                      }
    
                      // 空的二叉树也是叉树。
                      if (e.compareTo(node.e) < 0) {
                            // 将处理后的结果赋值给node的左子树
                            node.left =  add(node.left, e);
                      } else if (e.compareTo(node.e) > 0) {
                            // 将处理后的结果赋值给node的右子树
                            node.right =  add(node.right, e);
                      } // 若是相同 就什么都不作
    
                      // 最后返回这个node
                      return node;
                }
    
                // 查询二分搜索数中是否包含某个元素
                public boolean contains (E e) {
                      return contains(root, e);
                }
    
                // 向以node为根的二分搜索树 进行查找  递归算法
                private boolean contains (Node node, E e) {
    
                      // 解决最基本的问题 也就是遍历完全部节点都没有找到
                      if (node == null) {
                            return false;
                      }
    
                      // 若是 e 小于当前节点的e 则向左子树进发
                      if (e.compareTo(node.e) < 0) {
                          return contains(node.left, e);
                      } else if (e.compareTo(node.e) > 0) { // 若是 e 大于当前节点的e 则向右子树进发
                          return contains(node.right, e);
                      } else { // 若是e 等于 当前节点 e 则直接返回true
                            return true;
                      }
                }
    
                // 二分搜索树的前序遍历
                public void preOrder () {
                      preOrder(root);
                }
    
                // 前序遍历以node为根的二分搜索树 递归算法
                private void preOrder (Node node) {
                      if (node == null) {
                            return;
                      }
    
                      // 输出
                      System.out.println(node.e);
    
                      preOrder(node.left);
                      preOrder(node.right);
    
          //        // 这种逻辑也是能够的
          //        if (node != null) {
          //            // 输出
          //            System.out.println(node.e);
          //
          //            preOrder(node.left);
          //            preOrder(node.right);
          //        }
                }
    
                // 二分搜索树的前序遍历 非递归算法
                public void preOrderNonRecursive () {
    
                      Stack<Node> stack = new Stack<Node>();
                      stack.push(root);
    
                      Node node = null;
                      while (!stack.isEmpty()) {
                            node = stack.pop();
    
          //            // 第一种写法 不符合要求也能够压入栈,可是不符合要求的在出栈后不处理它
          //            if (node != null) {
          //                System.out.println(node.e);
          //                stack.push(node.right);
          //                stack.push(node.left);
          //            }
    
          //            // 第二种写法 不符合要求也能够压入栈,可是不符合要求的在出栈后不处理它
          //            if (node == null) continue;
          //
          //            System.out.println(node.e);
          //            stack.push(node.right);
          //            stack.push(node.left);
    
                            // 写法三 不符合要求就不压入栈
                            System.out.println(node.e);
    
                            if (node.right != null) {
                                  stack.push(node.right);
                            }
                            if (node.left != null) {
                                  stack.push(node.left);
                            }
                      }
                }
    
                // 二分搜索树的中序遍历
                public void inOrder () {
                      inOrder(root);
                }
    
                // 中序遍历以node为根的二分搜索树 递归算法
                private void inOrder (Node node) {
                      if (node == null) return;
    
                      inOrder(node.left);
                      System.out.println(node.e);
                      inOrder(node.right);
    
                }
    
                // 二分搜索树的中序遍历 非递归算法
                public void inOrderNonRecursive () {
                }
    
                // 二分搜索树的后序遍历
                public void postOrder () {
                      postOrder(root);
                }
    
                // 后续遍历以node为根的二分搜索树 递归算法
                private void postOrder (Node node) {
                      if (node == null) return;
    
                      postOrder(node.left);
                      postOrder(node.right);
                      System.out.println(node.e);
                }
    
                @Override
                public String toString () {
                      StringBuilder sb = new StringBuilder();
                      generateBSTString(root, 0, sb);
                      return sb.toString();
                }
    
                // 生成以node为根节点,深度为depth的描述二叉树的字符串
                private void generateBSTString (Node node, int depath, StringBuilder sb) {
                      if (node == null) {
                            sb.append(generateDepthString(depath) + "null\n");
                            return;
                      }
    
                      sb.append(generateDepthString(depath) + node.e + "\n");
    
                      generateBSTString(node.left, depath + 1, sb);
                      generateBSTString(node.right, depath + 1, sb);
    
                }
    
                // 生成路径字符串
                private String generateDepthString (int depth) {
                      StringBuilder sb = new StringBuilder();
                      for (int i = 0; i < depth; i++) {
                            sb.append("-- ");
                      }
                      return  sb.toString();
                }
          }
  2. Main

    public class Main {
    
             public static void main(String[] args) {
    
                   MyBinarySearchTree<Integer> mbst = new MyBinarySearchTree<Integer>();
    
                   int [] nums = {5, 3, 6, 8, 4, 2};
                   for (int i = 0; i < nums.length ; i++) {
                         mbst.add(nums[i]);
                   }
    
                   /////////////////
                   //      5      //
                   //    /   \    //
                   //   3    6    //
                   //  / \    \   //
                   // 2  4     8  //
                   /////////////////
    
                   // 前序遍历
                   mbst.preOrder(); // 5 3 2 4 6 8
                   System.out.println();
    
                   // 中序遍历
                   mbst.inOrder(); // 2 3 4 5 6 8
                   System.out.println();
    
                   // 后序遍历
                   mbst.postOrder(); // 2 4 3 8 6 5
                   System.out.println();
    
                   // 前序遍历 非递归
                   mbst.preOrderNonRecursive(); // 5 3 2 4 6 8
                   System.out.println();
    
       //        // 输出 调试字符串
       //        System.out.println(mbst.toString());
             }
       }

二分搜索树的层序遍历

  1. 二分搜索树的 前序、中序、后序遍历

    1. 它们本质上都是深度优先遍历。
  2. 对于二分搜索树来讲

    1. 每个节点都有一个相应的深度的值,
    2. 根节点做为深度为 0 相应的节点,
    3. 有一些教科书 会把根节点做为深度为 1 相应的节点,
    4. 若是以计算机世界里索引的定义为准那就是使用 0,
    5. 根节点就是第 0 层。
  3. 先遍历第 0 层、再遍历第 1 层、再遍历下一层,

    1. 这样的一层一层的遍历就称为广度优先遍历,
    2. 逐层向下遍历的节点在广度上进行拓展,
    3. 这样的一个遍历顺序就叫作层序遍历、广度优先遍历,
    4. 而不像以前那样 先顺着一个枝杈向着最深的地方走。
  4. 对于层序遍历的实现或者广度优先遍历的实现

    1. 一般不是使用递归的方式进行实现的,
    2. 而是使用非递归的方式进行实现的,
    3. 而且在其中须要使用另外的一个数据结构队列,
    4. 从根节点开始排着队的进入这个队列,
    5. 队列中存储的就是待遍历的元素,
    6. 每一次遍历的它的元素以后再将它的左右孩子也排进队列中,
    7. 整个过程依此类推。
  5. 先入队根节点,而后看队首是否有元素,

    1. 有的话就对队首的元素进行操做,
    2. 操做完毕后就将操做完毕的元素的左右孩子也入队,
    3. 而后再对队列中的元素进行操做,
    4. 队列中的元素又操做完毕了,
    5. 再让操做完毕的这些元素的左右孩子入队,
    6. 最后在对队列中的元素进行操做,
    7. 这些元素都是叶子节点没有左右孩子了,,
    8. 不用入队了,队列中没有元素,整个过程处理完毕,
    9. 这个处理过程就是一层一层的进行处理的一个顺序,
    10. 这就是二分搜索树的广度优先遍历,也叫层序遍历。
  6. 相对于深度优先遍从来说,广度优先遍历的优势

    1. 它能更快的找到你想要查询的那个元素,
    2. 这样的区别主要用于搜索策略上,
    3. 而不是用在遍历这个操做上,
    4. 虽然遍历要将整个二叉树上全部的元素都访问一遍,
    5. 这种状况下深度优先遍历和广度优先遍历是没有区别的。
    6. 可是若是想在一棵树中找到某一个问题的解,
    7. 那对于深度优先遍从来说
    8. 它会从根节点一股脑的跑到这棵树很是深的地方,
    9. 可是颇有可能这个问题的解并不在那么深的地方而是很浅的地方,
    10. 这样一来深度优先遍历要花很长时间才能访问到这个很浅的地方,
    11. 例如前序遍历,若是这个问题的解在右子树上很浅的位置,
    12. 你从一开始就从根节点遍历到左子树的最深处,那就不必了,
    13. 可是这个经常使用于算法设计中,如无权图的最短路径,
    14. 树这种结构在算法设计里也有很是重要的应用,
    15. 尤为是不少时候设计出一个算法,可能真正不须要把这个树发现出来,
    16. 可是这个算法的整个过程就是在一棵虚拟的树中完成的。
  7. 在图中也是有深度优先遍历和广度优先遍历的

    1. 在树中和图中进行深度优先遍历其实它们的实质是同样的,
    2. 不一样的点,对于图来讲须要记录一下对于某一个节点以前是否曾经遍历过,
    3. 由于对于图来讲每个节点的前驱或者放在树这个模型中
    4. 相应的术语就是每一节点它的父亲可能有多个,
    5. 从而产生重复访问这样的问题,而这样的问题在树结构中是不存在的,
    6. 因此在图结构中须要作一个相应的记录。

代码示例(class: MyBinarySearchTree, class: Main)

  1. MyBinarySearchTree

    import java.util.LinkedList;
       import java.util.Queue;
       import java.util.Stack;
    
       public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
    
             // 向二分搜索树中添加一个元素 e
             // 改进:直接调用add
             public void add (E e) {
                   root = add(root, e);
             }
    
             // 向以node为根的二分搜索树中插入元素E,递归算法
             // 改进:返回插入的新节点后二分搜索树的根
             private Node add (Node node, E e) {
    
                   // 处理最基本的问题
                   if (node == null) {
                         size ++;
                         return new Node(e);
                   }
    
                   // 空的二叉树也是叉树。
                   if (e.compareTo(node.e) < 0) {
                         // 将处理后的结果赋值给node的左子树
                         node.left =  add(node.left, e);
                   } else if (e.compareTo(node.e) > 0) {
                         // 将处理后的结果赋值给node的右子树
                         node.right =  add(node.right, e);
                   } // 若是相同 就什么都不作
    
                   // 最后返回这个node
                   return node;
             }
    
             // 查询二分搜索数中是否包含某个元素
             public boolean contains (E e) {
                   return contains(root, e);
             }
    
             // 向以node为根的二分搜索树 进行查找  递归算法
             private boolean contains (Node node, E e) {
    
                   // 解决最基本的问题 也就是遍历完全部节点都没有找到
                   if (node == null) {
                         return false;
                   }
    
                   // 若是 e 小于当前节点的e 则向左子树进发
                   if (e.compareTo(node.e) < 0) {
                       return contains(node.left, e);
                   } else if (e.compareTo(node.e) > 0) { // 若是 e 大于当前节点的e 则向右子树进发
                       return contains(node.right, e);
                   } else { // 若是e 等于 当前节点 e 则直接返回true
                         return true;
                   }
             }
    
             // 二分搜索树的前序遍历
             public void preOrder () {
                   preOrder(root);
             }
    
             // 前序遍历以node为根的二分搜索树 递归算法
             private void preOrder (Node node) {
                   if (node == null) {
                         return;
                   }
    
                   // 输出
                   System.out.println(node.e);
    
                   preOrder(node.left);
                   preOrder(node.right);
    
       //        // 这种逻辑也是能够的
       //        if (node != null) {
       //            // 输出
       //            System.out.println(node.e);
       //
       //            preOrder(node.left);
       //            preOrder(node.right);
       //        }
             }
    
             // 二分搜索树的前序遍历 非递归算法
             public void preOrderNonRecursive () {
    
                   Stack<Node> stack = new Stack<Node>();
                   stack.push(root);
    
                   Node node = null;
                   while (!stack.isEmpty()) {
                         node = stack.pop();
    
       //            // 第一种写法 不符合要求也能够压入栈,可是不符合要求的在出栈后不处理它
       //            if (node != null) {
       //                System.out.println(node.e);
       //                stack.push(node.right);
       //                stack.push(node.left);
       //            }
    
       //            // 第二种写法 不符合要求也能够压入栈,可是不符合要求的在出栈后不处理它
       //            if (node == null) continue;
       //
       //            System.out.println(node.e);
       //            stack.push(node.right);
       //            stack.push(node.left);
    
                         // 写法三 不符合要求就不压入栈
                         System.out.println(node.e);
    
                         if (node.right != null) {
                               stack.push(node.right);
                         }
                         if (node.left != null) {
                               stack.push(node.left);
                         }
                   }
             }
    
             // 二分搜索树的中序遍历
             public void inOrder () {
                   inOrder(root);
             }
    
             // 中序遍历以node为根的二分搜索树 递归算法
             private void inOrder (Node node) {
                   if (node == null) return;
    
                   inOrder(node.left);
                   System.out.println(node.e);
                   inOrder(node.right);
    
             }
    
             // 二分搜索树的中序遍历 非递归算法
             public void inOrderNonRecursive () {
             }
    
             // 二分搜索树的后序遍历
             public void postOrder () {
                   postOrder(root);
             }
    
             // 后续遍历以node为根的二分搜索树 递归算法
             private void postOrder (Node node) {
                   if (node == null) return;
    
                   postOrder(node.left);
                   postOrder(node.right);
                   System.out.println(node.e);
             }
    
             // 二分搜索树的层序遍历
             public void levelOrder () {
    
                   // java中的Queue是一个接口,可是它有链表和队列的实现,
                   // 因此你能够new 一个子类链表类来进行进行使用,能够达到一样的效果
                   Queue<Node> queue = new LinkedList<Node>();
                   queue.add(root);
    
                   while (!queue.isEmpty()) {
                         Node node = queue.remove();
                         System.out.println(node.e);
    
                         if (node.left != null) {
                               queue.add(node.left);
                         }
                         if (node.right != null) {
                               queue.add(node.right);
                         }
                   }
             }
    
             @Override
             public String toString () {
                   StringBuilder sb = new StringBuilder();
                   generateBSTString(root, 0, sb);
                   return sb.toString();
             }
    
             // 生成以node为根节点,深度为depth的描述二叉树的字符串
             private void generateBSTString (Node node, int depath, StringBuilder sb) {
                   if (node == null) {
                         sb.append(generateDepthString(depath) + "null\n");
                         return;
                   }
    
                   sb.append(generateDepthString(depath) + node.e + "\n");
    
                   generateBSTString(node.left, depath + 1, sb);
                   generateBSTString(node.right, depath + 1, sb);
    
             }
    
             // 生成路径字符串
             private String generateDepthString (int depth) {
                   StringBuilder sb = new StringBuilder();
                   for (int i = 0; i < depth; i++) {
                         sb.append("-- ");
                   }
                   return  sb.toString();
             }
       }
  2. Main

    public class Main {
    
             public static void main(String[] args) {
    
                   MyBinarySearchTree<Integer> mbst = new MyBinarySearchTree<Integer>();
    
                   int [] nums = {5, 3, 6, 8, 4, 2};
                   for (int i = 0; i < nums.length ; i++) {
                         mbst.add(nums[i]);
                   }
    
                   /////////////////
                   //      5      //
                   //    /   \    //
                   //   3    6    //
                   //  / \    \   //
                   // 2  4     8  //
                   /////////////////
    
       //        // 前序遍历
       //        mbst.preOrder(); // 5 3 2 4 6 8
       //        System.out.println();
       //
       //        // 中序遍历
       //        mbst.inOrder(); // 2 3 4 5 6 8
       //        System.out.println();
       //
       //        // 后序遍历
       //        mbst.postOrder(); // 2 4 3 8 6 5
       //        System.out.println();
       //
       //        // 前序遍历 非递归
       //        mbst.preOrderNonRecursive(); // 5 3 2 4 6 8
       //        System.out.println();
    
                   mbst.levelOrder(); // 5 3 6 2 4 8
                   System.out.println();
    
       //        // 输出 调试字符串
       //        System.out.println(mbst.toString());
             }
       }

学习方法

  1. 不少时候学习知识

    1. 并非简单的一起一起把它们学过了就能够了,
    2. 不少时候要想可以达到灵活运用可以达到理解的深入,都须要进行比对,
    3. 刻意的去找到从不一样方法之间它们的区别和联系,
    4. 以及本身去总结不一样的方法适用于什么样的场合,
    5. 只有这样,这些知识才可以在你的脑海中才不是一个一个的碎片,
    6. 而是有机的联系起来的,面对不一样的问题才能很是的快的
    7. 而且准确的说出来用怎样的方法去解决更加的好。

二分搜索树的删除节点-删除最大最小值

  1. 对于二分搜索树来讲删除一个节点相对来讲是比较复杂的

    1. 能够先对这个操做进行拆解,从最简单的开始。
  2. 删除二分搜索树的最小值和最大值

    1. 删除二分搜索树中任意元素会复用到
    2. 删除二分搜索树最大值和最小值相应的逻辑。
    3. 要想删除二分搜索树中最大值和最小值,
    4. 那么就要先找到二分搜索树中的最大值和最小值。
  3. 找到二分搜索树中的最大值和最小值是很是容易的

    1. 每个节点的左子树上全部的节点的值都小于当前这个节点,
    2. 每个节点的右子树上全部的节点的值都大于当前这个节点,
    3. 那么从根节点开始一直向左,直到不能再左了,就能找到最小值,
    4. 反之从根节点开始一直向右,知道不能再右了,就能找到最大值。
    5. 这个操做就像操做链表同样,就像是在找一条链上的尾节点。
  4. 删除最大元素节点

    1. 要删除最大元素的这个节点可能有左孩子节点可是没有右孩子节点,
    2. 因此可能会致使没法继续向右因而递归就终止了,
    3. 那么这个时候删除这个节点能够采用当前节点的左孩子替代当前这个节点,
    4. 覆盖操做也算是删除了当前这个节点了。
    5. 若是你像返回被删除的这个最大元素节点,你能够先查询出这个最大的元素节点,
    6. 而后存到一个变量中,最后再调用删除这个最大元素节点的方法,最终返回存的这个变量。
  5. 删除最小元素节点

    1. 要删除的最小元素的节点可能有右孩子节点可是没有左孩子节点,
    2. 会致使没法继续向左而递归终止,你不能删除这个节点的同时连右孩子一块儿删除,
    3. 因此这个时候删除这个节点能够采用当前节点的右孩子替代当前这个节点,
    4. 覆盖操做也算是删除了当前这个节点了,
    5. 其它的和删除最大元素同样,先查询出来,而后存起来,删除这个最大元素后,
    6. 再返回以前存起来的最大元素的变量。

代码示例(class: MyBinarySearchTree, class: Main)

  1. MyBinarySearchTree

    import java.util.LinkedList;
       import java.util.Queue;
       import java.util.Stack;
    
       public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
    
             // 向二分搜索树中添加一个元素 e
             // 改进:直接调用add
             public void add (E e) {
                   root = add(root, e);
             }
    
             // 向以node为根的二分搜索树中插入元素E,递归算法
             // 改进:返回插入的新节点后二分搜索树的根
             private Node add (Node node, E e) {
    
                   // 处理最基本的问题
                   if (node == null) {
                         size ++;
                         return new Node(e);
                   }
    
                   // 空的二叉树也是叉树。
                   if (e.compareTo(node.e) < 0) {
                         // 将处理后的结果赋值给node的左子树
                         node.left =  add(node.left, e);
                   } else if (e.compareTo(node.e) > 0) {
                         // 将处理后的结果赋值给node的右子树
                         node.right =  add(node.right, e);
                   } // 若是相同 就什么都不作
    
                   // 最后返回这个node
                   return node;
             }
    
             // 查询二分搜索数中是否包含某个元素
             public boolean contains (E e) {
                   return contains(root, e);
             }
    
             // 向以node为根的二分搜索树 进行查找  递归算法
             private boolean contains (Node node, E e) {
    
                   // 解决最基本的问题 也就是遍历完全部节点都没有找到
                   if (node == null) {
                         return false;
                   }
    
                   // 若是 e 小于当前节点的e 则向左子树进发
                   if (e.compareTo(node.e) < 0) {
                       return contains(node.left, e);
                   } else if (e.compareTo(node.e) > 0) { // 若是 e 大于当前节点的e 则向右子树进发
                       return contains(node.right, e);
                   } else { // 若是e 等于 当前节点 e 则直接返回true
                         return true;
                   }
             }
    
             // 二分搜索树的前序遍历
             public void preOrder () {
                   preOrder(root);
             }
    
             // 前序遍历以node为根的二分搜索树 递归算法
             private void preOrder (Node node) {
                   if (node == null) {
                         return;
                   }
    
                   // 输出
                   System.out.println(node.e);
    
                   preOrder(node.left);
                   preOrder(node.right);
    
       //        // 这种逻辑也是能够的
       //        if (node != null) {
       //            // 输出
       //            System.out.println(node.e);
       //
       //            preOrder(node.left);
       //            preOrder(node.right);
       //        }
             }
    
             // 二分搜索树的前序遍历 非递归算法
             public void preOrderNonRecursive () {
    
                   Stack<Node> stack = new Stack<Node>();
                   stack.push(root);
    
                   Node node = null;
                   while (!stack.isEmpty()) {
                         node = stack.pop();
    
       //            // 第一种写法 不符合要求也能够压入栈,可是不符合要求的在出栈后不处理它
       //            if (node != null) {
       //                System.out.println(node.e);
       //                stack.push(node.right);
       //                stack.push(node.left);
       //            }
    
       //            // 第二种写法 不符合要求也能够压入栈,可是不符合要求的在出栈后不处理它
       //            if (node == null) continue;
       //
       //            System.out.println(node.e);
       //            stack.push(node.right);
       //            stack.push(node.left);
    
                         // 写法三 不符合要求就不压入栈
                         System.out.println(node.e);
    
                         if (node.right != null) {
                               stack.push(node.right);
                         }
                         if (node.left != null) {
                               stack.push(node.left);
                         }
                   }
             }
    
             // 二分搜索树的中序遍历
             public void inOrder () {
                   inOrder(root);
             }
    
             // 中序遍历以node为根的二分搜索树 递归算法
             private void inOrder (Node node) {
                   if (node == null) return;
    
                   inOrder(node.left);
                   System.out.println(node.e);
                   inOrder(node.right);
    
             }
    
             // 二分搜索树的中序遍历 非递归算法
             public void inOrderNonRecursive () {
             }
    
             // 二分搜索树的后序遍历
             public void postOrder () {
                   postOrder(root);
             }
    
             // 后续遍历以node为根的二分搜索树 递归算法
             private void postOrder (Node node) {
                   if (node == null) return;
    
                   postOrder(node.left);
                   postOrder(node.right);
                   System.out.println(node.e);
             }
    
             // 二分搜索树的层序遍历
             public void levelOrder () {
    
                   // java中的Queue是一个接口,可是它有链表和队列的实现,
                   // 因此你能够new 一个子类链表类来进行进行使用,能够达到一样的效果
                   Queue<Node> queue = new LinkedList<Node>();
                   queue.add(root);
    
                   while (!queue.isEmpty()) {
                         Node node = queue.remove();
                         System.out.println(node.e);
    
                         if (node.left != null) {
                               queue.add(node.left);
                         }
                         if (node.right != null) {
                               queue.add(node.right);
                         }
                   }
             }
    
             // 寻找二分搜索树的最小值元素
             public E minimum () {
                   if (size == 0) {
                         throw new IllegalArgumentException("BST is empty.");
                   }
    
                   return minimum(root).e;
             }
    
             // 返回以node为根的二分搜索树的最小值所在的节点
             private Node minimum (Node node) {
                   // 向左走再也走不动了,就返回这个节点。
                   if (node.left == null) return node;
    
                   return minimum(node.left);
             }
    
             // 从二分搜索树种删除最小值所在节点,返回这个最小值
             public E removeMin () {
                   E result = minimum();
       //        removeMin(root);
                   root = removeMin(root);
                   return result;
             }
    
             // 删除掉以node为根的二分搜索树中的最小节点
             // 返回删除节点后新的二分搜索树的根
             private Node removeMin (Node node) {
       //        if (node.left == null) {
       //            node = node.right;
       //            size --;
       //            return node;
       //        }
       //
       //        return removeMin(node.left);
    
                   if (node.left == null) {
                         Node rightNode = node.right;
                         node.right = null;
                         size --;
                         return rightNode;
                   }
    
                   node.left = removeMin(node.left);
                   return node;
             }
    
             // 寻找二分搜索树的最大值元素
             public E maximum () {
                   if (size == 0) {
                         throw new IllegalArgumentException("BST is empty.");
                   }
    
                   return maximum(root).e;
             }
    
             // 返回以node为根的二分搜索树的最大值所在的节点
             private Node maximum (Node node) {
                   // 向右走再也走不动了,就返回这个节点。
                   if (node.right == null) return node;
    
                   return maximum(node.right);
             }
    
             // 从二分搜索树种删除最大值所在节点,返回这个最大值
             public E removeMax () {
                   E result = maximum();
                   root = removeMax(root);
                   return result;
             }
    
             // 删除掉以node为根的二分搜索树中的最大节点
             // 返回删除节点后新的二分搜索树的根
             private Node removeMax (Node node) {
    
                   if (node.right == null) {
                         Node leftNode = node.left;
                         node.left = null;
                         size --;
                         return leftNode;
                   }
    
                   node.right = removeMax(node.right);
                   return node;
             }
    
             @Override
             public String toString () {
                   StringBuilder sb = new StringBuilder();
                   generateBSTString(root, 0, sb);
                   return sb.toString();
             }
    
             // 生成以node为根节点,深度为depth的描述二叉树的字符串
             private void generateBSTString (Node node, int depath, StringBuilder sb) {
                   if (node == null) {
                         sb.append(generateDepthString(depath) + "null\n");
                         return;
                   }
    
                   sb.append(generateDepthString(depath) + node.e + "\n");
    
                   generateBSTString(node.left, depath + 1, sb);
                   generateBSTString(node.right, depath + 1, sb);
    
             }
    
             // 生成路径字符串
             private String generateDepthString (int depth) {
                   StringBuilder sb = new StringBuilder();
                   for (int i = 0; i < depth; i++) {
                         sb.append("-- ");
                   }
                   return  sb.toString();
             }
       }
  2. Main

    import java.util.ArrayList;
       import java.util.Random;
    
       public class Main {
    
             public static void main(String[] args) {
    
                   MyBinarySearchTree<Integer> mbst = new MyBinarySearchTree<Integer>();
    
                   Random random = new Random();
                   int n = 100;
    
                   for (int i = 0; i < n; i++) {
                         mbst.add(random.nextInt(Integer.MAX_VALUE));
                   }
    
                   // 动态数组
                   ArrayList<Integer> arrayList = new ArrayList<Integer>();
                   while (!mbst.isEmpty()) {
                         arrayList.add(mbst.removeMin());
       //            arrayList.add(mbst.removeMax());
                   }
    
                   // 数组中就是从小到大排序的
                   System.out.println(arrayList);
    
                   // 验证一下
                   for (int i = 1; i < arrayList.size() ; i++) {
                         // 若是前面的数大于后面的数就报异常
                         if (arrayList.get(i - 1) > arrayList.get(i)) {
                               // 若是前面的数小于后面的数就报异常
       //            if (arrayList.get(i - 1) < arrayList.get(i)) {
                               throw new IllegalArgumentException("error.");
                         }
                   }
                   System.out.println("removeMin test completed.");
       //        System.out.println("removeMax test completed.");
             }
       }

二分搜索树的删除节点-删除任意元素

  1. 在二分搜索树种删除最大值最小值的逻辑

    1. 从根节点开始,向左或者向右遍历,
    2. 遍历到最左或者最右时,
    3. 记录这个节点的右子树或者左子树,
    4. 而后返回,而后让这条分支上每一个节点的左或者右子树进行层层覆盖,
    5. 而后层层返回新的节点,直到最后返回给根节点、覆盖掉根节点,
    6. 从而达到了删除最小或最大节点的目的。
    7. 删除最小值的节点就不停的向左遍历,最后记录右子树,
    8. 由于被删除的节点要被这个节点的右子树替代掉,
    9. 只有这样才可以达到删除最小值的节点的效果。
    10. 删除最大值的节点就不停的向右遍历,最后记录左子树,
    11. 由于被删除的节点要被这个节点的左子树替代掉,
    12. 只有这样才可以达到删除最大值的节点的效果。
  2. 删除二分搜索树上任意节点会发生的状况

    1. 删除的这个节点只有左孩子,这个逻辑和上面的相似,
    2. 就让这个节点的左孩子取代这个节点的位置。
    3. 删除的这个节点只有右孩子,这个逻辑也是同样,
    4. 就让这个节点的右孩子取代这个节点的位置。
    5. 删除的这个节点是叶子节点,这个逻辑也同样,
    6. 由于 null 也是一个二分搜索树、也是一个节点、也是一个孩子,
    7. 直接让 null 取代这个节点的位置便可。
    8. 真正难的地方是去删除左右都有孩子这样的节点,
    9. 在 1962 年,Hibbard(计算机科学家)提出-Hibbard Deletion,
    10. 找到离这个节点的值最近而且大的那个节点来取代这个节点,
    11. 也就是找到 这个节点的右孩子的左孩子(右子树的左子树上最小的节点),
    12. 例如待删除的节点为 d,那么就是 s = min(d->right),
    13. 找到比当前节点大最小且最近的节点,这个 s 就是 d 的后继,
    14. 执行 s->right = delMin(d->right)这样的操做,
    15. 以后让 s->left = d->left,
    16. 删除的 d 后,s 是新的子树的根,返回这个 s 节点就能够了。
    17. 除了找待删除节点 d 的后继 s 以外,还能够找待删除节点的前驱 p,
    18. 也就是找到 这个节点的左孩子的右孩子(左子树的右子树上最大的节点)。
    19. 不管使用前驱仍是后继来取代待删除的这个节点
    20. 都可以继续保持二分搜索树的性质。
  3. 对于二分搜索树来讲

    1. 相对于数组、栈、队列、链表这些数据结构要复杂一些,
    2. 二分搜索树自己也是学习其它的树,如 平衡二叉树的基础。

代码示例(class: MyBinarySearchTree, class: Main)

  1. MyBinarySearchTree

    import java.util.LinkedList;
       import java.util.Queue;
       import java.util.Stack;
    
       public class MyBinarySearchTree<E extends Comparable<E>> {
    
             private class Node {
                 public E e;
                 public Node left, right;
    
                 public Node (E e) {
                       this.e = e;
                       left = null;
                       right = null;
                 }
             }
    
             private Node root;
             private int size;
    
             public MyBinarySearchTree () {
                   root = null;
                   size = 0;
             }
    
             public int getSize() {
                   return size;
             }
    
             public boolean isEmpty () {
                   return size == 0;
             }
    
             // 向二分搜索树中添加一个元素 e
             // 改进:直接调用add
             public void add (E e) {
                   root = add(root, e);
             }
    
             // 向以node为根的二分搜索树中插入元素E,递归算法
             // 改进:返回插入的新节点后二分搜索树的根
             private Node add (Node node, E e) {
    
                   // 处理最基本的问题
                   if (node == null) {
                         size ++;
                         return new Node(e);
                   }
    
                   // 空的二叉树也是叉树。
                   if (e.compareTo(node.e) < 0) {
                         // 将处理后的结果赋值给node的左子树
                         node.left =  add(node.left, e);
                   } else if (e.compareTo(node.e) > 0) {
                         // 将处理后的结果赋值给node的右子树
                         node.right =  add(node.right, e);
                   } // 若是相同 就什么都不作
    
                   // 最后返回这个node
                   return node;
             }
    
             // 查询二分搜索数中是否包含某个元素
             public boolean contains (E e) {
                   return contains(root, e);
             }
    
             // 向以node为根的二分搜索树 进行查找  递归算法
             private boolean contains (Node node, E e) {
    
                   // 解决最基本的问题 也就是遍历完全部节点都没有找到
                   if (node == null) {
                         return false;
                   }
    
                   // 若是 e 小于当前节点的e 则向左子树进发
                   if (e.compareTo(node.e) < 0) {
                       return contains(node.left, e);
                   } else if (e.compareTo(node.e) > 0) { // 若是 e 大于当前节点的e 则向右子树进发
                       return contains(node.right, e);
                   } else { // 若是e 等于 当前节点 e 则直接返回true
                         return true;
                   }
             }
    
             // 二分搜索树的前序遍历
             public void preOrder () {
                   preOrder(root);
             }
    
             // 前序遍历以node为根的二分搜索树 递归算法
             private void preOrder (Node node) {
                   if (node == null) {
                         return;
                   }
    
                   // 输出
                   System.out.println(node.e);
    
                   preOrder(node.left);
                   preOrder(node.right);
    
       //        // 这种逻辑也是能够的
       //        if (node != null) {
       //            // 输出
       //            System.out.println(node.e);
       //
       //            preOrder(node.left);
       //            preOrder(node.right);
       //        }
             }
    
             // 二分搜索树的前序遍历 非递归算法
             public void preOrderNonRecursive () {
    
                   Stack<Node> stack = new Stack<Node>();
                   stack.push(root);
    
                   Node node = null;
                   while (!stack.isEmpty()) {
                         node = stack.pop();
    
       //            // 第一种写法 不符合要求也能够压入栈,可是不符合要求的在出栈后不处理它
       //            if (node != null) {
       //                System.out.println(node.e);
       //                stack.push(node.right);
       //                stack.push(node.left);
       //            }
    
       //            // 第二种写法 不符合要求也能够压入栈,可是不符合要求的在出栈后不处理它
       //            if (node == null) continue;
       //
       //            System.out.println(node.e);
       //            stack.push(node.right);
       //            stack.push(node.left);
    
                         // 写法三 不符合要求就不压入栈
                         System.out.println(node.e);
    
                         if (node.right != null) {
                               stack.push(node.right);
                         }
                         if (node.left != null) {
                               stack.push(node.left);
                         }
                   }
             }
    
             // 二分搜索树的中序遍历
             public void inOrder () {
                   inOrder(root);
             }
    
             // 中序遍历以node为根的二分搜索树 递归算法
             private void inOrder (Node node) {
                   if (node == null) return;
    
                   inOrder(node.left);
                   System.out.println(node.e);
                   inOrder(node.right);
    
             }
    
             // 二分搜索树的中序遍历 非递归算法
             public void inOrderNonRecursive () {
             }
    
             // 二分搜索树的后序遍历
             public void postOrder () {
                   postOrder(root);
             }
    
             // 后续遍历以node为根的二分搜索树 递归算法
             private void postOrder (Node node) {
                   if (node == null) return;
    
                   postOrder(node.left);
                   postOrder(node.right);
                   System.out.println(node.e);
             }
    
             // 二分搜索树的层序遍历
             public void levelOrder () {
    
                   // java中的Queue是一个接口,可是它有链表和队列的实现,
                   // 因此你能够new 一个子类链表类来进行进行使用,能够达到一样的效果
                   Queue<Node> queue = new LinkedList<Node>();
                   queue.add(root);
    
                   while (!queue.isEmpty()) {
                         Node node = queue.remove();
                         System.out.println(node.e);
    
                         if (node.left != null) {
                               queue.add(node.left);
                         }
                         if (node.right != null) {
                               queue.add(node.right);
                         }
                   }
             }
    
             // 寻找二分搜索树的最小值元素
             public E minimum () {
                   if (size == 0) {
                         throw new IllegalArgumentException("BST is empty.");
                   }
    
                   return minimum(root).e;
             }
    
             // 返回以node为根的二分搜索树的最小值所在的节点
             private Node minimum (Node node) {
                   // 向左走再也走不动了,就返回这个节点。
                   if (node.left == null) return node;
    
                   return minimum(node.left);
             }
    
             // 从二分搜索树种删除最小值所在节点,返回这个最小值
             public E removeMin () {
                   E result = minimum();
       //        removeMin(root);
                   root = removeMin(root);
                   return result;
             }
    
             // 删除掉以node为根的二分搜索树中的最小节点
             // 返回删除节点后新的二分搜索树的根
             private Node removeMin (Node node) {
       //        if (node.left == null) {
       //            node = node.right;
       //            size --;
       //            return node;
       //        }
       //
       //        return removeMin(node.left);
    
                   if (node.left == null) {
                         Node rightNode = node.right;
                         node.right = null;
                         size --;
                         return rightNode;
                   }
    
                   node.left = removeMin(node.left);
                   return node;
             }
    
             // 寻找二分搜索树的最大值元素
             public E maximum () {
                   if (size == 0) {
                         throw new IllegalArgumentException("BST is empty.");
                   }
    
                   return maximum(root).e;
             }
    
             // 返回以node为根的二分搜索树的最大值所在的节点
             private Node maximum (Node node) {
                   // 向右走再也走不动了,就返回这个节点。
                   if (node.right == null) return node;
    
                   return maximum(node.right);
             }
    
             // 从二分搜索树种删除最大值所在节点,返回这个最大值
             public E removeMax () {
                   E result = maximum();
                   root = removeMax(root);
                   return result;
             }
    
             // 删除掉以node为根的二分搜索树中的最大节点
             // 返回删除节点后新的二分搜索树的根
             private Node removeMax (Node node) {
    
                   if (node.right == null) {
                         Node leftNode = node.left;
                         node.left = null;
                         size --;
                         return leftNode;
                   }
    
                   node.right = removeMax(node.right);
                   return node;
             }
    
             // 从二分搜索树中删除元素e的节点
             public void remove (E e) {
                   root = remove(root, e);
             }
    
             // 删除掉以node为根的二分搜索树中值为e的节点 递归算法
             // 返回删除节点后新的二分搜索树的根
             private Node remove(Node node, E e) {
    
                   if (node == null) return null;
    
                   if (e.compareTo(node.e) < 0) {
                         node.left = remove(node.left, e);
                         return node;
                   } else if (e.compareTo(node.e) > 0) {
                         node.right = remove(node.right, e);
                         return node;
                   } else { // e == node.e
    
                         // 待删除的节点左子树为空
                         if (node.left == null) {
                               Node rightNode = node.right;
                               node.right = null;
                               size --;
                               return rightNode;
                         }
    
                         // 待删除的节点右子树为空
                         if (node.right == null) {
                               Node leftNode = node.left;
                               node.left = null;
                               size --;
                               return leftNode;
                         }
    
                         // 待删除的节点左右子树都不为空的状况
                         // 找到比待删除节点大的最小节点,即待删除节点右子树的最小节点
                         // 用这个节点顶替待删除节点的位置
                         Node successor = minimum(node.right);
                         successor.right = removeMin(node.right);
    
                         // 在removeMin这个操做中维护了一次size --,可是并无删除节点
                         // 因此这里要进行一次size ++操做
                         size ++;
                         successor.left = node.left;
    
                         // 让node这个节点与当前这个二分搜索树脱离关系
                         node.left = node.right = null;
                         // 维护一下size
                         size --;
    
                         return successor;
                   }
             }
    
             @Override
             public String toString () {
                   StringBuilder sb = new StringBuilder();
                   generateBSTString(root, 0, sb);
                   return sb.toString();
             }
    
             // 生成以node为根节点,深度为depth的描述二叉树的字符串
             private void generateBSTString (Node node, int depath, StringBuilder sb) {
                   if (node == null) {
                         sb.append(generateDepthString(depath) + "null\n");
                         return;
                   }
    
                   sb.append(generateDepthString(depath) + node.e + "\n");
    
                   generateBSTString(node.left, depath + 1, sb);
                   generateBSTString(node.right, depath + 1, sb);
    
             }
    
             // 生成路径字符串
             private String generateDepthString (int depth) {
                   StringBuilder sb = new StringBuilder();
                   for (int i = 0; i < depth; i++) {
                         sb.append("-- ");
                   }
                   return  sb.toString();
             }
       }
  2. Main

    import java.util.ArrayList;
       import java.util.Random;
    
       public class Main {
    
             public static void main(String[] args) {
    
                   MyBinarySearchTree<Integer> mbst = new MyBinarySearchTree<Integer>();
                   // 动态数组
                   ArrayList<Integer> arrayList = new ArrayList<Integer>();
    
                   Random random = new Random();
                   int n = 10;
    
                   for (int i = 0; i < n; i++) {
                         int value = random.nextInt(Integer.MAX_VALUE);
                         mbst.add(value);
                         arrayList.add(value);
                   }
    
                   // 输出二分搜索树
                   System.out.println(mbst.getSize());
                   // 输出数组中内容
                   System.out.println(arrayList);
    
                   for (int i = 0; i < arrayList.size(); i++) {
                         mbst.remove(arrayList.get(i));
                   }
    
                   // 输出二分搜索树
                   System.out.println(mbst.getSize());
    
                   System.out.println("remove test completed.");
             }
       }

更多与二分搜索树相关

已经实现的二分搜索树功能

  1. 添加元素 add
  2. 删除元素 remove
  3. 查询元素 contains
  4. 遍历元素 order

其它实现的二分搜索树功能

  1. 能够很是方便的拿到二分搜索树中最大值和最小值,

    1. 这是由于二分搜索树自己有一个很是重要的特性,
    2. 也就是二分搜索树具备顺序性,
    3. 这个顺序性就是指 二分搜索树中全部的元素都是有序的,
    4. 例如使用中序遍历遍历的元素就是将元素从小到大排列起来,
    5. 也正是有顺序性才可以很方便的得到
    6. 二分搜索树中最大值(maximum)最小值(minimum),
    7. 包括给定一个值能够拿到它的前驱(predecessor)和后继(successor)。
  2. 也由于这个顺序性也能够对它进行 floor 和 ceil 的操做,

    1. 也就是找比某一个元素值大的元素或者值小的元素,
    2. 前驱、后继中指定的元素必定要在这棵二分搜索树中,
    3. 而 floor 和 ceil 中指定的这个元素能够不在这棵二分搜索树中。
  3. 相应的二分搜索树还能够实现 rank 和 select 方法,

    1. rank 也就是指定一个元素找到它的排名,
    2. select 是一个反向的操做,也就是找到排名为多少名的那个元素。
    3. 对于二分搜索树来讲均可以很是容易的实现这两个操做。
    4. 实现 rank 和 select 最好的方式是对于二分搜索树每个节点
    5. 同时还维护一个 size,
    6. 这个 size 就是指以这个节点为根的二分搜索树有多少个元素,
    7. 也就是每个节点为根的二分搜索树中有多少的元素,
    8. 那么这个 size 就为多少,
    9. 也就是每个节点包括本身以及下面的子节点的个数,
    10. 每个 node 在维护了一个 size 以后,
    11. 那么实现 rank 和 select 这两个操做就会容易不少,
    12. 也就是给 node 这个成员变量添加一个 size,
    13. 那么对于二分搜索树其它操做如添加和删除操做时,
    14. 也要去维护一下这个节点的 size,
    15. 只有这样实现这个 rank 和 select 就会很是简单,
    16. 这样作以后,对于整棵二分搜索树而言,
    17. 就再也不须要二分搜索树的 size 变量了,
    18. 若是要看整棵二分搜索树有多少个节点,
    19. 直接看root.size就行了,很是的方便。
  4. 维护 depth 的二分搜索树

    1. 对于二分搜索树的每个节点还能够维护一个深度值,
    2. 也就是这个节点的高度值,也就是这个节点处在第几层的位置,
    3. 维护这个值在一些状况下是很是有帮助的。
  5. 支持重复元素的二分搜索树

    1. 只须要定义每个根节点的左子树全部的节点都是
    2. 小于等于这个根节点值的,
    3. 而每个根节点的右子树全部的节点都是大于这个根节点值的,
    4. 这样的定义就很好的支持了重复元素的二叉树的实现。
  6. 还能够经过维护每个节点的 count 变量来实现重复元素的二分搜索树,

    1. 也就是记录一下这个节点所表明的元素在这个二分搜索树中存储的个数,
    2. 当你添加进重复的节点后,直接让相应节点的count++便可,
    3. 若是你删除这个重复的节点时,直接让相应节点的count--便可,
    4. 若是 count 减减以后为 0,那么就从二分搜索树中真正删除掉。

其它

  1. 在二分搜索树中相应的变种其实大可能是在 node 中维护一些数据

    1. 就能够方便你进行一些其它特殊状况的处理,
  2. 相关的习题能够去 leetcode 中找到,

    1. 树标签:https://leetcode-cn.com/tag/tree/
    2. 如第一题,二叉树的最大深度,这个题和链表是很是像的,
    3. 它有一个答题的模板,你提交的时候要按照这个模板来进行提交。
  3. 其它

    1. 二分搜索树的复杂度分析,
    2. 二分搜索树有两个重要的应用集合和映射,
    3. 其实用数组和链表也可以实现集合和映射,
    4. 二分搜索树也有它的局限性。
相关文章
相关标签/搜索