【数据结构】二分搜索树

什么是树?

在现实生活中有很多能体现出树的逻辑的例子。

例如:企业里的职位关系,也是一颗树。

树—例子1.png

再例如:操作系统的文件夹目录,也是一颗树。

树-例子2.png

那么以上的这些例子有什么共同点呢?为什么称它们为"树"呢?

因为它们都像自然界中的树一样,从同一个"根"衍生出许多的"枝干",再从每一个"枝干"衍生出许多更小的"枝干",最后衍生更多的"叶子"。

在数据结构中,树的定义如下:

树是n个节点的有限集。当n=0时,称为空树,在任意一个非空树中,有如下特点。

  • 有且仅有一个特定的称为根的节点。
  • 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一棵树,并称为根的子树。

标准树存储结构如图:

树结构.png

在上图中,节点1是根节点,节点4、5、6是树的末端,并且没有“孩子”,称为叶子节点。图中虚线的部分是一颗子树。

同时,树结构从根节点到叶子节点,分为不同的层级。从一个节点的角度来看,它的上下级和同级节点关系如下:

树结构2.png

在上图中,节点4的上一级节点,是节点4的父节点,从节点2衍生出来的节点,是节点2的孩子节点。和节点2同级,由同一个父节点衍生出来的节点是节点2的兄弟节点。

树的最大层数,被称为树的高度或深度。显然,上图中的这个数的高度为3。

什么是二叉树?

二叉树是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有两个孩子节点。注意,这里是最多有两个,也有可能有一个,或没有孩子节点。

二叉树的结构如图:

二叉树结构.png

二叉树节点的两个孩子节点,一个被称为左孩子,一个被称为右孩子。这两个孩子节点的顺序是固定的,就像人的左手与右手一样。

此外,二叉树还有两种特殊的形式。如下:

  • 满二叉树

    那么什么是满二叉树呢?

    一个二叉树的所有非叶子节点都存在左孩子与右孩子,并且所有的叶子节点都在同一层级上,那么这个树就是满二叉树。

    如图:

    满二叉树.png

    简单点说,满二叉树的每个节点的左右孩子都不为空。

  • 完全二叉树

    那么什么是完全二叉树呢?

二叉树1.png

对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个数的所有节点和同样深度的满二叉树的编号从1到n的节点位置相同,则这个二叉树为完全二叉树。

如图:

完全二叉树.png

在上图中,二叉树编号从1到6的6个节点,和前面的满二叉树编号从1到6的节点位置完全对应。因此这个树就是完全二叉树。

完全二叉树的条件没有满二叉树那么苛刻,满二叉树要求所有的节点必须有左右孩子,而完全二叉树只需要保证最后一个节点之前的节点都全即可。

==注意:==

二叉树不一定是满的。如图:

二叉树1.png

空也是一颗树。如图:

二叉树2.png

二叉树可以用哪些物理存储结构来进行存储呢?

  • 链式存储结构

    首先来看一看链式存储结构。如图:

    链式存储结构.png

    链式存储是二叉树最直观的存储方式。

    在链表中,链表是一对一的存储方式,每个链表拥有data变量和指向下一个节点的next指针。而二叉树稍微复杂一些,除了拥有data变量,还有指向左右孩子节点的变量。

  • 数组

    首先来看一看数组存储结构。如图:

    数组存储结构.png

    使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子为空,则数组的相应位置也为空。

    为什么这样设计呢?因为这样可以更方便地在数组中定位二叉树的孩子节点和父节点。

    假设一个父节点的索引是parent,那么它的左孩子节点索引就是2 * parent+1,右孩子节点索引就是2 * parent+2。

    反过来,假设一个左孩子节点的索引为leftChild,那么它的父节点索引就是(leftChild - 1)/ 2。

什么是二分搜索树?

二分搜索树是一颗二叉树,但是在二叉树的基础上加了一些条件。二分搜索树的每个节点的值大于其左子树的所有节点的值或小于其右子树的所有节点的值,并且每一刻子树也是二分搜索树,所以二分搜索树具有天然的递归结构。

如图:

二分搜索树.png

注意:

  • 二分搜索树存储的数据必须具有比较性!

  • 二分搜索树在极端的情况下会蜕化成链表。

    为什么会在极端的情况下蜕化成链表呢?假设,我们顺序的添加一组数据。例如1、2、3、4、5

    二分搜索树如图:

    二分搜索树2.png

    由于二分搜索树的特点,当我们顺序插入一组数据的话,会蜕化成一个链表。

    解决方式:平衡二叉树(AVL、红黑树)。

二分搜索树的实现

1、查找操作

二分搜索树.png

通过上图,例如我们查找值为22的节点,步骤如下:

  • 访问根节点28,进行比较,发现22<28,由于二分搜索树的特点,小于节点的值应访问它的左子树。
  • 访问节点16,进行比较,发现22>16,由于二分搜索树的特点,大于节点的值应访问它的右子树。
  • 访问节点22,进行比较,发现22=22,那么这个就是要查找的节点。

2、遍历操作

  • 深度优先遍历

    1、前序遍历

    二分搜索树的前序遍历,输出的顺序是当前节点、左孩子、右孩子。

    2、中序遍历

    二分搜索树的中序遍历,输出的顺序是左孩子、当前节点、右孩子。

    3、后序遍历

    二分搜索树的后序遍历,输出的顺序是左孩子、右孩子、当前节点。

  • 广度优先遍历

    1、层序遍历

    按层从左到右进行遍历。

其余操作大体都和查询操作的逻辑差不多,就不再一一讲解了。整体代码如下:

import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

/**
 * 描述:二分搜索树。
 * <p>
 * Create By ZhangBiao
 * 2020/5/13
 */
public class BinarySearch<E extends Comparable<E>> {

    /**
     * 根节点
     */
    private Node root;

    /**
     * 元素个数
     */
    private int size;

    public BinarySearch() {
        this.root = null;
        this.size = 0;
    }

    /**
     * 获取元素个数
     *
     * @return
     */
    public int getSize() {
        return size;
    }

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

    /**
     * 添加元素
     *
     * @param e
     */
    public void add(E e) {
        root = add(root, e);
    }

    /**
     * 向以node为根节点的二分搜索树中插入元素(递归算法)
     *
     * @param node
     * @param e
     * @return
     */
    private Node add(Node node, E e) {
        if (node == null) {
            size++;
            return new Node(e);
        }
        if (e.compareTo(node.e) < 0) {
            node.left = add(node.left, e);
        } else if (e.compareTo(node.e) > 0) {
            node.right = add(node.right, e);
        }
        return node;
    }

    /**
     * 查看二分搜索树是否包含元素e
     *
     * @param e
     * @return
     */
    public boolean contains(E e) {
        return contains(root, e);
    }

    /**
     * 查看以node为跟节点的二分搜索树是否包含元素e(递归算法)
     *
     * @param node
     * @param e
     * @return
     */
    private boolean contains(Node node, E e) {
        if (node == null) {
            return false;
        }
        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);
        }
    }

    /**
     * 二分搜索树前序遍历(深度优先遍历)
     */
    public void preOrder() {
        preOrder(root);
    }

    /**
     * 前序遍历以node为根节点的二分搜索树(递归算法)
     *
     * @param node
     */
    private void preOrder(Node node) {
        if (node == null) {
            return;
        }
        System.out.println(node.e);
        preOrder(node.left);
        preOrder(node.right);
    }

    /**
     * 二分搜索树非递归前序遍历(深度优先遍历)
     */
    public void preOrderNR() {
        Stack<Node> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            Node cur = stack.pop();
            System.out.println(cur.e);
            if (cur.right != null) {
                stack.push(cur.right);
            }
            if (cur.left != null) {
                stack.push(cur.left);
            }
        }
    }

    /**
     * 二分搜索树层序遍历(广度优先遍历)
     */
    public void levelOrder() {
        Queue<Node> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            Node cur = queue.remove();
            System.out.println(cur.e);
            if (cur.left != null) {
                queue.add(cur.left);
            }
            if (cur.right != null) {
                queue.add(cur.right);
            }
        }
    }

    /**
     * 二分搜索树中序遍历(深度优先遍历)
     */
    public void inOrder() {
        inOrder(root);
    }

    /**
     * 中序遍历以node为根节点的二分搜索树(递归算法)
     *
     * @param node
     */
    private void inOrder(Node node) {
        if (node == null) {
            return;
        }
        inOrder(node.left);
        System.out.println(node.e);
        inOrder(node.right);
    }

    /**
     * 二分搜索树后序遍历(深度优先遍历)
     */
    public void postOrder() {
        postOrder(root);
    }

    /**
     * 后序遍历以node为根节点的二分搜索树(递归算法)
     *
     * @param node
     */
    private void postOrder(Node node) {
        if (node == null) {
            return;
        }
        postOrder(node.left);
        postOrder(node.right);
        System.out.println(node.e);
    }

    /**
     * 寻找二分搜索树的最小元素
     *
     * @return
     */
    public E minimum() {
        if (size == 0) {
            throw new IllegalArgumentException("Binary Search Tree is empty!");
        }
        Node minNode = minimun(root);
        return minNode.e;
    }

    /**
     * 返回以node为跟节点的二分搜索树的最小值所在的节点
     *
     * @param node
     * @return
     */
    private Node minimun(Node node) {
        if (node.left == null) {
            return node;
        }
        return minimun(node.left);
    }

    /**
     * 寻找二分搜索树的最大元素
     *
     * @return
     */
    public E maximum() {
        if (size == 0) {
            throw new IllegalArgumentException("Binary Search Tree is empty!");
        }
        Node maxNode = maximum(root);
        return maxNode.e;
    }

    /**
     * 返回以node为跟节点的二分搜索树的最大值所在的节点
     *
     * @param node
     * @return
     */
    private Node maximum(Node node) {
        if (node.right == null) {
            return node;
        }
        return maximum(node.right);
    }

    /**
     * 从二分搜索树中删除最小元素的节点并返回最小值
     *
     * @return
     */
    public E removeMin() {
        E e = minimum();
        root = removeMin(root);
        return e;
    }

    /**
     * 删除掉以node为跟节点的二分搜索树中的最小元素节点并返回删除节点后的新的二分搜索树的根节点
     *
     * @param node
     * @return
     */
    private Node removeMin(Node node) {
        if (node.left == null) {
            Node rightNode = node.right;
            node.right = null;
            size--;
            return rightNode;
        }
        node.left = removeMin(node.left);
        return node;
    }

    /**
     * 从二分搜索树中删除最大元素的节点并返回最大值
     *
     * @return
     */
    public E removeMax() {
        E e = maximum();
        root = removeMax(root);
        return e;
    }

    /**
     * 删除掉以node为跟节点的二分搜索树中的最大元素节点并返回删除节点后的新的二分搜索树的根节点
     *
     * @param node
     * @return
     */
    private Node removeMax(Node node) {
        if (node.right == null) {
            Node leftNode = node.left;
            node.left = null;
            size--;
            return leftNode;
        }
        node.right = removeMax(node.right);
        return node;
    }

    /**
     * 从二分搜索树中删除元素为e的节点
     *
     * @param e
     */
    public void remove(E e) {
        root = remove(root, e);
    }

    /**
     * 删除掉以node为根节点的二分搜索树中值为e的节点(递归算法)并返回删除节点后新的二分搜索树的根节点
     *
     * @param node
     * @param e
     * @return
     */
    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 rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }
            //待删除节点右子树为空的情况
            if (node.right == null) {
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }
            //待删除节点左右子树均不为空的情况
            //1、找到比待删除节点大的最小节点,即待删除节点右子树的最小节点
            //2、用这个节点顶替待删除节点的位置
            Node successor = minimun(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;
            node.left = node.right = null;
            return successor;
        }
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();
        generateBSTString(root, 0, result);
        return result.toString();
    }

    /**
     * 生成以node为根节点,深度为depth的描述二叉树的字符串
     *
     * @param node
     * @param depth
     * @param result
     */
    private void generateBSTString(Node node, int depth, StringBuilder result) {
        if (node == null) {
            result.append(generateDepthString(depth) + "null\n");
            return;
        }
        result.append(generateDepthString(depth) + node.e + "\n");
        generateBSTString(node.left, depth + 1, result);
        generateBSTString(node.right, depth + 1, result);
    }

    private String generateDepthString(int depth) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < depth; i++) {
            result.append("--");
        }
        return result.toString();
    }

    private class Node {

        /**
         * 数据
         */
        public E e;

        /**
         * 左孩子
         */
        public Node left;

        /**
         * 右孩子
         */
        public Node right;

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

    }

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