樹及其外部存儲

術語

圖片描述


  1.     樹最頂端的節點稱爲「根」,一棵樹只有一個根
  2. 父節點
        每一個節點(除了根)都剛好有一條邊向上鏈接到另一個節點,上面這個節點就稱爲下面節點的「父節點」
  3. 子節點
        每一個節點均可能有一條或者多條邊向下鏈接到其它節點,下面的這些節點就稱爲它的「子節點」
  4. 葉節點
        沒有子節點的節點稱爲「葉子節點」或者簡稱爲「葉節點」
  5. 子樹
        每一個節點能夠做爲「子樹」的根,它和它全部的子節點構成了這棵樹的子樹
  6. 路徑
        設想順着鏈接節點的邊從一個節點走到另外一個節點,所通過節點的順序排列就稱爲「路徑」

二叉樹

    若是一棵樹中每一個節點最多隻能有兩個子節點,這樣的樹就稱爲「二叉樹」,二叉樹每一個節點的兩個子節點稱爲「左子節點」和「右子節點」。java

    若是咱們給二叉樹加一個額外的條件,就能夠獲得一種被稱做二叉搜索樹(binary search tree)的特殊二叉樹。二叉搜索樹要求:每一個節點都不比它左子樹的任意元素小,並且不比它的右子樹的任意元素大。(若是咱們假設樹中沒有重複的元素,那麼上述要求能夠寫成:每一個節點比它左子樹的任意節點大,並且比它右子樹的任意節點小)node

平衡二叉樹

    平衡二叉樹(Balanced Binary Tree)具備如下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,而且左右兩個子樹都是一棵平衡二叉樹。git

二叉搜索樹的查找過程

    二叉搜索樹能夠方便的實現搜索算法。在搜索元素x的時候,咱們能夠將x和根節點比較:程序員

  1. 若是x等於根節點,那麼找到x,中止搜索 (終止條件)
  2. 若是x小於根節點,那麼搜索左子樹
  3. 若是x大於根節點,那麼搜索右子樹

    當二叉搜索樹平衡時達到最高搜索效率,時間複雜度爲O(logN);當二叉搜索樹單調插入數據時,搜索效率最低,此時二叉搜索樹至關於鏈表,時間複雜度爲O(N)github

    二叉搜索樹的查找代碼以下(僅考慮數據不重複的狀況):算法

public Node find(int key) {
    Node current = root;
    while (current.data != key) {
        if (key < current.data) {
            current = current.left;
        } else {
            current = current.right;
        }
        if (current == null) {
            return null;
        }
    }
    return current;
}

二叉搜索樹插入過程

    二叉搜索樹的插入相對簡單,二叉查找樹的插入過程以下:數據庫

  1. 若當前的二叉搜索樹爲空,則插入的元素爲根節點
  2. 若插入的元素值小於根節點值,則將元素插入到左子樹中
  3. 若插入的元素值不小於根節點值,則將元素插入到右子樹中

    二叉搜索樹的插入代碼以下(僅考慮數據不重複的狀況):數組

public void insert(int key, double data) {
    Node newNode = new Node();
    newNode.key = key;
    newNode.data = data;
    if (root == null) {
        root = newNode;
    } else {
        Node current = root;
        Node parent;
        while (true) {
            parent = current;
            if (key < current.key) {
                current = current.left;
                if (current == null) {
                    parent.left = newNode;
                    return;
                }
            } else {
                current = current.right;
                if (current == null) {
                    parent.right = newNode;
                    return;
                }
            }
        }
    }
}

二叉搜索樹刪除過程

    二叉搜索樹刪除過程也分爲三種狀況:數據結構

  1. 待刪除節點是葉節點,此時只要刪除該節點,並修改其父節點的指針指向null便可
  2. 待刪除節點只有一個子節點,此時只要將父節點的指針指向該節點的子樹便可
  3. 待刪除節點有兩個子節點,此時須要找到該節點的後繼節點,用後繼節點來代替它

如何查找後繼節點

    一個節點的後繼節點即全部比該節點大的節點集合中最小的那個節點。爲此能夠查找該節點的右子樹的最左節點便可,如圖:
圖片描述數據結構和算法

    查找後繼節點代碼以下:

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;
    }
    // 節點移位,參照/docs/二叉搜索樹後繼節點
    if (successor != delNode.right) {
        successorParent.left = successor.right;
        successor.right = delNode.right;
    }
    return successor;
}

    待刪除節點有兩個子節點的刪除過程如圖:
圖片描述

    刪除節點的代碼以下:

public void delete(int key) {
    Node current = root;
    Node parent = root;
    boolean isLeftChild = true;
    while (current.data != key) {
        parent = current;
        if (key < current.data) {
            isLeftChild = true;
            current = current.left;
        } else {
            isLeftChild = false;
            current = current.right;
        }
        if (current == null) {
            // 未找到待刪除的節點
            return;
        }
    }

    // 沒有子節點
    if (current.left == null && current.right == null) {
        if (current == root) {
            root = null;
        } else if (isLeftChild) {
            parent.left = null;
        } else {
            parent.right = null;
        }
    } else if (current.right == null) {
        // 只有左節點
        if (current == root) {
            root = current.left;
        } else if (isLeftChild) {
            parent.left = current.left;
        } else {
            parent.right = current.left;
        }
    } else if (current.left == null) {
        if (current == root) {
            root = current.right;
        } else if (isLeftChild) {
            parent.left = current.right;
        } else {
            parent.right = current.right;
        }
    } else {
        // 兩個節點
        Node successor = getSuccessor(current);
        if (current == root) {
            root = successor;
        } else if (isLeftChild) {
            parent.left = successor;
        } else {
            parent.right = successor;
        }
        successor.left = current.left;
    }
}

紅黑樹

    紅黑樹(英語:Red–black tree)是平衡二叉搜索樹的一種實現方式,是在計算機科學中用到的一種數據結構,典型的用途是實現關聯數組。

    紅黑樹必須知足如下的規則:

  1. 每個節點不是紅色就是黑色
  2. 根老是黑色的
  3. 若是節點是紅色的,則它的子節點必須是黑色的(反之倒不必定必須爲真)
  4. 該樹是完美黑色平衡的,即任意空連接到根結點的路徑上的黑連接數量相同
  5. 若是一個黑色節點下面有一個紅色節點和一個黑色節點,那麼紅色節點只能是左節點

旋轉

    旋轉又分爲左旋和右旋。一般左旋操做用於將一個向右傾斜的紅色連接旋轉爲向左連接。

    左旋如圖所示:
圖片描述

代碼以下:

private Node rotateLeft(Node node) {
    Node x = node.right;
    node.right = x.left;
    x.left = node;
    x.color = x.left.color;
    x.left.color = RED;
    return x;
}

    右旋如圖所示:
圖片描述

代碼以下:

private Node rotateRight(Node node) {
    Node x = node.left;
    node.left = x.right;
    x.right = node;
    x.color = x.right.color;
    x.right.color = RED;
    return x;
}

顏色變換

    在插入數據過程當中,遇到一個黑色節點下面帶有兩個紅色的子節點就要進行顏色變換。顏色變換規則以下:兩個紅色子節點變爲黑色,黑色父節點一般變爲紅色,若是父節點是根節點的話,則父節點繼續保持爲黑色。

代碼以下:

private void flipColors(Node node) {
    node.color = !node.color;
    node.left.color = !node.left.color;
    node.right.color = !node.right.color;
}

紅黑樹的插入過程

    紅黑樹在插入時,跟二叉搜索樹的插入規則是一致的,惟一不一樣的是,紅黑樹要保持自身的平衡,而這能夠經過旋轉和顏色變換作到。切記,紅黑樹在旋轉和顏色變換的過程當中,必須遵照紅黑樹的幾條規則。

代碼以下:

public void insert(int key) {
    root = insert(root, key);
    // 根節點只能是黑色
    root.color = BLACK;
}

private Node insert(Node node, int key) {
    if (node == null) {
        return new Node(key, RED);
    }

    if (key < node.key) {
        node.left = insert(node.left, key);
    } else if (key > node.key) {
        node.right = insert(node.right, key);
    } else {
        node.key = key;
    }

    // 若是一個黑色節點下面的兩個節點一個黑色,一個紅色,則紅色節點只能是左節點
    if (isRed(node.right) && !isRed(node.left)) {
        node = rotateLeft(node);
    }

    // 紅色節點下面不能有紅色節點
    if (isRed(node.left) && isRed(node.left.left)) {
        node = rotateRight(node);
    }

    // 當一個黑色節點下有兩個紅色節點,則要進行顏色變換
    if (isRed(node.left) && isRed(node.right)) {
        flipColors(node);
    }
    return node;
}

紅黑樹的查找和刪除過程

    紅黑樹的查找跟二叉搜索樹的查找過程是徹底一致的
    紅黑樹的刪除過程過於複雜,以至於不少程序員用不一樣的方法去規避它,其中一種方法是:爲已刪除的節點作標記而不實際刪除它。這裏不作進一步的討論。

    紅黑樹的詳細實現能夠參考:紅黑樹完整代碼Java實現

2-3-4樹

    2-3-4樹是一種多叉樹,名字中的二、3和4的含義是指一個節點可能含有的子節點的個數。2-3-4樹性質以下:

  1. 任一節點只能是 2 度節點、3 度節點或 4 度節點,不存在元素數爲 0 的節點(2度節點和3度節點是指該節點有2個或者3個子節點)
  2. 全部葉子節點都擁有相同的深度(depth)
  3. 元素始終保持排序順序

    2-3-4樹結構圖以下:
圖片描述

2-3-4樹的組織

    爲了方便起見,用從0到2的數字給數據項編號,用0到3給子節點鏈編號。節點中的數據項按照關鍵字升序排列,習慣上從左到右升序。還加上如下幾點:

  1. 根是child0的子樹的全部子節點的關鍵字值小於key0
  2. 根是child1的子樹的全部子節點的關鍵字值大於key0而且小於key1
  3. 根是child2的子樹的全部子節點的關鍵字值大於key1而且小於key2
  4. 根是child3的子樹的全部子節點的關鍵字值大於key2

    如圖:
圖片描述

節點分裂

    2-3-4樹依靠節點分裂來保持自身的平衡性。2-3-4樹分裂的規則是自頂向下的,若是根節點或者待插入的節點中數據項已滿,就要進行分裂,分裂規則以下:

  1. 建立一個空節點,它是要分裂節點的兄弟,在要分裂節點的右邊
  2. 待分裂節點右邊的數據項移到右邊節點中,左邊的數據項保留在原有節點中,中間的數據項上升到父節點中

    如圖:
圖片描述

2-3樹

    2-3樹也是一種多叉樹,與2-3-4樹相似,如今在不少應用程序中還在應用,一些用於2-3樹的技術會在B-樹中應用。

    2-3樹比2-3-4樹少一個數據項和一個子節點。節點能夠保存1個或者2個數據項,能夠有0個、1個、2個或者3個子節點。其它方面,父節點和子節點的關鍵字值的排列順序和2-3-4樹是同樣的。

節點分裂

    2-3樹節點分裂和2-4樹節點分裂有很大的不一樣。2-3樹節點分裂是自底向上的(即若插入數據時根節點數據項已滿,不進行分裂,只有待插入的節點數據項滿時才進行分裂),並且2-3樹節點分裂必須用到新數據項。
圖片描述

樹的外部存儲

磁盤佈局

    計算機中的機械磁盤是由磁頭和圓盤組成,每一個圓盤上劃分爲多個磁道,每一個磁道又劃分爲多個扇區。

    磁盤的結構圖以下:
圖片描述

磁盤讀寫原理

    系統將文件存儲到磁盤上時,按柱面、磁頭、扇區的方式進行,即最早是第1磁道的第一磁頭(也就是第1盤面的第1磁道)下的全部扇區,而後,是同一柱面的下一磁頭,……,一個柱面存儲滿後就推動到下一個柱面,直到把文件內容所有寫入磁盤。

    系統也以相同的順序讀出數據。讀出數據時經過告訴磁盤控制器要讀出扇區所在的柱面號、磁頭號和扇區號(物理地址的三個組成部分)進行(目前可能是經過LBA線性尋址的方式定位)。

磁盤預讀

    因爲存儲介質的特性,磁盤自己存取就比主存慢不少,再加上機械運動耗費(磁盤旋轉和磁頭移動),磁盤的存取速度每每是主存的幾百分之一,所以爲了提升效率,要儘可能減小磁盤I/O。爲了達到這個目的,磁盤每每不是嚴格按需讀取,而是每次都會預讀,即便只須要一個字節,磁盤也會從這個位置開始,順序向後讀取必定長度的數據放入內存。

    預讀的長度通常爲頁(page)的整倍數。頁是計算機管理存儲器的邏輯塊,硬件及操做系統每每將主存和磁盤存儲區分割爲連續的大小相等的塊,每一個存儲塊稱爲一頁(在許多操做系統中,頁得大小一般爲4k),主存和磁盤以頁爲單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中,而後異常返回,程序繼續運行(詳情請參考頁面置換算法以及虛擬內存)。

擴展

  • 每一個扇區的弧長是同樣的嗎?

    目前大多數教程中給出的圖片都是老式的機械磁盤的組成。在老式機械磁盤中,每一個磁道的扇區弧長是不同的。越靠內的磁道密度越大,存儲的數據也就越多;越靠外的磁道密度越小,存儲的數據也就越少。因此,雖然內外磁道的扇區弧長不同,因爲密度的緣由,每一個扇區存儲的數據量仍然是同樣的,都是512B。在新式磁盤中,內外磁道的扇區密度都是相同的,因此新式磁盤每一個扇區的弧長都是同樣的。

B-樹和B+樹

    2-3樹和2-3-4樹是B樹的一種特例,B樹的操做與2-3樹和2-3-4樹大體相同,此處不在過多介紹。

B樹爲什麼適於外部存儲

    前面已經簡單介紹過,磁盤控制器每次預讀幾個文件塊的內容,因此對於磁盤讀寫來講,當須要的數據都在一個文件塊中時,磁盤讀寫次數最少,此時效率是最高的。而B樹設計將每一個節點的數據項恰好填滿一個文件塊.

    假設這樣一種極端狀況,若是每一個文件塊中只有一條記錄是咱們須要的。那麼當咱們獲取第二條記錄時又要從新從磁盤加載新的文件塊。此時因爲磁盤讀取次數增多,致使程序的性能大大降低。

B+樹

    B+樹是B-樹的變形。B+樹與B樹的區別在於:

  1. B+樹非葉子節點只保存索引,數據所有保存在葉子節點上
  2. B+樹的全部的葉子節點組成了一張鏈表,便於數據遍歷
  3. 對於有M個數據項的B+樹,最多隻會有M的子節點

如圖:
圖片描述

MySQL存儲引擎對B+樹的優化

    基於上文對2-3-4樹和2-3樹的討論,傳統的B+樹也是按照50%的分裂方式,這樣節點分裂後,新的節點中只有原來一半的數據量,不但浪費了空間,還形成節點的增多,從而加劇磁盤IO的次數。在目前絕大部分關係型數據庫中,都針對B+樹索引的遞增/遞減插入進行了優化,新的分裂策略在插入新數據時,不移動原有頁面的任何記錄,只是將新插入的記錄寫到新頁面之中,這樣原有頁面的利用率仍然是100%。

    因此對於MySQL數據庫來講,使用自增主鍵插入數據時就會造成一個緊湊的索引結構,近似順序填滿。因爲每次插入時也不須要移動已有數據,所以效率很高,也不會增長不少開銷在維護索引上。

如圖:
圖片描述

參考資料

相關文章
相關標籤/搜索