微軟在 .NET 3.5 新增了一個 HashSet 類,在 .NET 4 新增了一個 SortedSet 類,本文介紹他們的特性,並比較他們的異同。
.NET Collection 函數庫的 HashSet、SortedSet 這兩個泛型的類,都實現了 System.Collections.Generic.ISet 接口;但 Java 早在 1.2 (或更早) 之前的版本,即已提供了實現這兩種數據結構的同名類 ,且還有更嚴謹的 TreeSet (裡面存儲的項,連類型都必須一致。當年還沒有泛型)。
Set 是「集合」的意思,其在數學上的定義,是裡面元素的存放沒有特定的順序,且不允許重復。我們先看以下 HashSet、SortedSet 的示例:
ISet set = new HashSet() { 5, 9, 2, 1, 2, 2, 3, 7, 4, 9, 9 };
foreach (int element in set)
Response.Write(string.Format(" {0}", element));
執行結果:
圖 1 重復的元素自動被移除
同樣的代碼,把 HashSet 改成 SortedSet,如下:
ISet set = new SortedSet() { 5, 9, 2, 1, 2, 2, 3, 7, 4, 9, 9 };
foreach (int element in set)
Response.Write(string.Format(" {0}", element));
執行結果:
圖 2 重復的元素自動被移除,且內部會自動做排序
我們看到 HashSet、SortedSet 這兩個類,確實都不允許重復,但前者是按照元素加入的順序存儲,後者會再將加入的元素做排序,且能夠在插入、刪除和搜索元素時,仍維護數據的排列順序。因此若您有耐心讀到這裡,就多學了一招:若平常編程時想過濾重復的項的話,就可以用這兩個 Set 類,因為集合是不允許重復元素的。在 SortedSet 還沒出現的 .NET 3.5 時代,我們必須用 HashSet 先移除重復的項,然後再排序;而現在的 .NET 4,我們就可以改用 SortedSet 一步實現移除重復並排序。
當然,若您的需求不同 (暫不考慮性能差別),不希望默認自動排序,或不需要去除重復元素,可改用 List 類的 Sort 方法。此外,您也可把 HashSet 當作 key/value 配對的 Dictionary 類之中,只有 key 沒有 value 來使用,因為 Dictionary (泛型的 HashTable) 其特性,和 HastSet 類似,元素也是沒有特定的順序,且不允許重復 (key 必須為唯一)。
以下分別列出 .NET 平台上,HashSet、SortedSet 這兩個類各自的一些特性:
HastSet 的特性 :
它的 Contains 方法 (確定 HashSet 對象是否包含指定的元素) 執行速度很快,因其為基於「哈希」的查找 (hash-based lookup)。
(另 Dictionary 類的檢索速度也是非常快的,其「算法」的 Time Complexity 接近於 O(1),這是因為 Dictionary 類是以一個哈希表來實現的)
它的 Add 方法 (將指定的元素添加到 HashSet 對象中),如果 Count 小於內部數組的容量,則此方法的運算復雜度為 O(1)。如果必須調整 HashSet 對象的大小,則此方法的運算復雜度將為 O(n)。
它不能存儲重復的元素,而且當插入的元素有重復時,也會自動被忽略。
無法從特定的位置,訪問其中某個元素。
SortedSet 的特性:
它的 Contains 方法 (確定 HashSet 對象是否包含指定的元素) 執行速度很快,因其為基於「哈希」的查找 (hash-based lookup)。
它的 Add 方法 (將指定的元素添加到 HashSet 對象中),如果 Count 小於內部數組的容量,則此方法的運算復雜度為 O(1)。如果必須調整 HashSet 對象的大小,則此方法的運算復雜度將為 O(n)。(數據結構中的「紅黑樹 (Red-Black tree」) [3], [8]
它的 Add 方法,若添加了已存在的項時會被忽略,並且返回 False。
它不能存儲重復的元素,而且當插入的元素有重復時,也會自動被忽略。
無法從特定的位置,訪問其中某個元素。
以下我們來看 .NET 裡 HashSet 的一些示例:
示例一 - 測試查找的功能:
var set = new HashSet("我愛編程");
Response.Write(set.Contains('我')); //True
Response.Write(set.Contains('你')); //False
上述示例中,我們能夠將字符串,甚至中文字,傳入 HashSet 的構造函數,是因為 string 實現了 IEnumerable 接口,而 HastSet 類也實現了 IEnumerable。
示例二 - 測試 HashSet 內置的一些好用方法:
SymmetricExceptWith: 僅包含該對象或指定集合中存在的元素(但不可同時包含兩者中的元素)。
UnionWith: 包含該對象本身和指定集合中存在的所有元素。
ExceptWith: 從當前 HashSet 對象中移除指定集合中的所有元素。
IntersectWith: 僅包含該對象和指定集合中存在的元素。
using System;
using System.Collections.Generic;
class HashSetDemo
{
static void Main()
{
HashSet setA = new HashSet();
HashSet setB = new HashSet();
setA.Add('A');
setA.Add('B');
setA.Add('C');
setB.Add('C');
setB.Add('D');
setB.Add('E');
Show("Initial content of setA: ", setA);
Show("Initial content of setB: ", setB);
setA.SymmetricExceptWith(setB); //把 setA、setB 各自特有、對方沒有的元素列出來
Show("setA after Symmetric difference with SetB: ", setA);
setA.UnionWith(setB); //把 setA、setB 的全部元素列出來 (union 並集)
Show("setA after union with setB: ", setA);
setA.ExceptWith(setB); //把 setA 中,所擁有的 setB 元素移除
Show("setA after subtracting setB: ", setA);
setA.IntersectWith(setB); //把 setA 中,所擁有的 setB 元素列出
Show("setA after intersect with setB: ", setA);
Console.WriteLine();
Console.Read();
}
static void Show(string msg, HashSet set)
{
Console.Write(msg);
foreach (char ch in set)
Console.Write(ch + " ");
Console.WriteLine();
}
}
執行結果:
圖 3
由於 HastSet 實現了 IEnumerable 接口,因此我們可把其他任何 set 當作參數,傳入其他 set 類的運算方法裡。
此外,LINQ 也有類似上述示例的 Intersect、Except、Union、Distinct 的 set 運算功能,有興趣比較兩者特性的網友,可參考 msdn 或網絡上的文章 [5]。主要的差別在於,LINQ set 運算始終返回新的 IEnumerable 集合,而 HashSet 是修改當前的集合,且 HashSet 提供了比較多的 set 相關算符。
到了 .NET 4 才新建的 SortedSet 類,除了有前述 HashSet 類所擁有的 SymmetricExceptWith、UnionWith、ExceptWith、IntersectWith 等好用的方法外,還有「GetViewBetween (制定范圍)」、「Max (取最大值)」、「Min (取最小值)」等新增的好用方法。
以下我們來看 SortedSet 這三個方法的示例:
示例三 - 測試 GetViewBetween、Max、Min 方法:
using System;
using System.Collections.Generic;
using System.Linq; //此為 Max()、Min() 方法的必要引用
var set = new SortedSet() { 5, 9, 2, 1, 2, 2, 3, 7, 4, 9, 9 };
foreach (int element in set)
Response.Write(string.Format(" {0}", element));
Response.Write("
");
Response.Write("Max: " + set.Max() + "
");
Response.Write("Min: " + set.Min() + "
");
Response.Write("
取 2 ~ 5 之間的值:
");
//只取值為 2 ~ 5 之間的元素
var subSet = set.GetViewBetween(2, 5);
foreach (int i in subSet)
{
Response.Write(i + ",");
}
執行結果:
圖 4
此 GetViewBetween() 方法,也適用於 SortedSort 裡元素為字符串、字符的處理。