二叉搜索树的java实现详解编程语言

 转载请注明出处

一、概念

  二叉搜索树也成二叉排序树,它有这么一个特点,某个节点,若其有两个子节点,则一定满足,左子节点值一定小于该节点值,右子节点值一定大于该节点值,对于非基本类型的比较,可以实现Comparator接口,在本文中为了方便,采用了int类型数据进行操作。

  要想实现一颗二叉树,肯定得从它的增加说起,只有把树构建出来了,才能使用其他操作。

二、二叉搜索树构建

   谈起二叉树的增加,肯定先得构建一个表示节点的类,该节点的类,有这么几个属性,节点的值,节点的父节点、左节点、右节点这四个属性,代码如下

 1     static class Node{ 
 2         Node parent; 
 3         Node leftChild; 
 4         Node rightChild; 
 5         int val; 
 6         public Node(Node parent, Node leftChild, Node rightChild,int val) { 
 7             super(); 
 8             this.parent = parent; 
 9             this.leftChild = leftChild; 
10             this.rightChild = rightChild; 
11             this.val = val; 
12         } 
13          
14         public Node(int val){ 
15             this(null,null,null,val); 
16         } 
17          
18         public Node(Node node,int val){ 
19             this(node,null,null,val); 
20         } 
21  
22     }

        这里采用的是内部类的写法,构建完节点值后,再对整棵树去构建,一棵树,先得有根节点,再能延伸到余下子节点,那在这棵树里,也有一些属性,比如基本的根节点root,树中元素大小size,这两个属性,如果采用了泛型,可能还得增加Comparator属性,或提供其一个默认实现。具体代码如下

 public class SearchBinaryTree { 
     
    private Node root; 
    private int size; 
    public SearchBinaryTree() { 
        super(); 
    } 
}

三、增加

    当要进行添加元素的时候,得考虑根节点的初始化,一般情况有两种、当该类的构造函数一初始化就对根节点root进行初始化,第二种、在进行第一次添加元素的时候,对根节点进行添加。理论上两个都可以行得通,但通常采用的是第二种懒加载形式。

    在进行添加元素的时候,有这样几种情况需要考虑

       一、添加时判断root是否初始化,若没初始化,则初始化,将该值赋给根节点,size加一。

       二、因为二叉树搜索树满足根节点值大于左节点,小于右节点,需要将插入的值,先同根节点比较,若大,则往右子树中进行查找,若小,则往左子树中进行查找。直到某个子节点。

       这里的插入实现,可以采用两种,一、递归、二、迭代(即通过while循环模式)。

  3.1、递归版本插入

 1    public boolean add(int val){ 
 2         if(root == null){ 
 3             root = new Node(val); 
 4             size++; 
 5             return true; 
 6         } 
 7         Node node = getAdapterNode(root, val); 
 8         Node newNode = new Node(val); 
 9         if(node.val > val){ 
10             node.leftChild = newNode; 
11             newNode.parent = node; 
12         }else if(node.val < val){ 
13             node.rightChild = newNode; 
14             newNode.parent = node; 
15         }else{ 
16             // 暂不做处理 
17         } 
18         size++;19         return true; 
20     } 
21      
22     /** 
23      * 获取要插入的节点的父节点,该父节点满足以下几种状态之一 
24      *  1、父节点为子节点 
25      *  2、插入节点值比父节点小,但父节点没有左子节点 
26      *  3、插入节点值比父节点大,但父节点没有右子节点 
27      *  4、插入节点值和父节点相等。 
28      *  5、父节点为空 
29      *  如果满足以上5种情况之一,则递归停止。 
30      * @param node 
31      * @param val 
32      * @return 
33      */ 
34     private Node getAdapterNode(Node node,int val){ 
35         if(node == null){ 
36             return node; 
37         } 
38         // 往左子树中插入,但没左子树,则返回 
39         if(node.val > val && node.leftChild == null){ 
40             return node; 
41         } 
42         // 往右子树中插入,但没右子树,也返回 
43         if(node.val < val && node.rightChild == null){ 
44             return node; 
45         } 
46         // 该节点是叶子节点,则返回 
47         if(node.leftChild == null && node.rightChild == null){ 
48             return node; 
49         } 
50          
51         if(node.val > val && node.leftChild != null){ 
52             return getAdaptarNode(node.leftChild, val); 
53         }else if(node.val < val && node.rightChild != null){ 
54             return getAdaptarNode(node.rightChild, val); 
55         }else{ 
56             return node; 
57         } 
58     }

 

   使用递归,先找到递归的结束点,再去把整个问题化为子问题,在上述代码里,逻辑大致是这样的,先判断根节点有没有初始化,没初始化则初始化,完成后返回,之后通过一个函数去获取适配的节点。之后进行插入值。

3.2、迭代版本

public boolean put(int val){ 
        return putVal(root,val); 
    } 
    private boolean putVal(Node node,int val){ 
        if(node == null){// 初始化根节点 
            node = new Node(val); 
            root = node; 
            size++; 
            return true; 
        } 
        Node temp = node; 
        Node p; 
        int t; 
        /** 
         * 通过do while循环迭代获取最佳节点, 
         */ 
        do{  
            p = temp; 
            t = temp.val-val; 
            if(t > 0){ 
                temp = temp.leftChild; 
            }else if(t < 0){ 
                temp = temp.rightChild; 
            }else{ 
                temp.val = val; 
                return false; 
            } 
        }while(temp != null); 
        Node newNode = new Node(p, val); 
        if(t > 0){ 
            p.leftChild = newNode; 
        }else if(t < 0){ 
            p.rightChild = newNode; 
        } 
        size++; 
        return true; 
    }

 

原理其实和递归一样,都是获取最佳节点,在该节点上进行操作。

论起性能,肯定迭代版本最佳,所以一般情况下,都是选择迭代版本进行操作数据。

四、删除

    可以说在二叉搜索树的操作中,删除是最复杂的,要考虑的情况也相对多,在常规思路中,删除二叉搜索树的某一个节点,肯定会想到以下四种情况,

 二叉搜索树的java实现详解编程语言

   1、要删除的节点没有左右子节点,如上图的D、E、G节点

   2、要删除的节点只有左子节点,如B节点

   3、要删除的节点只有右子节点,如F节点

   4、要删除的节点既有左子节点,又有右子节点,如 A、C节点

对于前面三种情况,可以说是比较简单,第四种复杂了。下面先来分析第一种

 若是这种情况,比如 删除D节点,则可以将B节点的左子节点设置为null,若删除G节点,则可将F节点的右子节点设置为null。具体要设置哪一边,看删除的节点位于哪一边。

第二种,删除B节点,则只需将A节点的左节点设置成D节点,将D节点的父节点设置成A即可。具体设置哪一边,也是看删除的节点位于父节点的哪一边。

第三种,同第二种。

第四种,也就是之前说的有点复杂,比如要删除C节点,将F节点的父节点设置成A节点,F节点左节点设置成E节点,将A的右节点设置成F,E的父节点设置F节点(也就是将F节点替换C节点),还有一种,直接将E节点替换C节点。那采用哪一种呢,如果删除节点为根节点,又该怎么删除?

 对于第四种情况,可以这样想,找到C或者A节点的后继节点,删除后继节点,且将后继节点的值设置为C或A节点的值。先来补充下后继节点的概念。

一个节点在整棵树中的后继节点必满足,大于该节点值得所有节点集合中值最小的那个节点,即为后继节点,当然,也有可能不存在后继节点。

但是对于第四种情况,后继节点一定存在,且一定在其右子树中,而且还满足,只有一个子节点或者没有子节点两者情况之一。具体原因可以这样想,因为后继节点要比C节点大,又因为C节点左右子节一定存在,所以一定存在右子树中的左子节点中。就比如C的后继节点是F,A的后继节点是E。

有了以上分析,那么实现也比较简单了,代码如下

 1 public boolean delete(int val){ 
 2         Node node = getNode(val); 
 3         if(node == null){ 
 4             return false; 
 5         } 
 6         Node parent = node.parent; 
 7         Node leftChild = node.leftChild; 
 8         Node rightChild = node.rightChild; 
 9         //以下所有父节点为空的情况,则表明删除的节点是根节点 
10         if(leftChild == null && rightChild == null){//没有子节点 
11             if(parent != null){ 
12                 if(parent.leftChild == node){ 
13                     parent.leftChild = null; 
14                 }else if(parent.rightChild == node){ 
15                     parent.rightChild = null; 
16                 } 
17             }else{//不存在父节点,则表明删除节点为根节点 
18                 root = null; 
19             } 
20             node = null; 
21             return true; 
22         }else if(leftChild == null && rightChild != null){// 只有右节点 
23             if(parent != null && parent.val > val){// 存在父节点,且node位置为父节点的左边 
24                 parent.leftChild = rightChild; 
25             }else if(parent != null && parent.val < val){// 存在父节点,且node位置为父节点的右边 
26                 parent.rightChild = rightChild; 
27             }else{ 
28                 root = rightChild; 
29             } 
30             node = null; 
31             return true; 
32         }else if(leftChild != null && rightChild == null){// 只有左节点 
33             if(parent != null && parent.val > val){// 存在父节点,且node位置为父节点的左边 
34                 parent.leftChild = leftChild; 
35             }else if(parent != null && parent.val < val){// 存在父节点,且node位置为父节点的右边 
36                 parent.rightChild = leftChild; 
37             }else{ 
38                 root = leftChild; 
39             } 
40             return true; 
41         }else if(leftChild != null && rightChild != null){// 两个子节点都存在 
42             Node successor = getSuccessor(node);// 这种情况,一定存在后继节点 
43             int temp = successor.val; 
44             boolean delete = delete(temp); 
45             if(delete){ 
46                 node.val = temp; 
47             } 
48             successor = null; 
49             return true; 
50         } 
51         return false; 
52     } 
53      
54     /** 
55      * 找到node节点的后继节点 
56      * 1、先判断该节点有没有右子树,如果有,则从右节点的左子树中寻找后继节点,没有则进行下一步 
57      * 2、查找该节点的父节点,若该父节点的右节点等于该节点,则继续寻找父节点, 
58      *   直至父节点为Null或找到不等于该节点的右节点。 
59      * 理由,后继节点一定比该节点大,若存在右子树,则后继节点一定存在右子树中,这是第一步的理由 
60      *      若不存在右子树,则也可能存在该节点的某个祖父节点(即该节点的父节点,或更上层父节点)的右子树中, 
61      *      对其迭代查找,若有,则返回该节点,没有则返回null 
62      * @param node 
63      * @return 
64      */ 
65     private Node getSuccessor(Node node){ 
66         if(node.rightChild != null){ 
67             Node rightChild = node.rightChild; 
68             while(rightChild.leftChild != null){ 
69                 rightChild = rightChild.leftChild; 
70             } 
71             return rightChild; 
72         } 
73         Node parent = node.parent; 
74         while(parent != null && (node == parent.rightChild)){ 
75             node = parent; 
76             parent = parent.parent; 
77         } 
78         return parent; 
79     }

具体逻辑,看上面分析,这里不作文字叙述了,

除了这种实现,在算法导论书中,提供了另外一种实现。

 1    public boolean remove(int val){ 
 2         Node node = getNode(val); 
 3         if(node == null){ 
 4             return false; 
 5         } 
 6         if(node.leftChild == null){// 1、左节点不存在,右节点可能存在,包含两种情况  ,两个节点都不存在和只存在右节点 
 7             transplant(node, node.rightChild); 
 8         }else if(node.rightChild == null){//2、左孩子存在,右节点不存在 
 9             transplant(node, node.leftChild); 
10         }else{// 3、两个节点都存在 
11             Node successor = getSuccessor(node);// 得到node后继节点  
12             if(successor.parent != node){// 后继节点存在node的右子树中。 
13                 transplant(successor, successor.rightChild);// 用后继节点的右子节点替换该后继节点 
14                 successor.rightChild = node.rightChild;// 将node节点的右子树赋给后继节点的右节点,即类似后继与node节点调换位置 
15                 successor.rightChild.parent = successor;// 接着上一步  给接过来的右节点的父引用复制 
16             } 
17             transplant(node, successor); 
18             successor.leftChild = node.leftChild; 
19             successor.leftChild.parent = successor; 
20         } 
21         return true; 
22     } 
23     /** 
24      * 将child节点替换node节点 
25      * @param root    根节点 
26      * @param node    要删除的节点 
27      * @param child   node节点的子节点 
28      */ 
29     private void transplant(Node node,Node child){ 
30         /** 
31          * 1、先判断 node是否存在父节点 
32          *    1、不存在,则child替换为根节点 
33          *    2、存在,则继续下一步 
34          * 2、判断node节点是父节点的那个孩子(即判断出 node是右节点还是左节点), 
35          *    得出结果后,将child节点替换node节点 ,即若node节点是左节点 则child替换后 也为左节点,否则为右节点 
36          * 3、将node节点的父节点置为child节点的父节点 
37          */ 
38          
39         if(node.parent == null){ 
40             this.root = child; 
41         }else if(node.parent.leftChild == node){ 
42             node.parent.leftChild = child; 
43         }else if(node.parent.rightChild == node){ 
44             node.parent.rightChild = child; 
45         } 
46         if(child != null){ 
47             child.parent = node.parent; 
48         } 
49     }

 

五、查找

  查找也比较简单,其实在增加的时候,已经实现了。实际情况中,这部分可以抽出来单独方法。代码如下

 1   public Node getNode(int val){ 
 2         Node temp = root; 
 3         int t; 
 4         do{ 
 5             t = temp.val-val; 
 6             if(t > 0){ 
 7                 temp = temp.leftChild; 
 8             }else if(t < 0){ 
 9                 temp = temp.rightChild; 
10             }else{ 
11                 return temp; 
12             } 
13         }while(temp != null); 
14         return null; 
15     }

 

六、二叉搜索树遍历

  在了解二叉搜索树的性质后,很清楚的知道,它的中序遍历是从小到大依次排列的,这里提供中序遍历代码

 1    public void print(){ 
 2         print(root); 
 3     } 
 4     private void print(Node root){ 
 5         if(root != null){ 
 6             print(root.leftChild); 
 7             System.out.println(root.val);// 位置在中间,则中序,若在前面,则为先序,否则为后续 
 8             print(root.rightChild); 
 9         } 
10     }

 

——————————————————————————————————-华丽分割线———————————————————————————————-

 以上都是个人见解,若有错误或不足之处,还望指正!!!

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/12340.html

(0)
上一篇 2021年7月19日 14:34
下一篇 2021年7月19日 14:34

相关推荐

发表回复

登录后才能评论