1. 程式人生 > >Java資料結構和演算法--二叉樹

Java資料結構和演算法--二叉樹

在資料結構中,對於有序陣列來說查詢很快,但是插入和刪除慢,因為插入和刪除需要先找到指定的位置,後面所有的元素都要移動一個位置,為插入騰出一個位置或填入刪除的那個位置; 而對於連結串列來說,插入和刪除快,但是查詢很慢,插入和刪除只要更改一下元素的引用值即可,而查詢每次都要從頭開始遍歷直到找到目標元素為止。那麼有沒有一種資料結構能夠同時具備查詢、插入、刪除都快的呢?所以有了樹的誕生!

樹結構有很多種,我們這裡主要講講常見的二叉樹。二叉樹 :樹的每個節點最多有兩個位元組點。如下圖所示:

在上圖中,A是根節點,B是A的左節點,C是A的右節點,BDE是A的左子樹,CF是A的右子樹。在二叉樹中,有一種樹叫二叉搜尋樹,本文主要講解二叉搜尋樹的相關知識。二叉搜尋樹的要求是:如果一個節點存在左子樹,那麼左子樹中的所有節點的值都比此節點的值小;同樣,如果一個節點存在右子樹,右子樹中的所有節點的值都比此節點的值大。簡單地說,就是二叉搜尋樹的左邊的值由上到下不斷變小,樹的右邊的值由上到下不斷變大。二叉搜尋樹的結構圖示例如下:

二叉搜尋樹作為一種資料結構,那麼它的增刪改查又是如何實現的?下面我們就來看看

首先我們定義二叉樹節點的類如下:

public class Node {

    private Object data;    //節點資料
    private Node leftChild; //左子節點的引用
    private Node rightChild; //右子節點的引用

    //列印節點內容
    public void display() {
        System.out.println(data);
    }

    public Node(Object data) {
        this.data = data;
    }
}

定義二叉搜尋樹相關的介面

public interface Tree {

    //查詢節點
    public Node find(int key);
    //插入新節點
    public boolean insert(int data);

    //中序遍歷
    public void infixOrder(Node current);
    //前序遍歷
    public void preOrder(Node current);
    //後序遍歷
    public void postOrder(Node current);

    //查詢最大值
    public Node findMax();
    //查詢最小值
    public Node findMin();

    //刪除節點
    public boolean delete(int key);

    //......

}

下面我們就基於上面的介面定義的方法來講解一下,二叉搜尋樹是如何實現的?

1.查詢節點

查詢節點的演算法:我們首先從根節點開始遍歷,如果查詢值比當前節點值小,便去左子樹搜尋,如果查詢值比當前節點值大,便去右子樹搜尋,如果查詢值等於當前節點值,則停止搜尋。程式碼示例:

//查詢節點
public Node find(int key) {
    Node current = root; //根節點

    while(current != null){
        if(current.data > key){ //當前值比查詢值大,搜尋左子樹
            current = current.leftChild;
        }else if(current.data < key){ //當前值比查詢值小,搜尋右子樹
            current = current.rightChild;
        }else{
            return current;
        }
    }
    return null;//遍歷完整個樹沒找到,返回null
}

2.插入新節點

要插入節點,首先得找到待插入的位置。同樣,我們還是從根節點開始比較,如果節點值大於插入值,則搜尋左子樹,如果節點值小於插入值,則搜尋右子樹,這樣一直搜尋,直到找到比插入值小的所有節點中最大的那個或者比插入值大的所有節點中的最小的那個節點,如果插入值比這個節點小,則新節點插入到它的左邊,反之插入右邊。程式碼示例如下:

//插入節點
public boolean insert(int data) {
    //要插入的新節點
    Node newNode = new Node(data);

    if(root == null){ //當前樹為空樹,沒有任何節點
        root = newNode;
        return true;
    }else{
        Node current = root;
        Node parentNode = null;
        while(current != null){
            parentNode = current;
            if(current.data > data){ //當前值比插入值大,搜尋左子節點
                current = current.leftChild;
                //如果當前節點的左節點為空的時候,代表已經找到了比插入值小的所有節點中最大的那
                //個節點了,此時就將新節點插入到此節點的左邊
                if(current == null){ //左子節點為空,直接將新值插入到該節點
                    parentNode.leftChild = newNode;
                    return true;
                }
            }else{
                current = current.rightChild;
                if(current == null){ //右子節點為空,直接將新值插入到該節點
                    parentNode.rightChild = newNode;
                    return true;
                }
            }
        }
    }
    return false;
}

可以這樣理解插入的演算法,當我們拿插入值跟節點值比較的時候,只有兩種情況,插入值要麼比節點值大,要麼就比節點值小,當然排除相等的情況。當這個節點左右子節點都不為空的時候,還不是插入的時候,只有當至少有一個子節點為null的時候,才可以插入。

3.遍歷樹

遍歷樹分為三種情況,前序遍歷(根節點 --> 左節點 --> 右節點)、中序遍歷(左節點 --> 根節點 --> 右節點)、後序遍歷(左節點 --> 右節點 --> 根節點),我們主要看根節點的位置,左右節點的順序都是由左到右。比如下面的二叉樹:

結果:前序遍歷(ABDGCFK)、中序遍歷(DGBAFCK)、後序遍歷(GDBKFCA)。總結一下,前序遍歷是從根節點開始遍歷,然後是左子樹,然後是右子樹;中序遍歷是從左子樹開始遍歷,然後是根節點,然後是右子樹;後序遍歷是從左子樹開始遍歷,然後是右子樹,然後是根節點。

程式碼示例:

//中序遍歷
public void infixOrder(Node current){
    if(current != null){
        //這裡利用了遞迴的演算法,遍歷左子樹,直到左子樹為null的時候跳出
        infixOrder(current.leftChild);
        System.out.print(current.data+" ");
        //左子樹跳出的時候,開始遍歷右子樹,右子樹的遍歷規則和上面一樣,還是先遍歷右子樹中的左子樹
        //直到左子樹為null的時候跳出
        infixOrder(current.rightChild);
    }
}
 
//前序遍歷
public void preOrder(Node current){
    if(current != null){
        System.out.print(current.data+" ");
        preOrder(current.leftChild);
        preOrder(current.rightChild);
    }
}
 
//後序遍歷
public void postOrder(Node current){
    if(current != null){
        postOrder(current.leftChild);
        postOrder(current.rightChild);
        System.out.print(current.data+" ");
    }
}

4.查詢最大值和最小值

找最小值,先找根的左節點,再找左節點的左節點,直到找到一個節點沒有左節點了,那麼這個節點就是最小節點了。同理,找最大值是先找根節點的右節點,再找右節點的右節點,直到有一個節點沒有右節點了,那麼這個節點就是最大右節點了。程式碼示例:

//找到最大值
public Node findMax(){
    //從根節點開始
    Node current = root;
    //記錄最大節點
    Node maxNode = current;
    while(current != null){
        maxNode = current;
        current = current.rightChild;
    }
    return maxNode;
}
//找到最小值
public Node findMin(){
    Node current = root;
    Node minNode = current;
    while(current != null){
        minNode = current;
        current = current.leftChild;
    }
    return minNode;
}

5.刪除節點

刪除節點的操作比較複雜一點,要分三種情況:

  • 節點是一個葉節點(沒有子節點)
  • 節點只有一個子節點
  • 節點有兩個子節點,最複雜

下面我們分別來解析一下

5.1 刪除節點是一個葉節點,這個簡單,我們只要將父節點對該節點的引用置為null即可

public boolean delete(int key) {
    //從根節點開始查詢
    Node current = root;
    Node parent = root;
    boolean isLeftChild = false;
    //查詢刪除值,找不到直接返回false
    while(current.data != key){
        parent = current;
        //如果要刪的key小於當前節點值,那麼就去左節點尋找,否則去右節點尋找
        if(key < current.data){
            //記錄刪除節點是父節點的左節點還是右節點,在後面要刪除的時候才知道,是置父節點的左節點為null還是右節點為null
            isLeftChild = true;
            current = current.leftChild;
        }else{
            isLeftChild = false;
            current = current.rightChild;
        }
        //查詢到葉節點了,還是沒有扎到匹配的節點,那麼返回false
        if(current == null){
            return false;
        }
    }

    //如果當前節點沒有子節點
    if(current.leftChild == null && current.rightChild == null){
        if(current == root){
            root = null;
        }else if(isLeftChild){
            parent.leftChild = null;
        }else{
            parent.rightChild = null;
        }
        return true;
    }
    return false;
}

5.2 刪除有一個子節點的節點,我們只需要將父節點對刪除節點的引用指向刪除節點的子節點即可。程式碼示例如下

public boolean delete(int key) {
    //從根節點開始查詢
    Node current = root;
    Node parent = root;
    boolean isLeftChild = false;
    //查詢刪除值,找不到直接返回false
    while(current.data != key){
        parent = current;
        //如果要刪的key小於當前節點值,那麼就去左節點尋找,否則去右節點尋找
        if(key < current.data){
            //記錄刪除節點是父節點的左節點還是右節點,在後面要刪除的時候才知道,是置父節點的左節點為null還是右節點為null
            isLeftChild = true;
            current = current.leftChild;
        }else{
            isLeftChild = false;
            current = current.rightChild;
        }
        //查詢到葉節點了,還是沒有扎到匹配的節點,那麼返回false
        if(current == null){
            return false;
        }
    }

    //如果當前節點沒有子節點
    if(current.leftChild == null && current.rightChild == null){
        if(current == root){
            root = null;
        }else if(isLeftChild){
            parent.leftChild = null;
        }else{
            parent.rightChild = null;
        }
        return true;
    }
    //刪除節點有子節點
    else{
    //current.leftChild != null && current.rightChild == null
    if(current == root){
        root = current.leftChild;
    }else if(isLeftChild){
        parent.leftChild = current.leftChild;
    }else{
        parent.rightChild = current.leftChild;
    }
    return true;
        }

    return false;
}

5.3 刪除節點有兩個子節點,這個就比較複雜一點,圖示如下

我們需要找個節點來替代被刪除的節點, 根據二叉搜尋樹的排序規則,我們只要找到所有比6大的節點中最小的那個節點就是繼承節點了。比如上圖中的9就是繼承節點,我們只需要將9替代6,那麼此時也是符合搜尋二叉樹規則的。後繼節點就是比刪除節點大的最小節點。

演算法:首先找到刪除節點的右節點,如果該右節點還有左節點,順著左節點往下找,直到找到那個沒有左節點的節點就是繼承節點了。

下面我們分兩種情況討論,一種是後繼節點沒有右節點,一種是後繼節點還有右節點

① 後繼節點是刪除節點的右節點並且後繼節點沒有右節點,如上示例圖職工,9是繼承節點並且沒有右節點,那麼此時直接將9節點替代6節點即可。圖示如下

 

② 後繼節點不是刪除節點的直接右節點,此時我們需要將後繼節點父節點的對它的引用指向後繼節點的右節點

//刪除節點有兩個子節點
public Node delNodeWithTwoNodes(Node delNode){
    Node successorParent = delNode;
    Node successor = delNode;
    Node current = delNode.rightChild;
    while(current != null){
        successorParent = successor;
        successor = current;
        current = current.leftChild;
    }

    //將後繼節點替換刪除節點
    if(successor != delNode.rightChild){
        successorParent.leftChild = successor.rightChild;
        successor.rightChild = delNode.rightChild;
    }
     
    return successor;
}

上面我們可以看到,對二叉樹的操作都是需要從根節點開始往下一層一層查詢的。從效率來說,不管查詢還是刪除都是比較高的。本文完整程式碼連結:https://github.com/jiusetian/DataStructureDemo/tree/master/app/src/main/java/binarytree