引言
如果要寻找一段文本中出现频率最高的单词,或者出现频率最高的字符,那么首先要按单词或者字符出现的次数建立一个键值表,然后在这个键值表中寻找最大值的键。
方案一
在键值表中寻找最大值的键的最自然的方案如下所示:
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 方法更慢。并且也会微妙地影响方案四的行为,特别是在键值表中的最大值有多个的情况下。