重学数据结构(六、树和二叉树 :上)

239 阅读22分钟

树结构是一类重要的非线性数据结构。直观来看,树是以分支关系定义的层次结构。树结构在客观世界广泛存在,如人类社会的族谱和各种社会组织机构都可用树来形象表示。 树在计算机领域中也得到广泛应用,尤以二叉树最为常用。如在操作系统中,用树来表示文件目录的组织结构。在编译系统中,用树来表示源程序的语法结构。在数据库系统中,树结构也是信息的重要组织形式之一。


1、树的定义

1.1、树的定义

树(Tree)是n(n>=0)个结点的有限集,它或为空树(n= 0);,或为非空树,对千非空树T:

  • (1) 有且仅有一个称之为根的结点;
  • (2) 除根结点以外的其余结点可分为 m(m>0)个互不相交的有限集 T1, T2 , …,Tn,其中每 一个集合本身又是一棵树,并且称为根的子树(SubTree)。


图1:树的示例

在这里插入图片描述


1.2、树的相关术语

这里结合图1 (b)为例:

  • 结点:树中的一个独立单元。包含一个数据元素及若于指向其子树的分支。如图 A 、 B 、 C 、 D 等。
  • 结点的度:结点拥有的子树数称为结点的度。例如,A的度为 3, C的度为1, F的度为0。
  • 树的度:树的度是树内各结点度的最大值。图 1 (b) 所示的树的度为3。
  • 叶子/终端结点: 度为 0 的结点称为叶子或终端结点。结点 K 、 L 、 F 、 G 、 M 、 I 、 J都是树的叶子。
  • 非终端结点:度不为 0 的结点称为非终端结点或分支结点。除根结点之外,非终端结点也称为内部结点。
  • 双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。例如,B的双亲为A, B的孩子有E和F。
  • 兄弟:同一个双亲的孩子之间互称兄弟。例如,H 、 I 和J互为兄弟。
  • 祖先:从根到该结点所经分支上的所有结点。例如, M 的祖先为 A 、 D 和 H。
  • 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。如 B 的子孙为 E 、 K 、 L和 F。
  • 层次:结点的层次从根开始定义起,根为 第一层,根的孩子为第二层。树中任一结点的层次等千其双亲结点的层次加 1。
  • 堂兄弟:双亲在同 一层的结点互为堂兄弟。 例如,结点 G 与E 、 F、 H 、 I 、 J互为堂兄弟。
  • 树的深度:树中结点的最大层次称为树的深度或高度。图1 (b)所示的树的深度为4。
  • 有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
  • 森林:是 m (m>0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此,也可以用森林和树相互递归的定义来描述树。

1.3、二叉树的定义

二叉树(Binary Tree)是n(n>=0)个结点所构成的集合,它或为空树(n =0); 或为非空树,对于非空树T:

  • (1) 有且仅有一个称之为根的结点;
  • (2) 除根结点以外的其余结点分为两个互不相交的子集T1和T2, 分别称为T的左子树和右子树,且T1和T2本身又都是二叉树。

二叉树与树一样具有递归性质,二叉树与树的区别主要有以下两点:

  • (1) 二叉树每个结点至多只有两棵子树(即二叉树中不存在度大千2 的结点);
  • (2) 二叉树的子树有左右之分,其次序不能任意颠倒。


图2:二叉树的五种基本形态 在这里插入图片描述


1.4、二叉树的性质

二叉树具有下列重要特性:

  • 性质1 二叉树第i(i≥1)层上的结点数最多为 2^i-1^。
  • 性质2 高度为k的二叉树最多有 2^k^-1 个结点。
  • 性质3 对任何二叉树T,设n0、n1、n2 分别表示度数为 0、 1、 2 的结点个数, 则 n0=n2+1。

满二叉树和完全二叉树:

满二叉树和完全二叉树是二叉树的两种特殊情形。

一棵深度为 A 且有 2^k^ -1 个结点的二叉树称为满二叉树。

如图 3 所示是深度分别为 1、 2、 3 的满二叉树。 满二叉树的特点是每一层上的结点数都达到最大值, 即对给定的深度, 它是具有最多结点数的二叉树。 满二叉树不存在度数为 1 的结点, 每个分支结点均有两棵高度相同的子树, 且树叶都在最下一层上。


图3:三种不同深度的满二叉树

在这里插入图片描述

若一棵二叉树至多只有最下面的两层结点的度数可以小于 2, 并且最下一层上的结点都集中在该层最左边的若干位置上, 则此二叉树称为完全二叉树。如图4所示:


图4:完全二叉树

在这里插入图片描述 由定义及示例可以看出满二叉树是完全二叉树, 但完全二叉树不一定是满二叉树。

  • 性质4 具有 个结点的完全二叉树(包括满二叉树)的高度为 [log2n]+1(或者[log2(n+1)])。
  • 性质5 满二叉树原理非空满二叉树的叶结点数等于其分支结点数加1。
  • 性质6 —棵非空二叉树空子树的数目等于其结点数目加 1。

2、二叉树实现


2.1、二叉树存储结构

  • 顺序存储

和线性表类似,二叉树的存储结构也可采用顺序存储和链式存储两种方式。

顺序存储是将二叉树所有元素编号,存入到一维数组的对应位置。顺序存储比较适合完全二叉树,只要从根起按层序存储即可,依次自上而下、自左至右存储结点元素, 即将完全二叉树上编号为i 的结点元素存储在如 上定义的一维数组中下标为i-1的数组元素中。

对于非完全二叉树,存在空间的浪费。


图5:二叉树顺序存储示意图 在这里插入图片描述

  • 链式存储

由于采用顺序存储结构存储一般二叉树造成大量存储空间的浪费, 因此, 一般二叉树的存储结构更多地采用链接的方式。

在链式存储结构里,我们需要对节点进行定义,每个节点包含数据、左孩子、右孩子。


图6:二叉树链式存储节点示意图

在这里插入图片描述

左孩子指向节点的左孩子,右节点指向节点的右孩子。


图7:二叉树的二叉链表表示

在这里插入图片描述 下面来看一看链式存储结构的具体实现。


2.2、二叉树链式存储及常见操作实现

2.2.1、节点类

这里添加了一个父节点的属性,方便后面的一些操作

/**
 * @Author 三分恶
 * @Date 2020/10/8
 * @Description 二叉树节点
 */
public class BinaryTreeNode {
    private Object data;  //数据
    private BinaryTreeNode leftChild;  //左孩子
    private BinaryTreeNode rightChild; //右孩子
    private BinaryTreeNode parent;     //父节点
    //省略getter、setter
    
    /**
     * 重写equals方法,这里设置为数据相等即认为是同一节点(这个规则不合理,待改进)
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        //比较对象为BinaryTreeNode类实例
        if (obj instanceof BinaryTreeNode){
            BinaryTreeNode compareNode= (BinaryTreeNode) obj;
            //设置为数据相同即相同
            if (compareNode.getData().equals(this.getData())){
                return true;
            }
        }
        return false;
    }
}

2.2.2、创建

/**
 * @Author 三分恶
 * @Date 2020/10/8
 * @Description 二叉树-链式
 */
public class BinaryTree {
    private BinaryTreeNode root;     //根节点

    public BinaryTree() {
    }

    public BinaryTree(BinaryTreeNode root) {
        this.root = root;
    }

    public BinaryTreeNode getRoot() {
        return root;
    }

    public void setRoot(BinaryTreeNode root) {
        this.root = root;
    }
}

2.2.2、清空

    /**
     * 二叉树的清空
     * 递归清空某个节点的子树
     * @param node
     */
    public void clear(BinaryTreeNode node){
        if (node!=null){
            //递归清空左子树
            clear(node.getLeftChild());
            //递归清空右子树
            clear(node.getRightChild());
            //将该节点置为null
            node=null;
        }
    }

    /**
     * 清空二叉树
     */
    public void clear(){
        clear(root);
    }

2.2.3、判空

判断根节点是否存在。

    /**
     * 判断二叉树是否为空
     * @return
     */
    public boolean isEmpty(){
        return root==null;
    }

2.2.4、获取二叉树的高度

首先需要一种获取以某个节点为子树的高度方法,使用递归实现。如果一个节点为空,那么这个节点肯定是一颗空树,高度为0;如果不为空,则遍历地比较它的左右子树高度,高的一个为这颗子树的最大高度,然后加上自身的高度即可。


    /**
     * 获取指定节点的子树的高度
     * @param node
     * @return
     */
    public int height(BinaryTreeNode node){
        if (node==null){
            return 0;
        }
        //递归获取左子树高度
        int l=height(node.getLeftChild());
        //递归获取右子树高度
        int r=height(node.getRightChild());
        //子树高度+1,因为还有节点这一层
        return l>=r? (l=1):(r=1);
    }

    /**
     * 获取二叉树的高度
     * @return
     */
    public int height(){
        return height(root);
    }

2.2.5、获取节点个数

获取二叉树节点数,需要获取以某个节点为根的子树的节点数实现。

如果节点为空,则个数肯定为0;如果不为空,则算上这个节点之后,继续递归计算所有子树的节点数,全部相加即可

    /**
     * 获取某个节点及子树的节点个数
     * @param node
     * @return
     */
    public int size(BinaryTreeNode node){
       if (node==null){
           return 0;
       }
       //递归获取左子树节点数
       int l=size(node.getLeftChild());
       //递归获取右子树节点数
       int r=size(node.getRightChild());
       return l+r+1;
    }

    /**
     * 获取二叉树节点个数
     * @return
     */
    public int size(){
        return size(root);
    }

2.2.6、获取末端叶子节点

通过递归左右子树,获得左右子树末端的叶子节点。


    /**
     * 获取节点左子树的末端节叶子点
     * @param node
     * @return
     */
    public BinaryTreeNode getLeftLeaf(BinaryTreeNode node){
        if (node==null){
           return null;
        }
        if (node.getLeftChild()==null){
            return node;
        }else{
           node=getLeftLeaf(node.getLeftChild());
        }
        return node;
    }

    /**
     * 获取整个二叉树左子树最末端的叶子节点
     * @return
     */
    public BinaryTreeNode getLeftLeaf(){
        return getLeftLeaf(root);
    }

    /**
     * 获取某个节点右子树最末端的叶子节点
     * @param node
     * @return
     */
    public BinaryTreeNode getRightLeaf(BinaryTreeNode node){
        if (node==null){
            return null;
        }
        if (node.getRightChild()==null){
            return node;
        }else{
            node=getRightLeaf(node.getRightChild());
        }
        return node;
    }

    /**
     * 获取整个二叉树右子树最末端叶子节点
     * @return
     */
    public BinaryTreeNode getRightLeaf(){
        return getRightLeaf(root);
    }


2.2.7、插入

这里只是实现了给节点插入左孩子、右孩子,只考虑了插入的节点的左右孩子不存在的情况。

    /**
     *  给某个节点插入左孩子
     * @param parent
     * @param newNode
     */
    public void insertLeft(BinaryTreeNode parent,BinaryTreeNode newNode){
        parent.setLeftChild(newNode);
        newNode.setParent(parent);
    }

    /**
     * 给某个节点插入右孩子
     * @param parent
     * @param newNode
     */
    public void insertRitht(BinaryTreeNode parent,BinaryTreeNode newNode){
        parent.setRightChild(newNode);
        newNode.setParent(parent);
    }

2.2.8、删除

从二叉树中删除节点,稍微复杂一些,需要考虑三种情形。

  • 被删除的节点左右子树均为空

这是最简单的一种情形,只需要把被被删除节点的父亲节点的孩子节点指向null即可。


图8:二叉树节点删除——左右子树均为空

在这里插入图片描述

  • 被删除的节点左右子树有一个为空 这种情形,只需要将被删除元素的左子树的父节点移动到被删除元素的父节点,然后将被删除元素移除即可。


图9:二叉树节点删除——右子树为空 在这里插入图片描述
图9:二叉树节点删除——左子树为空 在这里插入图片描述

  • 被删除的节点左右子树均不为空 这是最复杂的一种情形。这里博主偷了一个懒,选择了填坑的方式,将需要删除的节点删除,挖了一个坑,找到二叉树左子树叶子节点,把这个节点给填进去。

具体代码实现:

    /**
     * 删除节点
     * @param subNode 遍历的节点
     * @param node 待删除节点
     * @return
     */
    public BinaryTreeNode deleteNode(BinaryTreeNode subNode,BinaryTreeNode node){
       if (subNode==null){
           return null;
       }
       //父节点
       BinaryTreeNode parent=null;
       if (subNode.equals(node)){
           parent=node.getParent();
           //情形1、当前节点没有孩子节点
           if (subNode.getLeftChild()==null&&subNode.getRightChild()==null){
               //删除父节点和当前节点的关联
               this.changeChild(parent,subNode,null);
               //情形2、当前节点只有左节点或右节点
           } else if (subNode.getLeftChild()==null){   //情形2.1只有右孩子节点
               //将父节点孩子节点设置为当前节点的右孩子
               this.changeChild(parent,subNode,subNode.getRightChild());
           }else if (subNode.getRightChild()==null){  //情形2,2只有左孩子节点
               //将父节点孩子节点设置为当前节点的左孩子
               this.changeChild(parent,subNode,subNode.getLeftChild());
           }else{                             //情形3、左右孩子节点都有
               //左子树末端叶子节点
               BinaryTreeNode leftLeaf=getLeftLeaf(subNode);
               //将父节点孩子节点设置为末端叶子节点
               this.changeChild(parent,subNode,leftLeaf);
               //将叶子节点父节点子节点置为null
               this.changeChild(leftLeaf.getParent(),leftLeaf,null);
               //叶子节点父节点
               leftLeaf.setParent(parent);
              //被删除的节点置为null,帮助gc
               subNode=null;
           }

       }
       //递归左子树
       if (deleteNode(subNode.getLeftChild(),node)!=null){
           deleteNode(subNode.getLeftChild(),node);
       }else {
           //递归右子树
           deleteNode(subNode.getRightChild(),node);
       }
       return subNode;
    }


    /**
     *  替换父亲节点的孩子节点
     * @param parent 父亲节点
     * @param replacedNode 被替换的节点
     * @param aimNode 替换的节点
     */
    void changeChild(BinaryTreeNode parent,BinaryTreeNode replacedNode,BinaryTreeNode aimNode){
        //被替换节点是左孩子
        if (replacedNode==parent.getLeftChild()){
            parent.setLeftChild(aimNode);
        }else{
            //被替换节点是右孩子
            parent.setRightChild(aimNode);
        }
    }

2.3、遍历二叉树

常见的二叉树遍历方法有前序、中序、后序、层次等。


2.3.1、前序遍历

前序遍历(Preorder Traversal)是先遍历根结点, 再遍历左子树, 最后才遍历右子树。 及时二叉树非空, 则依次进行如下操作:

  • 访问根节点
  • 前序遍历左子树
  • 前序遍历右子树


图8:前序遍历二叉树示意图

在这里插入图片描述

    /**
     * 从某个节点开始先序遍历子树
     * @param node
     */
    public void preOrder(BinaryTreeNode node){
        if (node!=null){
            //遍历根节点
            System.out.println(node.getData());
            //遍历左子树
            preOrder(node.getLeftChild());
            //遍历右子树
            preOrder(node.getRightChild());
        }
    }

    /**
     * 先序遍历整个二叉树
     */
    public void  preOrder(){
        preOrder(root);
    }

2.3.2、中序遍历

中序遍历(Inorder Traversal)是先遍历左子树, 再遍历根结点, 最后才遍历右子树。 即若二叉树非空, 则依次进行如下操作:

  • 中序遍历左子树;
  • 访问根结点;
  • 中序遍历右子树。


图9:中序遍历二叉树示意图

在这里插入图片描述


    /**
     * 从某个节点开始中序遍历子树
     * @param node
     */
    public void inOrder(BinaryTreeNode node){
        if (node!=null){
            //中序遍历左子树
            inOrder(node.getLeftChild());
            //访问根节点
            System.out.println(node.getData());
            //中序遍历右子树
            inOrder(node.getRightChild());
        }
    }

    /**
     * 中序遍历整个二叉树
     */
    public void inOrder(){
        inOrder(root);
    }

2.3.3、后序遍历

后序遍历(Postorder Traversal)是先遍历左子树, 再遍历右子树, 最后才遍历根结点。即若二叉树非空, 则依次进行如下操作:

  • 后序遍历左子树;
  • 后序遍历右子树;
  • 访问根结点。


图10:后序遍历二叉树示意图 在这里插入图片描述


    /**
     * 从某个节点开始后序遍历子树
     * @param node
     */
    public void postOrder(BinaryTreeNode node){
        if (node!=null){
            //后序遍历左子树
            preOrder(node.getLeftChild());
            //后序遍历右子树
            preOrder(node.getRightChild());
            //访问根节点
            System.out.println(node.getData());
        }
    }

    /**
     * 后序遍历整个二叉树
     */
    public void postOrder(){
        preOrder(root);
    }

由二叉树的先序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树。


2.3.4、层次遍历

层次遍历是指从二叉树的第一层(根结点)开始, 从上至下逐层遍历, 在同一层中, 则按从左到右的顺序对结点逐个访问。


图11:层次遍历二叉树示意图

在这里插入图片描述

由层次遍历的操作可以推知, 在进行层次遍历时, 对一层结点访问完后, 再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问, 就完成了对下一层从左到右的访问。

因此, 在进行层次遍历时, 需设置一个队列结构, 遍历从二叉树的根结点开始, 首先将根结点指针入队, 然后从队头取出一个元素, 每取出一个元素, 执行两个操作: 访问该元素所指结点; 若该元素所指结点的左、 右孩子结点非空, 则将该元素所指结点的左孩子指针和右孩子指针顺序入队。此过程循环进行, 直至队列为空, 表示二叉树的层次遍历结束。

所以, 对一棵非空的二叉树进行层次遍历可按照如下步骤进行:

  • (1) 初始化一个队列;
  • (2) 二叉树的根结点放入队列;
  • (3) 重复步骤(4)~(7)直至队列为空;
  • (4) 从队列中取出一个结点 x;
  • (5) 访问结点x;
  • (6) 如果 x 存在左子结点, 将左子结点放入队列;
  • (7) 如果 x 存在右子结点, 将右子结点放入队列。

3、线索二叉树

在上面我们了解了二叉树的常见遍历方法,接下来看一看二叉树的线索化。

3.1、二叉树的线索化

在线性结构中, 各结点的逻辑关系是顺序的, 寻找某一结点的前趋结点和后继结点很方便。 对于二叉树, 由于它是非线性结构, 所以树中的结点不存在前趋和后继的概念, 但当我们对二叉树以某种方式遍历后, 就可以得到二叉树中所有结点的一个线性序列, 在这种意义下, 二叉树中的结点就有了前趋结点和后继结点。

二叉树通常采用二叉链表作为存储结构, 在这种存储结构下, 由于每个结点有两个分别指向其左儿子和右儿子的指针, 所以寻找其左、 右儿子结点很方便, 但要找该结点的前趋结点和后继结点则比较困难。


图12:二叉链表示意图

在这里插入图片描述

为方便寻找二叉树中结点的前趋结点或后继结点, 可以通过一次遍历记下各结点在遍历所得的线性序列中的相对位置。 保存这种信息的一种简单的方法是在每个结点增加两个指针域, 使它们分别指向依某种次序遍历时所得到的该结点的前趋结点和后继结点, 显然这样做要浪费相当数量的存储单元。

如果仔细分析一棵具有 n 个结点的二叉树, 就会发现,当它采用二叉链表作存储结构时, 二叉树中的所有结点共有n+1 个空指针域。 因此, 可以设法利用这些空指针碎来存放结点的前趋结点和后继结点的指针信息, 这种附加的指针称为“ 线索” 。 我们可以作这样的规定, 当某结点的左指针域为空时, 令其指向依某种方式遍历时所得到的该结点的前趋结点, 否则指向它的左儿子; 当某结点的右指针域为空时,令其指向依某种方式遍历时所得到的该结点的后继结点, 否则指向它的右儿子。


图13:增加线索的中序遍历二叉树 在这里插入图片描述

增加了线索的二叉链表称为线索链表, 相应的二叉树称为线索二叉树(Threaded Binary Tree)。

为了区分一个结点的指针是指向其儿子的指针还是指向其前趋或者后继的线索, 可以在每个结点上增加两个线索标志域 leftType 和 rightType, 这样线索链表的结点结构为:


图14:线索二叉树节点

在这里插入图片描述

一棵二叉树以某种方式遍历并使其变成线索二叉树的过程称为二叉树的线索化。对同一棵二叉树遍历的方式不同, 所得到的线索树也不同, 二叉树主要有前序、 中序和后序 3 种遍历方式, 所以线索树也有前序线索二叉树、 中序线索二叉树和后序线索二叉树3种。


图15:线索二叉树示意图

在这里插入图片描述

3.2、中序线索二叉树的实现

这一节来实现中序遍历线索二叉树。


3.1.1、线索二叉树节点

/**
 * @Author 三分恶
 * @Date 2020/10/11
 * @Description 线索二叉树节点
 */
public class ClueBinaryTreeNode {
    //节点数据
    int data;
    //左儿子
    ClueBinaryTreeNode leftNode;
    //右儿子
    ClueBinaryTreeNode rightNode;
    //标识指针类型,其中0,1分别表示有无线索化,默认为0
    int leftType;
    int rightType;
}    

3.1.2、创建中序线索二叉树

建立线索二叉树, 或者说, 对二叉树线索化, 实质上就是遍历一棵二叉树, 在遍历的过程中, 检査当前结点的左、 右指针域是否为空, 如果为空, 将它们改为指向前趋结点或后继结点的线索。

以图12的二叉树为例:


图16:中序线索二叉树

在这里插入图片描述

  • 定义一个节点pre用来存储当前节点,类似指针。

  • 从根节点1开始递归,如果当前节点为空,返回;遍历到4,此时4的前驱结点为null,结点5的前驱结点为2

  • 遍历到5的时候指向前驱结点2,前驱结点2为上一层递归的指针,因此指向它的前驱结点就行,再把左指针类型置为1

  • 如果当前节点的前驱结点pre的右指针为null,则将它设置为当前节点,此时即4的后继结点为2,并将右指针类型置为1

  • 每处理一个节点,当前节点是下一个节点的前驱节点

来看一下具体实现:

/**
 * @Author 三分恶
 * @Date 2020/10/11
 * @Description 中序线索二叉树
 */
public class ClueBinaryTree {
    private ClueBinaryTreeNode root;   //根节点
    private ClueBinaryTreeNode pre;   //每个节点的前趋节点

    public ClueBinaryTreeNode getRoot() {
        return root;
    }

    public void setRoot(ClueBinaryTreeNode root) {
        this.root = root;
    }

    /**
     * 构建中序线索二叉树
     */
    public void clueBinaryNodes() {
        clueBinaryNodes(root);
    }

    /**
     * 构建中序线索二叉树
     * @param node 起始节点
     */
    public void clueBinaryNodes(ClueBinaryTreeNode node) {
        //当前节点如果为null,直接返回
        if(node==null) {
            return;
        }
        //递归处理左子树
        clueBinaryNodes(node.leftNode);
        //处理前驱节点
        if(node.leftNode==null){
            //让当前节点的左指针指向前驱节点
            node.leftNode=pre;
            //改变当前节点左指针的类型
            node.leftType=1;
        }
        //处理前驱的右指针,如果前驱节点的右指针是null(没有指下右子树)
        if(pre!=null&&pre.rightNode==null) {
            //让前驱节点的右指针指向当前节点
            pre.rightNode=node;
            //改变前驱节点的右指针类型
            pre.rightType=1;
        }
        //每处理一个节点,当前节点是下一个节点的前驱节点
        pre=node;
        //处理右子树
        clueBinaryNodes(node.rightNode);
    }

}

4、二叉查找树

二叉树的一个重要作用是用作查找。

4.1、二叉查找树的概念和操作

二叉查找树定义

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

  • 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
  • 左、右子树也分别为二叉排序树;
  • 没有键值相等的节点。


图17:二叉查找树的示意图

在这里插入图片描述 二叉查找树的高度决定了二叉查找树的查找效率。

和普通的二叉树相比,二叉查找树的节点是有序的。

  二叉查找树的插入过程如下:

  • 若当前的二叉查找树为空,则插入的元素为根节点;

  • 若插入的元素值小于根节点值,则将元素插入到左子树中;

  • 若插入的元素值不小于根节点值,则将元素插入到右子树中。


4.1、二叉查找树的实现

  • 节点类:因为要比较节点大小,所以继承Comparable类
    /**
     * 二叉查找树节点
     *
     * @param <T>
     */
    class BSTNode<T extends Comparable<T>> {
        T key;                // 关键字(键值)
        BSTNode<T> left;    // 左孩子
        BSTNode<T> right;    // 右孩子
        BSTNode<T> parent;    // 父结点
        //省略构造方法、getter、setter
     }    
  • 插入:插入需要比较插入节点和当前节点的大小
   /**
     * 将结点插入到二叉树中
     *
     * @param bst 二叉树
     * @param z   插入的节点
     */
    private void insert(BSTree<T> bst, BSTNode<T> z) {
        int cmp;
        BSTNode<T> y = null;
        BSTNode<T> x = bst.mRoot;
        // 查找z的插入位置
        while (x != null) {
            y = x;
            //与当前节点比较
            cmp = z.key.compareTo(x.key);
            //比当前节点小,插入为左孩子
            if (cmp < 0) {
                x = x.left;
            } else {
                //比当前节点大,插入为右孩子
                x = x.right;
            }
        }
        z.parent = y;
        if (y == null)
            bst.mRoot = z;
        else {
            cmp = z.key.compareTo(y.key);
            if (cmp < 0) {
                y.left = z;
            } else {
                y.right = z;
            }
        }
    }

    /**
     * 新建结点(key),并将其插入到二叉树中
     * @param key 插入结点的键值
     */
    public void insert(T key) {
        BSTNode<T> z = new BSTNode<T>(key, null, null, null);
        //插入新节点
        if (z != null) {
            insert(this, z);
        }
    }

  • 查找:查找的节点比当前节点大就去查找右子树,比当前节点小就去查找左子树。

    /**
     *  (递归实现)查找"二叉树x"中键值为key的节点
     * @param x
     * @param key
     * @return
     */
    private BSTNode<T> search(BSTNode<T> x, T key) {
        if (x == null) {
            return x;
        }
        int cmp = key.compareTo(x.key);
        if (cmp < 0) {
            return search(x.left, key);
        } else if (cmp > 0) {
            return search(x.right, key);
        } else {
            return x;
        }
    }

    public BSTNode<T> search(T key) {
        return search(mRoot, key);
    }

    
    /**
     * (非递归实现)查找"二叉树x"中键值为key的节点
     * @param x
     * @param key
     * @return
     */
    private BSTNode<T> iterativeSearch(BSTNode<T> x, T key) {
        while (x != null) {
            int cmp = key.compareTo(x.key);
            if (cmp < 0) {
                x = x.left;
            } else if (cmp > 0) {
                x = x.right;
            } else {
                return x;
            }
        }

        return x;
    }

    public BSTNode<T> iterativeSearch(T key) {
        return iterativeSearch(mRoot, key);
    }

其余操作遍历,删除,清空等这里不再赘言。


文章超字数限制了,所以拆成上下两篇

下一篇:重学数据结构(六、树和二叉树:下)