二叉搜索树

273 阅读9分钟

提出问题

n个动态的整数中搜索某个整数,你会选择什么数据结构?

  • 假设使用动态数组,则从第0个位置进行遍历搜索,平均时间复杂度为:O(n)

image.png

  • 假如维护一个有序的动态数组,使用二分搜索,最坏时间复杂度为:O(logn),但是添加和删除操作的平均时间复杂度为O(n)

image.png

如果使用二叉搜索树,无论添加、删除、搜索的最坏时间复杂度可以为:O(logn)

二叉搜索树(Binary Search Tree)

二叉搜索树二叉树的一种,又可称为:二叉查找树二叉排序树

image.png

特点:

  • 任一节点的值都大于其左子树所有节点的值

  • 任一节点的值都小于其右子树所有节点的值

  • 它的左右子树也都是二叉搜索树

  • 二叉搜索树可以大大提高搜索数据的效率

  • 二叉搜索树存储的元素必须具备可比较性

    a.例如intdouble

    b.如果是自定义类型,需要指定比较方式

    c.不允许为null

实现二叉树的接口设计

// 元素的数量
public int size() 
// 是否为空
public boolean isEmpty()
// 清空所有元素
public void clear()
// 添加元素
public void add(E element)
// 移除元素
public void remove(E element)
// 判断是否包含某元素
public boolean contains(E element)

创建节点类

节点中应该包含:

  • 存放的元素
  • 左节点
  • 右节点
  • 父节点
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;

    }

}

添加节点

例如对这棵树进行添加121的过程

image.png

步骤:

  • 找到根节点7,发现127大,找到右节点9
  • 129大,找到右节点11
  • 1211大,发现右节点为空
  • 创建新的节点,元素为12,父节点是11,成为11的右子树

实现:

image.png

之前说过二叉搜索树中的元素必须具有可比较性,可以看到在添加时会有一次比较的操作compare(E e1, E e2),例子中Integer类型本身具有可比较性,如果是自定义类型呢?

元素比较方案的设计

假如现在加入二叉搜索树的元素类型为Person,比较规则为比较年龄,年龄大的元素为大

通过Comparable接口

创建一个接口,其中定义比较方法

public interface Comparable<E> {

    int compareTo(E e); 

}

那么只需让二叉搜索树的泛型来实现这个接口的方法即可,即:

public class BinarySearchTree<E extends Comparable<E>>

那么在compare(E e1, E e2)方法中让元素类来比较即可

private int compare(E e1, E e2) {

     return e1.compareTo(e2);

}

但是这个方案的弊端在于比较的方法写死在元素类中,并不灵活。

外界传入比较器Comparator

public interface Comparator<E> {

int compare(E e1, E e2);

}

二叉搜索树中添加Comparator 属性,由外界传入比较器

public class BinarySearchTree<E> {

    private int size;

    private Node<E> root;

    private Comparator<E> comparator;


    public BinarySearchTree(Comparator<E> comparator) {

        this.comparator = comparator;

    }

    
}

那么在外部便可以自己定义比较规则

private static class PersonComparator1 implements Comparator<Person> {

    *@Override*

    public int compare(Person e1, Person e2) {

        // **TODO** Auto-generated method stub

        return e1.getAge() - e2.getAge();

    }

}

总结

第二种方案看似灵活了,但是会强制外部一定要传入一个比较器,这样也不是很合理,那么比较好得方案便是对这两种方案进行结合,不用强制外部一定传入比较器,那么内部判断,如果外部有比较器就使用比较器,如果没有就强制元素实现Comparable接口

private int compare(E e1, E e2) {

    if (comparator != null) {

        return comparator.compare(e1, e2);

       } else {

        // 不在类的地方去强制,在这里强制

        return ((Comparable<E>)e1).compareTo(e2);

      }

}

二叉树的遍历

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

  • 线性数据结构的遍历分为:正序遍历逆序遍历

  • 二叉树的常见遍历方式有四种:

    1.前序遍历(Preorder Traversal)

    2.中序遍历(Inorder Traversal)

    3.后序遍历(Postorder Traversal)

    4.层序遍历(Level Order Traversal)

前序遍历(Preorder Traversal)

image.png

访问顺序:

  • 根节点、前序遍历左子树、前序遍历右子树

  • 7、4、2、1、3、5、9、8、11、10、12

image.png

中序遍历(Inorder Traversal)

image.png

访问顺序可以有两种:

a.

  • 中序遍历左子树、根节点、中序遍历右子树
  • 1、2、3、4、5、6、7、8、9、10、11、12

b.

  • 中序遍历右子树、根节点、中序遍历左子树
  • 12、11、10、9、8、7、6、5、4、3、2、1

结论:

二叉搜索树的中序遍历结果是升序或者降序的。

image.png

后序遍历(Postorder Traversal)

image.png

访问顺序:

  • 后序遍历左子树、后序遍历右子树、根节点
  • 1、3、2、5、4、8、10、12、11、9、7

image.png

层序遍历(Level Order Traversal)

image.png

层序遍历顾名思义就是从上至下一层一层的访问,与之前三种实现方式不同,并不方便使用递归

访问顺序:

  • 从上至下,从左到右依次访问每一个节点
  • 7、4、9、2、5、8、11、1、3、10、12

实现思路:

  • 使用队列,首先将根节点入队

  • 循环以下操作,直到队列为空

    a. 将队头节点A出队进行访问

    b.如果A有左子节点,进行入队

    c.如果A有右子节点,进行入队

image.png

设计对外开放的遍历接口

上面实现了四种遍历方式,但是可以发现我们对于访问节点的方式均为打印,这样就很不灵活,如果接口对外开放的话,考虑遍历到的元素应该暴露给使用者。

设计一个访问接口

image.png

image.png

image.png

计算二叉树的高度

对于计算二叉树的高度方法大致有两种:递归迭代

递归

很容易想到树的高度可以看做是取其左右子树高度的最大值进行加1操作,从根节点一路向下,直到节点为空

image.png

迭代

使用层序遍历,在遍历完每一层后高度进行+1操作,现在问题在于如何判定对于一层的节点全部遍历完成?当每一层遍历完毕的时候,此时队列中存在的节点就是下一层所有的节点,这个时机可作为每层节点遍历完成的时机。

image.png

判断是否为完全二叉树

image.png

完全二叉树的特点:

  • 对节点从上至下,从左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应
  • 叶子节点只会出现在后两层,最后一层的叶子节点都靠左对齐
  • 完全二叉树从根节点到倒数第二层是一棵满二叉树
  • 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树

思路:

  • 如果树为空,返回false

  • 如果树不为空,利用层序遍历

    1.如果左子树不为空,将左节点入队

    2.如果左子树为空,右子树不为空,返回false

    3.如果右子树不为空,将右节点入队

    4.如果右子树为空,不论左子树为不为空,之后遍历的节点都应该为叶子节点,否则返回false

代码实现:

image.png

翻转二叉树

image.png

递归

这里前序遍历,访问节点时对该节点进行翻转,中序遍历和后序遍历同理,此处不再赘述。 image.png

迭代

可以想到使用层序遍历来操作

image.png

根据遍历结果重构二叉树

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

  • 前序遍历+中序遍历
  • 后序遍历+中序遍历

image.png 若一棵树为真二叉树前序遍历+后序遍历重构的二叉树结果是唯一的,否则不唯一,因为无法分辨出左右子树

image.png

例如:

已知前序遍历:4、2、1、3、5、6,中序遍历:1、2、3、4、5、6

即可重构出唯一的二叉树: image.png

删除节点

在操作删除之前需要了解前驱节点后继节点

前驱节点

前驱节点:中序遍历时的前一个节点,若是二叉搜索树前驱节点就是前一个比它小的节点

image.png

既然是找前一个比它小的节点,看看是不是有左子树

  • 如果有左子树,就从左子树中找最大的
  • 如果没有左子树,那么前一个比它小的一定是父节点祖父节点...往上找,一直找到是左子树为止
  • 如果既没有左子树也没有父节点,那么这个节点没有前驱节点

image.png

后继节点

后继节点:中序遍历时的后一个节点,若是二叉搜索树后继节点就是后一个比它大的节点 image.png 既然是找后一个比它大的节点,看看是不是有右子树

  • 如果有右子树,就从右子树中找到最小的,例如图中:1、8、4
  • 如果没有右子树,如果父节点祖父节点一路向上找一旦发现处于左子树的分支,就在这条线上找到比它大的,例如:7、6、3、11
  • 如果没有右子树,也没有父节点,那就没有后继节点 实现思路基本等同于寻找前驱节点

image.png

根据被删除节点的情况可分为:叶子节点度为1的节点度为2的节点

删除节点为叶子节点

删除叶子节点很简单,直接删除即可,但要注意如果删除的是根节点的情况 image.png

删除节点是度为1的节点

image.png

用被删除节点的父节点指向被删除节点的子节点,注意左右子树,如果删除根节点记得分开处理

删除节点是度为2的节点

image.png

例如:删除5再删除4

  • 先用前驱节点或者后继节点的值覆盖原节点的值
  • 删除相应的前驱或者后继节点
  • 如果一个节点的度为2,则它的前驱或者后继节点度只能为0或者1

实现:

image.png