你好,樹,你好
清晨就收到這個,既然是行文中出現的,不妨發上來,這樣會有一種感覺,自己還是個搞技術的:]
前言
本來是想起個類似“xxxx樹詳解”之類的名字,覺得可能詳解不了,所以還是小清新一點,篇幅一定不要很長。今天用屌絲的語言來行文,再次回複自由,屌絲的生活讓我熱情滿血:p
當初學資料結構的時候(嗯,如果你是抱著看生態文章的態度進來,那隻能說你被我坑了),樹就讓我恐懼,當我知道樹也是一種圖的時候,我幾乎已經不想吃飯。
現在,這篇文章,不可能出現關於樹性質的一些證明、推導和詳細釋義,只從應用分析的角度來看看樹,今天看看B+樹和紅/黑樹狀結構。其中紅/黑樹狀結構的虛擬碼本人手敲,是為了方便寫注釋,詳細請參見普林斯頓大學的演算法或演算法導論等經典。
B+樹
這裡的B+樹將和B*樹不加區分,只畫出邏輯結構,如果索引層的兄弟節點之間有互相指向的指標,你可以認為是B*樹,比如innoDB的索引組織。
這幅圖傳達幾個資訊:
1.這是一棵4階未滿的B+樹;
2.藍色是非分葉節點的索引記錄,不儲存實際資料記錄。索引節點一般從磁碟拉到記憶體後,會緩衝在記憶體中,根節點一般比緩衝,第二層看情況而定;
3.紅色代表指標或者說是地址.
葉子層,葉子之間的相互指標,你想到了什麼,對,就是:
select * from table where key > 5 and key < 100.
範圍查詢,邏輯上是不是很爽!這也是B+樹和B-樹的一個不同之一(B是Balanced,“-”也不是減號)。
而且你看到了資料在頁節點內部是有序的,你還要什嗎?
order by key asc/desc
4.綠色節點是葉子節點。
在這幅圖的情況下,一次select * from table where key = 5的操作,有緩衝地情況下大致會經曆三次記憶體尋找(找到根、第二層索引、葉子),用於在索引層找到正確的指向5的葉子節點。
無緩衝的情況下會經曆三次磁碟尋找根->第二層->葉子塊,用於將5所在的葉子塊的資料讀出。
記憶體尋找中,沒有磁碟I/O的顧慮,索引資料又是有序的,你想怎麼查那還不夠high?
磁碟尋找,會經曆尋道(柱面)、碟片旋轉(到對應扇區)、電子化讀的操作。
磁碟和記憶體速度差多少?極端情況下,一次磁碟操作,可以進行幾萬到幾十萬次記憶體訪問。
這一點也就是為什麼分布式、大並發系統的瓶頸大多最終會集中在資料持久化層。
5.block-oriented的檔案系統,一般都有塊兒這個概念,hadoop也不例外,block是hadoop的fs的原語,也是很多檔案系統的原語。
一般葉子最小是一個塊,塊內記錄邏輯有序且物理連續(磁碟上連續),但塊間不連續,HDFS也是,從檔案系統來講,塊位置的隨機化是一種全域磁碟空間高效利用的表現。
但資料庫的塊總想著特殊點兒,總想著連續,這是有原因的,誰不想穿過索引,直接從第一塊開始連續掃到最後(磁碟轉啊轉,這是磁碟的最愛),可惜很難,只能在塊粒度上做文章,大了浪費空間,小了不連續幾率更高,而且容易外部片段(360大師幫你整理的那個磁碟最佳化)。
facebook經典的邏輯預讀取,來自OLAP和邏輯全表備份的需求,但是表太大,幾十億甚至百億條資料,反覆的插、改、標記刪除,longtime aged的一棵B+,會導致葉子塊的物理連續性完全崩壞,和邏輯有序越來越遠。全表一次,最最極端的情況下,有多少個葉子塊,會有多少次磁碟尋道+旋轉。有興趣的同學請自行百度。
你說線性預讀取?開玩笑,一個塊才多大。命中率上不去的緩衝比直接讀磁碟還可怕!
B+樹實際意義在哪裡?為啥幾乎所有的關係型資料庫都有這種索引B-TREE,原因就是:
磁碟愛連續不愛隨機,機械磁碟比SSD便宜,SSD還容易壞!你有錢另當別論,而且很多公司做資料採礦和商業智慧的,不在乎那點錢.
put them in SSD as more as you can!
平衡樹-紅/黑樹狀結構和AVL樹
2-3樹演化而來,如果對紅/黑樹狀結構的代碼比較暈,建議從AVL樹和遞迴實現入手。
AVL樹和紅/黑樹狀結構都是平衡樹,而且都是二叉搜尋樹(Binary Search Tree,俗稱BST),平衡就是要壓低樹高,讓平均查詢路徑更短,看看就知道為什麼需要平衡:
展示了二叉搜尋樹在非隨機情況下退化為一個順序鏈表,二叉搜尋樹的尋找退化為O(N).
紅/黑樹狀結構比AVL樹的插入(put、write)效能好,雖然不如AVL樹那般嚴格平衡。
紅/黑樹狀結構的高效秘密只有一個:
任何簡單查詢路徑的長度不會大過最短路徑的兩倍。
為什嗎?
因為最短路徑全是黑,紅又不能連續出現,葉子又是黑,所以最長路徑紅黑交替著來,你算算。
平衡操作-旋轉
AVL樹和紅/黑樹狀結構最讓開始接觸的人崩潰的就是rotation,好多人,好多部落格、甚至好多書(非經典),左旋、右旋、左右雙旋、右左雙旋不分。
要分清,得有下面的認識:
什麼是case(型)?
left-left(LL)、left-right(LR)、right-left(RL)、right-right(RR),這些是case.
什麼是op(操作)?
left-rotation(左旋)、right-rotation(右旋),這些是操作。
AVL樹,處理RR型,左單旋(left-rotation):
AVL樹,處理LR型,左-右雙旋(left-right-rotation)
紅/黑樹狀結構的性質
1.節點有顏色(一個布爾量),紅或者黑;
2.根節點必為黑;
3.分葉節點(空NIL)必為黑;
4.紅節點的孩子必為黑;
5.對每個節點,從其到葉子的簡單路徑(一直往下),包含的黑節點相同.
第4、5條性質說明了任何簡單查詢路徑的長度不會大過最短路徑的兩倍。
紅/黑樹狀結構在旋轉上和AVL沒有區別(但有的實現中將顏色變換包含進去,比如普林斯頓 algorithm 4),先看紅黑的左單旋代碼,這裡上nignx的:
//左單旋static ngx_inline voidngx_rbtree_left_rotate(ngx_rbtree_node_t **root, ngx_rbtree_node_t *sentinel, ngx_rbtree_node_t *node){ ngx_rbtree_node_t *temp; //暫存右孩子 temp = node->right; //自己要佔據右孩子的左孩子的位置,所以先把人家的左孩子拎過來,拎到右邊,為什嗎?紅/黑樹狀結構也是搜尋樹,有保序要求(左小右大) node->right = temp->left; //讓原來右孩子的左孩子(你孫子)認個親,畢竟你要把你孫子拎走 if (temp->left != sentinel) { temp->left->parent = node; } //好了,拎了孫子,右孩子升級了成咱爹的兒了,和咱同輩?還有後續呢... temp->parent = node->parent; //一切手續都齊備了,現在自己的爹來認新兒子了 if (node == *root) { *root = temp; } else if (node == node->parent->left) { node->parent->left = temp; } else { node->parent->right = temp; } //果然,爹不僅不要咱了,咱還成了爹的孫子 temp->left = node; //成孫子了,那爹也換得了,換成咱平衡前的右孩子了... node->parent = temp;}
看這樣的代碼,需要以圖為相,參考上面的AVL樹的旋轉。
多畫畫,看圖寫碼,直接看代碼能想象出一個樹形的,是大師。
紅/黑樹狀結構的插入
先看看虛擬碼:
/*** 紅/黑樹狀結構插入* * @param t 紅/黑樹狀結構* @param z 待插入節點*/RB-INSERT(t,z) { y = T.nil x = T.root //和二叉搜尋樹一樣,找到插入位置 while(x != T.nil) { y = x if(z.key < x.key) { x = x.left }else{ x = x.right } } z.p = y if(y == T.nil) { T.root = z }else if(y.right == z) { z.left = T.nil }else{ z.right = T.nil } //新插入的節點著色為紅色,這一點會貫穿始終,也叫invariant. //如果你經常看演算法、jdk、java語言規範,對這個詞應該不陌生,暫且叫不變式吧 //比如java中的volatile關鍵字一條不變式就是保證其修飾的變數跨線程的記憶體可見度和一致性 z.color = RED //調整平衡及著色,保證紅/黑樹狀結構的性質 RB-INSERT-FIXUP(T,z)}
插入沒什麼好說的,和二叉搜尋樹類似,最後多了著色、哨兵處理,最重要的是這個函數RB-INSERT-FIXUP(T,z),下面給出圖,不然根本不會看懂虛擬碼:
註:圖來自演算法導論D版,以後會換掉此圖
情況1:z的叔叔節點y為紅色,z上升,其父親和叔叔變色;
情況2:z是棵右子樹,上升,左旋,同時保證紅/黑樹狀結構性質,進行對應變色
情況3:z在左旋後成了左孩子,因為一直要保證z是紅色,所以其父變黑,爺爺變紅,出現不平衡,所以右旋平衡,最終形成圖(d),滿足紅/黑樹狀結構的性質。
RB-INSERT-FIXUP(T,z)函數虛擬碼,結合著圖看效果更佳:
/*** 紅/黑樹狀結構調整,保證其性質* * @param t 紅/黑樹狀結構* @param z 待插入節點*/RB-INSERT-FIXUP(T,z) { //紅/黑樹狀結構性質,紅節點的孩子不能是紅節點 //所以調整到節點z的父親是黑就ok while(z.p.color == RED) { //z的父節點是棵左子樹 if(z.p == z.p.p.left) { //考察z的叔叔節點,這裡是y y = z.p.p.right //叔叔是紅色 if(y.color == RED) { //z變上升到其爺爺節點,並保證其為紅色 //同時z原來的父親和叔叔變為黑色 z.p.color = BLACK y.color = BLACK z.p.p.color = RED z = z.p.p }else if(z == z.p.right) { //z是右子樹,上升一層 z = z.p /* 這裡雖然不是先對孩子右旋再自己左旋, 而是自己先左旋,孩子右旋,同樣可滿足平衡 */ //對z所代表的子樹進行左線 LEFT-ROTATE(T,z) //左旋後的z成了左孩子,所以要將其新父親染黑 z.p.color = BLACK //保證紅/黑樹狀結構的性質,其爺爺必為紅色 z.p.p.color = RED RIGHT-ROTATE(T,z.p.p) } }else{ //鏡像操作,也就是z的父親是棵右子樹 } //紅/黑樹狀結構性質,根節點必為黑色 T.root.color = BLACK }}
紅/黑樹狀結構的刪除
本篇已經很長了,留個坑下次填上。
// todo: this is a 坑.
平衡樹的應用價值
1.不像B、B+、B*樹,平衡樹基本都是針對記憶體的資料結構,目的也很簡單,壓低樹高,保持平衡,減少平均尋找長度,同時繼承二叉搜尋樹對數級的操作效率(有序可二分)。
2.nginx採用紅/黑樹狀結構(rbtree)管理定時器(timer)和緩衝,具體可參考其源碼。
3.linux核心中也實現了一個rbtree,用於virtual memory area(VMA)的映射管理,就是包含在vm_area_struct,比如將一個vm_area_struct插入紅/黑樹狀結構:
static inline void vma_rb_insert(struct vm_area_struct *vma, struct rb_root *root){ /* All rb_subtree_gap values must be consistent prior to insertion */ validate_mm_rb(root, NULL); rb_insert_augmented(&vma->vm_rb, root, &vma_gap_callbacks);}
而具體怎麼和rbtree聯絡的?就是vm_area_struct:
/* * This struct defines a memory VMM memory area. There is one of these * per VM-area/task. A VM area is any part of the process virtual memory * space that has a special rule for the page-fault handlers (ie a shared * library, the executable area etc). */struct vm_area_struct { /* The first cache line has the info for VMA tree walking. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next, *vm_prev; struct rb_node vm_rb; /* * Largest free memory gap in bytes to the left of this VMA. * Either between this VMA and vma->vm_prev, or between one of the * VMAs below us in the VMA rbtree and its ->vm_prev. This helps * get_unmapped_area find a free area of the right size. */ unsigned long rb_subtree_gap; /* Second cache line starts here. */ struct mm_struct *vm_mm; /* The address space we belong to. */ pgprot_t vm_page_prot; /* Access permissions of this VMA. */ unsigned long vm_flags; /* Flags, see mm.h. */ /* * For areas with an address space and backing store, * linkage into the address_space->i_mmap interval tree, or * linkage of vma in the address_space->i_mmap_nonlinear list. */ union { struct { struct rb_node rb; unsigned long rb_subtree_last; } linear; struct list_head nonlinear; } shared; /* * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma * list, after a COW of one of the file pages. A MAP_SHARED vma * can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack * or brk vma (with NULL file) can only be in an anon_vma list. */ struct list_head anon_vma_chain; /* Serialized by mmap_sem & * page_table_lock */ struct anon_vma *anon_vma; /* Serialized by page_table_lock */ /* Function pointers to deal with this struct. */ const struct vm_operations_struct *vm_ops; /* Information about our backing store: */ unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */ struct file * vm_file; /* File we map to (can be NULL). */ void * vm_private_data; /* was vm_pte (shared mem) */#ifndef CONFIG_MMU struct vm_region *vm_region; /* NOMMU mapping region */#endif#ifdef CONFIG_NUMA struct mempolicy *vm_policy; /* NUMA policy for the VMA */#endif /* reserved for Red Hat */ unsigned long rh_reserved1; unsigned long rh_reserved2; unsigned long rh_reserved3; unsigned long rh_reserved4;};
總結
樹是資料結構中的明珠(自發讚美),在資料處理中,是幾乎除了基本的數組、鏈表、隊列、棧之外應用最廣泛的資料結構了,本篇只窺其一斑,其實什麼大並發、高效能、高可用、一致性等等被人掛在嘴上不放的名詞,幾乎都是資料結構、執行緒模式、演算法、通訊和軟體工程等基礎卻犀利的東西支撐著的。
如果能在多種應用中借鑒並迴歸原理,仔細揣摩,會對分布式、並行/並發、同步、效能,有新的認知,這句話是寫給自己的。
當然一個成功的系統或framework,離不開軟體工程和一流的專案管理,以及程式員自身的編碼素質,這個不懂,也就不妄言了。
希望對您有益。