清晨就收到這個,既然是行文中出現的,不妨發上來,這樣會有一種感覺,自己還是個搞技術的:]
前言
本來是想起個類似“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(右旋),這些是操作。
- LL型明顯右邊缺了,得平衡,怎么辦,右邊缺補右邊,右單旋;
- RR是個鏡像,所以左單旋。
- LR型你怎么單旋都平衡不了,所以考慮從孩子入手,孩子右旋,然后自己再左旋。
- RL是個鏡像,孩子左旋,自己右旋。
AVL樹,處理RR型,左單旋(left-rotation):
AVL樹,處理LR型,左-右雙旋(left-right-rotation)
紅黑樹的性質
1.節點有顏色(一個布爾量),紅或者黑;
2.根節點必為黑;
3.葉節點(空NIL)必為黑;
4.紅節點的孩子必為黑;
5.對每個節點,從其到葉子的簡單路徑(一直往下),包含的黑節點相同.第4、5條性質說明了任何簡單查詢路徑的長度不會大過最短路徑的兩倍。
紅黑樹在旋轉上和AVL沒有區別(但有的實現中將顏色變換包含進去,比如普林斯頓 algorithm 4),先看紅黑的左單旋代碼,這里上nginx的:
//左單旋
static ngx_inline void
ngx_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,離不開軟件工程和一流的項目管理,以及程序員自身的編碼素質,這個不懂,也就不妄言了。
希望對您有益。