数据结构--二分搜索树的实现与各种遍历方式(Binary-Search-Tree)

二分搜索树

前言
在计算机科学中,二分搜索树(Binary Search Tree)
(有时称为有序或者排序的二叉树)是一种能存储特定数据类型的容器,二叉搜索树 允许快速查找、添加或者删除某一个节点,并且它是动态的集合。
二叉搜索树按照关键字顺序地保存节点,因此查找和其他操作可以使用二叉搜索原理:当在树(或者寻找插入新节点的地方)中查找节点时,它从根节点遍历到叶子节点,与每个节点的关键字进行比较,然后基于比较的结果,决定继续在左子树还是在右子树中进行搜索比较。这样一来,每次比较理论上都活筛选掉一半的元素,这样使得每次查找,插入或者删除一个节点所花费的时间与树的节点个数的对数成(树的高度)正比,比线性表的性能要好很多。


定义
二叉搜索树是以一颗二叉树组织,每个节点就是一个对象,包括key,卫星数据。除此之外还包括一些维护树结构所需要的信息:left、light、parent,分别指向左孩子、右孩子、父节点。其中如果孩子节点或者父节点不存在时,用null表示。根节点时树中唯一一个父节点为null的节点。

一、二叉树性质简介

1.如果节点的左孩子不为空,则左孩子树上所有的节点的值均小于它的根节点的值。
2.如果节点的右孩子不为空,则右孩子树上所有的节点的值均大于它的根节点的值。
3.任意节点的左右孩子也分别为二叉搜索树(只要满足二叉搜索树的基本结构都属于二叉搜索树。)如下图!!!


二分搜索树

这里是本人写的一个不支持重复数据的简单的二叉搜索树的源码!
https://gitee.com/ligangyun/data_structure/tree/master/BinarySearchTree


构建二分搜索树

一、初始化二分搜索树对象(本实现树对象不满足包含重复数据)

首先需要明确二分搜索树的结构特点,二分搜索树需要维护一个Node对象

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

        /**
         * 提供构造方法
         *
         * @param e 真实元素
         */
        Node(E e) {
            this.e = e;
            left = null;
            right = null;
        }
    }

提供构造函数

  /**
     * 提供无参构造
     */
    public BST() {
        root = null;
        size = 0;
    }

维护整个树的元素大小:
private int size;
提供根节点:
private Node root;
一个简单的二分搜索树基本初始化完成。

二、添加功能

首先在网二分树中添加元素时,需要计算出该元素添加的位置,所以需要使用到递归算法,计算出添加的元素具体的添加位置。

  public void add(E e) {

        root = NewAdd(root, e);
    }

    private Node NewAdd(Node node, E e) {
        // 判断当前二分树是否为空
        if (node == null) {
            // 维护size 的大小
            size++;
            return new Node(e);
        }
        if (e.compareTo(node.e) < 0) {
            node.left = NewAdd(node.left, e);
        } else if (e.compareTo(node.e) > 0) {
            node.right = NewAdd(node.right, e);
        }
        return node;
    }

这里的元素对比是在二分树泛型中继承java comparable 类 实现的

其他一些列操作 包含 删除 查找等操作都是居于递归实现的!本人提供的源码均详细实现。


二分搜索树的深度遍历

说明:
二分搜索树的遍历分为三大类:先序遍历(preorder tree walk)、中序遍历(inorder tree walk)、后序遍历(postorder tree walk)


使用递归的方式访问节点每个节点都会被访问三次
1.首先访问当前节点。
2.再访问该节点的左孩子,再会回到该节点,访问该节点的右孩子
3.最终右孩子访问结束后,还是返回到该节点,标志着该节点即下面的所有子节点都访问完毕!

前序递归遍历

二分搜索树前序遍历.gif

所以上述图中遍历流程如下
1.第一次当问root节点时记录28;
2.然后访问root 节点的左孩子节点记录16;
3.再访问16节点的左孩子节点,记录13;
4.然后访问13的左孩子节点,为空,回到13节点,再访问13节点的右孩子节点也为空,有一次回到13节点;
5.此时访问回到16节点,此时访问16节点的右孩子节点,来到22节点,记录22;
6.22 节点左右孩子节点都为空,所以回到16节点(至此16节点的所有右孩子节点遍历完毕。)
7.来到28根节点(以此类推进行根节点的右孩子节点的遍历)
所以上述二分搜索树中遍历的结果为:28、16、13、22、30、29、42

   /**
     * 二分搜索树 递归前序遍历
     */
    public void preOrder() {
        preOrder(root);
    }

    private void preOrder(Node node) {
        if (node == null) {
            return;
        }
        System.out.println(node.e);
        // 递归遍历 左右孩子节点,这里一定要注意左孩子在前面
        preOrder(node.left);
        preOrder(node.right);
    }

中序递归遍历

原理与前序遍历基本相同,只是在节点第二次出现时,获取节点信息。

    /**
     * 二分搜索树 递归中序遍历
     */
    public void inOrder() {
        inOrder(root);
    }

    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);
    }

    private void postOrder(Node node) {
        if (node == null) {
            return;
        }
        postOrder(node.left);
        postOrder(node.right);
        System.out.println(node.e);
    }

二分搜索树非递归遍历

使用栈线性结构实现二分搜索树前序遍历

简单的流程动态图:


使用栈实现二分搜索树前序遍历.gif
    /**
     * 使用栈 实现前序遍历
     */
    public void preOrderByStack() {
        preOrderByStack(root);
    }

    private void preOrderByStack(Node node) {
        if (node == null) {
            return;
        }
        Stack<Node> stack = new Stack<>();
        stack.push(node);
        // 有序栈结构先进后出的特性,需要向将右孩子先于左孩子压入栈底
        while (!stack.isEmpty()) {
            Node pop = stack.pop();
            System.out.println(pop.e);
            if (pop.right != null) {
                stack.push(pop.right);
            }
            if (pop.left != null) {
                stack.push(pop.left);
            }
        }
    }

使用栈线性结构实现二分搜索树中序遍历

中序遍历相较于前序遍历会比较的复杂(前序遍历应用的比较广泛,这里视时间的充裕程度选择阅读
首先分析
在使用栈结果实现中序遍历的时候,需要重点考虑节点是否存在左孩子节点。当节点有左孩子节点时,需要将该节点优先入栈,如果该节点没有左孩子节点,此时应该访问该节点。再考虑有叶子节点。
操作步骤
步骤一:如果节点有左叶子节点,将该节点入栈,如果节点没有左叶子节点,访问当前节点。
步骤二:如果节点有右叶子节点,重复步骤一,如果节点没有有叶子节点(该节点下所有的子节点访问完毕)回退,让栈顶元素出栈,并且访问栈顶元素的右叶子元素,然后重复步骤一。
步骤二:当栈为空时,说明遍历结束
代码如下:

 /**
     * 栈 实现 中序遍历
     */
    public ArrayList<E> inOrderByStack() {
        return inOrderByStack(root);
    }

    private ArrayList<E> inOrderByStack(Node node) {
        ArrayList<E> result = new ArrayList<>();
        if (node == null) {
            return result;
        }
        Stack<Node> nodeStack = new Stack<>();
        /**
         * 分析:
         *  步骤1:节点如果有左叶子节点,该节点入栈,
         *      如果该节点没有左叶子节点,访问该节点
         *  步骤2:如果节点有右叶子节点,重复步骤1
         *      如果节点没有右叶子节点(说明访问完毕)回退,让栈顶元素出栈,并且访问栈顶元素的右叶子树,重复步骤1
         *  步骤3:当栈为空时,遍历结束
         */
        Node cur = node;
        // 判断 当前节点 是否为空,并且 栈是否遍历完结
        while (cur != null || !nodeStack.empty()) {
            // 将当前节点下所有的左叶子节点压入栈顶
            while (cur != null) {
                nodeStack.push(cur);
                cur = cur.left;// 定义当前变量
            }
            // 获取栈顶元素
            cur = nodeStack.peek();
            result.add(cur.e);
            // 弹出栈顶
            nodeStack.pop();
            cur = cur.right;
        }
        return result;
    }

使用栈线性结构实现二分搜索树后序遍历

    /**
     * 栈 实现后序遍历
     *
     * @return
     */
    public ArrayList<E> postOrderByStack() {
        return postOrderByStack(root);
    }

    private ArrayList<E> postOrderByStack(Node node) {
        ArrayList<E> result = new ArrayList<>();
        Stack<Node> nodeStack = new Stack<>();
        Node cur = node;
        while (cur != null || !nodeStack.empty()) {
            /**
             * 分析:
             *  后序遍历 在中序遍历的基础上,需要注意的是:节点的所有右孩子节点访问完毕后,该节点才可以出栈
             */
            // 先遍历所有的左孩子节点
            while (cur != null) {
                nodeStack.push(cur);
                cur = cur.left;
            }
            cur = nodeStack.peek();
            if (cur.right == null) {
                // 当前节点 为左节点的最后一个节点,添加到结果集中,并且将当前cur 设置为栈顶值。
                result.add(cur.e);
                // 该节点 出栈
                nodeStack.pop();
                //判断此时的栈顶的右孩子是否与当前的cur 相等,相等 则说明 该栈顶元素下面的所有元素遍历完毕,需要出栈
                while (!nodeStack.empty() && nodeStack.peek().right.equals(cur)) {
                    cur = nodeStack.peek();
                    result.add(nodeStack.pop().e);
                }
                //将 此时栈顶的右孩子 赋值给cur
                cur = nodeStack.empty() ? null : nodeStack.peek().right;
            } else {
                // 该节点没有左叶子树,但是有右叶子树,并将右叶子节点复制给cur
                cur = cur.right;
            }

        }
        return result;
    }

以上所有遍历都可以划分为 二分搜索树的深度优先遍历:对每一个可能的分支路径深入到不能再深入为止


二分搜索树的广度遍历(层序遍历)

广度遍历:从上往下对每一层依次访问,在每一层中,从左往右(也可以从右往左)访问结点,访问完一层就进入下一层,直到没有结点可以访问为止。广度优先遍历的非递归的通用做法是采用队列。
利用队列实现层序遍历

    /**
     * 广度优先遍历(层序遍历) 使用队列
     *
     * @return
     */
    public ArrayList<E> sequenceOrder() {
        return sequenceOrder(root);
    }

    private ArrayList<E> sequenceOrder(Node node) {
        ArrayList<E> result = new ArrayList<>();
        if (node == null)
            return result;
        Queue<Node> nodeQueue = new LinkedList<>();
        nodeQueue.add(node);
        while (!nodeQueue.isEmpty()) {
            Node cur = nodeQueue.remove();
            result.add(cur.e);
            if (cur.left != null)
                nodeQueue.add(cur.left);
            if (cur.right != null) {
                nodeQueue.add(cur.right);
            }
        }
        return result;
    }

此文经作为作者学习记录。如有不对的地方还望指出和谅解。谢谢
祝各位工作顺利!

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

推荐阅读更多精彩内容