弄懂了二叉樹之後,再來看2-3樹。網上、書上看了一堆文章和講解,大部分是概念,不多有代碼實現,尤爲是刪除操做的代碼實現。固然,由於2-3樹的特性,插入和刪除都是比較複雜的,所以通過思考,首創了刪除時分支收縮、從新展開的算法,保證了刪除後樹的平衡和完整。該算法相比網上的實現相比,相對比較簡潔;而且,重要的是,該刪除算法能夠推廣至2-3-4樹,甚至是多叉樹。算法
————聲明:原創,轉載請說明來源————數組
1、2-3樹的定義網絡
2-3樹是最簡單的B-樹(或-樹)結構,其每一個非葉節點都有兩個或三個子女,並且全部葉都在統一層上。2-3樹不是二叉樹,其節點可擁有3個孩子。不過,2-3樹與滿二叉樹類似。若某棵2-3樹不包含3-節點,則看上去像滿二叉樹,其全部內部節點均可有兩個孩子,全部的葉子都在同一級別。另外一方面,2-3樹的一個內部節點確實有3個孩子,故比相同高度的滿二叉樹的節點更多。高爲h的2-3樹包含的節點數大於等於高度爲h的滿二叉樹的節點數,即至少有2^h-1個節點。換一個角度分析,包含n的節點的2-3樹的高度不大於[log2(n+1)](即包含n個節點的二叉樹的最小高度)。函數
下圖顯示高度爲3的2-3樹。包含兩個孩子的節點稱爲2-節點,二叉樹中的節點都是2-節點;包含三個孩子的節點稱爲3-節點。測試
(圖片來自網絡)spa
先來看2-3樹的節點的定義:debug
1 class TerNode<E extends Comparable<E>> { 2 static final int capacity = 2; 3 List<E> items; 4 List<TerNode<E>> branches; 5 TerNode<E> parent; 6
7 factory TerNode(List<E> elements) { 8 if (elements.length > capacity) throw StateError('too many elements.'); 9 return TerNode._internal(elements); 10 } 11
12 TerNode._internal(List<E> elements) 13 : items = [], 14 branches = [] { 15 items.addAll(elements); 16 } 17
18 int get size => items.length; 19 bool get isOverflow => size > capacity; 20 bool get isLeaf => branches.isEmpty; 21 bool get isNotLeaf => !isLeaf; 22
23 bool contains(E value) => items.contains(value); 24 int find(E value) => items.indexOf(value); 25
26 String toString() => items.toString(); 27 }
2-3樹的定義:rest
1 class TernaryTree<E extends Comparable<E>> { 2 TerNode<E> _root; 3 int _elementsCount; 4
5 factory TernaryTree.of(Iterable<Comparable<E>> elements) { 6 var tree = TernaryTree<E>(); 7 for (var e in elements) tree.insert(e); 8 return tree; 9 } 10
11 TernaryTree() : _elementsCount = 0; 12
13 // ...
14
15 }
2、插入算法
首先,2-3樹的插入,都是在葉子上完成的。首先定位查找I的操做的葉子,而後將新的元素插入至對應節點。插入後,須要判斷是否須要修復,若是當前節點的元素個數大於2,則須要分裂;該節點分裂爲三個節點,左、右元素爲兩個新的葉子節點,中間元素成爲新的父節點;而後判斷是否須要吸取新的父節點;遞歸向上,直至知足條件或直至根節點。code
插入操做代碼以下:blog
1 void insert(E value) { 2 var c = root, i = 0; 3 while (c != null) { 4 i = 0; 5 while (i < c.size && c.items[i].compareTo(value) < 0) i++; 6 if (i < c.size && c.items[i] == value) return; 7 if (c.isLeaf) break; 8 c = c.branches[i]; 9 } 10 if (c != null) { 11 c.items.insert(i, value); 12 if (c.isOverflow) _fixAfterIns(c); 13 } else { 14 _root = TerNode([value]); 15 } 16 _elementsCount++; 17 }
注意 該行代碼,判斷是否須要修復:
1 if (c.isOverflow) _fixAfterIns(c);
若是須要修復,則進行節點分裂、吸取,遞歸至根節點或再也不溢出的節點爲止,修復代碼以下:
1 void _fixAfterIns(TerNode<E> c) { 2 while (c != null && c.isOverflow) { 3 var t = _split(c); 4 c = t.parent != null ? _absorb(t) : null; 5 } 6 } 7
8 TerNode<E> _split(TerNode<E> c) { 9 var mid = c.size ~/ 2, 10 l = TerNode._internal(c.items.sublist(0, mid)), 11 nc = TerNode._internal(c.items.sublist(mid, mid + 1)), 12 r = TerNode._internal(c.items.sublist(mid + 1)); 13 nc.branches.addAll([l, r]); 14 l.parent = r.parent = nc; 15
16 nc.parent = c.parent; 17 if (c.parent != null) { 18 var i = 0; 19 while (c.parent.branches[i] != c) i++; 20 c.parent.branches[i] = nc; 21 } else { 22 _root = nc; 23 } 24 if (c.isNotLeaf) { 25 l.branches 26 ..addAll(c.branches.getRange(0, mid + 1)) 27 ..forEach((b) => b.parent = l); 28 r.branches 29 ..addAll(c.branches.getRange(mid + 1, c.branches.length)) 30 ..forEach((b) => b.parent = r); 31 } 32 return nc; 33 } 34
35 TerNode<E> _absorb(TerNode<E> c) { 36 var i = 0, p = c.parent; 37 while (p.branches[i] != c) i++; 38 p.items.insertAll(i, c.items); 39 p.branches.replaceRange(i, i + 1, c.branches); 40 c.branches.forEach((b) => b.parent = p); 41 return p; 42 }
3、查找算法
查找實現比較簡單,由於插入操做時,其實已經先進行了查找。代碼以下:
1 TerNode<E> find(E value) { 2 var c = root; 3 while (c != null) { 4 var i = 0; 5 while (i < c.size && c.items[i].compareTo(value) < 0) i++; 6 if (i < c.size && c.items[i] == value) break; 7 c = c.isNotLeaf ? c.branches[i] : null; 8 } 9 return c; 10 }
4、刪除算法
刪除算法是最複雜的。
首先,爲了下降複雜度,咱們採用相似二叉樹或紅黑樹同樣的算法,若是待刪除的元素存在且爲非葉子節點的話,則用後繼的葉子節點的值替代要刪除的節點元素。此時則將刪除問題轉移到了葉子節點上,這樣避免了孩子分支的處理。
其次,刪除元素。刪除後,判斷是否須要修復。若是節點刪除後不爲空,則不須要;不然就須要修復。修復的核心思路是,將該節點的全部兄弟節點所有收縮至父節點,並記錄收縮的次數;而後判斷父節點的元素數量是否足夠展開爲一顆最小的平衡二叉樹,若是不夠,繼續遞歸向上收縮,直至夠了爲止,或者到達根節點。若是倒達了根節點,則將樹的高度減 1 ,進行展開。
如何判斷一個節點的元素數量,知足展開爲一顆最小的平衡二叉樹?其實有個最簡單的算法,一顆平衡二叉樹的高度和元素個數,有以下規律:
高度爲 1: 元素個數爲 1 ,2^1 - 1 ;
高度爲 2:元素個數爲 3 ,2^2 - 1 ;
……
高度爲 h: 元素個數爲 2^h -1 ;
父節點收縮後從新展開,須要將多餘的節點元素修剪掉,這些多餘的節點元素,後續在插入到這棵樹上便可。
刪除代碼以下:
1 bool delete(E value) { 2 var d = find(value); 3 if (d == null) return false; 4 var i = d.find(value); 5 if (d.isNotLeaf) { 6 var s = _successor(d.branches[i + 1]); 7 d.items[i] = s.items[0]; 8 d = s; 9 i = 0; 10 } 11 d.items.removeAt(i); 12 _elementsCount--; 13 if (d.items.isEmpty) _fixAfterDel(d); 14 return true; 15 }
查找後繼節點代碼以下:
1 TerNode<E> _successor(TerNode<E> p) { 2 while (p.isNotLeaf) p = p.branches[0]; 3 return p; 4 }
修復代碼以下:
1 void _fixAfterDel(TerNode<E> d) { 2 if (d == root) { 3 _root = null; 4 } else { 5 var ct = 0; 6 while (d.size < (1 << ct + 1) - 1 && d.parent != null) { 7 _collapse(d.parent); 8 d = d.parent; 9 ct++; 10 } 11 // if (d.size < (1 << ct + 1) - 1) ct--;
12 if (d == root) ct--; 13 var rest = _prune(d, (1 << ct + 1) - 1); 14 _expand(d, ct); 15 for (var e in rest) insert(e); 16 } 17 }
父節點塌縮孩子分支的代碼以下,這裏要注意,由於在修復時是遞歸向上塌縮的,所以,塌縮時須要遞歸塌縮父節點的全部分支,注意父節點p的元素、分支的處理:
1 void _collapse(TerNode<E> p) { 2 if (p.isLeaf) return; 3 for (var i = p.branches.length - 1; i >= 0; i--) { 4 _collapse(p.branches[i]); 5 p.items.insertAll(i, p.branches[i].items); 6 } 7 p.branches.clear(); 8 }
塌縮後,在從新展開以前,須要修剪掉多餘的元素。由於修剪掉的元素後續仍是要插入到樹中的,所以,保留的元素要儘可能的居中,以免從新插入時產生過多的分裂動做。代碼以下:
1 List<E> _prune(TerNode<E> d, int least) { 2 var t = d.size ~/ least, rest = <E>[]; 3 if (t < 2) { 4 rest.addAll(d.items.getRange(least, d.size)); 5 d.items.removeRange(least, d.size); 6 } else { 7 var list = <E>[]; 8 for (var i = 0; i < d.size; i++) { 9 if (i % t == 0 && list.length < least) 10 list.add(d.items[i]); 11 else
12 rest.add(d.items[i]); 13 } 14 d.items = list; 15 } 16 _elementsCount -= rest.length; 17 return rest; 18 }
從新展開的代碼以下,其實就是節點的遞歸向下分裂:
1 void _expand(TerNode<E> p, int ct) { 2 if (ct == 0) return; 3 p = _split(p); 4 for (var b in p.branches) _expand(b, ct - 1); 5 }
刪除操做至此完成。
最後,給一個判斷樹的高度的代碼:
1 int get height { 2 var h = 0, c = root; 3 while (c != null) { 4 h++; 5 c = c.isNotLeaf ? c.branches[0] : null; 6 } 7 return h; 8 }
那麼這些操做,是否每一步的插入或刪除完成後,樹仍然知足是一顆2-3樹呢?測試驗證代碼以下:
List<E> a能夠隨機生成一個千萬級的數組進行測試。若是要觀看每一步的輸出,把 print 前的註釋拿掉便可。通過上億次的驗證,以上代碼正確。
注意,dart 驗證時,若是爲非debug模式,則須要在terminal中加入 --enable-asserts參數,以打開assert開關。
1 void ternaryTest<E extends Comparable<E>>(List<E> a) { 2 var tree = TernaryTree.of(a); 3 // print('check result: ${check(tree)}');
4 check(tree); 5 // print('-------------------'); 6 // print('a.lenght: ${a.length}, tree.elementsCount: ${tree.elementsCount}'); 7 // print('root: ${tree.root} height: ${tree.height}'); 8 // stdin.readLineSync(); 9 // print('-------------------'); 10 // print('start to $i times ternary deleting test...');
11 for (var e in a) { 12 // print('-------------------'); 13 // print('delete: $e');
14 tree.delete(e); 15 // print('-------------------'); 16 // print('tree.elementsCount: ${tree.elementsCount}'); 17 // print('new root: ${tree.root} height: ${tree.height}'); 18 // print('check result: ${check(tree)}');
19 check(tree); 20 } 21 } 22
23 bool check(TernaryTree tree) { 24 if (!tree.isEmpty) assert(tree.height == _walk(tree.root)); 25 return true; 26 } 27
28 int _walk(TerNode r) { 29 assert(!r.isOverflow); 30 for (var i = 0; i + 1 < r.size; i++) 31 assert(r.items[i].compareTo(r.items[i + 1]) < 0); 32
33 if (r.isLeaf) return 1; 34 assert(r.size + 1 == r.branches.length); 35 var heights = <int>[]; 36 for (var b in r.branches) heights.add(_walk(b)); 37 for (var h in heights) assert(h == heights.first); 38 return heights.first + 1; 39 }
原本準備結束了,發現忘了給遍歷函數了:
1 void traverse(void func(List<E> items)) { 2 if (!isEmpty) _traverse(_root, func); 3 }
1 void _traverse(TerNode<E> r, void f(List<E> items)) { 2 f(r.items); 3 for (var b in r.branches) _traverse(b, f); 4 }