数据结构之二叉树(三)——二叉查找树

前言

大家都知道,链表适合需要频繁插入、删除数据的场景。但虽然说链表的插入、删除操作比数组性能好很多,但是在插入、删除之前仍需要从头遍历找到该元素,这同样是比较耗时的。因此,人们想到借助二分的方法优化链表的查找——二叉查找树登上舞台,提高数据插入和删除的效率。今天这篇博文带大家一起探讨一下二叉查找树的内容。本篇博文中的实现部分仍然沿用数据结构之二叉树(二)——二叉树遍历中定义的二叉树的数据结构。

定义和性质

二叉查找树(Binary Search Tree),又称为二叉排序树(Binary Sort Tree)。二叉查找树或者是一棵空树,或者是具有下列性质的二叉树:

  • 1)若左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 2)若右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;
  • 3)左、右子树也分别为二叉查找树;
  • 4)没有键值相等的节点。

下图二叉树便是一棵典型的二叉查找树。对二叉查找树进行中序遍历便可得到有序序列。

图1 - 一棵典型的二叉排序树

设计实现

查找元素

根据二叉查找树的性质,想要查找树中一个值,只需要从根节点开始查找,如果目标值小于根节点的值就在根节点的左子树中查找,否则就在根节点的右子树中查找;在子树的根节点仍然按照同样的原则进行查找。因此,这是一个递归过程。在理想情况下,每次比较过后,树会被砍掉一半,近乎折半查找。Java实现代码如下:

/**
 * 二叉查找树查找元素
 * @param root 二叉查找树的根节点
 * @param val 需要查找的目标值
 * @return
 */
public TreeNode search(TreeNode root, int val) {
    if (root == null) {
        return null;
    }
    if (val < root.val) {
        // 记录当前节点的父节点
        this.parent = root;
        return search(root.left, val);
    } else if (val > root.val) {
        // 记录当前节点的父节点
        this.parent = root;
        return search(root.right, val);
    } else {
        return root;
    }
}

需要说明的是parent是一个成员变量(public TreeNode parent = null;)。如果查找的目标元素在二叉查找树中,parent记录了目标节点的父亲节点;如果查找的目标元素不在二叉查找树中,parent则代表如果该目标元素在树中时该节点的父节点。这有助于向二叉查找树中增加新元素时快速定位插入位置。

增加元素

二叉排序树的插入是建立在二叉排序的查找之上的,原因很简单,添加一个节点到合适的位置,就是通过查找发现合适位置,把节点直接插入即可。Java实现代码如下:

/**
 * 增加新节点
 * @param root 二叉查找树的根节点
 * @param val 需要增加的目标值
 * @return
 */
public TreeNode insert(TreeNode root, int val) {
    // 由于性质4,因此插入之前先判断该值是否存在
    TreeNode node = search(root, val);
    // 该节点不存在,在合适位置插入该节点
    if (node == null) {
        TreeNode newNode = new TreeNode(val);
        if (parent == null){
            root = newNode;
        } else if (val < parent.val){
            parent.left = newNode;
        } else if (val > parent.val){
            parent.right = newNode;
        }
    }
    return root;
}

删除元素

在二叉查找树删去一个节点,分三种情况讨论:

  • 若目标节点O为叶子节点,即OL(左子树)和OR(右子树)均为空树。由于删去叶子节点不破坏整棵树的结构,则只需修改其双亲节点的指针即可。
  • 若目标节点O只有左子树OL或右子树OR,此时只要令OL或OR直接成为其父节点P的左子树(当O只有左子树时)或右子树(当O只有右子树时)即可,作此修改也不破坏二叉查找树的特性。
  • 若目标节点O的左子树和右子树均不空。在删去目标节点O之后,为保持其它元素之间的相对位置不变,可按中序遍历保持有序进行调整,可以有两种做法:
    (1) 令O的左子树为其父节点P的左/右子树(依O是P的左子树还是右子树而定),节点S为O左子树的最右下的节点,而O的右子树为P的右子树;
    (2) 令O的直接前驱(in-order predecessor)或直接后继(in-order successor)替代O,然后再从二叉查找树中删去它的直接前驱(或直接后继)。

由于前两种情况比较直观,因此这里只着重说明一下第3中情况。以图1中的二叉树为例,想要删除节点8(例子中,我们使用直接前驱替换O,然后再删除O的直接前驱策略,当然使用直接后继替换也是可以的)。

image

首先,找到该节点的左子树最右下的节点7,因为节点7是节点的直接前继
image

最后,用节点7代替节点8,然后删除节点7即可。


image

实现代码如下:

/**
 * 删除节点
 *
 * @param root 二叉查找树的根节点
 * @param val  需要删除的目标值
 * @return
 */
 public TreeNode delete(TreeNode root, int val) {
    if (root == null) {
        return root;
    }

    // 首先搜索该节点,如果不存在直接返回false
    TreeNode o = search(root, val);
    if (o == null) {
        return root;
    }

    if (o.left == null && o.right == null) {
        // 情况1:如果该节点为叶子节点,直接删除即可
        // o为左节点
        if (parent.left == o) {
            parent.left = null;
        } else {  // o为右节点
            parent.right = null;
        }
    } else if (o.right == null) {
        // 情况2-1:如果该节点只有左子树
        // o为左节点
        if (parent.left == o) {
            parent.left = o.left;
        } else {  // o为右节点
            parent.right = o.left;
        }
    } else if (o.left == null) {
        // 情况2-2:如果该节点只有右子树
        // o为左节点
        if (parent.left == o) {
            parent.left = o.right;
        } else {  // o为右节点
            parent.right = o.right;
        }
    } else {
        // 情况3:该节点既有左子树,又有右子树
        TreeNode q = o;
        TreeNode s = o.left;
        while (s.right != null) {
             q = s;
             s = s.right;
        } // 转左,然后向右到尽头
        // s指向被删节点的“前驱”
        o.val = s.val;
        if (q != o) {
             // 重接q的右子树
             q.right = s.left;
        } else {
             // 重接q的左子树
             q.left = s.left;
        }
    }
    return root;
}

完整代码

import java.util.LinkedList;

/**
 * @Author: Jeremy
 * @Date: 2019/6/22 20:30
 */
public class BinarySearchTree {
    public TreeNode parent = null;

    /**
     * 二叉查找树查找元素
     *
     * @param root 二叉查找树的根节点
     * @param val  需要查找的目标值
     * @return
     */
    public TreeNode search(TreeNode root, int val) {
        if (root == null) {
            return null;
        }
        if (val < root.val) {
            // 记录当前节点的父节点
            this.parent = root;
            return search(root.left, val);
        } else if (val > root.val) {
            // 记录当前节点的父节点
            this.parent = root;
            return search(root.right, val);
        } else {
            return root;
        }
    }

    /**
     * 增加新节点
     *
     * @param root 二叉查找树的根节点
     * @param val  需要增加的目标值
     * @return
     */
    public TreeNode insert(TreeNode root, int val) {
        // 由于性质4,因此插入之前先判断该值是否存在
        TreeNode node = search(root, val);
        if (node == null) {
            TreeNode newNode = new TreeNode(val);
            if (parent == null) {
                root = newNode;
            } else if (val < parent.val) {
                parent.left = newNode;
            } else if (val > parent.val) {
                parent.right = newNode;
            }
        }
        // 返回根节点
        return root;
    }

    /**
     * 删除节点
     *
     * @param root 二叉查找树的根节点
     * @param val  需要删除的目标值
     * @return
     */
    public TreeNode delete(TreeNode root, int val) {
        if (root == null) {
            return root;
        }

        // 首先搜索该节点,如果不存在直接返回false
        TreeNode o = search(root, val);
        if (o == null) {
            return root;
        }

        if (o.left == null && o.right == null) {
            // 情况1:如果该节点为叶子节点,直接删除即可
            // o为左节点
            if (parent.left == o) {
                parent.left = null;
            } else {  // o为右节点
                parent.right = null;
            }
        } else if (o.right == null) {
            // 情况2-1:如果该节点只有左子树
            // o为左节点
            if (parent.left == o) {
                parent.left = o.left;
            } else {  // o为右节点
                parent.right = o.left;
            }
        } else if (o.left == null) {
            // 情况2-2:如果该节点只有右子树
            // o为左节点
            if (parent.left == o) {
                parent.left = o.right;
            } else {  // o为右节点
                parent.right = o.right;
            }
        } else {
            // 情况3:该节点既有左子树,又有右子树
            TreeNode q = o;
            TreeNode s = o.left;
            while (s.right != null) {
                q = s;
                s = s.right;
            } // 转左,然后向右到尽头
            // s指向被删节点的“前驱”
            o.val = s.val;
            if (q != o) {
                // 重接q的右子树
                q.right = s.left;
            } else {
                // 重接q的左子树
                q.left = s.left;
            }
        }
        return root;
    }

    /**
     * 获取节点个数
     *
     * @param root
     * @return
     */
    public int size(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return size(root.left) + 1 + size(root.right);
    }

    /**
     * 获取二叉查找树的高度
     *
     * @param root
     * @return
     */
    public int height(TreeNode root) {
        if (root == null) {
            return 0;
        }

        int leftHeight = height(root.left) + 1;
        int rightHeight = height(root.right) + 1;
        return leftHeight > rightHeight ? leftHeight : rightHeight;
    }

    /**
     * 获取最大值
     * @param root
     * @return
     */
    public int max(TreeNode root){
        if (root == null){
            return 0;
        }
        if (root.right == null){
            return root.val;
        }

        return max(root.right);
    }

    /**
     * 获取最小值
     * @param root
     * @return
     */
    public int min(TreeNode root){
        if (root == null){
            return 0;
        }
        if (root.left == null){
            return root.val;
        }

        return min(root.left);
    }

    /**
     * 中序遍历二叉树,非递归
     *
     * @param root
     */
    public void inOrderTraverseNoRecursion(TreeNode root) {
        LinkedList<TreeNode> stack = new LinkedList<TreeNode>();
        TreeNode currentNode = root;
        while (currentNode != null || !stack.isEmpty()) {
            // 一直循环到二叉树最左端的叶子结点(currentNode是null)
            while (currentNode != null) {
                stack.push(currentNode);
                currentNode = currentNode.left;
            }
            currentNode = stack.pop();
            System.out.print(currentNode.val + " ");
            currentNode = currentNode.right;
        }
        System.out.print("\n");
    }

    public static void main(String[] args) {
        TreeNode a = new TreeNode(8);
        TreeNode b = new TreeNode(3);
        TreeNode c = new TreeNode(10);
        TreeNode d = new TreeNode(1);
        TreeNode e = new TreeNode(6);
        TreeNode f = new TreeNode(14);
        TreeNode g = new TreeNode(4);
        TreeNode h = new TreeNode(7);
        TreeNode i = new TreeNode(13);

        a.left = b;
        a.right = c;

        b.left = d;
        b.right = e;

        c.right = f;

        e.left = g;
        e.right = h;

        f.left = i;

        BinarySearchTree binarySearchTree = new BinarySearchTree();

        // 中序遍历
        binarySearchTree.inOrderTraverseNoRecursion(a);

        // 查询节点
        TreeNode node = binarySearchTree.search(a, 14);
        System.out.println(node.val);

        // 插入节点
        TreeNode root = binarySearchTree.insert(a, 9);
        System.out.println(c.left.val);
        // 中序遍历
        binarySearchTree.inOrderTraverseNoRecursion(root);

        // 删除节点
        root = binarySearchTree.delete(root, 8);
        binarySearchTree.inOrderTraverseNoRecursion(root);

        // 获取节点个数
        int size = binarySearchTree.size(root);
        System.out.println(size);

        // 获取树高度
        int height = binarySearchTree.height(root);
        System.out.println(height);

        // 获取最小值
        int min = binarySearchTree.min(root);
        System.out.println(min);

        // 获取最大值
        int max = binarySearchTree.max(root);
        System.out.println(max);
    }
}

效率分析

(1) 查找代价:任何一个数据的查找过程都需要从根结点出发,沿某一个路径朝叶子结点前进。因此查找中数据比较次数与树的形态密切相关。

  • 当树中每个节点左右子树高度大致相同时,树高为logN。则平均查找长度与logN成正比,查找的平均时间复杂度在O(logN)数量级上。
  • 当先后插入的关键字有序时,BST退化成单支树结构。此时树高n。平均查找长度为(n+1)/2,查找的平均时间复杂度在O(N)数量级上。如图所示。


    退化了的二叉查找树

    由此看来,二叉查找树的结构会决定查找的效率。这也是后来要讲的自平衡二叉查找树主要解决的问题。

(2) 插入代价:新节点插入到树的叶子上,完全不需要改变树中原有结点的组织结构。插入一个节点的代价与查找一个不存在的数据的代价完全相同。
(3) 删除代价:当删除一个节点P,首先需要定位到这个结点P,这个过程需要一个查找的代价。然后稍微改变一下树的形态。如果被删除节点的左、右子树只有一个存在,则改变形态的代价仅为O(1)。如果被删除节点的左、右子树均存在,只需要将当P的左孩子的最右下的叶子结点与P互换,再改变一些左右子树即可。因此删除操作的时间复杂度最大不会超过O(logN)。

总结

本篇博文主要介绍了二叉查找树的定义和性质,使用Java对二叉查找树进行了实现,并且对二叉查找树的查找、增加和删除效率进行了分析,希望能对大家有所帮助,也欢迎大家积极留言,批评指正。
接下来,我会继续介绍二叉查找树的优化方案——自平衡二叉查找树,包括AVL树和红黑树,敬请期待~

推荐阅读

写在最后

欢迎大家关注我的个人博客复旦猿

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,624评论 0 13
  • 树形结构是一种非常重要的非线性的数据结构。树结构与线性结构不同之处:线性结构中任意一个元素最多只有一个后继元素,而...
    zgwyvd阅读 2,250评论 0 7
  • 目录 1、什么是树 2、相关术语 3、二叉树 3.1、二叉树的类型 3.2、二叉树的性质 3.3、二叉树的结构 3...
    我哈啊哈啊哈阅读 2,523评论 0 10
  • 引言 前面提到的顺序表和单链表对于元素的访问都有自己的局限性,而二叉树的结构兼具完美解决了二者的访问元素效率问题,...
    kakaxicm阅读 709评论 1 1
  • 谈起钱,剩下的话就多了起来。 也许,生活费不够,是很多大学生共同的死穴。 开学第三个月,囊中羞涩,我对我的父亲说:...
    大苏少年_阅读 16,562评论 114 277