原来如此-二分搜索树

1,211 阅读9分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

二分搜索树

一些基本的定义

  • 每个节点最多只能有两个孩子节点,也可以没有孩子节点(也就是叶子节点)
  • 每个节点的左孩子都小于该节点
  • 每个节点的右孩子都大于该节点
  • 每个节点只有一个父亲节点,根节点没有父亲节点

二叉树示例

image.png

  • 文中,左孩子,左子树都是一个意思,右孩子,右子树也是一样。

二叉树的基本操作

通常来说对于一个二叉树我们会对其进行增加、查询、遍历、删除四种操作。

增加节点

核心思想 如果二叉树为空,则根节点等于新增元素就结束了。如果二叉树不为空,从根节点开始开始与插入元素进行比较,插入元素大于根节点则向根节点的右孩子进行比较,插入元素小于根节点则向根节点的左孩子进行比较,以此类推,直到找到与该元素比较的节点的左孩子或者右孩子为空(null)的时候,就是插入元素正确的位置。

【讲人话】:只要找到自己的位置就行了,因此,既然知道二叉搜索树的规则是每个节点只能有两个儿子,左边的比自己小,右边的比自己大,那就从根(root)开始对比,对比的值比自己大我就往左子树找自己的位置,比自己小那我就往右子树找自己的位置,当你发现下一个跟你对比的是个空的(null),那这就是你的位置了,直接坐下。

image.png

代码实现

首先编写二叉树的基本属性

  • E为存储在树中的元素,我们用范型表示
  • 因为需要比较元素的大小,所以我们需要指定E必须是继承了Comparable接口的对象
  • 内部类Node是我们的节点类,他有一个E表示存储的值,left,right表示左孩子、右孩子
public class BST<E extends Comparable<E>> {

    private class Node {
        public E e;
        public Node left, right;

        public Node(E e) {
            this.e = e;
            this.left = null;
            this.right = null;
        }

    }
    // 根节点
    private Node root;
    // 树的大小
    private int size;

    public BST() {
        root = null;
        size = 0;
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }
    
    // 查询树是否存在接口 
    public boolean contains(E e) {
        return contains(e);
    }
}

添加方法代码实现


public void add(E e) {
    // 一定要将root更新为新增节点之后返回的树
    root = addNode(root, e);
}

/**
 * 递归添加元素e到node中
 *
 * @param node
 * @param e
 * @return
 */
private Node addNode(Node node, E e) {
    // 当节点的值为空的时候 表示递归到底了,也就找到了元素的位置 
    if (node == null) {
        size++;
        return new Node(e);
    }
    // 如果元素小于当前节点,则继续递归比较该节点的左子树
    if (e.compareTo(node.e) < 0) {
        node.left = addNode(node.left, e);
    } else if (e.compareTo(node.e) > 0) { // 如果元素大于当前节点,则继续递归比较该节点的右子树
        node.right = addNode(node.right, e);
    }
    return node;
}

查询节点

核心思想 从根节点(root)开始递归查询,如果查询的值比递归到的当前节点大,就向当前节点右子树查询,如果查询的值比递归到的当前节点小,就向当前节点左子树查询,直到递归到的节点值与查询的值相等,即返回true,如果递归完整颗树,都没有,则返回false

【讲人话】:跟新增找自己的位置如出一辙,从根节点开始对比,比自己大往左子树找,比自己小往右子树找,一直对比,无非两个节点,如果发现跟自己对比的节点是个null,说明没有我,直接然后false,如果跟自己一样大,嘿,你猜怎么着,返回true呗。

代码实现
// 查询节点
public boolean contains(E e) {
    return contains(root, e);
}

// 递归函数
private boolean contains(Node node, E e) {
    if (node == null) {
        return false;
    }
    // 当前节点等于查询的值 return true
    if (e.compareTo(node.e) == 0) return true;
    // 当前节点的值大于查询的值 向当前节点的左子树继续递归查询
    else if (e.compareTo(node.e) < 0) return contains(node.left, e);
    // 当前节点的值小于查询的值 向当前节点的右子树继续递归查询
    else return contains(node.right, e);
}

树的遍历

想想树的遍历应该怎么进行了,遍历无非就是把怎个树里面的值打印出来,保证每一个都打印,不丢失任何一个就完事儿了。

核心思想 从根节点开始首先打印根节点自己,然后打印根节点的左子树,然后再打印根节点右子树。

代码实现

// 递归打印整棵树
private void printTree() {
    printTree(root);
}

// 递归函数肯定就长这样 传入一个节点,打印节点的所有值,打印整棵树就传根节点root就行了
private void printTree(Node node) {
    // 递归终止条件: 如果传入节点为空就表示节点以及到底了,就结束递归
    if (node == null) return;
    // 既然不为空就打印节点的值
    System.out.println(node.e);
    // 继续打印这个节点的左子树
    printTree(node.left);
    // 左子树打印完了就打印右子树 
    printTree(node.right);
}

有一个二叉树长这样

image.png

那么打印结果就是

image.png

常见的遍历方式

树的常见遍历方式有四种,你可能听说过,就是前序遍历中序遍历后续遍历层序遍历四种,是不是很耳熟,是不是很高大上,我上大学的时候,数据结构学的那叫一点不会,考试遇到给定一个二叉树,让我写出前序遍历我是直接懵逼,前序遍历是什么鬼,是不是需要计算? 感觉好难啊,考试之前都会找牛逼的人求情,待会给我抄抄,哈哈,但当我毕业了自己在回过来看,这啥玩意,这也能叫期末考试题?? OK, 屁话先不说了,开始.....

假如现在有一个树长下面这个样子,我们给每一个节点都标上1,2,3三个序号,分别在前,中,后三个位置。

image.png

图1

我们再来看看遍历的函数

// 我们可以看一下 遍历一个节点我们会遇到这个节点三次 分别是下面的1,2,3 就是对应【图1】的1,2,3
private void printTree(Node node) {

    if (node == null) return;
    // 1
    System.out.println(node.e);
    printTree(node.left);
    // 2
    printTree(node.right);
    // 3
}

好了下面开始拿捏前序,中序,后续遍历...

前序遍历

所谓前序遍历就是在1的位置打印节点的值,简单吧,上面我们写的打印方法其实就是前序遍历。

中序遍历

所谓中序遍历就是在2的位置打印节点的值,那么中序遍历的代码就很简单了

private void printTree(Node node) {

    if (node == null) return;
    // 1
    printTree(node.left);
    // 2
    System.out.println(node.e);
    printTree(node.right);
    // 3
}

中序遍历打印上面的值

image.png 可以看到中序遍历的值是从小到大进行排序的,所以二分搜索树其实也可以用来进行排序。哦哟,小技巧,拿本本记好!

后序遍历

那么后序遍历就自然是在3的位置打印节点的值了,这就是后序遍历。

private void printTree(Node node) {

    if (node == null) return;
    // 1
    printTree(node.left);
    // 2
    printTree(node.right);
    // 3
     System.out.println(node.e);
}

是不是突然发现,前序,中序,后续遍历也就这个样子,我要是早点用心学,也不至于考试的时候乞求别人给我看看,害。

删除节点

在二分搜索树的新增,查询,遍历,删除的几个操作中,删除是相对比较麻烦的,为啥呢,因为删除一个节点你需要维护整棵树,需要保证二分搜索树的性质不会被破坏,那么删除节点就会遇到一下四种可能的情况,每种情况对应的处理方式也各不一样。

待删除的节点的左右孩子都为空的

这种情况只需要将待节点删除就行

image.png

待删除节点的左孩子不为空,右孩子为空

这种情况只需要将该节点的左孩子代替该节点即可

image.png

待删除节点的右孩子不为空,左孩子为空

处理方式跟前一种一样,将该节点的右孩子代替该节点即可

image.png

待删除节点的左右孩子均不为空

此种情况是四种情况中最麻烦的一种,因为需要删除后维护二分搜索树的性质,因此比较麻烦,通常的处理方式有两种

  1. 找到被删除节点的后继节点中最小值代替被删除节点,被删除节点的左子树成为最小值的左子树,被删除节点的右子树删除最小值后成为最小值的右子树

image.png

  1. 找到被删除节点的前驱节点中最大值代替被删除节点,被删除节点的右子树成为最大值的右子树,被删除节点的左子树删除最大值后成为最大值的左子树

image.png

代码实现 首先删除节点的第四种情况,我们需要找到一棵树或者是子树的的最大值,或者最小值,因为我们知道二分搜索树的性质,一棵树中最小值一定是在树的最右下侧,即一直递归一课树的左子树,直到某个节点的左孩子为null的时候,该节点就是最小值。最大值与最小值相反,一直递归一棵树的右子树,直到某个节点的右孩子为null的时候,该节点就是该树最大值,如图 image.png 代码如下:

// 递归查询最小值
private Node findMinNodeNR(Node e) {
    if (e.left == null) {
        return e;
    } else {
        return findMinNodeNR(e.left);
    }
}
// 递归查询最大值
private Node findMaxNodeNR(Node e) {
    if (e.right == null) {
        return e;
    } else {
        return findMinNodeNR(e.right);
    }
}

同时找到了子树的最小值最大值,还要有相应的删除方法,删除方法也很简单,直接将对应节点设置为null就行了, 代码如下...

// 递归删除最小值
private Node removeMin(Node e) {
    if (e.left == null) {
        size--;
        return null;
    }
    e.left = removeMin(e.left);
    return e;
}

private Node removeMax(Node e) {
    if (e.right == null) {
        size--;
        return null;
    }
    e.right = removeMax(e.right);
    return e;
}

因此最后删除节点的完整代码如下:

public void remove(E e) {
    root = remove(root, 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 {
        // 当被删除节点左孩子为空的时候,用被删除节点的右孩子代替被删除节点
        if (node.left == null) {
            node = node.right;
            size--;
            return node;
        }
        // 当被删除节点右孩子为空的时候,用被删除节点的左孩子代替被删除节点
        if (node.right == null) {
            node = node.left;
            size--;
            return node;
        }
        // 当被删除节点左右孩子都不为空的时候,有两种方法
        // 1.找到被删除节点的后继节点中最小值代替被删除节点,被删除节点的左子树成为最小值的左子树,被删除节点的右子树删除最小值后成为最小值的右子树
        // 2.找到被删除节点的前驱节点中最大值代替被删除节点,被删除节点的右子树成为最大值的右子树,被删除节点的左子树删除最大值后成为最大值的左子树
        Node successor = findMinNodeNR(node.right);// 找到后继节点中的最小值
        successor.right = removeMin(node.right);// 删除后继节点中的最小值 
        successor.left = node.left;// 被删除节点的左子树成为最小值的左子树 并成为最小值的右子树
        node.left = node.right = null;
        return successor;
    }
}

好啦,该说的也都差不多说完了! 加油!

结束语

写文章是对自己学习成功的输出与记录,看到这二叉搜索树的基本概念跟基本操作就已经讲完了,纯手打,纯手画图,这是我在掘金的第一篇文章,如有不对的地方还请各位大佬指出来,最后,感谢你的浏览! bye-bye ! 下一篇原来如此-二叉堆再见!

种一棵树最好的时间是20年前,其次是现在!