C#集合全面解析
前言
在C#编程中,集合是存储和操作数据的重要工具。本文将详细介绍C#中最常用的哈希集合和字典类型,通过简单易懂的例子帮助理解它们的使用方法和底层原理。
基础概念说明
什么是哈希(Hash)
哈希是一种将任意长度的数据转换为固定长度数值的算法。在集合中,哈希用于快速定位数据位置。
通俗理解:哈希就像是给每个东西分配一个唯一的"房间号码",把复杂的东西转换成简单的数字编号,方便快速找到它。
想象一个图书管理员,图书馆里有100个房间(0-99号)。当有人给他一本书时,他需要决定把这本书放在哪个房间里。
哈希函数就像是他的"分配规则":
-
书名是"西游记" → 用某种方法计算 → 房间号23
-
书名是"红楼梦" → 用同样方法计算 → 房间号67
这样,下次想找"西游记"时,直接去23号房间就能找到,不用在整个图书馆里找。
什么是字典(Dictionary)
字典是一种键值对(Key-Value Pair)集合,通过键可以快速查找对应的值。
通俗理解:字典就像现实中的电话簿或字典,通过一个名字(键)快速找到对应的内容(值)。键不可重复存在。
现实中的字典:
-
查"苹果" → 翻到拼音"píngguǒ" → 找到对应页码 → 看到解释
-
查"香蕉" → 翻到拼音"xiāngjiāo" → 找到对应页码 → 看到解释
编程中的字典:
-
Key(键):"苹果" → Value(值):"一种水果"
-
Key(键):"香蕉" → Value(值):"黄色的水果"
什么是哈希表(HashTable)
哈希表是实现字典的一种数据结构,通过哈希函数将键映射到数组索引上。键不可重复存在。
通俗理解:哈希表就是用哈希技术实现的取餐柜
-
存东西时,输入物品名称(比如"猪脚饭")
-
系统自动计算出应该放在哪个格子(比如格子35)
-
系统告诉你:"请放在35号格子"
-
下次要取时,输入"猪脚饭",系统直接告诉你去35号格子拿
整个过程:
输入"猪脚饭" → 哈希函数计算 → 得到数字35 → 存到35号位置 输入"猪脚饭" → 哈希函数计算 → 得到数字35 → 直接去35号位置取
HashSet 哈希集合
概念说明
HashSet是一个不包含重复元素的集合,内部使用哈希表实现,查找、添加、删除操作的时间复杂度都是O(1)。
HashSet说是集合,但看起来使用方法更像List<T>数组。由于底层是通过哈希函数实现,所以便被称为集合。
基本使用示例
using System; using System.Collections.Generic; class Program { static void Main() { // 创建一个HashSet HashSet<string> fruits = new HashSet<string>(); // 添加元素 fruits.Add("苹果"); fruits.Add("香蕉"); fruits.Add("橙子"); fruits.Add("苹果"); // 重复元素不会被添加 Console.WriteLine("水果集合中的元素数量:" + fruits.Count); // 输出:水果集合中的元素数量:3 // 遍历集合并输出 Console.WriteLine("集合中的水果:"); foreach (string fruit in fruits) { Console.WriteLine(fruit); } // 输出: // 集合中的水果: // 苹果 // 香蕉 // 橙子 // 检查元素是否存在 bool hasApple = fruits.Contains("苹果"); Console.WriteLine("是否包含苹果:" + hasApple); // 输出:是否包含苹果:True // 移除元素 fruits.Remove("香蕉"); Console.WriteLine("移除香蕉后的集合:"); foreach (string fruit in fruits) { Console.WriteLine(fruit); } // 输出: // 移除香蕉后的集合: // 苹果 // 橙子 } }
HashSet底层原理图解
苹果.GetHashCode
香蕉.GetHashCode
橙子.GetHashCode
HashSet内部结构
哈希桶数组
哈希函数
桶0: null
桶1: 苹果
桶2: null
桶3: 香蕉
桶4: 橙子
桶5: null
计算哈希值
哈希值=1
哈希值=3
哈希值=4
HashSet常用方法
using System; using System.Collections.Generic; class HashSetMethods { static void Main() { HashSet<int> set1 = new HashSet<int> { 1, 2, 3, 4, 5 }; HashSet<int> set2 = new HashSet<int> { 4, 5, 6, 7, 8 }; Console.WriteLine("=== HashSet常用方法演示 ==="); // UnionWith - 并集 HashSet<int> union = new HashSet<int>(set1); union.UnionWith(set2); Console.WriteLine("并集:" + string.Join(", ", union)); // 输出:并集:1, 2, 3, 4, 5, 6, 7, 8 // IntersectWith - 交集 HashSet<int> intersect = new HashSet<int>(set1); intersect.IntersectWith(set2); Console.WriteLine("交集:" + string.Join(", ", intersect)); // 输出:交集:4, 5 // ExceptWith - 差集 HashSet<int> except = new HashSet<int>(set1); except.ExceptWith(set2); Console.WriteLine("差集:" + string.Join(", ", except)); // 输出:差集:1, 2, 3 // SymmetricExceptWith - 对称差集 HashSet<int> symmetric = new HashSet<int>(set1); symmetric.SymmetricExceptWith(set2); Console.WriteLine("对称差集:" + string.Join(", ", symmetric)); // 输出:对称差集:1, 2, 3, 6, 7, 8 } }
Dictionary<TKey, TValue> 字典
概念说明
Dictionary<TKey, TValue>是C#中最常用的键值对集合,通过键可以快速获取对应的值。它基于哈希表(HashTable)实现(但在数据存储类型方面有不同),提供了高效的查找性能。不同的是,Dictionary中的键值对是泛型类型,仅支持存储其在定义(创建)时就规定好的键值对类型。而HashTable中的键值对是Object类型,这意味着可以灵活、随意存储各种各样不同种类键值对。显而易见,使用Dictionary更安全一些。
基本使用示例
using System; using System.Collections.Generic; class DictionaryBasics { static void Main() { // 创建字典 Dictionary<string, int> studentScores = new Dictionary<string, int>(); // 添加键值对 studentScores.Add("张三", 85); studentScores.Add("李四", 92); studentScores["王五"] = 78; // 另一种添加方式 studentScores["赵六"] = 96; Console.WriteLine("学生人数:" + studentScores.Count); // 输出:学生人数:4 // 通过键获取值 int zhangsanScore = studentScores["张三"]; Console.WriteLine("张三的成绩:" + zhangsanScore); // 输出:张三的成绩:85 // 安全地获取值(避免KeyNotFoundException) if (studentScores.TryGetValue("李四", out int lisiScore)) { Console.WriteLine("李四的成绩:" + lisiScore); // 输出:李四的成绩:92 } // 遍历字典 Console.WriteLine("\n所有学生成绩:"); foreach (KeyValuePair<string, int> student in studentScores) { Console.WriteLine($"{student.Key}: {student.Value}分"); } // 输出: // 所有学生成绩: // 张三: 85分 // 李四: 92分 // 王五: 78分 // 赵六: 96分 // 遍历键和值 Console.WriteLine("\n所有学生姓名:"); foreach (string name in studentScores.Keys) { Console.WriteLine(name); } Console.WriteLine("\n所有成绩:"); foreach (int score in studentScores.Values) { Console.WriteLine(score); } } }
Dictionary底层原理图解
张三.GetHashCode
李四.GetHashCode
王五.GetHashCode
赵六.GetHashCode
Dictionary内部结构
哈希桶数组
哈希函数
桶0: null
桶1: Entry{Key=张三, Value=85}
桶2: null
桶3: Entry{Key=李四, Value=92}
桶4: Entry{Key=王五, Value=78}
桶5: Entry{Key=赵六, Value=96}
桶6: null
计算哈希值
哈希值=1
哈希值=3
哈希值=4
哈希值=5
Dictionary常用方法演示
using System; using System.Collections.Generic; class DictionaryMethods { static void Main() { Dictionary<string, string> capitals = new Dictionary<string, string>(); // 添加元素 capitals["中国"] = "北京"; capitals["美国"] = "华盛顿"; capitals["日本"] = "东京"; capitals["法国"] = "巴黎"; Console.WriteLine("=== Dictionary方法演示 ===\n"); // ContainsKey - 检查是否包含指定键 bool hasChina = capitals.ContainsKey("中国"); Console.WriteLine("是否包含'中国'键:" + hasChina); // 输出:是否包含'中国'键:True // ContainsValue - 检查是否包含指定值 bool hasTokyo = capitals.ContainsValue("东京"); Console.WriteLine("是否包含'东京'值:" + hasTokyo); // 输出:是否包含'东京'值:True // Remove - 移除指定键的元素 bool removed = capitals.Remove("日本"); Console.WriteLine("移除日本:" + (removed ? "成功" : "失败")); // 输出:移除日本:成功 // Clear - 清空所有元素 Console.WriteLine("清空前元素数量:" + capitals.Count); // 输出:清空前元素数量:4 capitals.Clear(); Console.WriteLine("清空后元素数量:" + capitals.Count); // 输出:清空后元素数量:0 } }
HashTable 哈希表
概念说明
HashTable是.NET Framework早期版本中的“哈希表直接实现键值对方式”(现在不常用,但老代码中会遇到,现在常用的是Dictionary),性能相对较低。HashTable存储的是object类型的键值对,这意味着需要进行装箱和拆箱(类型转换)操作。
HashTable与Dictionary的区别
| 特性 | HashTable | Dictionary |
|---|---|---|
| 泛型支持 | 不支持 | 支持 |
| 性能 | 较低(装箱/拆箱) | 较高 |
| 类型安全 | 运行时检查 | 编译时检查 |
| .NET版本 | 1.0+ | 2.0+ |
HashTable基本使用示例
using System; using System.Collections; class HashTableBasics { static void Main() { // 创建HashTable Hashtable employeeInfo = new Hashtable(); // 添加键值对(键和值都是object类型) employeeInfo["001"] = "张三"; employeeInfo["002"] = "李四"; employeeInfo[3] = "王五"; // 键可以是不同类型 employeeInfo["004"] = 25; // 值也可以是不同类型 Console.WriteLine("员工数量:" + employeeInfo.Count); // 输出:员工数量:4 // 通过键获取值 string name = (string)employeeInfo["001"]; // 需要类型转换 Console.WriteLine("员工001的姓名:" + name); // 输出:员工001的姓名:张三 // 安全地获取值 if (employeeInfo.ContainsKey("002")) { string empName = (string)employeeInfo["002"]; Console.WriteLine("员工002的姓名:" + empName); // 输出:员工002的姓名:李四 } // 遍历HashTable Console.WriteLine("\n所有员工信息:"); foreach (DictionaryEntry entry in employeeInfo) { Console.WriteLine($"键:{entry.Key}, 值:{entry.Value}"); } // 输出: // 所有员工信息: // 键:001, 值:张三 // 键:002, 值:李四 // 键:3, 值:王五 // 键:004, 值:25 // 移除元素 employeeInfo.Remove("001"); Console.WriteLine("\n移除员工001后的信息:"); foreach (DictionaryEntry entry in employeeInfo) { Console.WriteLine($"键:{entry.Key}, 值:{entry.Value}"); } } }
集合实现的底层原理:哈希冲突处理机制
什么是哈希冲突?——当不同的键值产生相同的哈希码时,就发生了哈希冲突。
为什么会发生哈希冲突?——由于哈希算法计算出的结果是有限度的,而键的取值范畴可能是无限的,所以哈希冲突是不可避免的。
为什么需要哈希冲突解决机制?——如果没有冲突解决方法,当发生哈希冲突时,新的元素会覆盖已存在的元素,导致数据丢失。因此需要有效的机制来处理这种情况。
继续图书馆的例子:
-
"西游记"按照规则应该放在23号房间
-
假设"三国演义"按照同样的规则也算出来是23号房间
这就尴尬了!两个不同的书要放在同一个房间,这就是哈希冲突。
解决办法:
-
链式处理:23号房间变成一个"小书架",可以放多本书
-
重新找房间:如果23号满了,就找24号、25号...直到找到空房间
开放寻址法示意图
开放寻址法是一种解决哈希冲突的方法,当发生冲突时,它会在哈希表中寻找下一个可用的位置来存储元素。所有的元素都存储在哈希表的数组中,没有使用额外的数据结构。
桶3为空
桶3被占用
桶4为空
桶4被占用
插入元素
计算哈希值
哈希值=3
检查桶3
插入到桶3
检查下一个桶
检查桶4
插入到桶4
继续检查下一个桶
链式哈希法示意图
链式哈希法的基本思想是:为哈希表的每个位置维护一个链表(或其他数据结构),当发生冲突时,将冲突的元素存储在对应位置的链表中。
哈希桶数组
桶0
桶1: 链表
桶2
桶3: 链表
桶4
Entry{Key=A, Value=1}
Entry{Key=B, Value=2}
Entry{Key=C, Value=3}
Entry{Key=X, Value=10}
Entry{Key=Y, Value=20}
性能对比分析
时间复杂度对比表
| 操作 | List | HashSet | Dictionary<K,V> | HashTable |
|---|---|---|---|---|
| 查找 | O(n) | O(1) | O(1) | O(1) |
| 插入 | O(1) | O(1) | O(1) | O(1) |
| 删除 | O(n) | O(1) | O(1) | O(1) |
最佳实践建议
1. 选择合适的集合类型
// 场景1:只需要存储唯一值,且对性能有要求,还不需要键值对 HashSet<string> uniqueNames = new HashSet<string>(); // 场景2:需要通过键快速查找值 Dictionary<string, int> nameToAge = new Dictionary<string, int>();
2. 避免KeyNotFoundException
Dictionary<string, int> scores = new Dictionary<string, int>(); scores["张三"] = 85; // 推荐方式:使用TryGetValue if (scores.TryGetValue("李四", out int score)) { Console.WriteLine($"李四的成绩:{score}"); } else { Console.WriteLine("未找到李四的成绩"); } // 不推荐方式:直接访问可能不存在的键 // int score = scores["李四"]; // 会抛出异常
3. 合理预估容量
// 如果知道大概需要存储的元素数量,可以预先指定容量 // 避免频繁扩容带来的性能开销 HashSet<int> numbers = new HashSet<int>(1000); // 预分配1000个元素的空间 Dictionary<string, int> mapping = new Dictionary<string, int>(500); // 预分配空间
总结
-
HashSet用于存储唯一元素,基于哈希表实现,查找效率高
-
Dictionary<TKey, TValue> 键值对集合(基于哈希表但有不同),通过键快速查找值
-
HashTable:早期的键值对(哈希表直接实现),性能较低
-
底层原理:哈希函数、哈希冲突处理机制
-
实际应用:如何在项目中合理使用这些集合类型
-
现代开发:优先使用
Dictionary<TKey, TValue>和HashSet<T> -
多线程环境:考虑使用
ConcurrentDictionary<TKey, TValue>(后面会讲到) -
上古代码:可能会遇到
HashTable,需要了解其特性和限制 -
性能要求高:
Dictionary>HashTable
-
选择合适的集合类型对程序性能至关重要。在需要快速查找、插入、删除操作时,优先考虑HashSet和Dictionary。
更多推荐



所有评论(0)