银河

SKYIV STUDIO

  博客园 :: 首页 :: 博问 :: 闪存 :: :: :: 订阅 订阅 :: 管理 ::

引言

如果要寻找一段文本中出现频率最高的单词,或者出现频率最高的字符,那么首先要按单词或者字符出现的次数建立一个键值表,然后在这个键值表中寻找最大值的键。

方案一

在键值表中寻找最大值的键的最自然的方案如下所示:

 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 方法更慢。并且也会微妙地影响方案四的行为,特别是在键值表中的最大值有多个的情况下。

参考资料

  1. MSDN: IDictionary<TKey, TValue> 接口
  2. MSDN: Dictionary<TKey, TValue>.Item 属性
  3. MSDN: Enumerable.First<TSource> 方法
  4. MSDN: Enumerable.FirstOrDefault<TSource> 方法
  5. MSDN: Enumerable.Count<TSource> 方法
posted on 2013-01-18 21:09  银河  阅读(2915)  评论(2编辑  收藏  举报