C#集合之字典的用法
作者:Ruby_Lu 发布时间:2022-04-11 23:27:19
字典表示一种复杂的数据结构,这种数据结构允许按照某个键来访问元素。字典也称为映射或散列表。
字典的主要特性是能根据键快速查找值。也可以自由添加和删除元素,这有点像List<T>(https://www.jb51.net/article/244084.htm),但没有在内存中移动后续元素的性能开销。
下图是一个简化表示,键会转换位一个散列。利用散列创建一个数字,它将索引和值关联起来。然后索引包含一个到值的链接。一个索引项可以关联多个值,索引可以存储为一个树型结构。
.NET Framework提供了几个字典类。最主要的类是Dictionary<TKey,TValue>。
1.键的类型
用作字典中的键的类型必须重写Object类的GetHashCode()方法。只要字典类需要确定元素的位置,它就要调用GetHashCode()方法。GetHashCode()方法返回的int有字典用于计算在对应位置放置元素的索引。后面介绍这个算法,现在只需要知道它涉及素数,所以字典的容量是一个素数。
GetHashCode()方法的实现代码必须满足的要求:
*相同的对象应总是返回相同的值
*不同的对象可以返回相同的值
*它应执行的比较快,计算的开销不大
*它不能抛出异常
*它应至少使用一个实例字段
*散列代码值应平均分布在int可以存储的这个数字范围上
*散列代码最好在对象的生存期中不发生变化
字典的性能取决于GetHashCode()方法的实现代码。
散列代码值应平均分布在int可以存储的这个数字范围上的原因:
如果两个键返回的散列代码值会得到相同的索引,字典类就必须寻找最近的可用空闲位置来存储第二个数据项,这需要进行一定的搜索,以便以后检索这一项。显然这会降低性能,如果在排序的时候许多键都有相同的索引这中冲突会更可能出现。根据Microsoft的算法工作方式,当计算出来的散列代码值平均分布在int.MinValue和int.MaxValue之间时,这种风险会降到最低。
除了实现GetHashCode()方法之外,键类型还必须实现IEquatable<T>.Equals()方法,或重写Object.Equals()方法。0因为不同的键对象可能返回相同的散列代码,所以字典使用Equals()方法来比较键。字典检查两个键A和B是否相等,并调用A.Equals(B)方法。这表示必须确保下述条件总是成立:
如果A.Equals(B)返回true,则A.GetHashCode()和B.GetHashCode()总是返回相同的散列代码。
这听起来有点奇怪,但它很重要。如果上述条件不成立,这个字典还能工作,但会出现,把一个对象放在字典中后,就再也检索不到它,或者返回了错误的项。
所以,如果为Equals()方法提供了重写版本,但没提供GetHashCode()方法的重写版本,C#编译器会显示一个警告。
对于System.Object,这个条件为true,因为Equals()方法只是比较引用,GetHashCode()方法实际上返回一个仅基于对象地址的散列代码。这说明,如果散列表基于一个键,而该键没有重写这些方法,这个散列表可以工作,但只有对象完全相同,键才被认为是相等的。也就是说,把一个对象放在字典中时,必须将它与该键的引用关联起来。也不能以后用相同的值实例化另一个键对象。如果没有重写Equals()方法和GetHashCode()方法,在字典中使用类型时就不太方便。
System.String实现了IEquatable接口,并重载了GetHashCode()方法。Equals()方法提供了值的比较,GetHashCode()方法根据字符串的值返回一个散列代码。因此,在字典中把字符串用在键很方便。
数字类型(如Int32)也实现了IEquatable接口,并重载了GetHashCode()方法。但是这些类型返回的散列代码只能映射到值上。如果希望用作键的数字本身没有分布在可能的整数值范围内,把整数用作键就不能满足键值的平均分布规则,于是不能获得最佳的性能。Int32并不适合在字典中使用。
如果需要使用的键类型没有实现IEquatable接口,并根据存储在字典中的键值重载GetHashCode()方法,就可以创建一个实现IEqualityComparer<T>接口的比较器。IEqualityComparer<T>接口定义了GetHashCode()方法和Equals()方法,并将传递的对象作为参数,这样可以提供与对象类型不同的实现方式。
2.演示字典
创建一个员工ID(EmployeeId)结构,用作字典的键。存储在字典中的数据是Employee类型的对象。
该结构的成员是表示员工的一个前缀字符和一个数字。这两个变量都是只读的,只能在构造函数中初始化。字典中的键不应改变,这是必须保证的。
public struct EmployeeId : IEquatable<EmployeeId>
{
private readonly char prefix;
private readonly int number;
public EmployeeId(string id)
{
Contract.Requires<ArgumentNullException>(id != null);
prefix = (id.ToUpper())[0];
int numLength = id.Length - 1;
try
{
number = int.Parse(id.Substring(1, numLength > 6 ? 6 : numLength));
}
catch (FormatException)
{
throw new Exception("Invalid EmployeeId format");
}
}
public override string ToString()
{
return prefix.ToString() + string.Format("{0,6:000000}", number);
}
//由于没有填满整数取值范围,GetHashCode方法将数字向左移动16位,再与原来的数字进行异或操作,
//最后将结果乘以16进制数0x15051505。这样,散列代码在整数取值区域上的分布就很均匀。
public override int GetHashCode()
{
return (number ^ number << 16) * 0x15051505;
}
public bool Equals(EmployeeId other)
{
if (other == null) return false;
return (prefix == other.prefix && number == other.number);
}
//比较两个EmployeeId对象的值
public override bool Equals(object obj)
{
return Equals((EmployeeId)obj);
}
public static bool operator ==(EmployeeId left, EmployeeId right)
{
return left.Equals(right);
}
public static bool operator !=(EmployeeId left, EmployeeId right)
{
return !(left == right);
}
}
public class Employee
{
private string name;
private decimal salary;
private readonly EmployeeId id;
public Employee(EmployeeId id, string name, decimal salary)
{
this.id = id;
this.name = name;
this.salary = salary;
}
public override string ToString()
{
return String.Format("{0}: {1, -20} {2:C}",
id.ToString(), name, salary);
}
}
客户端代码:
static void Main()
{
//构造函数指定了31个元素的容量。容量一般是素数。
//如果指定了一个不是素数的值,Dictionary<TKey,TValue>类会使用指定的整数后面紧接着的一个素数
var employees = new Dictionary<EmployeeId, Employee>(31);
var idTony = new EmployeeId("C3755");
var tony = new Employee(idTony, "Tony Stewart", 379025.00m);
employees.Add(idTony, tony);
Console.WriteLine(tony);
var idCarl = new EmployeeId("F3547");
var carl = new Employee(idCarl, "Carl Edwards", 403466.00m);
employees.Add(idCarl, carl);
Console.WriteLine(carl);
var idKevin = new EmployeeId("C3386");
var kevin = new Employee(idKevin, "Kevin Harwick", 415261.00m);
employees.Add(idKevin, kevin);
Console.WriteLine(kevin);
var idMatt = new EmployeeId("F3323");
var matt = new Employee(idMatt, "Matt Kenseth", 1589390.00m);
employees[idMatt] = matt;
Console.WriteLine(matt);
var idBrad = new EmployeeId("D3234");
var brad = new Employee(idBrad, "Brad Keselowski", 322295.00m);
employees[idBrad] = brad;
Console.WriteLine(brad);
}
3.Lookup类
Dictionary<TKey,TValue>类支持每个键关联一个值。Lookup<TKey,TElement>类把键映射到一个值集上。这个类在程序集System.Core中实现,用System.Linq定义。
Lookup<TKey,TElement>类不能像一般的字典那样创建,必须调用ToLookup()方法,该方法返回一个Lookup<TKey,TElement>对象。ToLookup()方法是一个扩展方法,它可以用于实现了IEnumerable<T>接口的所有类。
ToLookup()方法需要一个Func<TSource,Tkey>,Func<TSource,Tkey>定义了选择器。
static void Main()
{
var racers = new List<Racer>();
racers.Add(new Racer(26, "Jacques", "Villeneuve", "Canada", 11));
racers.Add(new Racer(18, "Alan", "Jones", "Australia", 12));
racers.Add(new Racer(11, "Jackie", "Stewart", "United Kingdom", 27));
racers.Add(new Racer(15, "James", "Hunt", "United Kingdom", 10));
racers.Add(new Racer(5, "Jack", "Brabham", "Australia", 14));
//国家相同的对象关联到一个键
var lookupRacers = racers.ToLookup(r => r.Country);
foreach (Racer r in lookupRacers["Australia"])
{
Console.WriteLine(r);
}
}
输出:
Alan Jones
Jack Brabham
4.有序字典
SortedDictionary<TKey,TValue>类是一个二叉搜索树,其中的元素根据键来排序。该键类型必须实现IComparable<TKey>接口。
如果键的类型不能排序,还可以创建一个实现了IComparer<TKey>接口的比较器,将比较器用作有序字典的构造函数的一个参数。
SortedDictionary<TKey,TValue>和有序列表SortedList<TKey,TValue>(https://www.jb51.net/article/244111.htm)的区别:
*SortedList<TKey,TValue>类使用的内存比SortedDictionary<TKey,TValue>少。
*SortedDictionary<TKey,TValue>元素的插入和删除操作比较快。
*在用已排序好的数据填充集合时,若不需要改变容量,ortedList<TKey,TValue>比较快。
来源:https://www.cnblogs.com/afei-24/p/6835222.html


猜你喜欢
- 本文实例讲述了C#禁止textbox复制、粘贴、剪切及鼠标右键的方法。分享给大家供大家参考。具体如下:class MyTextBox : S
- MyBatis在SQL语句中获取list大小需求:使用MyBatis进行开发时,在一个SQL语句中需要拼接list的大小。大家都知道,当我们
- 本文实例为大家分享了Android Scroller的使用方法,供大家参考,具体内容如下1、scrollTo和ScrollByView类定义
- Java时间格式转换大全import java.text.*;import java.util.Calendar;public class
- 最近在项目过程中遇到了保存数据的需求,对实体类的部分数据进行保存,打算采用反射+自定义特性来实现数据保存,利于扩展1. 采用反射实现能够灵活
- Thread生命周期生命周期概述Java的线程状态描述放在Thread类里面的枚举类State中.总共包含了6中状态(从出生到死亡)。pub
- 由于今天在网上搜了一下c#写的计算器,发现大多都太繁琐了,很多没必要并且不容易理解的东西就专门写了这个博客1.首先新建一个windows窗体
- Control.Invoke 方法 (Delegate) :在拥有此控件的基础窗口句柄的线程上执行指定的委托。Control.BeginIn
- 强调一下阅读系统源码,起码要对进程间通信要了解,对binder机制非常非常清楚,binder就是指南针,要不然你会晕头转向;强行阅读,就容易
- Java HashSetHashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。HashSet 允许有 null 值。
- Java当中的类和对象1. 类和对象的初步认知java 是一门面向对象的语言,所谓面向对象有别于面向过程,面向对象是只需对象之间的交互即可完
- 安卓应用闪退后总会出现一个“抱歉,App已经停止运行”的弹窗,这样的用户体验并不好。很多大厂的App都去除了这个弹窗,因此本文主要介绍如何去
- 本文实例为大家分享了flutter日期时间选择器的具体代码,供大家参考,具体内容如下1 日期选择器 //设置默认显示的日期为当前 DateT
- 字段策略 0:”忽略判断”,1:”非 NULL 判断”),2:”非空判断”问题描述:当字段策略为 0 “忽略判断” 的时候,如果实体和数据库
- 本文实例为大家分享了C#基于Socket实现多人聊天功能的具体代码,供大家参考,具体内容如下服务器服务器负责接受所有客户端发来的消息,和将接
- 支付宝的接入相对比较简单,看看支付宝官网的文档基本都能搞定,但是切记一点让你们的后台也要搞清楚支付宝的流程,重中之重。1、注意事项 开发前一
- 服务器端代码:import java.io.BufferedReader; import java.io.InputStreamR
- 在Java的逻辑运算符中,有这么四类:&&(短路与),&,|,||(短路或)。&&和&都是表
- 1、 namenode启动在本系列文章三中分析了hadoop的启动文件,其中提到了namenode启动的时候调用的类为org.apache.
- 因为课程需要,昨天好多同学在安装Android studio3.6.1后,无法构建,不知道什么原因,我的电脑上使用的是之前3.4版本的,可以