樹結構


1.1 樹的概念

  1、樹的特性

      1)一棵樹中的任意兩個結點有且僅有唯一的一條路徑連通; 

      2)一棵樹如果有n個結點,則它一定有n−1條邊; 

      3)在一棵樹中加一條邊將會構成一個回路。

  2、二叉樹

      1)二叉樹是一種特殊的樹,二叉樹的特點是每個結點最多有兩個兒子。

      2)二叉樹使用范圍最廣,一顆多叉樹也可以轉化為二叉樹。

  3、滿二叉樹

      1)二叉樹中每個內部節點都有兩個兒子,滿二叉樹所有的葉節點都有相同的深度。

      2)滿二叉樹是一棵深度為h且有2h−1個結點的二叉樹。

  4、完全二叉樹

      1)若設二叉樹的高度為h,除了第h層外,其他層的結點數都達到最大個數,第h層從右向左連續 缺若干個結點,則為完全二叉樹。

      

  5、樹的特點

      1. 如果一棵完全二叉樹的父節點編號為K,則其左兒子的編號是2K,右兒子的結點編號為2K+1

      2. 已知完全二叉樹的總節點數為n求葉子節點個數:
        當n為奇數時:(n+1)/2
        當n為偶數時 : (n)/2

      3. 已知完全二叉樹的總節點數為n求父節點個數:為:n/2

      4. 已知完全二叉樹的總節點數為n求葉子節點為2的父節點個數:
        當n為奇數時:n/2
        當n為偶數時 : n/2-1

      5、如果一棵完全二叉樹有N個結點,那么這棵二叉樹的深度為【log2(N+1)log2(N+1)】(向上取整)

1.2 二叉樹基本操作

    參考博客: https://www.cnblogs.com/freeman818/p/7252041.html

  1、生成樹結構

      1. 前序遍歷:  DBACEGF(根節點排最先,然后同級先左后右)
      2. 中序遍歷:  ABCDEFG  (先左后根最后右)
      3. 后序遍歷:  ACBFGED   (先左后右最后根)

#! /usr/bin/env python
# -*- coding: utf-8 -*-
class Node:
    def __init__(self,value=None,left=None,right=None):
        self.value=value
        self.left=left    #左子樹
        self.right=right  #右子樹

if __name__=='__main__':
    root=Node('D',Node('B',Node('A'),Node('C')),Node('E',right=Node('G',Node('F'))))
生成樹形結構
#! /usr/bin/env python
# -*- coding: utf-8 -*-
class Node:
    def __init__(self,value=None,left=None,right=None):
        self.value=value
        self.left=left    #左子樹
        self.right=right  #右子樹

def preTraverse(root):
     '''
     前序遍歷
     '''
     if root==None:
         return
     print(root.value)
     preTraverse(root.left)
     preTraverse(root.right)

if __name__=='__main__':
    root=Node('D',Node('B',Node('A'),Node('C')),Node('E',right=Node('G',Node('F'))))
    print('前序遍歷:')
    preTraverse(root)   #  DBACEGF
前序遍歷
#! /usr/bin/env python
# -*- coding: utf-8 -*-
class Node:
    def __init__(self,value=None,left=None,right=None):
        self.value=value
        self.left=left    #左子樹
        self.right=right  #右子樹

def midTraverse(root):
    '''
    中序遍歷
    '''
    if root == None:
        return
    midTraverse(root.left)
    print(root.value)
    midTraverse(root.right)

if __name__=='__main__':
    root=Node('D',Node('B',Node('A'),Node('C')),Node('E',right=Node('G',Node('F'))))
    print('中序遍歷:')
    midTraverse(root)   #  ACBFGED
中序遍歷
#! /usr/bin/env python
# -*- coding: utf-8 -*-
class Node:
    def __init__(self,value=None,left=None,right=None):
        self.value=value
        self.left=left    #左子樹
        self.right=right  #右子樹

def afterTraverse(root):
    '''
    后序遍歷
    '''
    if root == None:
        return
    afterTraverse(root.left)
    afterTraverse(root.right)
    print(root.value)

if __name__=='__main__':
    root=Node('D',Node('B',Node('A'),Node('C')),Node('E',right=Node('G',Node('F'))))
    print('后序遍歷:')
    afterTraverse(root)   #  ACBFGED
后序遍歷

   

前序排列原理:
#####此時執行preTraverse(root.left) 函數
'''
1、第一步 root=Node(D) print D,D入棧[D]
2、第二步 root=Node(D).left=Node(B) print B, B入棧[D,B]
3、第三步 root=Node(B).left=Node(A) print A, A入棧[D,B,A]
4、第四步 root=Node(A).left=None,沒有進入遞歸,順序執行preTraverse(root.right)
5、第五步 Node(A).right==None,也沒有進入遞歸,此時preTraverse(A) 函數才會正真返回,A出棧[D,B]
6、第六步 A的上級調用函數為:preTraverse(B.left),所以接着會順序執行preTraverse(B.right),B的左右節點訪問后B出棧[D]
7、第七步 Node(B).right==Node(C) print C,C入棧[D,C]
8、第八步 Node(C).left==None, Node(C).right==None,訪問完C的左右節點后函數返回C出棧,返回上級調用[D]
9、第九步 此時返回上級調用執行preTraverse(D.right)=Node(E) print E,D出棧,E入棧[E] 
'''

'''此時輸出結果:DBACE'''
前序遍歷步驟推演
#! /usr/bin/env python
# -*- coding: utf-8 -*-
class Node:
    def __init__(self,value=None,left=None,right=None):
        self.value=value
        self.left=left    #左子樹
        self.right=right  #右子樹

def layered_print( root):
    if not root:
        return []
    curLayer = [root]                           # 當前層的所有節點
    while curLayer:
        layerValue = []                         # 當前層的值
        nextLayer = []                          # 下一層的所有節點
        for node in curLayer:                   # 循環當前層所有節點並並獲取所有value值
            layerValue.append(node.value)
            if node.left:
                nextLayer.append(node.left)        # 將當前層的左節點加入列表
            if node.right:
                nextLayer.append(node.right)        # 將當前層的右節點加入列表
                
        print layerValue                           # 打印當前層的值
        curLayer = nextLayer                      # 將循環下移一層


'''
['D']
['B', 'E']
['A', 'C', 'G']
['F']
'''

if __name__=='__main__':
    root=Node('D',Node('B',Node('A'),Node('C')),Node('E',right=Node('G',Node('F'))))
    layered_print(root)
分層打印二叉樹

1.3 hash樹

  1、hash樹描述(就是散列樹)

      1. 散列樹選擇從2開始的連續質數來建立一個十層的哈希樹。

      2. 第一層結點為根結點,根結點下有2個結點;

      3. 第二層的每個結點下有3個結點;

      4. 依此類推,即每層結點的子節點數目為連續的質數。

  2、hash樹特點

    注:關系型數據庫中,索引大多采用B/B+樹來作為存儲結構,而全文搜索引擎的索引則主要采用hash的存儲結構,這兩種數據結構有什么區別?

      1. 如果是等值查詢,那么哈希索引明顯有絕對優勢,因為只需要經過一次算法即可找到相應的鍵值;

      2. 當然了,這個前提是,鍵值都是唯一的,如果鍵值不是唯一的,就需要先找到該鍵所在位置,然后再根據鏈表往后掃描,直到找到相應的數據;

      3. 如果是范圍查詢檢索,這時候哈希索引就毫無用武之地了,因為原先是有序的鍵值,經過哈希算法后,
          有可能變成不連續的了,就沒辦法再利用索引完成范圍查詢檢索;

      4. 同理,哈希索引也沒辦法利用索引完成排序,以及like ‘xxx%’ 這樣的部分模糊查詢(這種部分模糊查詢,其實本質上也是范圍查詢);

  3、建立hash樹 

      1. 選擇從2開始的連續質數來建立一個十層的哈希樹。
      2. 第一層結點為根結點,根結點下有2個結點;第二層的每個結點下有3個結點;

      3. 依此類推,即每層結點的子節點數目為連續的質數。到第十層,每個結點下有29個結點。

      4. 同一結點中的子結點,從左到右代表不同的余數結果。
        例如:第二層結點下有三個子節點。那么從左到右分別代表:除3余0,除3余1,除3余2.對質數進行取余操作得到的余數決定了處理的路徑。
      5. 以隨機的10個數的插入為例,來圖解HashTree的插入過程。

        

 

      6. 其實也可以把所有的鍵-值節點放在哈希樹的第10層葉節點處,這第10層的滿節點數就包含了所有的整數個數,
          但是如果這樣處理的話,所有的非葉子節點作為鍵-值節點的索引,這樣使樹結構龐大,浪費空間。

  4、查找編輯

      1. 哈希樹的節點查找過程和節點插入過程類似,就是對關鍵字用質數序列取余,根據余數確定下一節點的分叉路徑,直到找到目標節點。

      2. 如上圖,最小”哈希樹(HashTree)在從4G個對象中找出所匹配的對象,比較次數不超過10次,也就是說:最多屬於O(10)。

      3. 在實際應用中,調整了質數的范圍,使得比較次數一般不超過5次。

      4. 也就是說:最多屬於O(5),因此可以根據自身需要在時間和空間上尋求一個平衡點。

  5、刪除編輯

      1. 哈希樹的節點刪除過程也很簡單,哈希樹在刪除的時候,並不做任何結構調整。

      2. 只是先查到到要刪除的節點,然后把此節點的“占位標記”置為false即可(即表示此節點為空節點,但並不進行物理刪除)。

  6、hash樹優點

    1)結構簡單

        1. 從哈希樹的結構來說,非常的簡單,每層節點的子節點個數為連續的質數。
        2. 子節點可以隨時創建,因此哈希樹的結構是動態的,也不像某些哈希算法那樣需要長時間的初始化過程。
        3. 哈希樹也沒有必要為不存在的關鍵字提前分配空間。

    2)查找迅速

        1. 從算法過程我們可以看出,對於整數,哈希樹層級最多能增加到10。
        2. 因此最多只需要十次取余和比較操作,就可以知道這個對象是否存在,這個在算法邏輯上決定了哈希樹的優越性。

    3)結構不變

        1. 從刪除算法中可以看出,哈希樹在刪除的時候,並不做任何結構調整。

        2. 常規樹結構在增加元素和刪除元素的時候都要做一定的結構調整,否則他們將可能退化為鏈表結構,而導致查找效率的降低。

        3. 哈希樹采取的是一種“見縫插針”的算法,從來不用擔心退化的問題,也不必為優化結構而采取額外的操作,因此大大節約了操作時間。

  7、缺點編輯

      1. 哈希樹不支持排序,沒有順序特性。

      2. 如果在此基礎上不做任何改進的話並試圖通過遍歷來實現排序,那么操作效率將遠遠低於其他類型的數據結構。

  8、hash索引使用范圍

      總結:哈希適用在小范圍的精確查找,在列數據很大,又不需要排序,不需要模糊查詢,范圍查詢時有用

      1、hash索引僅滿足“=”、“IN”和“<=>”查詢,不能使用范圍查詢

        因為hash索引比較的是經常hash運算之后的hash值,因此只能進行等值的過濾,不能基於范圍的查找,
        因為經過hash算法處理后的hash值的大小關系,並不能保證與處理前的hash大小關系對應。

      2、hash索引無法被用來進行數據的排序操作

        由於hash索引中存放的都是經過hash計算之后的值,而hash值的大小關系不一定與hash計算之前的值一樣,
        所以數據庫無法利用hash索引中的值進行排序操作。

      3、對於組合索引,Hash 索引在計算 Hash 值的時候是組合索引鍵合並后再一起計算 Hash 值,

        而不是單獨計算 Hash 值,所以通過組合索引的前面一個或幾個索引鍵進行查詢的時候,Hash 索引也無法被利用。

      4、Hash 索引遇到大量Hash值相等的情況后性能並不一定就會比B-Tree索引高。

        對於選擇性比較低的索引鍵,如果創建 Hash 索引,那么將會存在大量記錄指針信息存於同一個 Hash 值相關聯。
        這樣要定位某一條記錄時就會非常麻煩,會浪費多次表數據的訪問,而造成整體性能低下。

1.4 B-tree 和 B+tree

    參考博客: https://blog.csdn.net/chuixue24/article/details/80027689

  1、一棵m階的B-Tree有如下特性

      1. 每個節點最多有m個孩子。
      2. 除了根節點和葉子節點外,其它每個節點至少有Ceil(m/2)個孩子(Ceil返回大於或者等於指定表達式的最小整數)。
      3. 若根節點不是葉子節點,則至少有2個孩子
      4. 所有葉子節點都在同一層,且不包含其它關鍵字信息
      5. 每個非終端節點包含n個關鍵字信息(P0,P1,…Pn, k1,…kn)
      6. 關鍵字的個數n滿足:ceil(m/2)-1 <= n <= m-1
      7. ki(i=1,…n)為關鍵字,且關鍵字升序排序。
      8. Pi(i=1,…n)為指向子樹根節點的指針。P(i-1)指向的子樹的所有節點關鍵字均小於ki,但都大於k(i-1)

  2、以一個3階的B-Tree舉例

      1. 每個節點占用一個盤塊的磁盤空間,一個節點上有兩個升序排序的關鍵字和三個指向子樹根節點的指針,指針存儲的是子節點所在磁盤塊的地址。

      2. 兩個關鍵詞划分成的三個范圍域對應三個指針指向的子樹的數據的范圍域。

      3. 以根節點為例,關鍵字為17和35,P1指針指向的子樹的數據范圍為小於17,P2指針指向的子樹的數據范圍為17~35,P3指針指向的子樹的數據范圍為大於35。

      

'''模擬查找關鍵字29的過程:'''
# 根據根節點找到磁盤塊1,讀入內存。【磁盤I/O操作第1次】
# 比較關鍵字29在區間(17,35),找到磁盤塊1的指針P2。
# 根據P2指針找到磁盤塊3,讀入內存。【磁盤I/O操作第2次】
# 比較關鍵字29在區間(26,30),找到磁盤塊3的指針P2。
# 根據P2指針找到磁盤塊8,讀入內存。【磁盤I/O操作第3次】
# 在磁盤塊8中的關鍵字列表中找到關鍵字29。
模擬查找關鍵字29的過程

  3、B+tree特點 

      1. B+Tree是在B-Tree基礎上的一種優化,使其更適合實現外存儲索引結構,InnoDB存儲引擎就是用B+Tree實現其索引結構。

      2. 從上一節中的B-Tree結構圖中可以看到每個節點中不僅包含數據的key值,還有data值。

      3. 而每一個頁的存儲空間是有限的,如果data數據較大時將會導致每個節點(即一個頁)能存儲的key的數量很小

      4. 當存儲的數據量很大時同樣會導致B-Tree的深度較大,增大查詢時的磁盤I/O次數,進而影響查詢效率。

      5. 在B+Tree中,所有數據記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只存儲key值信息,
          這樣可以大大加大每個節點存儲的key值數量,降低B+Tree的高度。

      6. B+Tree相對於B-Tree有幾點不同:

          1)非葉子節點只存儲鍵值信息。
          2)所有葉子節點之間都有一個鏈指針。
          3)數據記錄都存放在葉子節點中。

  4、B+tree(以每個節點可存4個建值及指針信息為例)

      1. B+Tree的非葉子節點只存儲鍵值信息,假設每個磁盤塊能存儲4個鍵值及指針信息

      2. 在B+Tree上有兩個頭指針,一個指向根節點,另一個指向關鍵字最小的葉子節點,而且所有葉子節點(即數據節點)之間是一種鏈式環結構。

      3. 因此可以對B+Tree進行兩種查找運算:一種是對於主鍵的范圍查找和分頁查找,另一種是從根節點開始,進行隨機查找。

      

  5、B+Tree優點

      1. InnoDB存儲引擎中頁的大小為16KB,一般表的主鍵類型為INT(占用4個字節)或BIGINT(占用8個字節),指針類型也一般為4或8個字節

      2. 也就是說一個頁(B+Tree中的一個節點)中大概存儲16KB/(8B+8B)=1K個鍵值(這里的K取值為〖10〗^3)。

      3. 也就是說一個深度為3的B+Tree索引可以維護10^3 * 10^3 * 10^3 = 10億 條記錄。

      說明:

        實際情況中每個節點可能不能填充滿,因此在數據庫中,B+Tree的高度一般都在2~4層。

        mysql的InnoDB存儲引擎在設計時是將根節點常駐內存的,也就是說查找某一鍵值的行記錄時最多只需要1~3次磁盤I/O操作。

  6、B-tree與哈希索引的區別

    1)B+tree的索引:

        是按照順序存儲的,所以,如果按照B+tree索引,可以直接返回,帶順序的數據,但這個數據只是該索引列含有的信息。因此是順序I/O

        適用於: 精確匹配 、范圍匹配 、最左匹配

    2)Hash索引:

        索引列值的哈希值+數據行指針:因此找到后還需要根據指針去找數據,造成隨機I/O 

        適合: 精確匹配 

        不適合: 模糊匹配 、范圍匹配 、不能排序


免責聲明!

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



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