一篇文章徹底理解二分搜索樹(附代碼實現)

本文使用JAVA語言進行描述。其實本質上什麼語言描述數據結構都是可以的。

二叉樹基礎

在這裏插入圖片描述

二叉樹的根節點

在這裏插入圖片描述
二叉樹遞歸結構:
在這裏插入圖片描述
上面是一個滿二叉樹。但是實際中也有二叉樹不是滿的。
在這裏插入圖片描述
在這裏插入圖片描述

二分搜索樹

在這裏插入圖片描述
二分搜索樹也不一定是滿的。
在這裏插入圖片描述
所以使用二分搜索樹需要具備的條件:

存儲的元素必須有可比較性。

樹結構的定義代碼實現:

/**
 * Created by hongyonghan on 2020/4/19.
 */
//爲了讓這個二分搜索樹具有可比較性。那麼繼承Comparable的接口。
public class BST<E extends Comparable<E>> {
    private class Node{
        private E e;
        private Node left,right;

        public Node(E e) {
            this.e = e;
            left=null;
            right=null;
        }
    }
    private Node root;
    private int size;

    public BST() {
        root=null;
    }

    public int size()
    {
        return size;
    }

    public  boolean isEmpty()
    {
        return size == 0;
    }
}

添加新元素

如果樹之前是空的話,那麼添加一個新元素之後,這個元素就是樹的根。比如這裏添加元素41.如果新加一個元素22,那麼會根據二分搜索樹的性質,將22作爲二分搜索樹的左子樹的根節點。
在這裏插入圖片描述
在這裏插入圖片描述
那麼該將60添加到哪個位置呢?邏輯如下:

60大於41,在右子樹,60大於58,在右子樹。所以添加如下:
在這裏插入圖片描述
這裏的二分搜索樹不包含重複元素。

如果想包含重複元素的話,只需要改變定義:左子樹小於等於節點,或者右子樹大於等於節點。

增加代碼和查詢代碼:

  public void add(E e)
    {
        root=add(root,e);
    }
    private Node add(Node node,E e)
    {
        if (node == null)
        {
            size++;
           return new Node(e);
        }
        if (e.compareTo(node.e) < 0)
        {
            node.left=add(node.left,e);
        }else if (e.compareTo(node.e) > 0)
        {
            node.right = add(node.right,e);
        }
        return node;
    }
		//是否包含元素e
    public boolean contains(E e)
    {
        //整體看以整顆二分搜索樹爲根的二分搜索樹中是否包含元素e
        return contains(root,e);
    }

    //看以node爲根的二分搜索樹中是否包含元素e,遞歸算法
    private boolean contains(Node node,E e)
    {
        if (node == null)
            return false;
        if (e.compareTo(node.e) == 0)
            return true;
        else if (e.compareTo(node.e) < 0)
        {
            //如果不在node上面,那就去node的左子樹去找,否則去node的右子樹去找
            return contains(node.left,e);
        }
        else
        {
            return contains(node.right,e);
        }
    }

遍歷操作

在這裏插入圖片描述

前序遍歷

在這裏插入圖片描述
前序遍歷代碼:

//二分搜索樹的前序遍歷
    public void preOrder()
    {
        preOrder(root);
    }
    //前序遍歷以node爲根的二分搜索樹,遞歸算法
    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);
    }

對每一個節點使用遞歸遍歷的方式,會有三次遍歷的機會。分別是:在遍歷左子樹之前會訪問這個根節點。在遍歷左子樹完成之後會回到根節點,會訪問這個根節點,在遍歷右子樹之後會回到這個根節點,會再遍歷一次。
在這裏插入圖片描述
那麼對一棵樹來說,前序遍歷的過程:
在這裏插入圖片描述
這是前序遍歷一棵樹的部分過程,從28->16->13,下面的點表示的是這個訪問了幾次。

訪問左子樹完成之後:
在這裏插入圖片描述
前序遍歷的完整過程:
在這裏插入圖片描述
這裏面的藍色的點就是第一次訪問這個節點時候。然後對這個節點進行輸出操作。

中序遍歷:

中序遍歷是當第二次訪問到這個節點時候進行操作,第一次訪問到該節點不進行操作。
在這裏插入圖片描述
例如這裏的16,第一次訪問16時,不輸出16,只是第二次訪問16時才輸出16.

下面是左子樹完成遍歷的過程。
在這裏插入圖片描述
圖中藍色的點是對節點進行操作的時候:
在這裏插入圖片描述
後序遍歷:
在這裏插入圖片描述
只有第三次訪問節點的時候,纔對節點進行操作。

部分樹的操作如下:
在這裏插入圖片描述

前序遍歷的非遞歸的寫法

前序遍歷的遞歸寫法:
在這裏插入圖片描述
這裏藉助棧的特性記錄遍歷的位置:

首先,訪問28節點,將節點壓入棧,也就是接下來要訪問28這個根節點了。
在這裏插入圖片描述
然後訪問28這個節點,節點訪問完成之後,28這個節點出棧
在這裏插入圖片描述
然後準備訪問兩個子節點,壓棧30,16兩個元素。因爲先訪問16,所以要先壓棧30.
在這裏插入圖片描述
然後16出棧,壓棧22和13.
在這裏插入圖片描述
然後出棧13,壓入13的子節點,但是子節點爲空,什麼都不用壓入了。然後出棧22.訪問22節點。
在這裏插入圖片描述
然後出棧30,訪問節點30;
在這裏插入圖片描述
然後將42和29入棧,然後訪1問29節點。
在這裏插入圖片描述
然後取棧頂元素42.
在這裏插入圖片描述
遍歷完成!


    //前序遍歷的非遞歸的遍歷。
    public void preOrderNR()
    {
        Stack<Node> stack=new Stack<>();
        //先入棧根節點
        stack.push(root);
        while (!stack.isEmpty())
        {
            //cur節點就是當前要訪問的節點
            Node cur=stack.pop();
            System.out.println(cur.e);

            if (cur.right != null)
                stack.push(cur.right);
            if (cur.left != null)
                stack.push(cur.left);
        }
    }

廣度優先遍歷

按照層數來進行遍歷。
在這裏插入圖片描述
這個是使用非遞歸的方式進行遍歷的。使用隊列的結構。

先將根節點入隊,然後再將左右孩子入隊。

具體展示如圖:

初始化的時候將隊首入隊:
在這裏插入圖片描述
然後遍歷的時候先看隊首是誰,訪問隊首之後,將隊首出隊。這裏是指28出隊。
在這裏插入圖片描述
然後將第二層16和30入隊。因爲隊列裏面是先進先出,所以將樹的左子樹先入隊,然後右子樹再入隊,這裏是16先入隊,然後30再入隊。
在這裏插入圖片描述
然後取出隊首元素16,對16進行訪問。然後將16的孩子13和22入隊。
在這裏插入圖片描述
然後30出隊,對30進行訪問。並對30的兩個孩子29和42進行入隊。
在這裏插入圖片描述
然後將13進行出隊,並進行訪問。13s是葉子節點,沒有元素入隊。
在這裏插入圖片描述
然後直到隊首爲空:遍歷結束:
在這裏插入圖片描述
編程實現:

 //層序遍歷(廣度優先遍歷)
    public void levelOrder()
    {
        Queue<Node> q=new LinkedList<>();
        //將根節點入隊
        q.add(root);
        while (!q.isEmpty())
        {
            Node cur=q.remove();
            System.out.println(cur.e);

            if (cur.left != null)
                q.add(cur.left);
            if(cur.right != null)
                q.add(cur.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);
    }

廣度優先遍歷可以更快的找到你想要查找的元素。主要用於搜索上面。

刪除節點

找到樹中的最小值和最大值
在這裏插入圖片描述
在這裏插入圖片描述
不同樹的結構是不同的,所以最小值可能不都在最左邊的葉子節點,這裏16就是最小值。

所以最小值是向左走再也走不動的位置。最大值是向右走再也走不動了。這裏最大值是30。

代碼實現:

 //找到樹中元素的最小值
    public E minimum()
    {
        if (size==0)
            throw new IllegalArgumentException("BST is empty");
        return minimum(root).e;
    }
    private Node minimum(Node node)
    {
        if (node.left == null)
            return node;
        return minimum(node.left);
    }

    //找到樹中元素的最大值
    public E maximum()
    {
        if (size==0)
            throw new IllegalArgumentException("BST is empty");
        return maximum(root).e;
    }

    private Node maximum(Node node)
    {
        if (node.right == null)
             return node;
        return maximum(node.right);
    }
刪除二分搜索樹的最小值

如果最小值是這種情況:
在這裏插入圖片描述
這個是個葉子節點,直接將這個節點刪除就行了。

但是如果最小值不是葉子節點呢?
在這裏插入圖片描述
那麼就可以將22刪除後,然後將後面的右子樹做成41的左子樹。
在這裏插入圖片描述

刪除二分搜索樹的最大值

如果是葉子節點,直接將葉子節點刪除就好。

在這裏插入圖片描述
刪除後:
在這裏插入圖片描述
如果不是葉子節點,例如這裏的58這個節點,將這個節點刪除之後,直接將這個節點的左子樹變成41的右子樹就可以了。
在這裏插入圖片描述

 //從二分搜索樹中刪除最小值所在節點,返回最小值
    public E removeMin()
    {
        E ret=minimum();
        root=removeMin(root);
        return ret;
    }

    //刪除掉以node爲根的二分搜索樹中的最小節點
    //返回刪除節點後的新的二分搜索樹的節點
    private Node removeMin(Node node)
    {
        if (node.left == null)
        {
            //但是可能有右子樹,那麼這個右子樹要保存起來
            Node rightNode=node.right;
            node.right=null;
            size--;
            return rightNode;
        }
        node.left=removeMin(node.left);
        return node;
    }

    //從二分搜索樹中刪除最大值所在的節點
    public E removeMax()
    {
        E ret=removeMax();
        root=removeMax(root);
        return ret;
    }
    //刪除掉以node爲根的二分搜索樹中的最大節點
    //返回刪除節點後新的二分搜索樹的根
    private Node removeMax(Node node)
    {
        if (node.right == null)
        {
            Node leftNode=node.left;
            node.left=null;
            size--;
            return leftNode;
        }

        node.right=removeMax(node.right);
        return node;
    }
刪除任意元素

1.刪除的節點只有左孩子(或者右孩子)的話。例如下面刪除的節點是58.
在這裏插入圖片描述
就是將這個節點刪除後,然後將這個50當作根節點的右孩子。

2.刪除左右都有孩子的節點d
在這裏插入圖片描述
刪除58這個節點之後,要找一個新的節點來替代他,那麼這裏找的是比58這個節點還大的那個節點。也就是找到58的右子樹中最小的那個節點。也就是59那個節點。
在這裏插入圖片描述
刪除之後:
在這裏插入圖片描述
然後刪除d,s成爲新的子樹的根。
在這裏插入圖片描述
代碼如下:

//從二分搜索樹中刪除元素e的節點
    public void remove(E e)
    {
        root=remove(root,e);
    }

    //刪除掉以node爲根的二分搜索樹中值爲e的節點,遞歸算法
    //返回刪除節點後新的二分搜索樹的根
    private Node remove(Node node,E e)
    {
        if (node == null)
        {
            return null;
        }
        if (e.compareTo(node.e) < 0)
        {
            //去左子樹去找待刪除的元素
            node.left=remove(node.left,e);
            return node;
        }
        else if (e.compareTo(node.e) > 0)
        {
            //將刪除的那個
            node.right=remove(node.right,e);
            return node;
        }
        else
        {
            //e==node.e
            //待刪除節點左子樹爲空的情況
            if (node.left == null)
            {
                Node rightNode=node.right;
                node.right=null;
                size--;
                return rightNode;
            }
            //待刪除節點右子樹爲空的情況
            if (node.right == null)
            {
                Node leftNode=node.left;
                node.left=null;
                size--;
                return leftNode;
            }
            //待刪除的節點左右子樹都不爲空的情況
            //找到比待刪除節點大的最小節點,即待刪除節點右子樹的最小節點。
            //用這個節點頂替待刪除節點的位置。
            Node successor = minimum(node.right);
            successor.right=removeMin(node.right);
            successor.left=node.left;

            node.left=node.right=null;
            return successor;



        }
    }

二分搜索樹的順序性

1.使用中序遍歷的方法得到的就是有序的。

2.使用刪除最大值和最小值的方法得到的也是有序的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章