簡單易懂帶你了解二叉樹


前言

上一篇博客為大家介紹了數組與鏈表這兩種數據結構,雖然它們在某些方面有着自己的一些優點,但是也存在着一些自身的缺陷,本篇博客為將為大家介紹一下數據結構---二叉樹,它在保留數組和鏈表的優點的同時也改善了它們的缺點(當然它也有着自己的缺點,同時它的實現也比較復雜).

1. 數組和鏈表的特點

數組的優點:

  • 簡單易用.
  • 無序數組的插入速度很快,效率為O(1)
  • 有序數組的查找速度較快(較無序數組),效率為O(logN)

數組的缺點:

  • 數組的查找、刪除很慢
  • 數組一旦確定長度,無法改變

鏈表的優點:

  • 可以無限擴容(只要內存夠大)
  • 在鏈表頭的新增、刪除很快,效率為O(1)

鏈表的缺點:

  • 查找很慢
  • 在非鏈表頭的位置新增、刪除很慢,效率為O(N)

2.樹和二叉樹

樹是一種數據結構,因為它數據的保存形式很像一個樹,所以得名為樹(樹狀圖).

而二叉樹是一種特殊的樹,它的每個節點最多含有兩個子樹,現實世界中的二叉樹:


圖1

但是實際中的二叉樹卻是倒掛的,如圖:


圖2

二叉樹的名詞解釋:

  • 根:樹頂端的節點稱為根。一棵樹只有一個根,如果要把一個節點和邊的集合稱為樹,那么從根到其他任何一個節點都必須有且只有一條路徑。A是根節點。
  • 父節點:若一個節點含有子節點,則這個節點稱為其子節點的父節點;B是D的父節點。
  • 子節點:一個節點含有的子樹的根節點稱為該節點的子節點;D是B的子節點。
  • 兄弟節點:具有相同父節點的節點互稱為兄弟節點;比如上圖的D和E就互稱為兄弟節點。
  • 葉節點:沒有子節點的節點稱為葉節點,也叫葉子節點,比如上圖的E、H、L、J、G都是葉子節點。
  • 子樹:每個節點都可以作為子樹的根,它和它所有的子節點、子節點的子節點等都包含在子樹中。
  • 節點的層次:從根開始定義,根為第一層,根的子節點為第二層,以此類推。
  • 深度:對於任意節點n,n的深度為從根到n的唯一路徑長,根的深度為0;
  • 高度:對於任意節點n,n的高度為從n到一片樹葉的最長路徑長,所有樹葉的高度為0;

深度與高度的區別在於: 深度為根到節點的距離,而高度是節點到葉的距離(記住根深葉高)。

3.二叉搜索樹以及它是通過什么方式改善的數組、鏈表的問題

二叉搜索樹是一種特殊的二叉樹,除了它的子節點不能超過兩個以外,它還擁有如下特點:

  • 一個節點的左子節點的關鍵字的值永遠小於該節點的值
  • 一個節點的右子節點的關鍵字的值永遠大於等於該節點的值


圖3 - 二叉搜索樹關鍵字的排序方式

從圖3還可以看出,二叉搜索樹的最小值就是它的最左節點的關鍵字的值,而最大值則是它的最右節點的值.

二叉搜索樹的查找、新增、刪除的效率為O(logN)(這是理想狀態下,如果樹是不平衡的效率會降到O(N),后面會介紹).

二叉搜索樹之所以效率高就在於:

  1. 它的數據是按照上述的有序的方式排列的.
  2. 進行新增、查找、刪除的時候使用了二分查找法.

4. 二叉樹的實現

二叉樹中數據是保存在一個個的節點中的,下面是保存數據的節點類:


/**
 * @author liuboren
 * @Title: 節點類
 * @Description:
 * @date 2019/11/28 9:33
 */
public class Node {
    // 用來進行排序的關鍵字數組
    int sortData ;

    // 其他類型的數據
    int other;

    // 該節點的左子節點
    Node leftNode;

    // 該節點的右子節點
    Node rightNode;

    public static void main(String[] args) {
        Node node = new Node();
        System.out.println("node.leftNode = " + node.leftNode);
        System.out.println(node.leftNode);
    }



}

在二叉搜索樹這個類中新增、修改、刪除數據:

public class Tree {

    // 根節點
    Node root;

    public Tree(Node root) {
        this.root = root;
    }

// 新增、查找、刪除 暫時省略,下面會一一介紹
}

4.1 新增數據

在二叉樹中插入數據的流程如下:

圖4


圖5

Java代碼:

 /*新增數據*/
    public void insertData(Node node) {
        int currentSortData = root.sortData;
        Node currentNode = root;
        Node currentLeftNode = root.leftNode;
        Node currentRightNode = root.rightNode;
        int insertSortData = node.sortData;
        while (true) {
            if (insertSortData < currentSortData) {
                if (currentLeftNode == null) {
                    currentNode.leftNode = node;
                    break;
                } else {
                    currentNode = currentNode.leftNode;
                    currentLeftNode = currentNode.leftNode;
                    currentRightNode = currentNode.rightNode;
                    currentSortData = currentNode.sortData;
                }
            } else {
                if (currentRightNode == null) {
                    currentNode.rightNode = node;
                    break;
                } else {
                    currentNode = currentNode.rightNode;
                    currentSortData = currentNode.sortData;
                    currentLeftNode = currentNode.leftNode;
                    currentRightNode = currentNode.rightNode;
                }
            }

        }
        System.out.println("root = " + root);
    }

4.3 查找方法

流程與插入方法類似.

Java代碼:

public void query(int sortData) {
    Node currentNode = root;
    while (true) {
        if (sortData != currentNode.sortData) {
            if (sortData < currentNode.sortData) {
                if (currentNode.leftNode != null) {
                    currentNode = currentNode.leftNode;
                } else {
                    System.out.println("對不起沒有查詢到數據");
                }
            } else {
                if (currentNode.rightNode != null) {
                    currentNode = currentNode.rightNode;
                } else {
                    System.out.println("對不起沒有查詢到數據");
                }
            }
        } else {
            System.out.println("二叉樹中有該數據");
        }
    }
}

4.3 刪除方法

刪除節點要分三種情況.

  • 刪除節點無子節點的情況
  • 刪除節點有一個子節點的情況
  • 刪除節點有兩個子節點的情況

刪除節點無子節點的情況是最簡單的,直接將該節點置為null就可以了:


圖6

刪除節點有一個子節點的情況:


圖7

刪除后:

圖8

最復雜的刪除節點有兩個子節點的情況,刪除流程如下:


圖9

刪除后:

圖10

為什么要以這種方式刪除節點呢? 再次回顧一下二叉搜索樹的特點:

  • 一個節點的左子節點的關鍵字的值永遠小於該節點的值
  • 一個節點的右子節點的關鍵字的值永遠大於等於該節點的值

之所以要找刪除節點的右子節點的最后一個左節點,是因為這個值是刪除節點的子節點中最小的值,為了滿足上面的這兩個特點,所以刪除要以這種算法去實現.

Java代碼:

 public boolean delete(int deleteData) {
        Node curr = root;
        Node parent = root;
        boolean isLeft = true;
        while (deleteData != curr.sortData) {
            if (deleteData <= curr.sortData) {
                isLeft = true;
                if (curr.leftNode != null) {
                    parent = curr;
                    curr = curr.leftNode;
                }
            } else {
                isLeft = false;
                if (curr.rightNode != null) {
                    parent = curr;
                    curr = curr.rightNode;
                }
            }
            if (curr == null) {
                return false;
            }
        }
        // 刪除節點沒有子節點的情況
        if (curr.leftNode == null && curr.rightNode == null) {
            if (curr == root) {
                root = null;
            } else if (isLeft) {
                parent.leftNode = null;
            } else {
                parent.rightNode = null;
            }
            //刪除節點只有左節點
        } else if (curr.rightNode == null) {
            if (curr == root) {
                root = root.leftNode;
            } else if (isLeft) {
                parent.leftNode = curr.leftNode;
            } else {
                parent.rightNode = curr.leftNode;
            }
            //如果被刪除節點只有右節點
        } else if (curr.leftNode == null) {
            if (curr == root) {
                root = root.rightNode;
            } else if (isLeft) {
                parent.leftNode = curr.rightNode;
            } else {
                parent.rightNode = curr.rightNode;
            }
        } else {
            Node successor = getSuccessor(curr);
            if (curr == root) {
                root = successor;
            } else if (curr == parent.leftNode) {
                parent.leftNode = successor;
            } else {
                parent.rightNode = successor;
            }
            successor.leftNode = curr.leftNode;
        }
        return true;

    }

    public Node getSuccessor(Node delNode) {
        Node curr = delNode.rightNode;
        Node successor = curr;
        Node sucParent = null;
        while (curr != null) {
            sucParent = successor;
            successor = curr;
            curr = curr.leftNode;
        }
        if (successor != delNode.rightNode) {
            sucParent.leftNode = successor.rightNode;
            successor.rightNode = delNode.rightNode;
        }
        return successor;
    }

5. 遍歷

遍歷二叉樹中的數據,有三種遍歷方式:

  • 前序
  • 中序(最常用)
  • 后續

前序、中序和后序三種遍歷方式的步驟是相同的,只是順序不同.

前序遍歷順序:

  • 先輸出當前節點
  • 再遍歷左子節點
  • 再遍歷右子節點

中序遍歷順序:

  • 先遍歷左子節點
  • 再輸出當前節點
  • 再遍歷右子節點

后序遍歷順序:

  • 先遍歷左子節點
  • 再遍歷又子節點
  • 再輸出當前節點

什么當前節點?什么左右子節點?太抽象!!!!沒關系繼續看圖.

前序遍歷輸出順序圖:

圖11

中序遍歷輸出順序圖:

圖12

后序遍歷輸出順序圖:

圖13

可以看出所謂的前中后序是輸出當前節點的順序,前序是在第一個輸出當前節點,中序是第二個輸出當前節點,后序是第三個當前節點.

又因為中序遍歷是按照關鍵值由小到大的順序輸出的,所以中序遍歷最為常用.

前、后序遍歷在解析或分析二叉樹(不是二叉搜索樹)的算術表達式的時候比較有用,用的不太多,看下圖:

6. 二叉樹的效率

我們用二叉樹與數組和鏈表進行對比,在有100w個數據項的無序數組或鏈表中,查找數據項平均會比較50w次,但在有100w個節點的樹中,只需要20(或更少)次的比較.

有序數組可以很快的找到數據項,但插入數據項平均需要移動50w個數據項,在100w個節點的樹中插入數據項需要比較20或更少次的比較,再加上很短的時間來連接數據項.

同樣,從有100w個數據項的數組中刪除一個數據項需要平均移動50w個數據項,而在100w個節點的樹中刪除節點只需要20次或更少的比較來找到它,再加上(可能的話)一點比較的時間來找到它的后繼,一點時間來斷開這個節點的鏈接,以及連接它的后繼.

結論: 樹對所有常用的數據存儲操作都有很高的效率

遍歷不如其他操作快. 但是,遍歷在大型數據庫中不是常用的操作.它更長用於程序中的輔助方法來解析算術或其他的表達式,而且表達式一般都不會很長.

如果二叉樹是平衡的,它的效率為: O(logN),如果二叉樹是不平衡的(最極端的情況,存入樹中的數據是升序或降序排列的,那么二叉樹就是鏈表),效率為: O(N)

所以二叉搜索樹在保存隨機數值的時候,效率才是最高的

7. 二叉樹的缺點

如果二叉樹是極端不平衡的(此時的二叉樹就是一個鏈表),它的效率為O(N),即使數值是隨機的,如果數據的量夠大,也有可能有一部分的數值是有序的(就像你拋硬幣的時間足夠長,會有一段時間出現一直拋正面或反面),造成二叉樹會變成是局部不平衡的,這樣它的效率會介於O(logN)到O(N).

如何使二叉樹的效率始終保持在O(logN)呢? 下篇博客為您介紹紅黑樹.


免責聲明!

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



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