C#学习笔记——字典篇

📅 发布时间:2026/7/10 9:33:35 👁️ 浏览次数:
C#学习笔记——字典篇
在C#中哈希表 ( Hashtable ) 与泛型集合类 (DictionaryK,V) 是字典中常见的两个键对值集合它们都基于哈希算法实现高效的数据存储与检索。在现代开发中更推荐使用类型安全的泛型集合DictionaryTKey, TValue。一. Hashtable1.1 核心概念Hashtable通常被称为哈希表是C#早期提供的一种非泛型的键/值对集合其每个元素都是一个存储在DictionaryEntry对象中的键/值对。位于System.Collections命名空间。基于哈希算法实现的键值对集合类支持高效的查找、插入和删除操作。平均时间复杂度为O(1)。1.2 基本特性与结构键值结构键不能为null但值可以为null。键必须唯一每个键唯一关联一个值值可重复。存储结构内部使用Bucket结构体数组来存储数据, 但使用开放寻址法处理冲突。private struct Bucket { public object? key; //键 public object? val; //值 public int hash_coll; // 低31位存储键的哈希码,最高位(第32位)用作冲突标志位(1表示发生过冲突) }哈希算法通过键的哈希码GetHashCode()方法计算定位存储位置平均时间复杂度为O(1)。冲突处理使用开放寻址法解决哈希冲突内容不相等, hash算法得到哈希值可能一样 。通过哈希码计算元素下标存储位置若计算下标处有元素元素Key与添加元素Key不一样下标加增量获得新下标判断新下标是否有元素如果没有添加该元素否则继续执行上一步操作。动态扩容当元素数量超过当前容量时容量 / (0.72f * 加载因子) Hashtable 会自动扩展并重新分配存储空间。取最小质数无序性键值对的存储顺序由哈希码决定与插入顺序无关。1.3 构造方法默认构造函数//使用默认初始容量通常为 3、加载因子1f等默认配置创建空实例。 Hashtable()指定容量的构造函数//允许设置初始容量加载因子使用默认值1f Hashtable(int capacity)基于已有字典的构造函数//从现有字典复制所有键值对初始容量与原始字典元素数量一致加载因子默认 Hashtable(IDictionary d)1.4 Hashtable属性Count获取包含在 Hashtable 中的键/值对的数目。Values返回一个包含哈希表中所有值的 ICollection 集合。Keys返回一个包含哈希表中所有键的 ICollection 集合。Item[Object]获取或设置与指定的键关联的值。1.5 关键操作1.添加元素1Add(object key, object value)若键已存在抛出ArgumentException异常。2索引器[ Key ] value 若键已存在覆盖旧值若键不存在创建新键值对。2. 删除与清空元素1Remove(Key) : 根据键移除对应的键值对若键不存在操作静默完成不抛异常。2Clear() : 清空所有元素。3. 检索元素1) Contains(key) : 快速判断值是否存在。(2) ContainsKey(key) 快速判断值是否存在。(3) ContainsValue(key) 遍历所有元素值的查找复杂度为O(n)。4. 遍历元素(1) foreach(DictionaryEntry entry in ht) : 使用DictionaryEntry直接遍历键值对。(2) foreach(var k in ht.Values) / foreach(var k in ht.Keys) : 分离键与值的遍历。(3)foreach遍历中修改Hashtable大小(增删元素)会抛出InvalidOperationException。二.DictionaryK,V2.1 核心概念DictionaryK, V是C#中的泛型集合类位于System.Collections.Generic命名空间。它同样基于哈希表实现以泛型参数K,V作为键值对类型避免了Hashtable的装箱拆箱操作性能更优类型更安全。2.2 Dictionary特征与底层原理键值结构键必须唯一每个键唯一关联一个值值可重复。存储结构有两个核心数组构成----_buckets桶数组存储条目的索引与_entries条目数组实际存储键值对的数据结构。类型安全通过泛型避免装箱拆箱操作。高效查找基于哈希表实现平均O(1)时间复杂度。动态扩容容量不足负载因子超过0.72 时自动扩展取最小质数。哈希冲突使用链地址法(拉链法) 。每个桶通过条目数组中的next字段形成链表链接冲突元素。底层原理图2.3 基础语法与操作1. 创建与初始化支持多种初始化方式预估数据量并指定初始容量可以减少扩容次数提升性能。其中初始容量(Entities数组长度)是3, 且初始化(数组)是在第一次添加元素时。// 基本声明与实例化 Dictionarystring, int dict1 new Dictionarystring, int(); // 集合初始化器 (C# 3.0) Dictionarystring, int dict2 new Dictionarystring, int { { Alice, 30 }, { Bob, 25 } }; // 索引器初始化 (C# 6.0) Dictionarystring, int dict3 new Dictionarystring, int { [Alice] 30, [Bob] 25 }; // 指定初始容量优化性能 Dictionarystring, int dict4 new Dictionarystring, int(100);2. 添加元素Add ( key , value若键存在抛出异常。索引器 [ key ] value : 若键存在覆盖旧值。TryAdd (key , value) : 安全添加不抛异常 需要基于.Net Core。// Add方法 - 键必须不存在 如果存在,抛异常 dict1.Add(张三, 35); // 索引器 - 键不存在则添加存在则更新覆盖 dict1[李四] 41; // 更新值 // TryAdd - 安全添加不抛异常 需要基于.Net Core bool added dict1.TryAdd(王五, 28); if (!added) Console.WriteLine(键已存在);3. 访问元素TryGetValue( key ) 安全访问方式。索引器 [ key ] : 直接索引访问 键必须存在 否则抛异常。// 直接索引访问 - 键必须存在 否则抛异常 int zhangsanAge dict1[张三]; // TryGetValue - 推荐的安全访问方式 if (dict1.TryGetValue(李四, out int lisiAge)) { Console.WriteLine($李四的年龄: {lisiAge}); } else { Console.WriteLine(键不存在); }4. 修改与删除元素Remove key根据键Key从字典中删除元素。索引器 [ key ] value : 索引访问修改。Clear() : 清空字典。// 修改值 dict1[张三] 31; // 删除指定键 bool removed dict1.Remove(李四); // 删除并获取值 需要在.Net Core中使用 if (dict1.Remove(王五, out int wangwuAge)) { Console.WriteLine($已删除Charlie年龄: {wangwuAge}); } // 清空字典 dict1.Clear();5. 遍历字典foreach循环遍历字典中的所有键值对。foreach循环分别遍历键和值。// 遍历键值对 foreach (KeyValuePairstring, int kvp in dict1) { Console.WriteLine(${kvp.Key}: {kvp.Value}); } // 简化写法 foreach (var kvp in dict1) { Console.WriteLine(${kvp.Key}: {kvp.Value}); } // 只遍历键 foreach (string name in dict1.Keys) { Console.WriteLine(name); } // 只遍历值 foreach (int age in dict1.Values) { Console.WriteLine(age); }三. Hashtable与Dictionary的区别特性HashtableDictionaryK,V类型安全非泛型键值均为Object类泛型性能需装箱拆箱效率慢无需装箱拆箱效率快空值支持Key 不能为null取决于TKey/TValue(string类型的键可为null)线程安全是性能低否性能高迭代顺序不保证.NET Core 保留插入顺序推荐使用已过时不推荐推荐现代代码使用四. 使用建议旧项目兼容问题推荐使用Hashtable但建议逐步迁移至泛型集合。新项目开发推荐使用DictionaryK,V性能更优类型更安全。多线程开发推荐使用使用ConcurrentDictionaryTKey, TValue。线程安全问题推荐使用Hashtable。五. 题目分享题目思路定义商品类Product以及商品类的三个属性NamePriceQuantity。在Main主函数中定义商品集合Directionary字典。定义打印商品菜单函数。定义添加商品到购物车函数。定义删除商品函数。定义修改商品数量函数。定义清除购物车函数。定义显示购物车函数。定义结账函数。在Main主函数中使用Switch条件语句通过输入特定参数执行相应功能函数。PS作者为偷懒直接写函数了其实最好还是把这些函数封装成一个购物车类。//商品类 public class Product { //商品名 public string Name { get; set; } //商品价格 public decimal Price { get; set; } //商品数量 public int Quantity { get; set; } public Product(string name, decimal price, int quantity) { Name name; Price price; Quantity quantity; } }internal class Program { static void Main(string[] args) { //商品集合字典 Dictionarystring,Product cart new Dictionarystring,Product(); while (true) { //展示购物菜单 ShowMenu(); string choice Console.ReadLine(); //选择操作 switch (choice) { case 1: AddProduct(cart); break; case 2: RemoveProduct(cart); break; case 3: UpdateQuantity(cart); break; case 4: ClearCart(cart); break; case 5: ShowCart(cart); break; case 6: Checkout(cart); return; default: Console.WriteLine(无效输入请重新选择。); break; } Console.WriteLine(按任意键继续....); Console.ReadKey(); Console.Clear(); } } static void ShowMenu() { Console.WriteLine( 购物车菜单 ); Console.WriteLine(1. 放入商品到购物车); Console.WriteLine(2. 删除商品); Console.WriteLine(3. 修改商品数量); Console.WriteLine(4. 清除购物车); Console.WriteLine(5. 显示购物车); Console.WriteLine(6. 结账); Console.Write(请选择操作: ); } //添加商品 static void AddProduct(Dictionarystring,Product cart) { Console.Write(输入商品名: ); string name Console.ReadLine(); Console.Write(输入价格: ); if (!decimal.TryParse(Console.ReadLine(), out decimal price)) { Console.WriteLine(价格格式错误); return; } Console.Write(输入数量: ); if (!int.TryParse(Console.ReadLine(), out int quantity)) { Console.WriteLine(数量格式错误); return; } //判断购物车是否存在商品 if (cart.ContainsKey(name)) { //购物车商品数量增加 cart[name].Quantity quantity; Console.WriteLine($商品{name}数量{quantity}); } else { //购物车添加商品 cart.Add(name, new Product(name, price, quantity)); Console.WriteLine($商品 {name} 已加入购物车。); } } //删除商品 static void RemoveProduct(Dictionarystring, Product cart) { Console.Write(输入要删除的商品名: ); string name Console.ReadLine(); if (cart.Remove(name)) { Console.WriteLine(删除成功。); } else { Console.WriteLine(商品不存在。); } } //更新商品数量 static void UpdateQuantity(Dictionarystring,Product cart) { Console.Write(输入商品名称: ); string name Console.ReadLine(); if (cart.ContainsKey(name)) { Console.WriteLine($当前数量: {cart[name].Quantity}); Console.Write(输入更新数量: ); if (int.TryParse(Console.ReadLine(), out int newQty)) { cart[name].Quantity newQty; Console.WriteLine(数量更新成功。); } else { Console.WriteLine(输入数量格式错误!); } } else { Console.WriteLine(商品不存在。); } } //清除购物车 static void ClearCart(Dictionarystring,Product cart) { Console.Write(是否清除购物车 (Y/N): ); string confirm Console.ReadLine().ToUpper(); if (confirm Y) { cart.Clear(); Console.WriteLine(购物车已清空。); } else { Console.WriteLine(操作已取消。); } } //显示购物车 static void ShowCart(Dictionarystring,Product cart) { if (cart.Count 0) { Console.WriteLine(购物车为空。); return; } Console.WriteLine(\n--- 购物车清单 ---); foreach (var item in cart.Values) { Console.WriteLine($商品: {item.Name}, 单价: {item.Price}, 数量: {item.Quantity}); } } //结账 static void Checkout(Dictionarystring,Product cart) { int totalCount 0; decimal totalPrice 0; foreach (var item in cart.Values) { totalCount item.Quantity; totalPrice item.Price * item.Quantity; } Console.WriteLine(\n 结账信息 ); Console.WriteLine($商品总个数: {totalCount}); Console.WriteLine($总金额: {totalPrice}); } }六. 面试题分享Dictionary数据存储在Entry数组中, 为什么要还有一个int[] buckets(存储下标)Entry数组中存储的一个一个Entry对象, Entry包含hashCode属性,key,value, 但是还是一个next属性, 这个next属性作用?使用无参构造方法创建DIctionary字典对象, 初始容量(Entities数组长度)是多少?, 初始化(数组)在什么时候?