數據結構之二叉樹解析


曾經有個朋友問我:二叉樹可以用來干啥況?

我回答他:可以搜索、可以排序呀?

可是,排序有快速排序,歸並排序,查找有二分法,甚至直接遍歷查找,我干啥要使用二叉樹呢?

……

  這位朋友說的是有道理的,二叉樹確實在實際中用的比較少,因為有更高級的樹,但是二叉樹作為一種最基本最典型的排序樹,是研究其他樹的基礎。除此之外,在面試數據結構的時候,二叉樹原理被問到的概率是相當高的。言歸正傳,我們來分析分析二叉樹。

  我們知道,在有序數組中,可以快速找到特定的值,但是想在有序數組中插入一個新的數據項,就必須首先找出新數據項插入的位置,然后將比新數據項大的數據項向后移動一位,來給新的數據項騰出空間,刪除同理,這樣移動很費時。顯而易見,如果要做很多的插入和刪除操作和刪除操作,就不該選用有序數組

  另一方面,鏈表中可以快速添加和刪除某個數據項但是在鏈表中查找數據項可不容易,必須從頭開始訪問鏈表的每一個數據項,直到找到該數據項為止,這個過程很慢。

  樹這種數據結構,既能像鏈表那樣快速的插入和刪除,又能想有序數組那樣快速查找。這里主要實現一種特殊的樹——二叉(搜索)樹。二叉搜索樹有如下特點:一個節點的左子節點的關鍵字值小於這個節點,右子節點的關鍵字值大於或等於這個節點。插入一個節點需要根據這個規則進行插入

  刪除節點時二叉搜索樹中最復雜的操作,但是刪除節點在很多樹的應用中又非常重要,所以詳細研究並總結下特點。刪除節點要從查找要刪的節點開始入手,首先找到節點,這個要刪除的節點可能有三種情況需要考慮。

  • 該節點是葉節點,沒有子節點

  • 該節點有一個子節點

  • 該節點有兩個子節點

  第一種最簡單,第二種也還是比較簡單的,第三種就相當復雜了。下面分析這三種刪除情況:

  要刪除葉節點,只需要改變該節點的父節點對應子字段的值即可,由指向該節點改為 null 就可以了。垃圾回收器會自動回收葉節點,不需要自己手動刪掉;當節點有一個子節點時,這個節點只有兩個連接:連向父節點和連向它唯一的子節點。需要從這個序列中剪斷這個節點,把它的子節點直接連到它的父節點上即可,這個過程要求改變父節點適當的引用(左子節點還是右子節點),指向要刪除節點的子節點即可;第三種情況最復雜,如果要刪除有兩個子節點的節點,就不能只用它的一個子節點代替它,比如要刪除節點25,如果用35取代它,那35的左子節點是15呢還是30?

  因此需要考慮另一種方法,尋找它的中序后繼來代替該節點。下圖顯示的就是要刪除節點用它的后繼代替它的情況,刪除后還是有序的。(這里還有更麻煩的情況,即它的后繼自己也有右子節點,下面再討論。)

  那么如何找后繼節點呢?首先得找到要刪除的節點的右子節點,它的關鍵字值一定比待刪除節點的大。然后轉到待刪除節點右子節點的左子節點那里(如果有的話),然后到這個左子節點的左子節點,以此類推,順着左子節點的路徑一直向下找,這個路徑上的最后一個左子節點就是待刪除節點的后繼如果待刪除節點的右子節點沒有左子節點,那么這個右子節點本身就是后繼。尋找后繼的示意圖如下:

  找到了后繼節點,現在開始刪除了,先看第一種情況,后繼節點是delNode右子節點的做后代,這種情況要執行以下四個步驟:

  • 把后繼父節點的leftChild字段置為后繼的右子節點;

  • 把后繼的rightChild字段置為要刪除節點的右子節點;

  • 把待刪除節點從它父節點的leftChild或rightChild字段刪除,把這個字段置為后繼;

  • 把待刪除的左子節點移除,將后繼的leftChild字段置為待刪除節點的左子節點。

這下圖所示:

  如果后繼節點就是待刪除節點的右子節點,這種情況就簡單了,因為只需要把后繼為跟的子樹移到刪除的節點的位置即可。如下圖所示:

  看到這里,就會發現刪除時相當棘手的操作。實際上,因為它非常復雜,一些程序員都嘗試着躲開它,他們在Node類中加了一個Boolean字段來標識該節點是否已經被刪除,在其他操作之前會先判斷這個節點是不是已經刪除了,這樣刪除節點不會改變樹的結構。當然樹中還保留着這種已經刪除的節點,對存儲造成浪費,但是如果沒有那么多刪除的話,這也不失為一個好方法。

  另外二叉樹有三種遍歷方式:前序中序后序。這個比較簡單,直接看下代碼即可。

下面手寫個二叉搜索樹的代碼:

public class BNode {
    public int key;
    public double data;
    public BNode parent;
    public BNode leftChild;
    public BNode rightChild;

    public void displayNode() {
        System.out.println("{" + key + ":" + data + "}");
    }
}
public class BinaryTree {
    private BNode root;  // 根節點

    public BinaryTree(BNode root) {
        root = null;
    }

    //二搜索樹查找的時間復雜度為O(logN)
    //find node with given key
    public BNode find(int key) {
        BNode current = root;
        while (current.key != key) {
            if (key < current.key) {
                current = current.leftChild;
            } else {
                current = current.rightChild;
            }
            if (current == null) {
                return null;
            }
        }
        return current;
    }

    //插入節點
    public void inset(int key, int value) {
        BNode newNode = new BNode();
        newNode.key = key;
        newNode.data = value;
        if (root == null) {
            root = newNode;
        } else {
            BNode current = root;
            BNode parent;
            while (true) {
                parent = current;
                if (key < current.key) {
                    //turn left
                    current = current.leftChild;
                    if (current == null) {
                        parent.leftChild = newNode;
                        newNode.parent = parent;
                        return;
                    }
                } else {
                    //trun right
                    current = current.rightChild;
                    if (current == null) {
                        parent.rightChild = newNode;
                        newNode.parent = parent;
                        return;
                    }
                }
            }
        }
    }

    //遍歷二叉樹
    public void traverse(int traverseType) {
        switch (traverseType) {
            case 1:
                System.out.println("PreOrder Traversal!");
                preOrder(root);
                break;
            case 2:
                System.out.println("InOrder Traversal!");
                inOrder(root);
                break;
            case 3:
                System.out.println("PostOrder Traversal");
                postOrder(root);
            default:
                System.out.println("PreOrder Traversal!");
                preOrder(root);
                break;

        }
        System.out.println("");
    }

    //前序遍歷
    public void preOrder(BNode localRoot) {
        if (localRoot != null) {
            System.out.println(localRoot.data + " ");
            preOrder(localRoot.leftChild);
            preOrder(localRoot.rightChild);
        }
    }

    //中序遍歷
    public void inOrder(BNode localRoot) {
        if (localRoot != null) {
            inOrder(localRoot.leftChild);
            System.out.println(localRoot.data + " ");
            inOrder(localRoot.rightChild);
        }
    }

    //后序遍歷
    public void postOrder(BNode localRoot) {
        if (localRoot != null) {
            postOrder(localRoot.leftChild);
            postOrder(localRoot.rightChild);
            System.out.println(localRoot.data + " ");
        }
    }

    //查找最小值
    /*根據二叉搜索樹存儲規則:最小值應該是左邊那個沒有子節點的那個節點*/
    public BNode minNumber() {
        BNode current = root;
        BNode parent = root;
        while (current != null) {
            parent = current;
            current = current.leftChild;
        }
        return parent;
    }

    //查找最大值
    /*根據二叉搜索樹的存儲規則:最大值應該是右邊那個沒有子節點的那個節點*/
    public BNode maxNumber() {
        BNode current = root;
        BNode parent = root;
        while (current != null) {
            parent = current;
            current = current.rightChild;
        }
        return parent;
    }

    //刪除節點
    /*
    刪除節點在二叉樹中是最復雜的,主要有三種情況:
    1、該節點沒有子節點(簡單)
    2、該節點有一個子節點(還行)
    3、該節點有兩個子節點(復雜)
    刪除節點的時間復雜度為O(logN)
    */
    public boolean delete(int key) {
        BNode current = root;
        boolean isLeftChild = true;
        if (current == null) {
            return false;
        }
        //尋找要刪除的節點
        while (current.data != key) {
            if (key < current.data) {
                isLeftChild = true;
                current = current.leftChild;
            } else {
                isLeftChild = false;
                current = current.rightChild;
            }
            if (current == null) {
                return false;
            }
        }
        //找到了要刪除的節點,下面開始刪除
        //1、要刪除的節點沒有子節點,直接將其父節點的左子節點或者右子節點賦為null即可
        if (current.leftChild == null && current.rightChild == null) {
            return false;
        }
        //3、要刪除的節點有兩個子節點
        else if (current.leftChild != null && current.rightChild != null) {
            return false;
        }
        //2、要刪除的節點有一個子節點,直接將其砍斷,將其子節點與其父節點連接起來即可,要考慮特殊情況就是刪除根節點,因為根節點沒有父節點
        else {
            return false;
        }
    }

    public boolean deleteNoChild(BNode node, boolean isLeftChild) {
        if (node == root) {
            root = null;
            return true;
        }
        if (isLeftChild) {
            node.parent.leftChild = null;
        } else {
            node.parent.rightChild = null;
        }
        return true;
    }

    public boolean deleteOneChild(BNode node, boolean isLeftChild) {
        if (node.leftChild == null) {
            if (node == root) {
                root = node.rightChild;
                node.parent = null;
                return true;
            }
            if (isLeftChild) {
                node.parent.leftChild = node.rightChild;
            } else {
                node.parent.rightChild = node.rightChild;
            }
        } else {
            if (node == root) {
                root = node.leftChild;
                node.parent = null;
                return true;
            }
            if (isLeftChild) {
                node.parent.leftChild = node.leftChild;
            } else {
                node.parent.rightChild = node.rightChild;
            }
        }
        return true;
    }

    public boolean deleteTwoChild(BNode node, boolean isLeftChild) {
        BNode successor = getSuccessor(node);
        if (node == root) {
            successor.leftChild = root.leftChild;
            successor.rightChild = root.rightChild;
            successor.parent = null;
            root = successor;
        } else if (isLeftChild) {
            node.parent.leftChild = successor;
        } else {
            node.parent.rightChild = successor;
        }
        //connect successor to node's left child
        successor.leftChild = node.leftChild;
        return true;
    }

    //獲得要刪除節點的后繼節點(中序遍歷的下一個節點)
    public BNode getSuccessor(BNode delNode) {
        BNode successor = delNode;
        BNode current = delNode.rightChild;
        while (current != null) {
            successor = current;
            current = current.leftChild;
        }
        if (successor != delNode.rightChild) {
            (successor).leftChild = successor.rightChild;
            if (successor.rightChild != null) {
                //刪除后續節點在原來的位置
                successor.rightChild.parent = successor.parent;
            }
            //將后續節點放到正確的位置,與右邊連上
            successor.rightChild = delNode.rightChild;
        }
        return successor;
    }
}

 

 

 

 

原文參考微信公眾號【程序員私房菜】


免責聲明!

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



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