目录✨
一、Dictionary 常规使用
1. 基础声明与初始化
2. 高频API速查
3. 取值的三种方式与坑点
4. 遍历 Dictionary
二、基本概念
1. 什么是 Dictionary
2. KeyValuePair 结构
3. 键的哈希与相等性
4. 常用初始化技巧
三、散列表(哈希表)
1. 散列表核心思想
2. 哈希冲突
3. 装填因子与扩容
4. 时间复杂度分析
5. 哈希函数的质量决定性能
6. Dictionary vs Hashtable
面试回答精简版
总结
一、Dictionary 常规使用
Dictionary<TKey, TValue>是C#中最常用的键值对泛型集合,命名空间为System.Collections.Generic。它以「键-值」形式存储数据,键唯一不可重复,通过键可以O(1)快速查找对应值,是缓存、映射、配置管理场景的首选容器。
1. 基础声明与初始化
//声明空的Dictionary,键为string,值为int Dictionary<string, int> scoreDict = new Dictionary<string, int>(); //初始化直接填充键值对 Dictionary<string, string> configDict = new Dictionary<string, string>() { { "PlayerName", "张三" }, { "Level", "10" }, { "Hp", "100" } };2. 高频API速查
| 方法/属性 | 功能说明 |
|---|---|
Add(key, value) | 添加一个键值对,键重复会抛异常 |
Remove(key) | 根据键删除对应键值对,返回是否删除成功 |
Clear() | 清空所有键值对 |
ContainsKey(key) | 判断是否存在指定键,返回bool |
ContainsValue(value) | 判断是否存在指定值,返回bool |
TryGetValue(key, out value) | 安全取值,键存在返回true并输出值,不存在返回false |
Count | 当前键值对总数 |
Keys | 获取所有键的集合 |
Values | 获取所有值的集合 |
示例代码:
Dictionary<string, int> score = new Dictionary<string, int>(); //添加 score.Add("数学", 95); score.Add("语文", 88); //安全取值(推荐) if (score.TryGetValue("数学", out int mathScore)) { Console.WriteLine($"数学成绩:{mathScore}"); } //判断键是否存在 if (score.ContainsKey("英语")) { score["英语"] = 90; //键存在则修改值 } else { score.Add("英语", 90); //不存在则添加 } //删除 score.Remove("语文"); //遍历所有键 foreach (string key in score.Keys) { Console.WriteLine($"{key}:{score[key]}"); } score.Clear();3. 取值的三种方式与坑点
//方式1:索引器直接取值 —— 键不存在会抛 KeyNotFoundException int s1 = score["数学"]; //方式2:先判断再取值 —— 安全但查了两次字典 if (score.ContainsKey("数学")) { int s2 = score["数学"]; } //方式3:TryGetValue —— 只查一次,最推荐 ✅ if (score.TryGetValue("数学", out int s3)) { Console.WriteLine(s3); }重要坑点:用索引器
dict[key]取值时,如果键不存在会直接抛异常。生产环境优先使用TryGetValue,只做一次哈希查找,性能更好且安全。
4. 遍历 Dictionary
//遍历键值对(KeyValuePair) foreach (KeyValuePair<string, int> kvp in score) { Console.WriteLine($"{kvp.Key}:{kvp.Value}"); } //只遍历键 foreach (string key in score.Keys) { Console.WriteLine(key); } //只遍历值 foreach (int value in score.Values) { Console.WriteLine(value); }注意:和List一样,foreach遍历过程中不能Add/Remove,会抛出集合被修改异常。需要遍历删除时,先把Keys转成List再遍历。
//✅正确:遍历删除 foreach (string key in score.Keys.ToList()) { if (score[key] < 60) { score.Remove(key); } }二、基本概念
1. 什么是 Dictionary
Dictionary 是基于散列表(Hash Table)实现的键值对集合。它不按插入顺序存储,而是通过键的哈希值计算存储位置,从而实现接近O(1)的查找、插入、删除效率。
核心特征:
- 键唯一:同一个键只能存在一个,重复Add会抛异常
- 键不可变:作为键的对象,其哈希值在存入后不能改变,否则找不到数据
- 无序:遍历顺序不等于插入顺序(.NET Core 3.0+ 实际保持插入顺序,但这是实现细节,不应依赖)
- 泛型强类型:编译期确定键和值的类型,无需装箱拆箱
2. KeyValuePair 结构
Dictionary 中每个元素都是一个KeyValuePair<TKey, TValue>结构体,包含两个属性:
Key:键Value:值
KeyValuePair<string, int> kvp = new KeyValuePair<string, int>("年龄", 20); Console.WriteLine($"{kvp.Key} = {kvp.Value}");3. 键的哈希与相等性
Dictionary 判断两个键是否「相同」,依赖两个方法:
GetHashCode():计算哈希值,决定存储桶位置Equals():哈希冲突时,判断两个键是否真的相等
自定义类作为键时,必须同时重写
GetHashCode()和Equals(),否则可能出现「逻辑上相等的两个对象被当成不同键」的问题。
//自定义类作为键的正确写法 public class Player { public int Id { get; set; } public string Name { get; set; } public override bool Equals(object obj) { return obj is Player player && Id == player.Id; } public override int GetHashCode() { return HashCode.Combine(Id); //用Id计算哈希 } }4. 常用初始化技巧
//集合初始化器(C# 6+) var dict1 = new Dictionary<string, int> { ["A"] = 1, ["B"] = 2 }; //从现有集合创建 var dict2 = list.ToDictionary(item => item.Id, item => item.Name);三、散列表(哈希表)
Dictionary 的底层数据结构就是散列表(Hash Table),理解散列表是掌握 Dictionary 性能的关键。
1. 散列表核心思想
散列表的核心是**「键 → 哈希值 → 数组下标 → 存储位置」**的映射:
对键调用
GetHashCode()得到一个整数(哈希值)用哈希值对数组长度取模,得到存储位置(桶,Bucket)
将键值对存入该位置对应的桶中
理想情况下,每个键对应唯一桶,查找只需一次计算,时间复杂度O(1)。
2. 哈希冲突
不同的键可能计算出相同的哈希值(或取模后落到同一个桶),这就是哈希冲突。
Dictionary 解决冲突的方式:拉链法(Separate Chaining)
每个桶不只是存一个元素,而是一个链表(或数组)
冲突的元素都挂在同一个桶的链表里
查找时:先算哈希定位到桶,再在桶内链表中用
Equals()逐个比较找到目标
桶数组 [0] → (键A,值1) → (键D,值4) ← 哈希冲突,挂在同一桶 [1] → (键B,值2) [2] → (键C,值3) [3] → null3. 装填因子与扩容
装填因子(Load Factor)= 元素数量 / 桶数组长度
装填因子越大,冲突概率越高,查找越慢
.NET Dictionary 默认装填因子阈值约为1.0,超过后触发扩容
扩容机制:
新建一个长度约为原来2倍的桶数组
重新计算所有元素的哈希值和新桶位置(rehash)
将所有元素迁移到新数组
旧数组交给GC回收
✨性能优化:预知数据量时,初始化指定容量,减少扩容和rehash开销
Dictionary<string, int> dict = new Dictionary<string, int>(1000);
4. 时间复杂度分析
| 操作 | 平均情况 | 最坏情况 |
|---|---|---|
| 查找(通过键) | O(1) | O(n),所有键冲突到一个桶 |
| 插入 | O(1) | O(n),触发扩容时为O(n) |
| 删除 | O(1) | O(n) |
| 遍历 | O(n) | O(n) |
最坏情况O(n)极少出现,只要哈希函数分布均匀,实际性能始终接近O(1)。
5. 哈希函数的质量决定性能
一个好的哈希函数应该:
分布均匀:不同键的哈希值尽量分散,减少冲突
计算快:哈希计算本身不能太慢
一致性:相同的键必须返回相同的哈希值
字符串的
GetHashCode()在 .NET Core 中使用了改进的哈希算法,分布均匀且每次进程启动不同(防止哈希碰撞攻击)。
6. Dictionary vs Hashtable
| 对比项 | Dictionary<TKey,TValue> | Hashtable |
|---|---|---|
| 类型安全 | 泛型,编译期检查 | 非泛型,存储object |
| 性能 | 无装箱拆箱,更快 | 有装箱拆箱开销 |
| 线程安全 | 非线程安全 | 部分线程安全(已过时) |
| 推荐度 | ✅现代开发首选 | ❌已被Dictionary替代 |
面试回答精简版
Dictionary底层是散列表,通过键的哈希值取模定位存储桶,哈希冲突用拉链法解决,每个桶挂一个链表。查找、插入、删除平均O(1),最坏O(n)。装填因子超过阈值会触发2倍扩容并rehash。自定义类做键必须同时重写GetHashCode和Equals。生产环境取值优先用TryGetValue,避免键不存在抛异常。
总结
常规使用:掌握增删改查API,牢记键唯一、取值优先用
TryGetValue,遍历删除需先转Keys为List。基本概念:理解键值对结构、KeyValuePair、键的哈希与相等性,自定义类做键必须重写两个方法。
散列表原理:哈希映射定位桶,拉链法解决冲突,装填因子控制扩容,平均O(1)查找;预估容量减少rehash,是性能优化关键。