数据结构----二叉树与二叉搜索树

前言

本文参考视频:小马哥教育(SEEMYGO) 2019年 恋上数据结构与算法(第一季)
相关网址推荐:
http://520it.com/binarytrees/
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
http://btv.melezinek.cz/binary-search-tree.html

1. 树
1.1 树形结构

在这里插入图片描述

1.2 树的基本概念

节点,根结点,父节点,子节点,兄弟节点等
在这里插入图片描述

  • 节点:每个圆圈就是一个节点
  • 根结点:一颗树最顶层的节点
  • 父节点:具有分支的节点
  • 子节点:没有分支的节点

其余补充

节点的度: 子树的个数
树的度:所有节点度中的最大值
叶子节点(leaf): 度为0的节点
非叶子节点:度不为0的节点
层数:根结点在第一层,根结点的子节点在第2层,依次类推。(有的也可以把根结点归为第0层)
节点的深度(depth): 从根结点到当前节点的唯一的节点总数。比如节点2的深度为2,节点222的深度为4
节点的高度(height):从当前节点到最远叶子节点的路径上的节点总数,比如节点2的高度为3, 节点22的高度为2
树的深度:所有节点的深度中的最大值
树的高度:所有节点高度重的最大值
树的深度等于树饿高度

1.3 有序树,无序树,森林
  • 有序树:树中的任意节点的子节点之间有顺序关系(这里的顺序不是大小的排序,而是说节点的顺序改变了就是不同的树)
  • 无序树:舒总任意节点之间没有顺序关系,也称为“自由树”
  • 森林:由 m (m >=0) 棵互不相交的树组成的集合。
2. 二叉树
2.1 二叉树的特点
  • 每个节点的度最大为2 (最多拥有2棵子树)
  • 左子树和右子树是有顺序的
  • 即使某节点只有一棵子树,也要区分左右子树
    在这里插入图片描述
    性质

非空二叉树的第 i 层,最多有 2^(i-1) 个节点(i >= 1)
在高度为 h 的二叉树上最多有 (2^h) - 1 个节点 (h >= 1)
对于任何一棵非空二叉树,如果叶子节点个数为 n0 ,度为 2 的节点个数为 n2 ,则有: n0 = n2 + 1

  • 假设度为1的节点个数为 n1 ,那么二叉树的节点总数为 n = n0 + n1 + n2 。
  • 二叉树的边数 T = n1 + 2 * n2 = n - 1 = n0 + n1 + n2 - 1 。(边数等于单个节点度之和;除根节点外,每个节点都有一条边)
2.2 真二叉树与满二叉树

真二叉树
所有的节点的度要么为0, 要么为2
在这里插入图片描述
满二叉树
所有的节点要么都为0,要么都为2,且所有的叶子节点都是在最后一层
在这里插入图片描述
相关性质:

  • 在同样的高度的二叉树中,满二叉树的叶子节点数最多总节点数量最多
  • 满二叉树一定是真二叉树,真二叉树不一定是满二叉树
  • 假设满二叉树的高度为 h (h >=1), 那么第 i 层的节点数量为: 2 ^ ( i - 1)
    叶子节点的数量为 : 2 ^ ( h -1)
    总节点的数量为: n = 2 ^ 0 + 2 ^ 1 + 2 ^ 2 + … + 2 ^ (h -1) = (2 ^ h) - 1
    h = log 2 (n + 1)
2.3 完全二叉树(Complete Binary Tree)

叶子节点只会出现在最后2层,且最后1层叶子节点靠左对齐。
在这里插入图片描述
完全二叉树从根结点到至倒数第2层时一棵满二叉树
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树

性质:

  • 度为1 的节点只有左子树
  • 度为1的节点要么是1个,要么是0个
  • 同样节点数量的二叉树,完全二叉树的高度最小
  • 假设完全二叉树的高度为 h (h >= 1), 那么
    至少有 2 ^ ( h - 1) 个节点 (2^0 + 2^1 + 2^2+…+ 2^(h-2) + 1)
    最多有 (2 ^ h) - 个节点 (2^0 + 2^1 + 2^2+…+ 2^(h-1), 满二叉树)
    总节点数量为 n

2 ^ (h-1) <= n < 2 ^ h
h - 1 < = log2 n < h
h = floor(log2 n) + 1

在这里插入图片描述
其他性质
如果有一棵 n 个节点的完全二叉树(n > 0),从上到下,从左到右对节点从 0 开始 编号,对任意第 i 个 节点

  • 如果 i = 0 , 它是根结点
  • 如果 i > 0, 它的父节点编号为 floor( (i-1) /2 )
  • 如果 2i + 1 <= n-1 , 它的左子节点编号为 2i + 1
  • 如果 2i + 1 > n -1 , 它无左子节点
  • 如果 2i + 2 <= n-1, 它的右子节点编号为 2i + 2
  • 如果 2i + 2 > n-1, 它无右子节点
    在这里插入图片描述
    关于完全二叉树的面试题
    如果一棵完全二叉树有 768 个节点,求叶子节点的个数?
    解题思路:
    在这里插入图片描述
    相关推导公式
    总节点数量为n
    n 如果是偶数, 叶子节点的数量 n0 = n / 2
    n 如果是奇数,叶子节点的数量为 n0 = (n + 1) /2
    n0 = floor ((n+1) / 2)
    编写代码时,可以直接表示为 n0 = (n+1)/2 或 n0 = (n + 1) >> 1 (因为Java运算中默认是向下取整的)
3. 二叉搜素树(Binary Search Tree)

思考

  • 在n个动态整数中搜索某个整数?(查看其是否存在)
  • 假设使用动态数组存放元素,从第0个位置开始遍历搜索,平均时间复杂度为 O(n)
    在这里插入图片描述
  • 如果维护的是一个有序的动态数组,使用二分搜索。最坏的时间复杂度为:O (log n)
  • 但是添加,删除的平均时间复杂度为 O (n)
    在这里插入图片描述
  • 针对这种需求,是否有更好的方案呢?
    使用二叉搜索树,添加,删除,搜索的最坏时间复杂度可优化至:O(log n)

性质

二叉搜索树是二叉树的一种,是应用非常广泛的一种二叉树,英文简称为BST,又称为二叉查找树,二叉排序树。
任意一个节点的值都大于其左子树的所有值,都小于器右子树的所有值
它的左右子树也是一棵二叉搜索树

  • 二叉搜索树可以大大提高搜索数据的效率
  • 二叉搜索树存储的元素必须具备可比性。
    • 如 int, double等
    • 如果是自定义类型,需要指定比较方式
    • 不允许为null

二叉搜索树的接口设计

元素的数量: int size();
是否为空: boolean isEmpty();
清空所有元素: void clear();
添加元素: void add(E element);
删除元素: void remove(E element);
是否包含某个元素: boolean contains(E element);

需要注意的是:我们现在使用的二叉搜索树(二叉树)来说,它的元素是没有索引的概念的。
为什么?很简单,用不上! 因为二叉搜索树的性质,我们插入元素位置是不确定的,无法确定插入元素位置,因此索引也就用不上了。
相关代码的实现上可参考:https://mofan212.gitee.io/posts/DataStructure-BinarySearchTree/

4. 代码思路

[1] 编写添加时,我们就会想到节点类,这里我们定义为内部类,这个类存放节点的元素值,左右节点以及父节点。

// 一个节点就是一个Node
private static class Node<E>{
    E element;  // 节点元素值
    Node<E> left;   // 左节点
    Node<E> right;  // 右节点
    Node<E> parent; // 父节点
    public Node(E element, Node<E> parent){
        this.element = element;
        this.parent = parent;
    }
}

[2] 根据这个内部类我们可以进而推出二叉搜索树类中的成员变量,用size 来表示节点数量;用root来记录根结点,方便后面的遍历,添加,删除等操作

public class BinarySearchTree<E> {
    private int size;   // 节点个数
    private Node<E> root; //根节点

    public int size(){
        return size;
    }

    public boolean isEmpty(){
        return size == 0;
    }
    
    ......
}

[3] 由于使用添加操作需要比较,所以这里需要确保值不为空,并定义一个比较的方法(比较方法下面再具体刻画)

// 节点空值检查
private void elementNotNullCheck(E element){
    if (element == null){
        throw new IllegalArgumentException("element must not be null");
    }
}
/**
 * @return 返回值等于0,表示e1和e2相等;返回值大于0,表示e1大于e2;返回值小于0,表示e1小于e2
*/
private int compare(E e1, E e2){
    ......
    return 0;
}
4.1 添加节点

思路:
当我们添加节点时,首先要做一个非空判断。
当添加节点时,可以分为两种情况,一种是当前为空树,要插入一个根结点;另外一种是当前已有根节点,插入的节点是子节点。

当添加的节点是根节点时(即:root == null):直接令root等于节点,然后size加一,最后返回即可。

当添加的节点不是根节点时:
先确认插入节点位置的父节点:采取遍历并比较的方式。遍历需要一个循环,循环结束条件就是当前的节点为null,因为插入的节点只能是原BST的叶子结点的子节点;
确定好父节点后,根据前一步获取的比较方法的返回值,确定插入节点的位置(父节点的左节点还是右节点)。

public void add(E element){
    elementNotNullCheck(element);
    // 添加第一个节点 (根节点)
    if (root == null){
        root = new Node<>(element, null);
        size++;
        return;
    }
    // 添加的不是第一个节点
    // 找到插入节点的父节点
    Node<E> parent = root;
    Node<E> node = root;
    int cmp = 0;
    while (node != null){
        cmp = compare(element, node.element);
        parent = node;  // 保存父节点
        if (cmp > 0){
            node = node.right;
        }else if (cmp < 0){
            node = node.left;
        }else { // 相等
            node.element = element;	// 相等时覆盖
            return;
        }
    }
    // 看看插入到父节点的哪个位置
    Node<E> newNode = new Node<>(element,parent);
    if (cmp > 0){
        parent.right = newNode;
    }else {
        parent.left = newNode;
    }
    size++;
}

等值处理的情况

我们插入的值若已存在,我们采用覆盖的方式,即把新值替换掉老值。
原因:在应用中,我们有时插入的是一个Person对象,它是以id号来区分的,若我们定义了两个相同的id值,但Person里的 name, age等属性都不同,那么我们就可以知道这种覆盖的意义所在了。

4.2 比较逻辑

我们已经编写好添加节点的逻辑,但是其中需要进行节点比较以确定插入的位置,我们并没有进行编写,因此在这里将对比较逻辑进行编写。
如果数据类型是 int、double,我们直接比较数值大小就可以了,但是实际环境下可能不是基本数据类型,因此我们需要对不同的类型制定比较规则。
我们可以将规则的制定交给使用者。

设计一 —— 可比较接口

创建可比较接口Comparable,并在其中添加比较方法。然后让BinarySearchTree中的范型实现(继承)这个接口,同时用户自己编写的实体类也要实现这个接口。

Comparable.java:

public interface Comparable<E> {
    int compareTo(E e);
}

BinarySearchTree.java:

public class BinarySearchTree<E extends Comparable> { ...... }

而BinarySearchTree中的int compare(E e1, E e2);方法就可以这么书写:

/**
* @return 返回值等于0,表示e1和e2相等;返回值大于0,表示e1大于e2;返回值小于0,表示e1小于e2
*/
private int compare(E e1, E e2){
    return e1.compareTo(e2);
}

当用户使用自己定义的实体类时,必须实现Comparable接口,重写compareTo()方法,自定义实体类比较方式,例如

public class Person implements Comparable<Person>{
    private int age;

    public Person(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(Person person) {
        return age - person.age;
    }
}

根据上述代码不难得出,我们的比较方法是:年龄大的Person对象比年龄小的Person对象要大。(年龄大指的是age成员变量数值大,最后一个“要大”指的是在BST中,年龄大的Person放在右节点)

但是这样会带来一个问题,比如我先创建了一棵BST,然后插入节点的类型是Person,我又定义了其比较规则,这个时候年龄大的Person对象会放在右节点。然后,我又创建了一棵BST,插入节点类型也是Person,但这个时候,我想要将年龄小的Person对象放在右节点,就是说,这个时候我认为年龄小的Person对象比年龄大的Person对象要大。😕

但是我们已经在Person实体类中写死了比较方法,显然无法实现上述需求。 😟

这个时候,我们可以编写一个比较器接口Comparator,让用户自己编写比较器来实现比较器接口,从而达到高自定义比较方法。☺️

设计二——比较器

创建比较器接口Comparator,添加比较器的比较方法:

// 比较器
public interface Comparator<E> {
    int compare(E e1, E e2);
}

在BinarySearchTree中定义比较器的成员变量,让int compare(E e1, E e2);方法可以使用比较器进行比较:

public class BinarySearchTree<E> {
    private int size;   // 节点个数
    private Node<E> root; //根节点
    private Comparator<E> comparator;

    public BinarySearchTree(Comparator<E> comparator) {
        this.comparator = comparator;
    }
    
    /**
     * @return 返回值等于0,表示e1和e2相等;返回值大于0,表示e1大于e2;返回值小于0,表示e1小于e2
     */
    private int compare(E e1, E e2){
        return comparator.compare(e1,e2);
    }

    ......
}

然后,用户在使用BinarySearchTree时,就需要创建一个比较器:

public class Main {

    public static class PersonComparator implements Comparator<Person>{
        @Override
        public int compare(Person e1, Person e2) {
            return e1.getAge() - e2.getAge();
        }
    }

    public static class PersonComparator2 implements Comparator<Person>{
        @Override
        public int compare(Person e1, Person e2) {
            return e2.getAge() - e1.getAge();
        }
    }

    public static void main(String[] args) {
        BinarySearchTree<Person> bst1 = new BinarySearchTree<>(new PersonComparator());
        bst1.add(new Person(12));
        bst1.add(new Person(15));
        BinarySearchTree<Person> bst2= new BinarySearchTree<>(new PersonComparator2());
        bst2.add(new Person(12));
        bst2.add(new Person(15));
    }
}

注意: 因为Person类中成员变量age是私有的,外部无法直接访问,因此我们需要在Person类中添加get/set方法来获取age。

使用比较器时,除了可以创建一个类来实现Comparator接口外,我们还可以直接使用匿名类:

// Java中的匿名类,类似于iOS中的Block、JS中的闭包
BinarySearchTree<Person> binarySearchTree = new BinarySearchTree<>(new Comparator<Person>() {
    @Override
    public int compare(Person o1, Person o2) {
        return 0;
    }
});

但是! 这样使用比较器的方法会强制让用户在使用BinarySearchTree时需要构建比较器,而且上述说的需求也不一定是每次都需要的,比较器的构建就变得不是必要的。😵

那么有没有什么方法可以让这两种情况结合一下呢?让用户需要构建比较器时可以构建,不需要构建时不被强制要求构建。👇

设计三——二者结合

将二者结合的总结目标就是:让用户需要构建比较器时可以构建,不需要构建时不被强制要求构建。但是还有一点必须满足,那就是插入的节点必须要具备可比较性,构建比较器后,就相当于给节点创造了比较性,如果我们不构建比较性就要要求节点自身拥有比较性。

要想将二者结合,那么需要再增加一个构造方法:

public BinarySearchTree() {
    this(null);
}

有了这个构造方法,我们在创建BinarySearchTree对象时就可以不用构建比较器了。

但是,我们还需要考虑没有比较器时,强制用户使用的节点拥有比较性。

那么,可以改写int compare(E e1, E e2)方法:

/**
* @return 返回值等于0,表示e1和e2相等;返回值大于0,表示e1大于e2;返回值小于0,表示e1小于e2
*/
private int compare(E e1, E e2){
    if (comparator != null){
        return comparator.compare(e1,e2);
    }
    // 未创建比较器时,强制节点拥有比较性
    return ((Comparable<E>)e1).compareTo(e2);
}

这样,我们就将两者结合在一起了。当我们想要创建比较器时,我们可以传递创建的比较器至BinarySearchTree对象,然后节点的比较就会按照创建的比较器规则;如果我们没有创建比较器,那么强制要求节点拥有可比较性 ,即:实体类实现实现Comparable接口,重写compareTo()方法。

JDK中也提供了可比较接口Comparable和比较器Comparator,因此我们可以删除我们自己编写的,然后使用JDK官方的接口。

同时,Java中内置的类型,比如:Integer、Double、String等类都实现了Comparable接口,它们默认是可比较的,因此,BinarySearchTree的范型指定为内置数据类型时,就不用创建比较器了,直接使用就行。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence { ...... }

public final class Integer extends Number implements Comparable<Integer> { ...... }

PS:比较器Comparator位于java.util包中,使用时需要使用import;可比较接口Comparable位于java.lang包中,实体类实现时不要使用import (Java语法)。

4.3 遍历二叉树

遍历是数据结构中常见的操作(把所有的元素都访问一遍)

  • 线性数据结构的遍历比较简单(正序遍历和逆序遍历)
    根据节点顺序的不同,二叉树常见的遍历方式有4种
  • 前序遍历(Preorder Traversal)
    访问顺序:根结点,前序左子树,前序右子树
    则有遍历顺序为: 7、4、2、1、3、5、9、8、11、10、12
    在这里插入图片描述
    实现代码:
// 前序遍历
public void preorderTraversal(){
    // root 是二叉搜索树的根节点(一个成员变量)
    preorderTraversal(root);
}

private void preorderTraversal(Node<E> node){
    if (node == null) return;
    System.out.println(node.element);
    preorderTraversal(node.left);
    preorderTraversal(node.right);
}

  • 中序遍历(Inorder Traversal)
    访问顺序:中序遍历左子树,根结点,中序遍历右子树
    在这里插入图片描述
    从上面的遍历顺序可以看出,如果我们对二叉搜索树使用中序遍历,得到的遍历结果是升序的;如果将访问顺序修改为:中序遍历右子树、根节点、中序遍历左子树,那么得到的遍历结果是降序的。
// 中序遍历
public void inorderTraversal(){
    // root 是二叉搜索树的根节点(一个成员变量)
    inorderTraversal(root);
}
private void inorderTraversal(Node<E> node){
    if (node == null) return;
    inorderTraversal(node.left);
    System.out.println(node.element);
    inorderTraversal(node.right);
}
  • 后序遍历 (Postorder Traversal)
    访问顺序: 后续遍历左子树、后序遍历右子树、根节点
    则有遍历顺序为: 1、3、2、5、4、8、10、12、11、9、7
    在这里插入图片描述
// 后序遍历
public void postorderTraversal(){
    // root 是二叉搜索树的根节点(一个成员变量)
    postorderTraversal(root);
}
private void postorderTraversal(Node<E> node){
    if (node == null) return;
    postorderTraversal(node.left);
    postorderTraversal(node.right);
    System.out.println(node.element);
}

  • 层序遍历 (Level Order Travsal) ===>可以借助队列实现
    访问顺序:从上到下,从左到右依次访问每一个节点
    则有遍历顺序为: 7、4、9、2、5、8、11、1、3、10、12 (不同的颜色区分不同的层次)
    在这里插入图片描述

我们可以使用队列的方式来实现层序遍历:
1.将根节点入队
2.循环执行一下操作,直到队列为空

  • 将队头节点 A 出队,进行访问
  • 将 A 的左子节点入队
  • 将 A 的右子节点入队

代码实现

// 层序遍历
public void levelOrderTraversal() {
    if (root == null) return;
    Queue<Node<E>> queue = new LinkedList<>();
    // 根节点入队
    queue.offer(root);
    while (!queue.isEmpty()){
        // 头结点出队
        Node<E> node = queue.poll();
        System.out.println(node.element);

        if (node.left != null){
            queue.offer(node.left);
        }
        if (node.right != null){
            queue.offer(node.right);
        }
    }
}
4.4 遍历的应用

前序遍历: 树状结构展示(注意左右子树的顺序)
中序遍历:BST的中序遍历可以按升序或降序处理节点
后序遍历:适用于一些先子后父的操作。
层序遍历:计算二叉树的高度、判断一棵树是否为完全二叉树。

[1] 树状结构的展示(前序遍历的应用)
在BinarySearchTree中重写toString()方法:

public String toString() {
    StringBuilder sb = new StringBuilder();
    toString(root,sb,"");
    return sb.toString();
}
private void toString(Node<E> node, StringBuilder sb, String prefix){
    if (node == null) return;
    sb.append(prefix).append(node.element).append("\n");
    toString(node.left,sb,prefix+"【L】--");
    toString(node.right,sb,prefix+"【R】--");
}

测试代码:

static void test4() {
    Integer[] data = new Integer[]{
        7, 4, 2, 1, 3, 5, 9, 8, 11, 10, 12
    };
    BinarySearchTree<Integer> binarySearchTree = new BinarySearchTree<>();
    for (int i = 0; i < data.length; i++) {
        binarySearchTree.add(data[i]);
    }
    System.out.println(binarySearchTree);
}

[2] 计算二叉树的高度(层序遍历的使用)

递归方式

在BinarySearchTree中添加以下代码:

// 获取整棵树的高度
public int height(){
    return height(root);
}

// 获取某一节点的高度
private int height(Node<E> node){
    if (node == null) return 0;
    return 1 + Math.max(height(node.left),height(node.right));
}

迭代方式

在BinarySearchTree中添加以下代码:
用levelSize来表示层节点的数量,当每一层节点遍历完,高度就加1;而层节点的数量则通过后序遍历依次把节点入队后,统计队列的长度。

public int height(){
    if (root == null) return 0;
    // 树的高度
    int height = 0;
    // 存储每一层的元素数量 根节点一定会访问
    int levelSize = 1;
    Queue<Node<E>> queue = new LinkedList<>();
    // 根节点入队
    queue.offer(root);
    while (!queue.isEmpty()) {
        // 头结点出队
        Node<E> node = queue.poll();
        levelSize--;
        if (node.left != null) {
            queue.offer(node.left);
        }
        if (node.right != null) {
            queue.offer(node.right);
        }
        // 意味着即将访问下一层
        if (levelSize ==0){
            levelSize = queue.size();
            height++;
        }
    }
    return height;
}

测试代码:

static void test4() {
    Integer[] data = new Integer[]{
        7, 4, 2, 5, 9, 8, 11
    };
    BinarySearchTree<Integer> binarySearchTree = new BinarySearchTree<>();
    for (int i = 0; i < data.length; i++) {
        binarySearchTree.add(data[i]);
    }
    System.out.println(binarySearchTree.height());	// 3层
}

[3] 完全二叉树的判断(层序遍历的使用)
在这里插入图片描述

思路

  • 如果树为空,则返回 false
  • 如果树不为空,开始层序遍历二叉树(用队列)
    • 如果node.left != null, 将 node.left 入队
    • 如果node.left == null && node.right != null , 返回false
    • 如果node.right ! = null, 将node.right 入队
    • 如果 node.right == null
      • 那么后面遍历的节点应该都为叶子节点,才是完全二叉树
      • 否则返回false
    • 遍历结束,返回true

为了便于判断,可以对BinarySearchTree中的静态内部类Node添加一些方法:

// 一个节点就是一个Node
private static class Node<E> {
    E element;  // 节点元素值
    Node<E> left;   // 左节点
    Node<E> right;  // 右节点
    Node<E> parent; // 父节点

    public Node(E element, Node<E> parent) {
        this.element = element;
        this.parent = parent;
    }

    public boolean isLeaf() {
        return left == null && right == null;
    }

    public boolean hasTwoChildren() {
        return left != null && right != null;
    }
}

然后在BinarySearchTree中添加以下代码:

 //判断是否为完全二叉树
    public boolean isComplete(){
        if(root == null) return false;

        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        boolean leaf = false;

        while (!queue.isEmpty()){
            Node<E> node = queue.poll();
            //若要遍历时已经要求是叶子节点
            if (leaf && !node.isLeaf()) return false;

            if(node.left != null){
                queue.offer(node.left);
            }else if(node.right != null){ //node.left==null && node.right!=null
                return false;
            }

            if(node.right != null){
                queue.offer(node.right);
            }else{
                //node.right == null && node.left == null
                //node.right == null && node.left !=null
                leaf = true;
            }


        }
        return true;

    }

测试代码

static void test5() {
    Integer[] data = new Integer[]{
        7, 4, 9, 2, 1
    };
    BinarySearchTree<Integer> binarySearchTree = new BinarySearchTree<>();
    for (int i = 0; i < data.length; i++) {
        binarySearchTree.add(data[i]);
    }
    System.out.println(binarySearchTree.isComplete()); // false
}

4.5 重构二叉树

以下结果可以保证重构出唯一的一棵二叉树

  • 前序遍历 + 中序遍历
  • 后序遍历 + 中序遍历
    (前两种一定能得到唯一的一棵二叉树)
  • 前序遍历+ 后序遍历 (因为这种情况左右子树不确定)
    • 如果它是一棵真二叉树(Proper Binary Tree),结果唯一的
    • 不然结果不唯一

在这里插入图片描述

4.6 前驱节点(predecessor)

前驱结点(Predecessor):中序遍历时的前一个节点。
现有一二叉树,并给出其中序遍历结果:
在这里插入图片描述
因此, 根节点 8 的前驱结点为 节点 7 。

设计思路

如果是BST,某一节点的前驱结点就是前一个比它小的节点。针对一般情况:
当选中节点的左子树不为空(node.left != null)时(上述二叉树中的6、13、8):

  • predecessor = node.left.right.right.right.right…
  • 终止条件是: right 为 null

当选中节点的左子树为空,但其父节点不为空(node.left == null && node.parent != null)时(上述二叉树中的7、11、9、1):

  • predecessor = node.parent.parent.parent…
  • 终止条件: node 在 parent 的右子树中

当选中节点的左子树和父节点都为空(node.left == null && node.parent == null)时:

  • 选中节点无前驱节点

代码实现

private Node<E> predecessor(Node<E> node) {
    if (node == null) return null;
    // 前驱节点在左子树中
    Node<E> p = node.left;
    if (p != null) {
        while (p.right != null) {
            p = p.right;
        }
        return p;
    }
    // 从“祖宗节点”中寻找前驱节点
    while (node.parent != null && node == node.parent.left) {
        node = node.parent;
    }

    // node.parent == null || node == node.parent.right
    return node.parent;
}

4.7 后继节点

后继结点(Successor):中序遍历时的后一个节点。
现有一二叉树,并给出其中序遍历结果:
在这里插入图片描述
因此,根节点 4 的后继节点为 节点 5 。

设计思路

如果是BST,某一节点的后继结点就是后一个比它大的节点。针对一般情况:
当选中节点的右节点不为空(node.right != null)时(上述二叉树中的1、8、4):

  • successor = node.right.left.left.left…
  • 终止条件是: left 为 null

当选中节点的右节点为空,但其父节点不为空(node.right == null && node.parent != null)时(上述二叉树中的7、6、3、11):

  • successor = node.parent.parent.parent…
  • 终止条件是: node 在 parent 的左子树中

当选中节点的右节点和父节点都为空(node.right == null && node.parent == null)时(上述二叉树中没有右子树的根节点):

  • 选中节点无后继节点

代码实现

private Node<E> successor(Node<E> node) {
    if (node == null) return null;

    Node<E> p = node.right;
    if (p != null) {
        while (p.left != null) {
            p = p.left;
        }
        return p;
    }

    while (node.parent != null && node == node.parent.right) {
        node = node.parent;
    }

    return node.parent;
}

4.8 节点删除

[1] 度为0、1的节点

删除叶子节点

即:删除度为0的节点。
方法: 直接删除即可!
假设node表示需要被删除的叶子节点:

  • 如果 node == node.parent.left(删除的节点是其父节点的左节点):
    直接node.parent.left = null即可。
  • 如果node == node.parent.right(删除的节点是其父节点的右节点):
    直接node.parent.right = null即可。
  • 如果node.parent == null(删除的节点是当前二叉树的根节点,且树只有一个节点):
    直接root == null即可。

删除度为1的节点

方法: 用子节点替代原节点的位置。
在这里插入图片描述
在上图中,就相当于要删除节点 4 或节点 9。

假设node表示需要被删除的度为1的节点,child表示删除节点的左子节点或右子节点,那么我们使用child代替node的位置即可。

  • 如果node是其父节点的左子节点:
    child.parent = node.parent 维护child的父节点
    node.parent.left = child 删除node节点
  • 如果node是其父节点的右子节点:
    child.parent = node.parent 维护child的父节点
    node.parent.right = child 删除node节点
  • 如果node是根节点:
    root = child 删除根节点
    child.parent = null 设置当前根节点的父节点为null

[2] 度为2的节点
在这里插入图片描述

删除节点 5

如果我们直接删除节点 5,会破坏BST的结构,因此我们需要找一个办法,使用这个办法后不会破坏BST的结构,还要满足BST的性质。
通过前面前驱节点和后继节点的说明,我们不难想到使用前驱节点或后继节点来代替被删除的度为2的节点:

  • 先用前驱节点或后继节点的值覆盖需要被删除节点的值
  • 然后删除相应的前驱结点或后继节点

删除节点 5 后的BST结构:
在这里插入图片描述
通过上面的删除操作,我们不难发现:
如果一个节点的度为 2 ,那么它的前驱、后继节点的度只可能是 1 或 0。
删除度为 2 的节点时,真正被销毁内存的节点并不是这个节点,而是它的前驱或后继节点。

代码实现

由于我们提供的接口是根据元素来删除的,而不是根据节点,但是前面的分析是根据节点删除,因此我们需要编写一个根据节点删除的方法(同时还需要一个根据元素查找节点的方法):

// 删除某一元素
public void remove(E element) {
    remove(node(element));
}
// 删除某一节点
private void remove(Node<E> node) {
    if (node == null) return;
    size--;
    if (node.hasTwoChildren()) { // 度为2的节点
        // 找到后继节点
        Node<E> s = successor(node);
        // 用后继节点的值覆盖被删除节点的值
        node.element = s.element;
        // 变量node指向其后继节点,等待后续删除
        node = s;
    }
    // 删除node节点(node的度必然为1或0)
    Node<E> replacement = node.left != null ? node.left : node.right;
    if (replacement != null) {   // node度为1
        // 更改parent
        replacement.parent = node.parent;
        // 更改noded的parent的left、right的指向
        if (node.parent == null) { // node度为1,且为根节点
            root = replacement;
        } else if (node == node.parent.left) {
            node.parent.left = replacement;
        } else { // node == node.parent.right
            node.parent.right = replacement;
        }

    } else if (node.parent == null) {     // node度为0,是叶子节点,并且是根节点
        root = null;
    } else { // node是叶子节点,但不是根节点
        if (node == node.parent.left) {
            node.parent.left = null;
        } else {
            node.parent.right = null;
        }
    }
}

// 根据元素找寻节点
private Node<E> node(E element) {
    Node<E> node = root;
    while (node != null) {
        int cmp = compare(element, node.element);
        if (cmp == 0) return node;
        if (cmp > 0) {
            node = node.right;
        } else { // cmp < 0
            node = node.left;
        }
    }
    return null;
}

4.9 完善接口

还有clear()和contains(E element)还没编写,进行简单的编写:

public void clear() {
    root = null;
    size = 0;
}
public boolean contains(E element) {
    return node(element) != null;
}

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值