引言
如果要尋找一段文本中出現頻率最高的單詞,或者出現頻率最高的字符,那么首先要按單詞或者字符出現的次數建立一個鍵值表,然后在這個鍵值表中尋找最大值的鍵。
方案一
在鍵值表中尋找最大值的鍵的最自然的方案如下所示:
1 T FindMaxItem<T>(IDictionary<T, int> items) 2 { 3 var key = default(T); 4 var max = int.MinValue; 5 foreach (var kvp in items) 6 if (kvp.Value > max) 7 { 8 key = kvp.Key; 9 max = kvp.Value; 10 } 11 return key; 12 }
這個方案使用變量 max 來跟蹤最大值,使用變量 key 來跟蹤最大值對應的鍵。
方案二
如果把方案一中的變量 key 和 max 組合到一個 Tuple<T, int> 類中,就得到方案二:
1 T FindMaxItem<T>(IDictionary<T, int> items) 2 { 3 var pair = Tuple.Create(default(T), int.MinValue); 4 foreach (var kvp in items) 5 if (kvp.Value > pair.Item2) 6 pair = Tuple.Create(kvp.Key, kvp.Value); 7 return pair.Item1; 8 }
方案二和方案一沒有本質區別,但是通過使用變量 pair 代替變量 key 和 max,將相關的變量 key 和 max 組合在一起,應該說這個重構使代碼更優雅了一點。
方案三
實際上還可以使用 KeyValuePair<T, int> 結構代替 Tuple<T, int> 類,就得到方案三:
1 T FindMaxItem<T>(IDictionary<T, int> items) 2 { 3 var pair = new KeyValuePair<T, int>(default(T), int.MinValue); 4 foreach (var kvp in items) 5 if (kvp.Value > pair.Value) 6 pair = kvp; 7 return pair.Key; 8 }
方案三和方案二的區別僅在於變量 pair 的數據類型不同。由於循環變量 kvp 的數據類型就是 KeyValuePair<T, int>,所以在方案三中第 6 行就可以直接將 kvp 賦值給 pair 。這個重構又使代碼更優雅了一點。
方案四
上述三個方案都使用變量同時跟蹤最大值和相應的鍵,其實只需要跟蹤最大值對應的鍵就行了,這就得到方案四:
1 T FindMaxItem<T>(IDictionary<T, int> items) 2 { 3 var key = items.First().Key; 4 foreach (var kvp in items) 5 if (kvp.Value > items[key]) 6 key = kvp.Key; 7 return key; 8 }
這里要注意幾點:
- 因為我們需要尋找鍵值表中最大值對應的鍵,所以需要一個變量 key 來跟蹤這個鍵。
- 上述程序第 5 行使用 items[key] 來得到變量 key 對應的值,以便判斷是否有新的最大值。
- 良好實現的鍵值表數據結構中,通過鍵檢索相應的值的操作的時間復雜度應該是接近 O(1)。不然,這個算法的效率就大大地有問題了。
- 變量 key 的初值必須是鍵值表中的某一項,具體到方案四中,就是鍵值表中的第一項。如若不然,第 5 行中的 items[key] 就會出問題了。
幾點說明
1. 這些方案中 FindMaxItem 方法的參數的類型是 IDictionary<T, int> 接口,這樣就可以適用於 Dictionary<T, int>、SortedDictionary<T, int> 和 SortedList<T, int> 這些數據類型。
1.1 SortedDictionary<T, int> 和 Sorted<T, int> 中的 Sorted 是對於 Key 的排序,而不是對於 Value 的排序,所以在這個場合和未排序的 Dictionary<T, int> 是沒有區別的。
1.2 IDictionary<T, int> 接口中 Key 是不允許為 null,也不允許重復的。但 Value 沒有這個限制,既可以為 null,也可以重復。
2. 這些方案中的 FindMaxItem 方法可適用於多種場合。
2.1 如果是在一段文本中尋找出現頻率最高的單詞,T 的類型是 string,這是一個引用類型。
2.2 如果是在一段文本中尋找出現頻率最高的字符,T 的類型是 char,這是一個值類型。
3. 如果鍵值表是空表的話,前三個方案會返回 default(T),而方案四會拋出 InvalidOperationException 異常。
3.1 如果需要方案四的行為和前三個方案相同,則在方案四的第 3 行使用 FirstOrDefault 方法代替 First 方法就行了。
3.2 如果需要前三個方案的行為和方案四相同,則在前三個方案一開始增加一行以下語句就行了:
if (items.Count == 0) throw new InvalidOperationException("The items is empty.");
4. 前三個方案中 FindMaxItem 方法的參數類型也可以從 IDictionary<T, int> 接口改為 IEnumerable<KeyValuePair<T, int>> 接口。
4.1 在 3.2 中的 IDictionary<T, int> 接口的(實際上是 IColletion<T> 接口的) Count 屬性也就需要改為 IEnumerable<KeyValuePair<T, int>> 接口的 Count() 擴展方法了。
4.2 實際上 IDictionary<T, int> 接口也實現了 IEnumerable<KeyValuePair<T, int>> 接口,所以在 3.2 中可以直接使用 Count() 擴展方法代替 Count 屬性。
4.3 這樣做並不會降低效率,因為如果 source 的類型實現了 ICollection<T> 接口的話,則 Count() 擴展方法會將該接口的 Count 屬性用於獲取元素計數。
5. 如果鍵值表中的最大值不只一個,這些方案將返回第一個達到最大值的鍵。
6. 在前三個方案中,如果鍵值表中的值只有 int.MinValue,且沒有 default(T) 的鍵的話,這三個方案將錯誤地返回 default(T)。
6.1 這可以通過將循環中比較語句中的“>”改為“>=”來避免。當然,如果鍵值表中的最大值不只一個,這些方案將返回最后一個達到最大值的鍵。
6.2 在方案一中,還可以將 int.MinValue 改為 long.MinValue 來解決。
6.3 在方案二中,除了將 int.MinValue 改為 long.MinValue 外,在第 6 行還需要將 kvp.Value 強制轉換為 long 類型。
7. 在方案四中,將第 3 行的 First 方法改為 Last 方法也行,但 Last 方法可能比 First 方法更慢。並且也會微妙地影響方案四的行為,特別是在鍵值表中的最大值有多個的情況下。