二叉搜索樹詳解(Java實現)


二叉搜索樹定義


二叉搜索樹,是指一棵空樹或者具有下列性質的二叉樹:

  1. 若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
  2. 若任意節點的右子樹不空,則右子樹上所有節點的值均大於它的根節點的值;
  3. 任意節點的左,右子樹也分別為二叉搜索樹;
  4. 沒有鍵值相等的節點。

用Java來表示二叉樹


public class BinarySearchTree
{ // 二叉搜索樹類
    private class Node
    { // 節點類
        int data; // 數據域
        Node right; // 右子樹
        Node left; // 左子樹
    }

    private Node root; // 樹根節點
}

首先,需要一個節點對象的類。這個對象包含數據域和指向節點的兩個子節點的引用。

其次,需要一個樹對象的類。這個對象包含一個根節點root。

創建樹(insert)

    public void insert(int key)
    {    
        Node p=new Node(); //待插入的節點
        p.data=key;
        
        if(root==null)
        {
            root=p;
        }
        else
        {
            Node parent=new Node();
            Node current=root;
            while(true)
            {
                parent=current;
                if(key>current.data)    
                {
                    current=current.right; // 右子樹
                    if(current==null)
                    {
                        parent.right=p;
                        return;
                    }
                }
                else //本程序沒有做key出現相等情況的處理,暫且假設用戶插入的節點值都不同
                {
                    current=current.left; // 左子樹
                    if(current==null)
                    {
                        parent.left=p;
                        return;
                    }
                }
            }
        }
    }

創建樹的時候,主要用到了parent,current來記錄要插入節點的位置。哪么怎么檢驗自己是否正確地創建了一顆二叉搜索樹呢,我們通過遍歷來輸出各個節點的值

遍歷樹(travel)

遍歷指的是按照某種特定的次序來訪問二叉搜索樹中的每個節點,主要有三種遍歷的方法:

  1. 前序遍歷,“中左右”
  2. 中序遍歷,“左中右”
  3. 后續遍歷,“左右中”

上面的口訣“中左右”表示的含義是,先訪問根節點,再訪問左子,最后訪問右子。舉個例子:

  • 前序遍歷:39 24 23 30 64 53 60
  • 中序遍歷:23 24 30 39 53 60 64
  • 后序遍歷:23 30 24 60 53 64 39

你會發現,按照中序遍歷的規則將一個二叉搜索樹輸入,結果為按照正序排列。

    public void preOrder(Node root)
    { // 前序遍歷,"中左右"
        if (root != null)
        {
            System.out.print(root.data + " ");
            preOrder(root.left);
            preOrder(root.right);
        }
    }

    public void inOrder(Node root)
    { // 中序遍歷,"左中右"
        if (root != null)
        {
            inOrder(root.left);
            System.out.print(root.data + " ");
            inOrder(root.right);
        }
    }

    public void postOrder(Node root)
    { // 后序遍歷,"左右中"
        if (root != null)
        {
            postOrder(root.left);
            postOrder(root.right);
            System.out.print(root.data + " ");
        }
    }

    public void traverse(int traverseType)
    {    // 選擇以何種方式遍歷
        switch (traverseType)
        {
        case 1:
            System.out.print("preOrder traversal ");
            preOrder(root);
            System.out.println();
            break;
        case 2:
            System.out.print("inOrder traversal ");
            inOrder(root);
            System.out.println();
            break;
        case 3:
            System.out.print("postOrder traversal ");
            postOrder(root);
            System.out.println();
            break;
        }
    }

以上的代碼采用遞歸的方式實現三種遍歷,為了方便我們使用,又寫了一個traverse函數來實現選擇哪種方式進行樹的遍歷。

這會兒就可以寫單元測試了,我們首先創建一個二叉搜索樹,然后分別使用“前序”,“中序”,“后序”來遍歷輸出樹的所有節點。

    public static void main(String[] args)    //unit test
    {    
        BinarySearchTree tree=new BinarySearchTree();
        
        tree.insert(39);
        tree.insert(24);
        tree.insert(64);
        tree.insert(23);
        tree.insert(30);
        tree.insert(53);
        tree.insert(60);
        
        tree.traverse(1);
        tree.traverse(2);
        tree.traverse(3);
    }

運行該單元測試,可以看到如下的結果:

查找節點(find)

    public Node find(int key)
    { // 從樹中按照關鍵值查找元素
        Node current = root;
        while (current.data != key)
        {
            if (key > current.data)
                current = current.right;
            else
                current = current.left;
            if (current == null) return null;
        }
        return current;
    }
    
    public void show(Node node)
    {    //輸出節點的數據域
        if(node!=null)
            System.out.println(node.data);
        else
            System.out.println("null");
    }

 

查找節點比較簡單,如果找到節點則返回該節點,否則返回null。為了方便在控制台輸出,我們有添加了一個show函數,用來輸出節點的數據域。

刪除節點(delete)

刪除節點是二叉搜索樹中,最復雜的一種操作,但是也不是特別難,我們分類討論:

  • 要刪除節點有零個孩子,即葉子節點

如圖所示,只需要將parent.left(或者是parent.right)設置為null,然后Java垃圾自動回收機制會自動刪除current節點。

  • 要刪除節點有一個孩子

如圖所示,只需要將parent.left(或者是parent.right)設置為curren.right(或者是current.left)即可。

  • 要刪除節點有兩個孩子

這種情況比較復雜,首先我們引入后繼節點的概念,如果將一棵二叉樹按照中序周游的方式輸出,則任一節點的下一個節點就是該節點的后繼節點。例如:上圖中24的后繼節點為25,64的后繼節點為70.找到后繼節點以后,問題就變得簡單了,分為兩種情況:

1.后繼節點為待刪除節點的右子,只需要將curren用successor替換即可,注意處理好current.left和successor.right.

注意:這種情況下,successor一定沒有左孩子,一但它有左孩子,哪它必然不是current的后繼節點。

2.后繼節點為待刪除結點的右孩子的左子樹,這種情況稍微復雜點,請看動態圖片演示。

算法的步驟是:

  1. successorParent.left=successor.right
  2. successor.left=current.left
  3. parent.left=seccessor

弄懂原理后,我們來看具體的代碼實現:

private Node getSuccessor(Node delNode)    //尋找要刪除節點的中序后繼結點
    {
        Node successorParent=delNode;
        Node successor=delNode;
        Node current=delNode.right;
        
        //用來尋找后繼結點
        while(current!=null)
        {
            successorParent=successor;
            successor=current;
            current=current.left;
        }
        
        //如果后繼結點為要刪除結點的右子樹的左子,需要預先調整一下要刪除結點的右子樹
        if(successor!=delNode.right)
        {
            successorParent.left=successor.right;
            successor.right=delNode.right;
        }
        return successor;
    }
    
    public boolean delete(int key) // 刪除結點
    {
        Node current = root;
        Node parent = new Node();
        boolean isRightChild = true;
        while (current.data != key)
        {
            parent = current;
            if (key > current.data)
            {
                current = current.right;
                isRightChild = true;
            }
            else
            {
                current = current.left;
                isRightChild = false;
            }
            if (current == null) return false; // 沒有找到要刪除的結點
        }
        // 此時current就是要刪除的結點,parent為其父結點
        // 要刪除結點為葉子結點
        if (current.right == null && current.left == null) 
        {
            if (current == root)
            {
                root = null; // 整棵樹清空
            }
            else
            {
                if (isRightChild)
                    parent.right = null;
                else
                    parent.left = null;
            }
            return true;
        }
        //要刪除結點有一個子結點
        else if(current.left==null)
        {
            if(current==root)
                root=current.right;
            else if(isRightChild)
                parent.right=current.right;
            else
                parent.left=current.right;
            return true;
        }
        else if(current.right==null)
        {
            if(current==root)
                root=current.left;
            else if(isRightChild)
                parent.right=current.left;
            else
                parent.left=current.left;
            return true;
        }
        //要刪除結點有兩個子結點
        else 
        {
            Node successor=getSuccessor(current);    //找到要刪除結點的后繼結點
            
            if(current==root)
                root=successor;
            else if(isRightChild)
                parent.right=successor;
            else
                parent.left=successor;
            
            successor.left=current.left;
            return true;
        }
    }
二叉搜索樹刪除操作

大家注意哪個私有函數getSuccessor的功能,它不僅僅是用來找后繼結點的。

總結

二叉搜索樹其實不是特別難,理解以后,多練習幾次,應該可以掌握。以下是全部的代碼:

package org.yahuian;

public class BinarySearchTree
{ // 二叉搜索樹類
    private class Node
    { // 節點類
        int data; // 數據域
        Node right; // 右子樹
        Node left; // 左子樹
    }

    private Node root; // 樹根節點

    public void insert(int key)
    {
        Node p = new Node(); // 待插入的節點
        p.data = key;

        if (root == null)
        {
            root = p;
        }
        else
        {
            Node parent = new Node();
            Node current = root;
            while (true)
            {
                parent = current;
                if (key > current.data)
                {
                    current = current.right; // 右子樹
                    if (current == null)
                    {
                        parent.right = p;
                        return;
                    }
                }
                else // 本程序沒有做key出現相等情況的處理,暫且假設用戶插入的節點值都不同
                {
                    current = current.left; // 左子樹
                    if (current == null)
                    {
                        parent.left = p;
                        return;
                    }
                }
            }
        }
    }

    public void preOrder(Node root)
    { // 前序遍歷,"中左右"
        if (root != null)
        {
            System.out.print(root.data + " ");
            preOrder(root.left);
            preOrder(root.right);
        }
    }

    public void inOrder(Node root)
    { // 中序遍歷,"左中右"
        if (root != null)
        {
            inOrder(root.left);
            System.out.print(root.data + " ");
            inOrder(root.right);
        }
    }

    public void postOrder(Node root)
    { // 后序遍歷,"左右中"
        if (root != null)
        {
            postOrder(root.left);
            postOrder(root.right);
            System.out.print(root.data + " ");
        }
    }

    public void traverse(int traverseType)
    { // 選擇以何種方式遍歷
        switch (traverseType)
        {
        case 1:
            System.out.print("preOrder traversal ");
            preOrder(root);
            System.out.println();
            break;
        case 2:
            System.out.print("inOrder traversal ");
            inOrder(root);
            System.out.println();
            break;
        case 3:
            System.out.print("postOrder traversal ");
            postOrder(root);
            System.out.println();
            break;
        }
    }

    public Node find(int key)
    { // 從樹中按照關鍵值查找元素
        Node current = root;
        while (current.data != key)
        {
            if (key > current.data)
                current = current.right;
            else
                current = current.left;
            if (current == null) return null;
        }
        return current;
    }
    
    public void show(Node node)
    {    //輸出節點的數據域
        if(node!=null)
            System.out.println(node.data);
        else
            System.out.println("null");
    }
    
    private Node getSuccessor(Node delNode)    //尋找要刪除節點的中序后繼結點
    {
        Node successorParent=delNode;
        Node successor=delNode;
        Node current=delNode.right;
        
        //用來尋找后繼結點
        while(current!=null)
        {
            successorParent=successor;
            successor=current;
            current=current.left;
        }
        
        //如果后繼結點為要刪除結點的右子樹的左子,需要預先調整一下要刪除結點的右子樹
        if(successor!=delNode.right)
        {
            successorParent.left=successor.right;
            successor.right=delNode.right;
        }
        return successor;
    }
    
    public boolean delete(int key) // 刪除結點
    {
        Node current = root;
        Node parent = new Node();
        boolean isRightChild = true;
        while (current.data != key)
        {
            parent = current;
            if (key > current.data)
            {
                current = current.right;
                isRightChild = true;
            }
            else
            {
                current = current.left;
                isRightChild = false;
            }
            if (current == null) return false; // 沒有找到要刪除的結點
        }
        // 此時current就是要刪除的結點,parent為其父結點
        // 要刪除結點為葉子結點
        if (current.right == null && current.left == null) 
        {
            if (current == root)
            {
                root = null; // 整棵樹清空
            }
            else
            {
                if (isRightChild)
                    parent.right = null;
                else
                    parent.left = null;
            }
            return true;
        }
        //要刪除結點有一個子結點
        else if(current.left==null)
        {
            if(current==root)
                root=current.right;
            else if(isRightChild)
                parent.right=current.right;
            else
                parent.left=current.right;
            return true;
        }
        else if(current.right==null)
        {
            if(current==root)
                root=current.left;
            else if(isRightChild)
                parent.right=current.left;
            else
                parent.left=current.left;
            return true;
        }
        //要刪除結點有兩個子結點
        else 
        {
            Node successor=getSuccessor(current);    //找到要刪除結點的后繼結點
            
            if(current==root)
                root=successor;
            else if(isRightChild)
                parent.right=successor;
            else
                parent.left=successor;
            
            successor.left=current.left;
            return true;
        }
    }
    
    public static void main(String[] args) // unit test
    {
        BinarySearchTree tree = new BinarySearchTree();

        tree.insert(39);
        tree.insert(24);
        tree.insert(64);
        tree.insert(23);
        tree.insert(30);
        tree.insert(53);
        tree.insert(60);

        tree.traverse(1);
        tree.traverse(2);
        tree.traverse(3);
        
        tree.show(tree.find(23));
        tree.show(tree.find(60));
        tree.show(tree.find(64));
        
        tree.delete(23);
        tree.delete(60);
        tree.delete(64);
        
        tree.show(tree.find(23));
        tree.show(tree.find(60));
        tree.show(tree.find(64));
    }
}
二叉搜索樹詳解

動態圖片來自於:https://visualgo.net/en/bst


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM