數據結構系列之2-3樹的插入、查找、刪除和遍歷完整版代碼實現(dart語言實現)


  弄懂了二叉樹以后,再來看2-3樹。網上、書上看了一堆文章和講解,大部分是概念,很少有代碼實現,尤其是刪除操作的代碼實現。當然,因為2-3樹的特性,插入和刪除都是比較復雜的,因此經過思考,獨創了刪除時分支收縮、重新展開的算法,保證了刪除后樹的平衡和完整。該算法相比網上的實現相比,相對比較簡潔;並且,重要的是,該刪除算法可以推廣至2-3-4樹,甚至是多叉樹。

 

————聲明:原創,轉載請說明來源————

 

一、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-節點。

  

 

(圖片來自網絡)

  先來看2-3樹的節點的定義:

 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樹的定義:

 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-3樹的插入,都是在葉子上完成的。首先定位查找I的操作的葉子,然后將新的元素插入至對應節點。插入后,需要判斷是否需要修復,如果當前節點的元素個數大於2,則需要分裂;該節點分裂為三個節點,左、右元素為兩個新的葉子節點,中間元素成為新的父節點;然后判斷是否需要吸收新的父節點;遞歸向上,直至滿足條件或直至根節點。

  插入操作代碼如下:

 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   }

 

三、查找算法

  查找實現比較簡單,因為插入操作時,其實已經先進行了查找。代碼如下:

 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   }

 

四、刪除算法

  刪除算法是最復雜的。

  首先,為了降低復雜度,我們采用類似二叉樹或紅黑樹一樣的算法,如果待刪除的元素存在且為非葉子節點的話,則用后繼的葉子節點的值替代要刪除的節點元素。此時則將刪除問題轉移到了葉子節點上,這樣避免了孩子分支的處理。

  其次,刪除元素。刪除后,判斷是否需要修復。如果節點刪除后不為空,則不需要;否則就需要修復。修復的核心思路是,將該節點的所有兄弟節點全部收縮至父節點,並記錄收縮的次數;然后判斷父節點的元素數量是否足夠展開為一顆最小的平衡二叉樹,如果不夠,繼續遞歸向上收縮,直至夠了為止,或者到達根節點。如果倒達了根節點,則將樹的高度減 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   }

 

 

 

 


免責聲明!

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



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