一直很想寫一個關於樹結構的專題,再一個就是很多初級點的碼農會認爲樹結構無用論,其實歸根到底還是不清楚樹的實際用途。
一:場景:
1:現狀
前幾天我的一個大學同學負責的網站出現了嚴重的性能瓶頸,由於業務是寫入和讀取都是密集型,如果做緩存,時間間隔也只能在30s左
右,否則就會引起客戶糾紛,所以同學也就沒有做緩存,通過測試發現慢就慢在數據讀取上面,總共需要10s,天啊...原來首頁的加載關聯
到了4張表,而且表數據中最多的在10w條以上,可以想象4張巨大表的關聯,然後就是排序+範圍查找等等相關的條件,讓同學抓狂。
2:我個人的提供解決方案
① 讀取問題
既然不能做緩存,那沒辦法,我們需要自己維護一套”內存數據庫“,數據如何組織就靠我們的算法功底了,比如哈希適合等於性的查找,
樹結構適合”範圍查找“,lucene適合字符串的查找,我們在添加和更新的時候同時維護自己的內存數據庫,最終杜絕表關聯,老同學,還
是先應急,把常用的表灌倒內存,如果真想項目好的話,改架構吧...
② 添加問題
或許你的Add操作還沒有達到瓶頸這一步,如果真的達到了那就看情況來進行”表切分“,”數據庫切分“吧,讓用戶的Add或者Update
操作分流,雖然做起來很複雜,但是沒辦法,總比用戶糾紛強吧,可對...
二:二叉查找樹
正式切入主題,從上面的說明我們知道了二叉樹非常適合於範圍查找,關於樹的基本定義,這裏我就默認大家都知道,我就直接從
查找樹說起了。
1:定義
查找樹的定義非常簡單,一句話就是左孩子比父節點小,右孩子比父節點大,還有一個特性就是”中序遍歷“可以讓結點有序。
2:樹節點
爲了具有通用性,我們定義成泛型模板,在每個結點中增加一個”數據附加域”。
1 /// <summary> 2 /// 二叉樹節點 3 /// </summary> 4 /// <typeparam name="K"></typeparam> 5 /// <typeparam name="V"></typeparam> 6 public class BinaryNode<K, V> 7 { 8 /// <summary> 9 /// 節點元素 10 /// </summary> 11 public K key; 12 13 /// <summary> 14 /// 節點中的附加值 15 /// </summary> 16 public HashSet<V> attach = new HashSet<V>(); 17 18 /// <summary> 19 /// 左節點 20 /// </summary> 21 public BinaryNode<K, V> left; 22 23 /// <summary> 24 /// 右節點 25 /// </summary> 26 public BinaryNode<K, V> right; 27 28 public BinaryNode() { } 29 30 public BinaryNode(K key, V value, BinaryNode<K, V> left, BinaryNode<K, V> right) 31 { 32 //KV鍵值對 33 this.key = key; 34 this.attach.Add(value); 35 36 this.left = left; 37 this.right = right; 38 } 39 }
3:添加
根據查找樹的性質我們可以很簡單的寫出Add的代碼,一個一個的比唄,最終形成的效果圖如下
這裏存在一個“重複節點”的問題,比如說我在最後的樹中再插入一個元素爲15的結點,那麼此時該怎麼辦,一般情況下,我們最好
不要在樹中再追加一個重複結點,而是在“重複節點"的附加域中進行”+1“操作。
1 #region 添加操作 2 /// <summary> 3 /// 添加操作 4 /// </summary> 5 /// <param name="key"></param> 6 /// <param name="value"></param> 7 public void Add(K key, V value) 8 { 9 node = Add(key, value, node); 10 } 11 #endregion 12 13 #region 添加操作 14 /// <summary> 15 /// 添加操作 16 /// </summary> 17 /// <param name="key"></param> 18 /// <param name="value"></param> 19 /// <param name="tree"></param> 20 /// <returns></returns> 21 public BinaryNode<K, V> Add(K key, V value, BinaryNode<K, V> tree) 22 { 23 if (tree == null) 24 tree = new BinaryNode<K, V>(key, value, null, null); 25 26 //左子樹 27 if (key.CompareTo(tree.key) < 0) 28 tree.left = Add(key, value, tree.left); 29 30 //右子樹 31 if (key.CompareTo(tree.key) > 0) 32 tree.right = Add(key, value, tree.right); 33 34 //將value追加到附加值中(也可對應重複元素) 35 if (key.CompareTo(tree.key) == 0) 36 tree.attach.Add(value); 37 38 return tree; 39 } 40 #endregion
4:範圍查找
這個纔是我們使用二叉樹的最終目的,既然是範圍查找,我們就知道了一個”min“和”max“,其實實現起來也很簡單,
第一步:我們要在樹中找到min元素,當然min元素可能不存在,但是我們可以找到min的上界,耗費時間爲O(logn)。
第二步:從min開始我們中序遍歷尋找max的下界。耗費時間爲m。m也就是匹配到的個數。
最後時間複雜度爲M+logN,要知道普通的查找需要O(N)的時間,比如在21億的數據規模下,匹配的元素可能有30個,那麼最後
的結果也就是秒殺和幾個小時甚至幾天的巨大差異,後面我會做實驗說明。
1 #region 樹的指定範圍查找 2 /// <summary> 3 /// 樹的指定範圍查找 4 /// </summary> 5 /// <param name="min"></param> 6 /// <param name="max"></param> 7 /// <returns></returns> 8 public HashSet<V> SearchRange(K min, K max) 9 { 10 HashSet<V> hashSet = new HashSet<V>(); 11 12 hashSet = SearchRange(min, max, hashSet, node); 13 14 return hashSet; 15 } 16 #endregion 17 18 #region 樹的指定範圍查找 19 /// <summary> 20 /// 樹的指定範圍查找 21 /// </summary> 22 /// <param name="range1"></param> 23 /// <param name="range2"></param> 24 /// <param name="tree"></param> 25 /// <returns></returns> 26 public HashSet<V> SearchRange(K min, K max, HashSet<V> hashSet, BinaryNode<K, V> tree) 27 { 28 if (tree == null) 29 return hashSet; 30 31 //遍歷左子樹(尋找下界) 32 if (min.CompareTo(tree.key) < 0) 33 SearchRange(min, max, hashSet, tree.left); 34 35 //當前節點是否在選定範圍內 36 if (min.CompareTo(tree.key) <= 0 && max.CompareTo(tree.key) >= 0) 37 { 38 //等於這種情況 39 foreach (var item in tree.attach) 40 hashSet.Add(item); 41 } 42 43 //遍歷右子樹(兩種情況:①:找min的下限 ②:必須在Max範圍之內) 44 if (min.CompareTo(tree.key) > 0 || max.CompareTo(tree.key) > 0) 45 SearchRange(min, max, hashSet, tree.right); 46 47 return hashSet; 48 } 49 #endregion
5:刪除
對於樹來說,刪除是最複雜的,主要考慮兩種情況。
<1>單孩子的情況
這個比較簡單,如果刪除的節點有左孩子那就把左孩子頂上去,如果有右孩子就把右孩子頂上去,然後打完收工。
<2>左右都有孩子的情況。
首先可以這麼想象,如果我們要刪除一個數組的元素,那麼我們在刪除後會將其後面的一個元素頂到被刪除的位置,如圖
那麼二叉樹操作同樣也是一樣,我們根據”中序遍歷“找到要刪除結點的後一個結點,然後頂上去就行了,原理跟"數組”一樣一樣的。
同樣這裏也有一個注意的地方,在Add操作時,我們將重複元素的值追加到了“附加域”,那麼在刪除的時候,就可以先判斷是
不是要“-1”操作而不是真正的刪除節點,其實這裏也就是“懶刪除”,很有意思。
1 #region 刪除當前樹中的節點 2 /// <summary> 3 /// 刪除當前樹中的節點 4 /// </summary> 5 /// <param name="key"></param> 6 /// <returns></returns> 7 public void Remove(K key, V value) 8 { 9 node = Remove(key, value, node); 10 } 11 #endregion 12 13 #region 刪除當前樹中的節點 14 /// <summary> 15 /// 刪除當前樹中的節點 16 /// </summary> 17 /// <param name="key"></param> 18 /// <param name="tree"></param> 19 /// <returns></returns> 20 public BinaryNode<K, V> Remove(K key, V value, BinaryNode<K, V> tree) 21 { 22 if (tree == null) 23 return null; 24 25 //左子樹 26 if (key.CompareTo(tree.key) < 0) 27 tree.left = Remove(key, value, tree.left); 28 29 //右子樹 30 if (key.CompareTo(tree.key) > 0) 31 tree.right = Remove(key, value, tree.right); 32 33 /*相等的情況*/ 34 if (key.CompareTo(tree.key) == 0) 35 { 36 //判斷裏面的HashSet是否有多值 37 if (tree.attach.Count > 1) 38 { 39 //實現惰性刪除 40 tree.attach.Remove(value); 41 } 42 else 43 { 44 //有兩個孩子的情況 45 if (tree.left != null && tree.right != null) 46 { 47 //根據二叉樹的中順遍歷,需要找到”有子樹“的最小節點 48 tree.key = FindMin(tree.right).key; 49 50 //刪除右子樹的指定元素 51 tree.right = Remove(key, value, tree.right); 52 } 53 else 54 { 55 //單個孩子的情況 56 tree = tree.left == null ? tree.right : tree.left; 57 } 58 } 59 } 60 61 return tree; 62 } 63 #endregion
6天通喫樹結構—— 第二天 平衡二叉樹
上一篇我們聊過,二叉查找樹不是嚴格的O(logN),導致了在真實場景中沒有用武之地,誰也不願意有O(N)的情況發生,
作爲一名碼農,肯定會希望能把“範圍查找”做到地球人都不能優化的地步。
當有很多數據灌到我的樹中時,我肯定會希望最好是以“完全二叉樹”的形式展現,這樣我才能做到“查找”是嚴格的O(logN),
比如把這種”樹“調正到如下結構。
這裏就涉及到了“樹節點”的旋轉,也是我們今天要聊到的內容。
一:平衡二叉樹(AVL)
1:定義
父節點的左子樹和右子樹的高度之差不能大於1,也就是說不能高過1層,否則該樹就失衡了,此時就要旋轉節點,在
編碼時,我們可以記錄當前節點的高度,比如空節點是-1,葉子節點是0,非葉子節點的height往根節點遞增,比如在下圖
中我們認爲樹的高度爲h=2。
1 #region 平衡二叉樹節點 2 /// <summary> 3 /// 平衡二叉樹節點 4 /// </summary> 5 /// <typeparam name="K"></typeparam> 6 /// <typeparam name="V"></typeparam> 7 public class AVLNode<K, V> 8 { 9 /// <summary> 10 /// 節點元素 11 /// </summary> 12 public K key; 13 14 /// <summary> 15 /// 增加一個高度信息 16 /// </summary> 17 public int height; 18 19 /// <summary> 20 /// 節點中的附加值 21 /// </summary> 22 public HashSet<V> attach = new HashSet<V>(); 23 24 /// <summary> 25 /// 左節點 26 /// </summary> 27 public AVLNode<K, V> left; 28 29 /// <summary> 30 /// 右節點 31 /// </summary> 32 public AVLNode<K, V> right; 33 34 public AVLNode() { } 35 36 public AVLNode(K key, V value, AVLNode<K, V> left, AVLNode<K, V> right) 37 { 38 //KV鍵值對 39 this.key = key; 40 this.attach.Add(value); 41 42 this.left = left; 43 this.right = right; 44 } 45 } 46 #endregion
2:旋轉
節點再怎麼失衡都逃不過4種情況,下面我們一一來看一下。
① 左左情況(左子樹的左邊節點)
我們看到,在向樹中追加“節點1”的時候,根據定義我們知道這樣會導致了“節點3"失衡,滿足“左左情況“,可以這樣想,把這
棵樹比作齒輪,我們在“節點5”處把齒輪往下拉一個位置,也就變成了後面這樣“平衡”的形式,如果用動畫解釋就最好理解了。
1 #region 第一種:左左旋轉(單旋轉) 2 /// <summary> 3 /// 第一種:左左旋轉(單旋轉) 4 /// </summary> 5 /// <param name="node"></param> 6 /// <returns></returns> 7 public AVLNode<K, V> RotateLL(AVLNode<K, V> node) 8 { 9 //top:需要作爲頂級節點的元素 10 var top = node.left; 11 12 //先截斷當前節點的左孩子 13 node.left = top.right; 14 15 //將當前節點作爲temp的右孩子 16 top.right = node; 17 18 //計算當前兩個節點的高度 19 node.height = Math.Max(Height(node.left), Height(node.right)) + 1; 20 top.height = Math.Max(Height(top.left), Height(top.right)) + 1; 21 22 return top; 23 } 24 #endregion
② 右右情況(右子樹的右邊節點)
同樣,”節點5“滿足”右右情況“,其實我們也看到,這兩種情況是一種鏡像,當然操作方式也大同小異,我們在”節點1“的地方
將樹往下拉一位,最後也就形成了我們希望的平衡效果。
1 #region 第二種:右右旋轉(單旋轉) 2 /// <summary> 3 /// 第二種:右右旋轉(單旋轉) 4 /// </summary> 5 /// <param name="node"></param> 6 /// <returns></returns> 7 public AVLNode<K, V> RotateRR(AVLNode<K, V> node) 8 { 9 //top:需要作爲頂級節點的元素 10 var top = node.right; 11 12 //先截斷當前節點的右孩子 13 node.right = top.left; 14 15 //將當前節點作爲temp的右孩子 16 top.left = node; 17 18 //計算當前兩個節點的高度 19 node.height = Math.Max(Height(node.left), Height(node.right)) + 1; 20 top.height = Math.Max(Height(top.left), Height(top.right)) + 1; 21 22 return top; 23 } 24 #endregion
③左右情況(左子樹的右邊節點)
從圖中我們可以看到,當我們插入”節點3“時,“節點5”處失衡,注意,找到”失衡點“是非常重要的,當面對”左右情況“時,我們將
失衡點的左子樹進行"右右情況旋轉",然後進行”左左情況旋轉“,經過這樣兩次的旋轉就OK了,很有意思,對吧。
1 #region 第三種:左右旋轉(雙旋轉) 2 /// <summary> 3 /// 第三種:左右旋轉(雙旋轉) 4 /// </summary> 5 /// <param name="node"></param> 6 /// <returns></returns> 7 public AVLNode<K, V> RotateLR(AVLNode<K, V> node) 8 { 9 //先進行RR旋轉 10 node.left = RotateRR(node.left); 11 12 //再進行LL旋轉 13 return RotateLL(node); 14 } 15 #endregion
④右左情況(右子樹的左邊節點)
這種情況和“情景3”也是一種鏡像關係,很簡單,我們找到了”節點15“是失衡點,然後我們將”節點15“的右子樹進行”左左情況旋轉“,
然後進行”右右情況旋轉“,最終得到了我們滿意的平衡。
1 #region 第四種:右左旋轉(雙旋轉) 2 /// <summary> 3 /// 第四種:右左旋轉(雙旋轉) 4 /// </summary> 5 /// <param name="node"></param> 6 /// <returns></returns> 7 public AVLNode<K, V> RotateRL(AVLNode<K, V> node) 8 { 9 //執行左左旋轉 10 node.right = RotateLL(node.right); 11 12 //再執行右右旋轉 13 return RotateRR(node); 14 15 } 16 #endregion
3:添加
如果我們理解了上面的這幾種旋轉,那麼添加方法簡直是輕而易舉,出現了哪一種情況調用哪一種方法而已。
1 #region 添加操作 2 /// <summary> 3 /// 添加操作 4 /// </summary> 5 /// <param name="key"></param> 6 /// <param name="value"></param> 7 /// <param name="tree"></param> 8 /// <returns></returns> 9 public AVLNode<K, V> Add(K key, V value, AVLNode<K, V> tree) 10 { 11 if (tree == null) 12 tree = new AVLNode<K, V>(key, value, null, null); 13 14 //左子樹 15 if (key.CompareTo(tree.key) < 0) 16 { 17 tree.left = Add(key, value, tree.left); 18 19 //如果說相差等於2就說明這棵樹需要旋轉了 20 if (Height(tree.left) - Height(tree.right) == 2) 21 { 22 //說明此時是左左旋轉 23 if (key.CompareTo(tree.left.key) < 0) 24 { 25 tree = RotateLL(tree); 26 } 27 else 28 { 29 //屬於左右旋轉 30 tree = RotateLR(tree); 31 } 32 } 33 } 34 35 //右子樹 36 if (key.CompareTo(tree.key) > 0) 37 { 38 tree.right = Add(key, value, tree.right); 39 40 if ((Height(tree.right) - Height(tree.left) == 2)) 41 { 42 //此時是右右旋轉 43 if (key.CompareTo(tree.right.key) > 0) 44 { 45 tree = RotateRR(tree); 46 } 47 else 48 { 49 //屬於右左旋轉 50 tree = RotateRL(tree); 51 } 52 } 53 } 54 55 //將value追加到附加值中(也可對應重複元素) 56 if (key.CompareTo(tree.key) == 0) 57 tree.attach.Add(value); 58 59 //計算高度 60 tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1; 61 62 return tree; 63 } 64 #endregion4:刪除
刪除方法跟添加方法也類似,當刪除一個結點的時候,可能會引起祖先結點的失衡,所以在每次”結點“回退的時候計算結點高度。
1 #region 刪除當前樹中的節點 2 /// <summary> 3 /// 刪除當前樹中的節點 4 /// </summary> 5 /// <param name="key"></param> 6 /// <param name="tree"></param> 7 /// <returns></returns> 8 public AVLNode<K, V> Remove(K key, V value, AVLNode<K, V> tree) 9 { 10 if (tree == null) 11 return null; 12 13 //左子樹 14 if (key.CompareTo(tree.key) < 0) 15 { 16 tree.left = Remove(key, value, tree.left); 17 18 //如果說相差等於2就說明這棵樹需要旋轉了 19 if (Height(tree.left) - Height(tree.right) == 2) 20 { 21 //說明此時是左左旋轉 22 if (key.CompareTo(tree.left.key) < 0) 23 { 24 tree = RotateLL(tree); 25 } 26 else 27 { 28 //屬於左右旋轉 29 tree = RotateLR(tree); 30 } 31 } 32 } 33 //右子樹 34 if (key.CompareTo(tree.key) > 0) 35 { 36 tree.right = Remove(key, value, tree.right); 37 38 if ((Height(tree.right) - Height(tree.left) == 2)) 39 { 40 //此時是右右旋轉 41 if (key.CompareTo(tree.right.key) > 0) 42 { 43 tree = RotateRR(tree); 44 } 45 else 46 { 47 //屬於右左旋轉 48 tree = RotateRL(tree); 49 } 50 } 51 } 52 /*相等的情況*/ 53 if (key.CompareTo(tree.key) == 0) 54 { 55 //判斷裏面的HashSet是否有多值 56 if (tree.attach.Count > 1) 57 { 58 //實現惰性刪除 59 tree.attach.Remove(value); 60 } 61 else 62 { 63 //有兩個孩子的情況 64 if (tree.left != null && tree.right != null) 65 { 66 //根據平衡二叉樹的中順遍歷,需要找到”有子樹“的最小節點 67 tree.key = FindMin(tree.right).key; 68 69 //刪除右子樹的指定元素 70 tree.right = Remove(tree.key, value, tree.right); 71 } 72 else 73 { 74 //自減高度 75 tree = tree.left == null ? tree.right : tree.left; 76 77 //如果刪除的是葉子節點直接返回 78 if (tree == null) 79 return null; 80 } 81 } 82 } 83 84 //統計高度 85 tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1; 86 87 return tree; 88 } 89 #endregion5: 測試
不像上一篇不能在二叉樹中灌有序數據,平衡二叉樹就沒關係了,我們的需求是檢索2012-7-30 4:00:00 到 2012-7-30 5:00:00
的登陸用戶的ID,數據量在500w,看看平衡二叉樹是如何秒殺對手。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using System.Diagnostics;
namespace DataStruct
{
class Program
{
static void Main(string[] args)
{
AVLTree<int, int> avl = new AVLTree<int, int>();
Dictionary<DateTime, int> dic = new Dictionary<DateTime, int>();
AVLTree<DateTime, int> tree = new AVLTree<DateTime, int>();
//500w
for (int i = 1; i < 5000000; i++)
{
dic.Add(DateTime.Now.AddMinutes(i), i);
tree.Add(DateTime.Now.AddMinutes(i), i);
}
//檢索2012-7-30 4:00:00 到 2012-7-30 5:00:00的登陸人數
var min = Convert.ToDateTime("2012/7/30 4:00:00");
var max = Convert.ToDateTime("2012/7/30 5:00:00");
var watch = Stopwatch.StartNew();
var result1 = dic.Keys.Where(i => i >= min && i <= max).Select(i => dic[i]).ToList();
watch.Stop();
Console.WriteLine("字典查找耗費時間:{0}ms", watch.ElapsedMilliseconds);
watch = Stopwatch.StartNew();
var result2 = tree.SearchRange(min, max);
watch.Stop();
Console.WriteLine("平衡二叉樹查找耗費時間:{0}ms", watch.ElapsedMilliseconds);
}
}
#region 平衡二叉樹節點
/// <summary>
/// 平衡二叉樹節點
/// </summary>
/// <typeparam name="K"></typeparam>
/// <typeparam name="V"></typeparam>
public class AVLNode<K, V>
{
/// <summary>
/// 節點元素
/// </summary>
public K key;
/// <summary>
/// 增加一個高度信息
/// </summary>
public int height;
/// <summary>
/// 節點中的附加值
/// </summary>
public HashSet<V> attach = new HashSet<V>();
/// <summary>
/// 左節點
/// </summary>
public AVLNode<K, V> left;
/// <summary>
/// 右節點
/// </summary>
public AVLNode<K, V> right;
public AVLNode() { }
public AVLNode(K key, V value, AVLNode<K, V> left, AVLNode<K, V> right)
{
//KV鍵值對
this.key = key;
this.attach.Add(value);
this.left = left;
this.right = right;
}
}
#endregion
public class AVLTree<K, V> where K : IComparable
{
public AVLNode<K, V> node = null;
#region 添加操作
/// <summary>
/// 添加操作
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void Add(K key, V value)
{
node = Add(key, value, node);
}
#endregion
#region 添加操作
/// <summary>
/// 添加操作
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="tree"></param>
/// <returns></returns>
public AVLNode<K, V> Add(K key, V value, AVLNode<K, V> tree)
{
if (tree == null)
tree = new AVLNode<K, V>(key, value, null, null);
//左子樹
if (key.CompareTo(tree.key) < 0)
{
tree.left = Add(key, value, tree.left);
//如果說相差等於2就說明這棵樹需要旋轉了
if (Height(tree.left) - Height(tree.right) == 2)
{
//說明此時是左左旋轉
if (key.CompareTo(tree.left.key) < 0)
{
tree = RotateLL(tree);
}
else
{
//屬於左右旋轉
tree = RotateLR(tree);
}
}
}
//右子樹
if (key.CompareTo(tree.key) > 0)
{
tree.right = Add(key, value, tree.right);
if ((Height(tree.right) - Height(tree.left) == 2))
{
//此時是右右旋轉
if (key.CompareTo(tree.right.key) > 0)
{
tree = RotateRR(tree);
}
else
{
//屬於右左旋轉
tree = RotateRL(tree);
}
}
}
//將value追加到附加值中(也可對應重複元素)
if (key.CompareTo(tree.key) == 0)
tree.attach.Add(value);
//計算高度
tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1;
return tree;
}
#endregion
#region 計算當前節點的高度
/// <summary>
/// 計算當前節點的高度
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public int Height(AVLNode<K, V> node)
{
return node == null ? -1 : node.height;
}
#endregion
#region 第一種:左左旋轉(單旋轉)
/// <summary>
/// 第一種:左左旋轉(單旋轉)
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public AVLNode<K, V> RotateLL(AVLNode<K, V> node)
{
//top:需要作爲頂級節點的元素
var top = node.left;
//先截斷當前節點的左孩子
node.left = top.right;
//將當前節點作爲temp的右孩子
top.right = node;
//計算當前兩個節點的高度
node.height = Math.Max(Height(node.left), Height(node.right)) + 1;
top.height = Math.Max(Height(top.left), Height(top.right)) + 1;
return top;
}
#endregion
#region 第二種:右右旋轉(單旋轉)
/// <summary>
/// 第二種:右右旋轉(單旋轉)
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public AVLNode<K, V> RotateRR(AVLNode<K, V> node)
{
//top:需要作爲頂級節點的元素
var top = node.right;
//先截斷當前節點的右孩子
node.right = top.left;
//將當前節點作爲temp的右孩子
top.left = node;
//計算當前兩個節點的高度
node.height = Math.Max(Height(node.left), Height(node.right)) + 1;
top.height = Math.Max(Height(top.left), Height(top.right)) + 1;
return top;
}
#endregion
#region 第三種:左右旋轉(雙旋轉)
/// <summary>
/// 第三種:左右旋轉(雙旋轉)
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public AVLNode<K, V> RotateLR(AVLNode<K, V> node)
{
//先進行RR旋轉
node.left = RotateRR(node.left);
//再進行LL旋轉
return RotateLL(node);
}
#endregion
#region 第四種:右左旋轉(雙旋轉)
/// <summary>
/// 第四種:右左旋轉(雙旋轉)
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public AVLNode<K, V> RotateRL(AVLNode<K, V> node)
{
//執行左左旋轉
node.right = RotateLL(node.right);
//再執行右右旋轉
return RotateRR(node);
}
#endregion
#region 是否包含指定元素
/// <summary>
/// 是否包含指定元素
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool Contain(K key)
{
return Contain(key, node);
}
#endregion
#region 是否包含指定元素
/// <summary>
/// 是否包含指定元素
/// </summary>
/// <param name="key"></param>
/// <param name="tree"></param>
/// <returns></returns>
public bool Contain(K key, AVLNode<K, V> tree)
{
if (tree == null)
return false;
//左子樹
if (key.CompareTo(tree.key) < 0)
return Contain(key, tree.left);
//右子樹
if (key.CompareTo(tree.key) > 0)
return Contain(key, tree.right);
return true;
}
#endregion
#region 樹的指定範圍查找
/// <summary>
/// 樹的指定範圍查找
/// </summary>
/// <param name="min"></param>
/// <param name="max"></param>
/// <returns></returns>
public HashSet<V> SearchRange(K min, K max)
{
HashSet<V> hashSet = new HashSet<V>();
hashSet = SearchRange(min, max, hashSet, node);
return hashSet;
}
#endregion
#region 樹的指定範圍查找
/// <summary>
/// 樹的指定範圍查找
/// </summary>
/// <param name="range1"></param>
/// <param name="range2"></param>
/// <param name="tree"></param>
/// <returns></returns>
public HashSet<V> SearchRange(K min, K max, HashSet<V> hashSet, AVLNode<K, V> tree)
{
if (tree == null)
return hashSet;
//遍歷左子樹(尋找下界)
if (min.CompareTo(tree.key) < 0)
SearchRange(min, max, hashSet, tree.left);
//當前節點是否在選定範圍內
if (min.CompareTo(tree.key) <= 0 && max.CompareTo(tree.key) >= 0)
{
//等於這種情況
foreach (var item in tree.attach)
hashSet.Add(item);
}
//遍歷右子樹(兩種情況:①:找min的下限 ②:必須在Max範圍之內)
if (min.CompareTo(tree.key) > 0 || max.CompareTo(tree.key) > 0)
SearchRange(min, max, hashSet, tree.right);
return hashSet;
}
#endregion
#region 找到當前樹的最小節點
/// <summary>
/// 找到當前樹的最小節點
/// </summary>
/// <returns></returns>
public AVLNode<K, V> FindMin()
{
return FindMin(node);
}
#endregion
#region 找到當前樹的最小節點
/// <summary>
/// 找到當前樹的最小節點
/// </summary>
/// <param name="tree"></param>
/// <returns></returns>
public AVLNode<K, V> FindMin(AVLNode<K, V> tree)
{
if (tree == null)
return null;
if (tree.left == null)
return tree;
return FindMin(tree.left);
}
#endregion
#region 找到當前樹的最大節點
/// <summary>
/// 找到當前樹的最大節點
/// </summary>
/// <returns></returns>
public AVLNode<K, V> FindMax()
{
return FindMin(node);
}
#endregion
#region 找到當前樹的最大節點
/// <summary>
/// 找到當前樹的最大節點
/// </summary>
/// <param name="tree"></param>
/// <returns></returns>
public AVLNode<K, V> FindMax(AVLNode<K, V> tree)
{
if (tree == null)
return null;
if (tree.right == null)
return tree;
return FindMax(tree.right);
}
#endregion
#region 刪除當前樹中的節點
/// <summary>
/// 刪除當前樹中的節點
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public void Remove(K key, V value)
{
node = Remove(key, value, node);
}
#endregion
#region 刪除當前樹中的節點
/// <summary>
/// 刪除當前樹中的節點
/// </summary>
/// <param name="key"></param>
/// <param name="tree"></param>
/// <returns></returns>
public AVLNode<K, V> Remove(K key, V value, AVLNode<K, V> tree)
{
if (tree == null)
return null;
//左子樹
if (key.CompareTo(tree.key) < 0)
{
tree.left = Remove(key, value, tree.left);
//如果說相差等於2就說明這棵樹需要旋轉了
if (Height(tree.left) - Height(tree.right) == 2)
{
//說明此時是左左旋轉
if (key.CompareTo(tree.left.key) < 0)
{
tree = RotateLL(tree);
}
else
{
//屬於左右旋轉
tree = RotateLR(tree);
}
}
}
//右子樹
if (key.CompareTo(tree.key) > 0)
{
tree.right = Remove(key, value, tree.right);
if ((Height(tree.right) - Height(tree.left) == 2))
{
//此時是右右旋轉
if (key.CompareTo(tree.right.key) > 0)
{
tree = RotateRR(tree);
}
else
{
//屬於右左旋轉
tree = RotateRL(tree);
}
}
}
/*相等的情況*/
if (key.CompareTo(tree.key) == 0)
{
//判斷裏面的HashSet是否有多值
if (tree.attach.Count > 1)
{
//實現惰性刪除
tree.attach.Remove(value);
}
else
{
//有兩個孩子的情況
if (tree.left != null && tree.right != null)
{
//根據平衡二叉樹的中順遍歷,需要找到”有子樹“的最小節點
tree.key = FindMin(tree.right).key;
//刪除右子樹的指定元素
tree.right = Remove(tree.key, value, tree.right);
}
else
{
//自減高度
tree = tree.left == null ? tree.right : tree.left;
//如果刪除的是葉子節點直接返回
if (tree == null)
return null;
}
}
}
//統計高度
tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1;
return tree;
}
#endregion
}
}wow,相差98倍,這個可不是一個級別啊...AVL神器。
6天通喫樹結構—— 第三天 Treap樹
我們知道,二叉查找樹相對來說比較容易形成最壞的鏈表情況,所以前輩們想盡了各種優化策略,包括AVL,紅黑,以及今天
要講的Treap樹。
Treap樹算是一種簡單的優化策略,這名字大家也能猜到,樹和堆的合體,其實原理比較簡單,在樹中維護一個"優先級“,”優先級“
採用隨機數的方法,但是”優先級“必須滿足根堆的性質,當然是“大根堆”或者“小根堆”都無所謂,比如下面的一棵樹:
從樹中我們可以看到:
①:節點中的key滿足“二叉查找樹”。
②:節點中的“優先級”滿足小根堆。
一:基本操作
1:定義
1 #region Treap樹節點 2 /// <summary> 3 /// Treap樹 4 /// </summary> 5 /// <typeparam name="K"></typeparam> 6 /// <typeparam name="V"></typeparam> 7 public class TreapNode<K, V> 8 { 9 /// <summary> 10 /// 節點元素 11 /// </summary> 12 public K key; 13 14 /// <summary> 15 /// 優先級(採用隨機數) 16 /// </summary> 17 public int priority; 18 19 /// <summary> 20 /// 節點中的附加值 21 /// </summary> 22 public HashSet<V> attach = new HashSet<V>(); 23 24 /// <summary> 25 /// 左節點 26 /// </summary> 27 public TreapNode<K, V> left; 28 29 /// <summary> 30 /// 右節點 31 /// </summary> 32 public TreapNode<K, V> right; 33 34 public TreapNode() { } 35 36 public TreapNode(K key, V value, TreapNode<K, V> left, TreapNode<K, V> right) 37 { 38 //KV鍵值對 39 this.key = key; 40 this.priority = new Random(DateTime.Now.Millisecond).Next(0,int.MaxValue); 41 this.attach.Add(value); 42 43 this.left = left; 44 this.right = right; 45 } 46 } 47 #endregion節點裏面定義了一個priority作爲“堆定義”的旋轉因子,因子採用“隨機數“。
2:添加
首先我們知道各個節點的“優先級”是採用隨機數的方法,那麼就存在一個問題,當我們插入一個節點後,優先級不滿足“堆定義"的
時候我們該怎麼辦,前輩說此時需要旋轉,直到滿足堆定義爲止。
旋轉有兩種方式,如果大家玩轉了AVL,那麼對Treap中的旋轉的理解輕而易舉。
①: 左左情況旋轉
從圖中可以看出,當我們插入“節點12”的時候,此時“堆性質”遭到破壞,必須進行旋轉,我們發現優先級是6<9,所以就要進行
左左情況旋轉,最終也就形成了我們需要的結果。
②: 右右情況旋轉
既然理解了”左左情況旋轉“,右右情況也是同樣的道理,優先級中發現“6<9",進行”右右旋轉“最終達到我們要的效果。
1 #region 添加操作 2 /// <summary> 3 /// 添加操作 4 /// </summary> 5 /// <param name="key"></param> 6 /// <param name="value"></param> 7 public void Add(K key, V value) 8 { 9 node = Add(key, value, node); 10 } 11 #endregion 12 13 #region 添加操作 14 /// <summary> 15 /// 添加操作 16 /// </summary> 17 /// <param name="key"></param> 18 /// <param name="value"></param> 19 /// <param name="tree"></param> 20 /// <returns></returns> 21 public TreapNode<K, V> Add(K key, V value, TreapNode<K, V> tree) 22 { 23 if (tree == null) 24 tree = new TreapNode<K, V>(key, value, null, null); 25 26 //左子樹 27 if (key.CompareTo(tree.key) < 0) 28 { 29 tree.left = Add(key, value, tree.left); 30 31 //根據小根堆性質,需要”左左情況旋轉” 32 if (tree.left.priority < tree.priority) 33 { 34 tree = RotateLL(tree); 35 } 36 } 37 38 //右子樹 39 if (key.CompareTo(tree.key) > 0) 40 { 41 tree.right = Add(key, value, tree.right); 42 43 //根據小根堆性質,需要”右右情況旋轉” 44 if (tree.right.priority < tree.priority) 45 { 46 tree = RotateRR(tree); 47 } 48 } 49 50 //將value追加到附加值中(也可對應重複元素) 51 if (key.CompareTo(tree.key) == 0) 52 tree.attach.Add(value); 53 54 return tree; 55 } 56 #endregion
3:刪除
跟普通的二叉查找樹一樣,刪除結點存在三種情況。
①:葉子結點
跟普通查找樹一樣,直接釋放本節點即可。
②:單孩子結點
跟普通查找樹一樣操作。
③:滿孩子結點
其實在treap中刪除滿孩子結點有兩種方式。
第一種:跟普通的二叉查找樹一樣,找到“右子樹”的最左結點(15),拷貝元素的值,但不拷貝元素的優先級,然後在右子樹中
刪除“結點15”即可,最終效果如下圖。
第二種:將”結點下旋“,直到該節點不是”滿孩子的情況“,該賦null的賦null,該將孩子結點頂上的就頂上,如下圖:
當然從理論上來說,第二種刪除方法更合理,這裏我寫的就是第二種情況的代碼。
1 #region 刪除當前樹中的節點 2 /// <summary> 3 /// 刪除當前樹中的節點 4 /// </summary> 5 /// <param name="key"></param> 6 /// <returns></returns> 7 public void Remove(K key, V value) 8 { 9 node = Remove(key, value, node); 10 } 11 #endregion 12 13 #region 刪除當前樹中的節點 14 /// <summary> 15 /// 刪除當前樹中的節點 16 /// </summary> 17 /// <param name="key"></param> 18 /// <param name="tree"></param> 19 /// <returns></returns> 20 public TreapNode<K, V> Remove(K key, V value, TreapNode<K, V> tree) 21 { 22 if (tree == null) 23 return null; 24 25 //左子樹 26 if (key.CompareTo(tree.key) < 0) 27 { 28 tree.left = Remove(key, value, tree.left); 29 } 30 //右子樹 31 if (key.CompareTo(tree.key) > 0) 32 { 33 tree.right = Remove(key, value, tree.right); 34 } 35 /*相等的情況*/ 36 if (key.CompareTo(tree.key) == 0) 37 { 38 //判斷裏面的HashSet是否有多值 39 if (tree.attach.Count > 1) 40 { 41 //實現惰性刪除 42 tree.attach.Remove(value); 43 } 44 else 45 { 46 //有兩個孩子的情況 47 if (tree.left != null && tree.right != null) 48 { 49 //如果左孩子的優先級低就需要“左旋” 50 if (tree.left.priority < tree.right.priority) 51 { 52 tree = RotateLL(tree); 53 } 54 else 55 { 56 //否則“右旋” 57 tree = RotateRR(tree); 58 } 59 60 //繼續旋轉 61 tree = Remove(key, value, tree); 62 } 63 else 64 { 65 //如果旋轉後已經變成了葉子節點則直接刪除 66 if (tree == null) 67 return null; 68 69 //最後就是單支樹 70 tree = tree.left == null ? tree.right : tree.left; 71 } 72 } 73 } 74 75 return tree; 76 } 77 #endregion
4:總結
treap樹在CURD中是期望的logN,由於我們加了”優先級“,所以會出現”鏈表“的情況幾乎不存在,但是他的Add和Remove相比嚴格的
平衡二叉樹有更少的旋轉操作,可以說性能是在”普通二叉樹“和”平衡二叉樹“之間。
最後是總運行代碼,不過這裏我就不做測試了。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace DataStruct 7 { 8 #region Treap樹節點 9 /// <summary> 10 /// Treap樹 11 /// </summary> 12 /// <typeparam name="K"></typeparam> 13 /// <typeparam name="V"></typeparam> 14 public class TreapNode<K, V> 15 { 16 /// <summary> 17 /// 節點元素 18 /// </summary> 19 public K key; 20 21 /// <summary> 22 /// 優先級(採用隨機數) 23 /// </summary> 24 public int priority; 25 26 /// <summary> 27 /// 節點中的附加值 28 /// </summary> 29 public HashSet<V> attach = new HashSet<V>(); 30 31 /// <summary> 32 /// 左節點 33 /// </summary> 34 public TreapNode<K, V> left; 35 36 /// <summary> 37 /// 右節點 38 /// </summary> 39 public TreapNode<K, V> right; 40 41 public TreapNode() { } 42 43 public TreapNode(K key, V value, TreapNode<K, V> left, TreapNode<K, V> right) 44 { 45 //KV鍵值對 46 this.key = key; 47 this.priority = new Random(DateTime.Now.Millisecond).Next(0,int.MaxValue); 48 this.attach.Add(value); 49 50 this.left = left; 51 this.right = right; 52 } 53 } 54 #endregion 55 56 public class TreapTree<K, V> where K : IComparable 57 { 58 public TreapNode<K, V> node = null; 59 60 #region 添加操作 61 /// <summary> 62 /// 添加操作 63 /// </summary> 64 /// <param name="key"></param> 65 /// <param name="value"></param> 66 public void Add(K key, V value) 67 { 68 node = Add(key, value, node); 69 } 70 #endregion 71 72 #region 添加操作 73 /// <summary> 74 /// 添加操作 75 /// </summary> 76 /// <param name="key"></param> 77 /// <param name="value"></param> 78 /// <param name="tree"></param> 79 /// <returns></returns> 80 public TreapNode<K, V> Add(K key, V value, TreapNode<K, V> tree) 81 { 82 if (tree == null) 83 tree = new TreapNode<K, V>(key, value, null, null); 84 85 //左子樹 86 if (key.CompareTo(tree.key) < 0) 87 { 88 tree.left = Add(key, value, tree.left); 89 90 //根據小根堆性質,需要”左左情況旋轉” 91 if (tree.left.priority < tree.priority) 92 { 93 tree = RotateLL(tree); 94 } 95 } 96 97 //右子樹 98 if (key.CompareTo(tree.key) > 0) 99 { 100 tree.right = Add(key, value, tree.right); 101 102 //根據小根堆性質,需要”右右情況旋轉” 103 if (tree.right.priority < tree.priority) 104 { 105 tree = RotateRR(tree); 106 } 107 } 108 109 //將value追加到附加值中(也可對應重複元素) 110 if (key.CompareTo(tree.key) == 0) 111 tree.attach.Add(value); 112 113 return tree; 114 } 115 #endregion 116 117 #region 第一種:左左旋轉(單旋轉) 118 /// <summary> 119 /// 第一種:左左旋轉(單旋轉) 120 /// </summary> 121 /// <param name="node"></param> 122 /// <returns></returns> 123 public TreapNode<K, V> RotateLL(TreapNode<K, V> node) 124 { 125 //top:需要作爲頂級節點的元素 126 var top = node.left; 127 128 //先截斷當前節點的左孩子 129 node.left = top.right; 130 131 //將當前節點作爲temp的右孩子 132 top.right = node; 133 134 return top; 135 } 136 #endregion 137 138 #region 第二種:右右旋轉(單旋轉) 139 /// <summary> 140 /// 第二種:右右旋轉(單旋轉) 141 /// </summary> 142 /// <param name="node"></param> 143 /// <returns></returns> 144 public TreapNode<K, V> RotateRR(TreapNode<K, V> node) 145 { 146 //top:需要作爲頂級節點的元素 147 var top = node.right; 148 149 //先截斷當前節點的右孩子 150 node.right = top.left; 151 152 //將當前節點作爲temp的右孩子 153 top.left = node; 154 155 return top; 156 } 157 #endregion 158 159 #region 樹的指定範圍查找 160 /// <summary> 161 /// 樹的指定範圍查找 162 /// </summary> 163 /// <param name="min"></param> 164 /// <param name="max"></param> 165 /// <returns></returns> 166 public HashSet<V> SearchRange(K min, K max) 167 { 168 HashSet<V> hashSet = new HashSet<V>(); 169 170 hashSet = SearchRange(min, max, hashSet, node); 171 172 return hashSet; 173 } 174 #endregion 175 176 #region 樹的指定範圍查找 177 /// <summary> 178 /// 樹的指定範圍查找 179 /// </summary> 180 /// <param name="range1"></param> 181 /// <param name="range2"></param> 182 /// <param name="tree"></param> 183 /// <returns></returns> 184 public HashSet<V> SearchRange(K min, K max, HashSet<V> hashSet, TreapNode<K, V> tree) 185 { 186 if (tree == null) 187 return hashSet; 188 189 //遍歷左子樹(尋找下界) 190 if (min.CompareTo(tree.key) < 0) 191 SearchRange(min, max, hashSet, tree.left); 192 193 //當前節點是否在選定範圍內 194 if (min.CompareTo(tree.key) <= 0 && max.CompareTo(tree.key) >= 0) 195 { 196 //等於這種情況 197 foreach (var item in tree.attach) 198 hashSet.Add(item); 199 } 200 201 //遍歷右子樹(兩種情況:①:找min的下限 ②:必須在Max範圍之內) 202 if (min.CompareTo(tree.key) > 0 || max.CompareTo(tree.key) > 0) 203 SearchRange(min, max, hashSet, tree.right); 204 205 return hashSet; 206 } 207 #endregion 208 209 #region 找到當前樹的最小節點 210 /// <summary> 211 /// 找到當前樹的最小節點 212 /// </summary> 213 /// <returns></returns> 214 public TreapNode<K, V> FindMin() 215 { 216 return FindMin(node); 217 } 218 #endregion 219 220 #region 找到當前樹的最小節點 221 /// <summary> 222 /// 找到當前樹的最小節點 223 /// </summary> 224 /// <param name="tree"></param> 225 /// <returns></returns> 226 public TreapNode<K, V> FindMin(TreapNode<K, V> tree) 227 { 228 if (tree == null) 229 return null; 230 231 if (tree.left == null) 232 return tree; 233 234 return FindMin(tree.left); 235 } 236 #endregion 237 238 #region 找到當前樹的最大節點 239 /// <summary> 240 /// 找到當前樹的最大節點 241 /// </summary> 242 /// <returns></returns> 243 public TreapNode<K, V> FindMax() 244 { 245 return FindMin(node); 246 } 247 #endregion 248 249 #region 找到當前樹的最大節點 250 /// <summary> 251 /// 找到當前樹的最大節點 252 /// </summary> 253 /// <param name="tree"></param> 254 /// <returns></returns> 255 public TreapNode<K, V> FindMax(TreapNode<K, V> tree) 256 { 257 if (tree == null) 258 return null; 259 260 if (tree.right == null) 261 return tree; 262 263 return FindMax(tree.right); 264 } 265 #endregion 266 267 #region 刪除當前樹中的節點 268 /// <summary> 269 /// 刪除當前樹中的節點 270 /// </summary> 271 /// <param name="key"></param> 272 /// <returns></returns> 273 public void Remove(K key, V value) 274 { 275 node = Remove(key, value, node); 276 } 277 #endregion 278 279 #region 刪除當前樹中的節點 280 /// <summary> 281 /// 刪除當前樹中的節點 282 /// </summary> 283 /// <param name="key"></param> 284 /// <param name="tree"></param> 285 /// <returns></returns> 286 public TreapNode<K, V> Remove(K key, V value, TreapNode<K, V> tree) 287 { 288 if (tree == null) 289 return null; 290 291 //左子樹 292 if (key.CompareTo(tree.key) < 0) 293 { 294 tree.left = Remove(key, value, tree.left); 295 } 296 //右子樹 297 if (key.CompareTo(tree.key) > 0) 298 { 299 tree.right = Remove(key, value, tree.right); 300 } 301 /*相等的情況*/ 302 if (key.CompareTo(tree.key) == 0) 303 { 304 //判斷裏面的HashSet是否有多值 305 if (tree.attach.Count > 1) 306 { 307 //實現惰性刪除 308 tree.attach.Remove(value); 309 } 310 else 311 { 312 //有兩個孩子的情況 313 if (tree.left != null && tree.right != null) 314 { 315 //如果左孩子的優先級低就需要“左旋” 316 if (tree.left.priority < tree.right.priority) 317 { 318 tree = RotateLL(tree); 319 } 320 else 321 { 322 //否則“右旋” 323 tree = RotateRR(tree); 324 } 325 326 //繼續旋轉 327 tree = Remove(key, value, tree); 328 } 329 else 330 { 331 //如果旋轉後已經變成了葉子節點則直接刪除 332 if (tree == null) 333 return null; 334 335 //最後就是單支樹 336 tree = tree.left == null ? tree.right : tree.left; 337 } 338 } 339 } 340 341 return tree; 342 } 343 #endregion 344 } 345 }