二叉树与BST树详解 附java实现代码

本文详细介绍了二叉树的基础概念,包括定义、术语、性质和遍历方法。重点阐述了二叉搜索树(BST树)的特性,包括查询、插入、删除操作及其代码实现。同时,对BST树的遍历进行了深入探讨,并分析了其性能。


二叉树与BST树

引言:本问大概说明了二叉树的性质,着重介绍了二叉搜索树的性质,用法和代码实现。

1. 二叉树

1.1 二叉树的定义

每个结点最多有两个子树的树,称为二叉树。一棵深度为k,且有2^k-1个结点的二叉树,称为满二叉树。这种树的特点是每一层上的结点数都是最大结点数。而在一棵二叉树中,除最后一层外,若其余层都是满的,并且或者最后一层是满的,或者是在右边缺少连续若干结点,则此二叉树为完全二叉树

1.2 二叉树的相关术语

  • 树的结点(node):包含一个数据元素及若干指向子树的分支;
  • 孩子结点(child node):结点的子树的根称为该结点的孩子;
  • 双亲结点:B 结点是A 结点的孩子,则A结点是B 结点的双亲;
  • 兄弟结点:同一双亲的孩子结点; 堂兄结点:同一层上结点;
  • 祖先结点: 从根到该结点的所经分支上的所有结点
  • 子孙结点:以某结点为根的子树中任一结点都称为该结点的子孙
  • 结点层:根结点的层定义为1;根的孩子为第二层结点,依此类推;
  • 树的深度:树中最大的结点层
  • 结点的度:结点子树的个数
  • 树的度: 树中最大的结点度。
  • 叶子结点:也叫终端结点,是度为 0 的结点;
  • 分枝结点:度不为0的结点;
  • 有序树:子树有序的树,如:家族树;
  • 无序树:不考虑子树的顺序;

1.3 二叉树的性质

(1) 在非空二叉树中,第i层的结点总数不超过 2 i − 1 , i > = 1 2^{i-1} , i>=1 2i1,i>=1

(2) 深度为h的二叉树最多有 2 h − 1 2^h-1 2h1个结点 ( h > = 1 ) (h>=1) (h>=1),最少有h个结点;

(3) 对于任意一棵二叉树,如果其叶结点数为 N 0 N_0 N0,而度数为2的结点总数为 N 2 N_2 N2,则 N 0 = N 2 + 1 N_0=N_2+1 N0=N2+1

(4) 具有 n n n个结点的完全二叉树的深度为 [ log ⁡ 2 n ] + 1 [\log_2 n]+1 [log2n]+1(注:[ ]表示向下取整)

(5) 有 N N N个结点的完全二叉树各结点如果用顺序方式(如数组)存储,若 i i i为结点编号,则结点之间有如下关系:

  • 如果 i > 1 i>1 i>1,则其父结点的编号为 i / 2 i/2 i/2
  • 如果 2 i < = N 2i<=N 2i<=N,则其左孩子(即左子树的根结点)的编号为 2 i 2i 2i;若 2 ∗ i > N 2*i>N 2i>N,则无左孩子;
  • 如果 2 i + 1 < = N 2i+1<=N 2i+1<=N,则其右孩子的结点编号为 2 i + 1 2i+1 2i+1;若 2 ∗ i + 1 > N 2*i+1>N 2i+1>N,则无右孩子。

1.4 二叉树的遍历

二叉树的遍历分为前序遍历中序遍历后序遍历层序遍历。这里的前中后是访问根的相对位置。

  1. 前序遍历

    首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树。即 V L R的顺序

  2. 中序遍历

    首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树。即L V R

  3. 后序遍历

    首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根。即 L R V

  4. 层序遍历

    即按照层次访问,从上到下,从左到右依序访问。先访问根,访问子女,再访问子女的子女(越往后的层次越低)

2. BST树

2.1. BST树定义

BST树(Binary Sort Tree),二叉排序树,在二叉树的前提下,每个节点和他的左右不为空的孩子满足以下条件:左孩子的值<双亲结点的值<右孩子的值

2.2 BST树的增删查功能

介绍之前,首先给出BST树使用的节点类

/**
 * 树的结点类
 *
 * @param <T>
 */
class Entry<T extends Comparable<T>> {
    T data;  //数据
    Entry<T> left;  //左孩子
    Entry<T> right; //右孩子

    /**
     * 构造方法
     *
     * @param data
     */
    public Entry(T data) {
        this(data, null, null,1);
    }

    /**
     * 构造方法
     *
     * @param data
     * @param left
     * @param right
     */
    public Entry(T data, Entry<T> left, Entry<T> right) {
        this.data = data;
        this.left = left;
        this.right = right;
    }
}
2.2.1 BST树的查询

BST树的查询操作,比较节点与目标值的大小。节点值大于目标值,向左深入查找;节点值小于目标值,向右深入查找;节点值等于目标值,说明找到该节点。若找到叶子节点,还是没找到,则说明该值不存在。

查询操作的代码实现如下:

    /**
     * 非递归
     * 查询操作
     *
     * @param value
     * @return
     */
    public boolean n_query(T value) {
        if (root == null) {
            return false;
        }

        Entry<T> cur = root;
        while (cur != null) {
            if (cur.data.compareTo(value) < 0) {
                cur = cur.right;
            } else if (cur.data.compareTo(value) > 0) {
                cur = cur.left;
            } else {
                return true;
            }
        }
        return false;
    }


	/**
     * 递归
     * 查询操作
     *
     * @param value
     * @return
     */
    public boolean query(T value) {
        if (root == null)
            return false;

        return query(this.root, value);
    }

    /**
     * 以node节点开始 寻找value值的节点
     *
     * @param node
     * @param value
     * @return
     */
    private boolean query(Entry<T> node, T value) {
        if (node == null)
            return false;

        if (node.data.compareTo(value) > 0)
            return query(node.left, value);
        else if (node.data.compareTo(value) < 0)
            return query(node.right, value);
        else
            return true;
    }
2.2.2 BST树的增加

BST树的增加,也要使节点满足有序。插入新节点时,首先找到其该放的位置,过程为首先和根节点比较,比根节点小往左节点走,比根节点大往右走,之后如果为空,则放入节点,若不为空,一样的比较流程,直到找到一个该放的位置。

代码实现如下:

	 /**
     * 非递归
     * 插入操作
     *
     * @param value
     */
    public void n_insert(T value) {
        //空树 直接返回
        if (root == null) {
            root = new Entry<>(value);
            return;
        }
        //寻找该插入的位置
        Entry<T> cur = root;
        Entry<T> parent = cur;
        boolean ifLeft = false;//该插入的节点位于父节点的左 还是 右
        while (cur != null) {
            parent = cur;
            if (cur.data.compareTo(value) < 0) {
                cur = cur.right;
                ifLeft = false;
            } else if (cur.data.compareTo(value) > 0) {
                cur = cur.left;
                ifLeft = true;
            } else {
                return;//不插入重复值
            }
        }
        //cur == null 插入新节点
        if (ifLeft) {
            parent.left = new Entry<>(value);
        } else {
            parent.right = new Entry<>(value);
        }
    }


	/**
     * 递归
     * 插入操作
     *
     * @param value
     */
    public void insert(T value) {
        if (root == null) {
            root = new Entry<>(value);
            return;
        }

        insert(this.root, value);
    }

    /**
     * 以node节点开始,寻找合适位置插入
     *
     * @param node
     * @param value
     * @return
     */
    private Entry<T> insert(Entry<T> node, T value) {
        if (node == null) {
            return new Entry<>(value);
        }
        if (node.data.compareTo(value) > 0) {
            node.left = insert(node.left, value);
        } else if (node.data.compareTo(value) < 0) {
            node.right = insert(node.right, value);
        } else {
            ///相等什么也不做
        }
        return node;
    }
2.2.3 BST树的删除

BST树的删除,首先需要找到待删除节点,然后判断该节点是哪种类型:

  1. 叶子节点,即无孩子节点,直接删除该节点

  2. 有一个孩子节点,用孩子节点代替该节点

  3. 有两个孩子节点,这时需要注意,需要找到以该节点为根的树中,最接近该节点值的节点,用之替代该节点。这里有两种合适节点,称为前驱节点和后继节点

    • 前驱节点:最大的小于待删除节点的数据的节点。寻找方式为:待删除结点的左孩子开始,若有右孩子,则一直往右寻找,直到找到叶节点,即为前驱节点;

    • 后继节点:最小的大于该待删除节点的数据的节点。寻找方式为:待删除结点的右孩子开始,若有左孩子,则一直往左寻找,直到找到叶节点,即为后继节点;

    前驱节点和后继节点即为中序遍历时,待删除节点的前一个节点和后一个节点。找到前驱节点或后继节点后,用前驱或后继的值覆盖待删除节点,再删除前驱或后继节点即可。

代码实现如下:

	/**
     * 非递归
     * 删除操作
     *
     * @param value
     */
    public void n_remove(T value) {
        //空树
        if (root == null) {
            return;
        }

        //先找到目标节点
        Entry<T> cur = root;//记录该删除的节点
        Entry<T> parent = null;//记录该删除的节点的父节点
        while (cur != null) {
            if (cur.data.compareTo(value) > 0) {
                parent = cur;
                cur = cur.left;
            } else if (cur.data.compareTo(value) < 0) {
                parent = cur;
                cur = cur.right;
            } else break;//找到目标节点
        }

        //没找到目标节点
        if (cur == null) return;

        //进行删除
        //#3 有左右两个孩子
        if (cur.left != null && cur.right != null) {
            //找到目标节点的前驱节点
            Entry<T> entry = cur;//记录目标节点位置
            parent = cur;
            cur = cur.left;
            while (cur.right != null) {
                parent = cur;
                cur = cur.right;//找到前驱节点
            }

            entry.data = cur.data;//用前驱节点的值覆盖目标节点
        }

        //删除cur #1无左右孩子 #2有一个孩子
        Entry<T> child = cur.left;//记录cur的孩子
        if (child == null) {
            child = cur.right;
        }
        if (parent == null) {//删除的是根节点
            root = child;
        } else if (parent.left == cur) {//链接cur的父节点与cur的孩子节点
            parent.left = child;
        } else {
            parent.right = child;
        }

        //切断cur与child的关联 防止内存泄漏
        cur.right = null;
        cur.left = null;
    }


/**
     * 递归
     * 删除操作
     *
     * @param value
     * @return
     */
    public void remove(T value) {
        root = remove(this.root, value);
    }

    /**
     * 以node节点开始 删除value值节点
     * 删除完成后,把新的子树的根节点进行返回
     *
     * @param node
     * @param value
     * @return 返回每个节点 写到父节点的相应左右子节点域中
     */
    private Entry<T> remove(Entry<T> node, T value) {
        if (node == null)
            return null;


        if (node.data.compareTo(value) > 0)
            node.left = remove(node.left, value);
        else if (node.data.compareTo(value) < 0)
            node.right = remove(node.right, value);
        else {
            //找到该结点
            if (node.left != null && node.right != null) {
                //该节点有两个子节点 找到其前驱节点  #3
                Entry<T> pre = node.left;//前驱节点
                while (pre.right != null)
                    pre = pre.right;

                node.data = pre.data;//用前驱节点的值代替待删除结点的值
                //删除前驱节点
                node.left = remove(node.left, pre.data);
            } else {
                // 至少有一个孩子结点
                if (node.left != null) {//节点的左孩子不为空
                    return node.left;
                } else if (node.right != null) {//节点的右孩子不为空
                    return node.right;
                } else
                    return null;//删除是叶子节点
            }
        }

        return node;
    }

2.3 BST树的遍历

2.3.1 前序遍历

BST树的前序遍历,如同上述增删查操作一样,有递归和非递归两种实现方式。

  • 递归实现:按照V L R的顺序,首先打印节点的值,然后递归访问左节点,再递归访问右节点,直到访问到根节点,返回。

    	/**
         * 递归 前序遍历
         */
        public void proOrder() {
            System.out.print("前序遍历:");
            proOrder(root);
            System.out.println();
        }
    
        /**
         * 以node开始前序遍历
         *
         * @param node
         */
        private void proOrder(Entry<T> node) {
            if (node == null) return;
    
            System.out.print(node.data + " ");
            proOrder(node.left);
            proOrder(node.right);
        }
    
  • 非递归实现:非递归实现需要借助栈。首先将根节点入栈,然后借助一个循环,做如下操作:

    1. 栈顶元素出栈
    2. V L R 因此首先打印其值,若有右孩子,则将右孩子入栈;若其有左孩子,则将左孩子入栈。这里是因为栈先进后出的性质,首先需要L 所以让R先入栈
     	/**
         * 非递归 前序遍历 VLR
         */
        public void n_proOrder() {
            if (root == null)
                return;//空树 返回
    
            LinkedList<Entry<T>> stack = new LinkedList<>();//栈
            stack.push(root);
    
            Entry<T> top;//栈顶元素
            while (!stack.isEmpty()) {
                top = stack.pop(); //元素出栈
                System.out.print(top.data + " ");//打印V
                if (top.right != null)
                    stack.push(top.right); //入栈R 先入后出
                if (top.left != null)
                    stack.push(top.left);  //入栈L
            }
            System.out.println();
        }
    
2.3.2 中序遍历

这里需要强调,BST树中序遍历得到的结果就是各节点的值从小到大的有序序列。

  • 递归实现:类似于前序遍历的递归实现,不同的是输入是按L V R的顺序进行

    	/**
         * 递归 中序遍历
         */
        public void inOrder() {
            System.out.print("中序遍历:");
            inOrder(root);
            System.out.println();
        }
    
        /**
         * 以node开始中序遍历
         *
         * @param node
         */
        private void inOrder(Entry<T> node) {
            if (node == null) return;
    
            inOrder(node.left);
            System.out.print(node.data + " ");
            inOrder(node.right);
        }
    
  • 非递归实现:同样的,需要借助一个栈。首先将节点入栈,因为是L V R的顺序,需要一直向左深度遍历,直到最左最深的子节点,之后将其弹出,输出该节点值,判断其右孩子,存在,继续从该节点向左深度遍历,还是上述步骤;右孩子不存在则再弹出一个子节点,这个子节点肯定是上一个弹出的父节点;如此一直下去,就做到了L V R的顺序

    	/**
         * 非递归 中序遍历 LVR
         */
        public void n_inOrder() {
            if (root == null)
                return;//空树 返回
    
            LinkedList<Entry<T>> stack = new LinkedList<>();
            Entry<T> cur = root;//记录栈顶元素
    
            while (!stack.isEmpty() || cur != null) {
                if (cur != null) { //当前节点未到最左节点
                    stack.push(cur);
                    cur = cur.left; //深度遍历 入栈
                } else {
                    //此时栈顶元素的左孩子为空,出栈更新栈顶元素 打印 V 若存在则将其右孩子入栈
                    cur = stack.pop();
                    System.out.print(cur.data + " ");
    
                    //R
                    cur = cur.right;
                }
            }
            System.out.println();
        }
    
2.3.3 后序遍历
  • 递归实现:类似于上面的两种,只不过是L R V的顺序

     /**
         * 递归后序遍历
         */
        public void postOrder() {
            System.out.print("后序遍历:");
            postOrder(root);
            System.out.println();
        }
    
        /**
         * 以node开始后序遍历
         *
         * @param node
         */
        private void postOrder(Entry<T> node) {
            if (node == null) return;
    
            postOrder(node.left);
            postOrder(node.right);
            System.out.print(node.data + " ");
        }
    
  • 非递归实现:后序遍历的非递归实现更复杂,一个栈无法满足,我们需要借助两个栈。我们要实现L R V的顺序,可以先实现V R L的顺序,然后让其入栈,先进后出,出来就变成了L R V,即后序遍历。而实现V R L的顺序遍历,则可以参照前序遍历的非递归遍历方法,只不过放入顺序变成了先放入L再放入R,以达到R比L先出的效果。

    	/**
         * 非递归 后序遍历  LRV
         */
        public void n_postOrder() {
            if (root == null)
                return;//空树 返回
    
            //实现类前序遍历 将结果放入另一个栈中
            LinkedList<Entry<T>> stack_1 = new LinkedList<>();
            LinkedList<Entry<T>> stack_2 = new LinkedList<>();
            stack_1.push(root);
            Entry<T> top;//栈顶元素
    
            while (!stack_1.isEmpty()) {
                top = stack_1.pop();//V
                stack_2.push(top);//放入另一个栈
    
                //R L
                if (top.left != null)
                    stack_1.push(top.left);//先入后出
                if (top.right != null)
                    stack_1.push(top.right);
            }
    
            //栈元素全部按照VRL的顺序出栈 放入栈2中 栈2再出栈顺序则为 LRV
            while (!stack_2.isEmpty()) {
                top = stack_2.pop();
                System.out.print(top.data + " ");
            }
    
            System.out.println();
        }	
    
2.3.4 层序遍历
  • 递归实现:层序遍历首先有一个工具方法:递归计算树的层数。代码如下:

     	/**
         * 计算树的层数
         *
         * @return
         */
        public int level() {
            return level(root);
        }
    
        /**
         * 递归
         * 以node开始计算树的层数
         *
         * @param node
         * @return
         */
        private int level(Entry<T> node) {
            if (node == null) {
                return 0;
            } else {
                int left = level(node.left);
                int right = level(node.right);
                return left > right ? left + 1 : right + 1;
            }
        }
    

    递归层序遍历代码如下:深度遍历树,给定一个层数的传参,每次向下递归都减一,当参数为0,代表到达目标层数,打印该节点的数据并返回。

    	/**
         * 递归
         * 层序遍历
         */
        public void levelOrder() {
            System.out.print("层序遍历:");
            int l = level();
            for (int i = 0; i < l; i++) {
                levelOrder(root, i);//打印第i层
            }
            System.out.println();
        }
    
        /**
         * 从node开始遍历 打印i层节点
         *
         * @param node
         * @param i
         */
        private void levelOrder(Entry<T> node, int i) {
            if (node == null) {
                return;
            }
    
            if (i == 0) {//找到该层
                System.out.print(node.data + " ");
                return;
            }
    
            levelOrder(node.left, i - 1);
            levelOrder(node.right, i - 1);
        }
    
  • 非递归实现:层序遍历的非递归实现需要借助一个队列。首先将根节点入队,进行如下操作:

    1. 对头元素出队 打印该值
    2. 判断对头元素是否有左右孩子,有的话将其入队

    由于队列先进先出的性质,先出的节点打印时入队的其孩子节点也会先出队。

    	/**
         * 非递归 层序遍历
         */
        public void n_levelOrder() {
            if (root == null)
                return;
    
            LinkedList<Entry<T>> queue = new LinkedList<>();//队列
            queue.offer(root);
            Entry<T> pro;
    
            while (!queue.isEmpty()) {
                pro = queue.poll();     //出队
                System.out.print(pro.data + " ");
    
                if (pro.left != null)
                    queue.offer(pro.left);    //入队
                if (pro.right != null)
                    queue.offer(pro.right);
            }
    
            System.out.println();
        }
    

2.4 BST树的性能分析

每个结点的 C ( i ) C(i) Ci为该结点的层次数。最坏情况下,当先后插入的关键字有序时,构成的二叉排序树蜕变为单支树,树的深度为其平均查找长度 ( n + 1 ) / 2 (n+1)/2 (n+1)/2 (和顺序查找相同),最好的情况是二叉排序树的形态和折半查找的判定树相同,其平均查找长度和 log ⁡ 2 n \log_2 n log2n成正比。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值