2w字长篇 手把手带你彻底搞懂二叉树(三)

简介: 2w字长篇 手把手带你彻底搞懂二叉树(三)

二叉搜索树

概念

二叉搜索树(BST)满足的性质:


每个节点中的值必须大于(或等于)存储在其左子树中的任何值。 每个节点中的值必须小于(或等于)存储在其右子树中的任何值。


一个二叉搜索树的例子如下所示:

2.png


二叉搜索树的操作—查找

核心思想

2.png


二叉搜索树的操作—插入

核心思想

1:此时我们想向如下的二叉树插入一个元素:

2.png

2:那么我们就需要定义一个cur变量指向我们二叉树的根节点,然后定义一个空的指针parent

3:每次插入的时候判断我们所要插入的元素与我们当前cur所指向的元素的大小,如果比我们cur所指向的元素大,那么先让parent指向我们的cur,然后让cur指向我们的cur.right,如果比我们cur所指向的元素小,那么先让parent指向我们的cur,然后让cur指向我们的cur.left

4:最终我们的cur一定会等于null,因为此时我们的cur一定会走到这一步,那么此时当cur等于null的时候,就要判断我们所想要插入的元素的位置了,如果所要插入的元素比我们此时parent指针所指向的元素大,就将这个元素插入到parent.right,否则插入到parent.left.


二叉搜索树的操作—删除(难点)

核心思想

设待删除结点为 cur, 待删除结点的双亲结点为 parent

二叉搜索树的删除分为以下几种大情况,每个大情况下又有几种小情况.

1. cur.left == null

(1) cur 是 root,则 root = cur.right

2.png


假设此时要删除的是cur所指向的5这个节点,因为cur的左子树为null,所以直接让根节点指向cur的右子树即可.即root =cur.right

2.png


(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.right

2.png


此时我们的cur是parent.left,并不是root,并且cur的左子树为空,parent此时也指向我们cur的父节点,此时若要删除我们的cur所指向的3这个数字的话,可直接让parent.left = cur.right即可

2.png


(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.right

2.png


此时我们的cur是root.right,并不是root,并且cur的左子树为空,parent此时也指向我们cur的父节点,此时若要删除我们的cur所指向的7这个数字的话,可直接让parent.right=cur.right即可

2.png


cur.right == null

(1) cur 是 root,则 root = cur.left

2.png

假设此时要删除的是cur所指向的5这个节点,因为cur的右子树为null,所以直接让根节点指向cur的左子树即可.即root=cur.left

2.png

(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.left

2.png


此时我们的cur是root.left,并不是root,并且cur的右子树为空,parent此时也指向我们cur的父节点,此时若要删除我们的cur所指向的3这个数字的话,可直接让parent.left= cur.left即可

2.png

(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.left

2.png


此时我们的cur是parent.right,并不是root,并且cur的左子树不为空,parent此时也指向我们cur的父节点,此时若要删除我们的cur所指向的7这个数字的话,可直接让parent.right=cur.left即可

2.png


cur.left != null && cur.right != null

2.png

此时我们的cur指向所要删除的点,也就是我们的20,parent指向当前cur的父亲节点。那么问题来了:

当我们将20删除后,到底放哪个点到cur这个位置呢?

我们发现可以放的点有两个:一个是12,一个是25,这两个分别是20左子树中最大的值,25是20右子树中最小的值

所以我们总结出来就是当我们需要找将谁放上去的时候,在当前需要删除的节点的左边找最大的,在右边找最小的.

而左边最大的值一定在左子树的最右边,右边最小的值一定在右子树的最左边

此时还有一个问题:当我们像之前那样删除,那么一个节点就会出现两个父亲节点,例如我们将20删除掉后,此时假设12顶替了20,那么12既有一个父亲是9这个节点,顶替了20后,还会有一个新的父亲也就是我们parent所指向的8这个节点.此时该怎么办呢?

答:

此时我们采取替罪羊的删除方式

当我们在右子树找最左边的元素的时候,同样也有两种情况:

情况1:还是先来看刚才的图:

2.png

我们现在找到25就是我们想要替换到20处的点,所以此时我们就让25来当这个替罪羊:

2.png


首先将25替换到20处,注意此时如果我们的25是最终要替换到20的地方,就说明25这个点的左子树一定为null,因为我们25在20的右子树,需要替换20的元素一定是20的右子树中最左边的元素,很显然,假如25还有左子树,去替换20的一定是25的左子树了,哪还能轮得到我们的25自己呢,但是25这个节点的右子树就未必是空了,所以最终我们可以将25的右子树作为我们29的左子树

当然还要注意的是我们再将25的右子树作为我们29的左子树的时候,需要获取到25的父节点29的位置,我们将替罪羊25命名为target,将替罪羊的父亲命名为tp(targetParent).


情况2:

当然也有可能我们20的右子树中是没有左子树的,如下图所示:

2.png


此时需要去替代20的就变成了我们的替罪羊29,替换完成后再将右子树指向29原先的右子树.


所有操作代码综合

注意此处我们拿内部类来实现我们的代码:

class BinarySearchTree {
    static class BSNode {
        public int val;
        public BSNode left;
        public BSNode right;
        public BSNode(int val) {
            this.val = val;
        }
    }
    public BSNode root = null;
    public BSNode search(int val) {
        if (root == null) {
            return null;
        }
        BSNode cur = root;
        while (cur != null) {
            if (cur.val == val) {
                return cur;
            } else if (cur.val < val) {
                cur = cur.right;
            } else {
                cur = cur.left;
            }
        }
        return null;
    }
    public boolean insert(int val) {
        BSNode bsNode = new BSNode(val);
        if (root == null) {
            root = bsNode;
            return true;
        }
        BSNode cur = root;
        BSNode parent = null;
        while (cur != null) {
            //注意我们在二叉搜索树中插入的元素不能与已知二叉树的元素相同.
            if (cur.val == val) {
                return false;
            } else if (cur.val < val) {
                parent = cur;
                cur = cur.right;
            } else {
                parent = cur;
                cur = cur.left;
            }
        }
        //这块是当cur为null的时候,此时就要插入我们的元素了
        if (parent.val < val) {
            parent.right = bsNode;
        } else {
            parent.left = bsNode;
        }
        return true;
    }
    public void remove(int val) {
        //假设整棵二叉搜索树都为空的话,直接结束
        if (root == null) {
            return;
        }
        //使用cur来遍历寻找我们所要删除的节点
        BSNode cur = root;
        //父亲节点parent
        BSNode parent = null;
        while (cur != null) {
            if (cur.val == val) {
                //此时我们使用removeNode方法来进行删除操作,这样的模块化设计代码看起来更加清晰,并且二叉搜索树的删除操作比较繁琐,需要一个单独的方法
                removeNode(parent, cur, val);
                //删除完成直接return走人
                return;
            } else if (cur.val < val) {
                //让parent去记录每个节点的父节点
                parent = cur;
                //小于val就说明我们所要删除的值在我们当前节点的右边
                cur = cur.right;
            } else {
                //让parent去记录每个节点的父节点
                parent = cur;
                大于val就说明我们所要删除的值在我们当前节点的左边
                cur = cur.left;
            }
        }
    }
    /**
     * 这个方法中的参数第一个代表当前所有需要删除节点的父类,第二个是需要删除的节点,第三个是要删除的节点的值
     *
     * @param parent
     * @param cur
     * @param val
     */
    private void removeNode(BSNode parent, BSNode cur, int val) {
        //第一种大情况:cur.left = null
        if (cur.left == null) {
            if (cur == root) {
                root = cur.right;
            } else if (parent.left == cur) {
                parent.left = cur.right;
            } else {
                parent.right = cur.right;
            }
            //第二种大情况:cur.right = null
        } else if (cur.right == null) {
            if (cur == root) {
                root = cur.left;
            } else if (parent.left == cur) {
                parent.left = cur.left;
            } else {
                parent.right = cur.left;
            }
        } else {
            //这块的else就是第三种情况了:cur.left != null && cur.right != null
            //首先让我们替罪羊的父亲指向cur
            BSNode targetParent = cur;
            //因为我们想让即将被删除的节点的右子树中的最小值作为替罪羊,所以此处我们就让替罪羊先指向右子树
            BSNode target = cur.right;
            //开始让下遍历寻找右子树中的最小值
            while (target.left != null) {
                //每走一步之前想让父亲节点指向我们target所在的位置
                targetParent = target;
                //然后让target一直往左下走
                target = target.left;
            }
            //走到这里就代表target.left = null了,说明target此时指向的节点就是 右边的最小值
            //然后将target所指向的值赋给我们的cur,也就是替换掉当前要删除掉的值
            cur.val = target.val;
            //下面就是分的那两种情况
            //第一种情况是我们要删除节点的右子树含有左子树
            if (target == targetParent.left) {
                targetParent.left = target.right;
                //下面的是我们所删除的节点的右子树不含有左子树的时候
            } else {
                targetParent.right = target.right;
            }
        }
    }
}
public class TestDemo {
    //注意这里的传参方式
    public static void preOrder(BinarySearchTree.BSNode root) {
        if (root == null) {
            return;
        }
        System.out.print(root.val + " ");
        preOrder(root.left);
        preOrder(root.right);
    }
    public static void inOrder(BinarySearchTree.BSNode root) {
        if (root == null) {
            return;
        }
        inOrder(root.left);
        System.out.print(root.val + " ");
        inOrder(root.right);
    }
    public static void main(String[] args) {
        /*
        以下代码是对我们二叉搜索树的插入,删除,搜索操作进行测试的
         */
        System.out.println("插入操作测试....................................");
        BinarySearchTree binarySearchTree = new BinarySearchTree();
        binarySearchTree.insert(4);
        binarySearchTree.insert(3);
        binarySearchTree.insert(1);
        binarySearchTree.insert(15);
        binarySearchTree.insert(11);
        //对我们的二叉搜索树进行前序遍历
        System.out.println("前序遍历结果为:");
        preOrder(binarySearchTree.root);
        System.out.println();
        //对我们的二叉搜索树进行中序遍历
        System.out.println("中序遍历结果为:");
        inOrder(binarySearchTree.root);
        System.out.println();
        System.out.println("查找操作测试......................................");
        try {
            BinarySearchTree.BSNode ret = binarySearchTree.search(4);
            //如果搜索到了当前的4这个节点,就直接输出
            System.out.println("找到"+ret.val+"这个节点啦");
        } catch (NullPointerException e) {
            //如果没有找到这个节点,就输出没有找到的话
            System.out.println("没有找到当前的节点............");
            e.printStackTrace();
        }
        System.out.println("删除操作测试....................................");
        binarySearchTree.remove(15);
        //对我们的二叉搜索树进行前序遍历
        System.out.println("前序遍历结果为:");
        preOrder(binarySearchTree.root);
        System.out.println();
        //对我们的二叉搜索树进行中序遍历
        System.out.println("中序遍历结果为:");
        inOrder(binarySearchTree.root);
        System.out.println();
    }
}

性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

2.png

最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: log2N

也就是二叉树的高度,因为在二叉搜索树中查找相当于是二分查找,大了去右子树,小了去左子树,查找的效率与二叉树的高度有关.例如查找8的时间复杂度为O(log2N)

最差情况下,二叉搜索树退化为单支树,其平均比较次数为: N/2

例如查找8的时间复杂度最好为O(1),也就是一开始就找到了,最坏为O(N)


和 java 类集的关系

TreeMap 和 TreeSet 即 java 中利用搜索树实现的 Map 和 Set;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证,关于红黑树的内容后序再进行讲解。


练习题

1:二叉搜索树转换成排序双向链表 OJ链接

2:验证是否为二叉搜索树 OJ链接

相关文章
|
6月前
|
算法
【数据结构与算法 经典例题】反转链表(图文详解)
【数据结构与算法 经典例题】反转链表(图文详解)
|
7月前
|
算法 测试技术
数据结构与算法⑤(第二章OJ题,上)前五道链表面试题(下)
数据结构与算法⑤(第二章OJ题,上)前五道链表面试题
37 0
|
存储 人工智能 算法
2022 数据结构与算法《王道》学习笔记 (十)串 KMP算法 串的总结 课后习题笔记
2022 数据结构与算法《王道》学习笔记 (十)串 KMP算法 串的总结 课后习题笔记
|
存储 C语言
C语言指针笔试真题整理(8道)(上)
C语言指针笔试真题整理(8道)
82 0
|
存储 C语言 C++
C语言指针笔试真题整理(8道)(下)
C语言指针笔试真题整理(8道)(下)
101 0
|
缓存 算法 C语言
【数据结构与算法篇】栈与队列(详解)附加Leetcode经典笔试题
【数据结构与算法篇】栈与队列(详解)附加Leetcode经典笔试题
62 0
【初阶C语言】有关的经典题型内含数组及递归函数题型讲解(入门适用)(二)
【初阶C语言】有关的经典题型内含数组及递归函数题型讲解(入门适用)(二)
【初阶C语言】有关的经典题型内含数组及递归函数题型讲解(入门适用)(一)
【初阶C语言】有关的经典题型内含数组及递归函数题型讲解(入门适用)(一)
|
存储 测试技术 索引
【数据结构初阶】(栈和队列)图文详解四道oj+三道easy概念题
【数据结构初阶】(栈和队列)图文详解四道oj+三道easy概念题
|
C语言 索引
C语言中链表经典面试题目
环形链表 环形链表 II