几种常见的集合类型
SortedList :有序集合
SortedList是和Array的混合。当使用Item索引器属性按照元素的键访问元素时,其行为类似于hashtable,当使用索引访问元素时,其行为类似于Array.Sortedlist在内部维护了两个数组以将数组存储在列表中,一个数组用于键,另一个用于相关联的值,每个元素都可以作为一个DictionaryEntry对象进行访问,因此SortedList一共有三种访问方式。SortedList列表的容量会根据其所拥有的元素个数进行调整,随着向SortedList中添加元素,容量通过重新分配,按需自动增加。
索引顺序基于排序顺序,当添加元素时,元素按正确的排序顺序插入列表内,其它元素的索引也可以进行相应的调整,因此,列表每增加一个元素和减少一个元素,列表内部都会进行一次排序,每个元素的索引也都在动态的变化着。下面通过一个例子来说明SortedList的三种访问方式

class Car
{
private string _date;
private string _name;
public Car(string date,string name)
{
this._date = date;
this._name = name;
}
public string Date
{
set { _date = value; }
get { return _date; }
}
public string Name
{
set { _name = value; }
get { return _name; }
}
}
class Test
{
static void Main()
{
SortedList sortedlist = new SortedList();
sortedlist.Add("1991",new Car("1991","baoma"));
sortedlist.Add("1992",new Car("1992","xiali"));
sortedlist.Add("1993",new Car("1993", "fengtian"));
//遍历sortedist对象
foreach (string keyValue in sortedlist.Keys) //方法一:通过键值遍历
{
Console.WriteLine(((Car)sortedlist[keyValue]).Name);
}
foreach (Car carObj in sortedlist.Values) //通过values遍历
{
Console.WriteLine(carObj.Name);
}
foreach (DictionaryEntry objDE in sortedlist) //通过DictionaryEntry对象访问
{
Console.WriteLine(((Car)objDE.Value).Name);
}
Console.ReadLine();
}
}
SortedList 集合具有高和有序的特点,由于要进行排序,所以在SortedList上操作比在hashtable上操作要慢,但是SortedList灵活的访问方式让它也普遍受到应用。
Queue:队列
在System.Collections命名空间里Queue也是性能比较出众的一种集合,Queue的特点是:队列先进先出,enqueue()方法入队列,dequeue()方法出队列,使用队列与使用Stack相似,但他们之间还是有一定的区别,下面区里说明其先进先出的特点:

class Test
{
static void Main()
{
Queue quueu = new Queue();
quueu.Enqueue("guo");
quueu.Enqueue("wen");
quueu.Enqueue("hui");
foreach (object obj in quueu)
{
Console.WriteLine(obj.ToString());
}
Console.ReadLine();
}
}
输出结果:guo
wen
hui
我们来分析一下上面的代码,我们先创建了一个Queue对象集合queue,然后在这个集合中依次添加了"guo","wen","hui",这是入队列的顺序,然后我们遍历这个集合里的对象,输出顺序也为
"guo","wen","hui",从而体现了其先进先出的特点。下面是图解分析:
Stack:栈
栈在计算机科学中是限定仅在表尾进行插入和删除操作的线性表,栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈底,需要数据的时候从栈顶弹出数据。
插入一般称为进栈(PUSH),删除则称为退栈(POP),下面我们看一下下面的代码:

class Test
{
static void Main()
{
Stack stack = new Stack();
stack.Push("guo");
stack.Push("wen");
stack.Push("hui");
foreach (object obj in stack)
{
Console.WriteLine(obj.ToString());
}
Console.ReadLine();
}
}
输出结果:hui
wen
guo
下面我们看一下图解分析:
Quenu和Stack其实一种特殊的ArrayList,提供大量不同类型的数据对象的存储,只不过访问这些元素的顺序受到了限制。.Net Framework基类库提供了ArrayList数据结构,它可以存储不同类型的数据,并且不需要显式地指定长度。Queue和Stack本质上是存储object类型的数据,因此在存储和访问非过程中一般会发生装箱和拆箱。Stack和Queue均不提供索引访问,原因很简单,如果可以通过索引访问,那么就破坏了Stack和Queue特有的访问机制。
Queue并不象ArrayList那样可以随机访问,这一点非常重要。也就是说,在没有使前两个元素出列之前,我们不能直接访问第三个元素。(当然,Queue类提供了Contains()方法,它可以使你判断特定的值是否存在队列中。)如果你想随机的访问数据,那么你就不能使用Queue这种数据结构,而只能用ArrayList。Queue最适合这种情况,就是你只需要处理按照接收时的准确顺序存放的元素项。
注:在System.Collection命名空间内,非泛型集合所存储的对象的均将被装箱成Object类型。(如:SortList , ArrayList , Queue , Stack , Hashtable等)
System.Collections.Hashtable 类 (以下类容为转载)
哈希算法概念:
计算理论中,没有Hash函数的说法,只有单向函数的说法。所谓的单向函数,是一个复杂的定义,大家可以去看计算理论或者密码学方面的数据。用“人 类”的语言描述单向函数就是:如果某个函数在给定输入的时候,很容易计算出其结果来;而当给定结果的时候,很难计算出输入来,这就是单项函数。各种加密函 数都可以被认为是单向函数的逼近。Hash函数(或者成为散列函数)也可以看成是单向函数的一个逼近。即它接近于满足单向函数的定义。
Hash函数还有另外的含义。实际中的Hash函数是指把一个大范围映射到一个小范围。把大范围映射到一个小范围的目的往往是为了节省空间,使得数据容易保存。除此以外,Hash函数往往应用于查找上。所以,在考虑使用Hash函数之前,需要明白它的几个限制:
1. Hash的主要原理就是把大范围映射到小范围;所以,你输入的实际值的个数必须和小范围相当或者比它更小。不然冲突就会很多。
2. 由于Hash逼近单向函数;所以,你可以用它来对数据进行加密。
3. 不同的应用对Hash函数有着不同的要求;比如,用于加密的Hash函数主要考虑它和单项函数的差距,而用于查找的Hash函数主要考虑它映射到小范围的冲突率。
在C#语言中,哈希函数的概念大致可理解为:任意类型的对象均可以通过一定的哈希算法得到其唯一对应的整数数值。(在C#中,GetHashCode()就是来完成这一工作的),在一个集合中,如果某一对象通过哈希算法所得到的整数值与已有对象的哈希值冲突,更换哈希算法,如果还是得到的值仍然冲突,继续更换哈希算法,直到得到不冲突的值。
.Net Framework 基类库包括了Hashtable类的实现。当我们要添加元素到哈希表中时,我们不仅要提供元素(item),还要为该元素提供关键字(key)。Key和item可以是任意类型。在员工例子中,key为员工的社保号,item则通过Add()方法被添加到哈希表中。
要获得哈希表中的元素(item),你可以通过key作为索引访问,就象在数组中用序数作为索引那样。下面的C#小程序演示了这一概念。它以字符串值作为key添加了一些元素到哈希表中。并通过key访问特定的元素。
using System;
using System.Collections;
public class HashtableDemo
{
private static Hashtable ages = new Hashtable();
public static void Main()
{
// Add some values to the Hashtable, indexed by a string key
ages.Add("Scott", 25); //Add方法内部将执行复杂的哈希算法,以得到“Scott“所对应的序列值。
ages.Add("Sam", 6);
ages.Add("Jisun", 25);
// Access a particular key
if (ages.ContainsKey("Scott"))
{
int scottsAge = (int) ages["Scott"];
Console.WriteLine("Scott is " + scottsAge.ToString());
}
else
Console.WriteLine("Scott is not in the hash table...");
}
}
程序中的ContainsKey()方法,是根据特定的key判断是否存在符合条件的元素,返回布尔值。Hashtable类中包含keys属性(property),返回哈希表中使用的所有关键字的集合。这个属性可以通过遍历访问,如下:
foreach(string key in ages.Keys)
Console.WriteLine("Value at ages[\"" + key + "\"] = " + ages[key].ToString());
要认识到插入元素的顺序和关键字集合中key的顺序并不一定相同。关键字集合是以存储的关键字对应的元素为基础
哈希函数返回的值是序数,我们自然想明白哈希函数是怎样将string转换为数字的,这种奇妙的转换应该归功于GetHashCode()方法,它定义在System.Object类中。Object类中GetHashCode()默认的实现是返回一个唯一的整数值以保证在object的生命期中不被修改。既然每种类型都是直接或间接从Object派生的,因此所以object都可以访问该方法。自然,字符串或其他类型都能以唯一的数字值来表示。既然每种类型都是直接或间接从Object派生的,因此所有的object都可以访问该方法。自然,字符串或其他类型都能以唯一的数字值来表示。
Hashtable类中的冲突解决方案
当我们在哈希表中添加或获取一个元素时,会发生冲突。插入元素时,必须查找内容为空的位置,而获取元素时,即使不在预期的位置处,也必须找到该元素。在Hashtable类中使用的是一种完全不同的技术,成为二度哈希(rehasing)(有的资料也将其称为双精度哈希double hashing)。
二度哈希的工作原理如下:有一个包含多个哈希函数(H1……Hn)的集合。当我们要从哈希表中添加或获取元素时,首先使用哈希函数H1。如果导致冲突,则尝试使用H2,一直到Hn。各个哈希函数极其相似,不同的是它们选用的乘法因子。通常,哈希函数Hk的定义如下:
Hk(key) = [GetHash(key) + k * (1 + (((GetHash(key) >> 5) + 1) % (hashsize – 1)))] % hashsize
二度哈希较前两种机制较好地避免了冲突。
调用因子(load factors)和扩充哈希表
Hashtable类中包含一个私有成员变量loadFactor,它指定了哈希表中元素个数与表位置总数之间的最大比例。例如:loadFactor等于0.5,则说明哈希表中只有一半的空间存放了元素值,其余一半皆为空。
哈希表的构造函数以重载的方式,允许用户指定loadFactor值,定义范围为0.1到1.0。要注意的是,不管你提供的值是多少,范围都不超过72%。即使你传递的值为1.0,Hashtable类的loadFactor值还是0.72。微软认为loadFactor的最佳值为0.72,因此虽然默认的loadFactor为1.0,但系统内部却自动地将其改变为0.72。所以,建议你使用缺省值1.0(事实上是0.72,有些迷惑,不是吗?)
注:我花了好几天时间去咨询微软的开发人员为什么要使用自动转换?我弄不明白,为什么他们不直接规定值为0.072到0.72之间。最后我从编写Hashtable类的开发团队的到了答案,他们非常将问题的缘由公诸于众。事实上,这个团队经过测试发现如果loadFactor超过了0.72,将会严重的影响哈希表的性能。他们希望开发人员能够更好地使用哈希表,但却可能记不住0.72这个无规律数,相反如果规定1.0为最佳值,开发者会更容易记住。于是,就形成现在的结果,虽然在功能上有少许牺牲,但却使我们能更加方便地使用数据结构,而不用感到头疼。
向Hashtable类添加新元素时,都要进行检查以保证元素与空间大小的比例不会超过最大比例。如果超过了,哈希表空间将被扩充。步骤如下:
1. 哈希表的位置空间近似地成倍增加。准确地说,位置空间值从当前的素数值增加到下一个最大的素数值。(回想一下前面讲到的二度哈希的工作原理,哈希表的位置空间值必须是素数。)
2. 既然二度哈希时,哈希表中的所有元素值将依赖于哈希表的位置空间值,所以表中所有值也需要二度哈希(因为在第一步中位置空间值增加了)。
幸运的是,Hashtable类中的Add()方法隐藏了这些复杂的步骤,你不需要关心它的实现细节。
调用因子(load factor)对冲突的影响决定于哈希表的总体长度和进行挖掘操作的次数。Load factor越大,哈希表越密集,空间就越少,比较于相对稀疏的哈希表,进行挖掘操作的次数就越多。如果不作精确地分析,当冲突发生时挖掘操作的预期次数大约为1/(1-lf),这里lf指的是load factor。
如前所述,微软将哈希表的缺省调用因子设定为0.72。因此对于每次冲突,平均挖掘次数为3.5次。既然该数字与哈希表中实际元素个数无关,因此哈希表的渐进访问时间为O(1),显然远远好于数组的O(n)。
最后,我们要认识到对哈希表的扩充将以性能损耗为代价。因此,你应该预先估计你的哈希表中最后可能会容纳的元素总数,在初始化哈希表时以合适的值进行构造,以避免不必要的扩充。
关于集合类型的几点总结:
1.普通数组(值类型和引用类型)、ArrayList、hashtable类型(SortList)均提供索引访问机制,Stack和Queue不提供索引访问机制。
2.普通数组中的Object[]、ArrayList、hashtable类型(SortList)、Stack和Queue均存储的是对象类型。
3.ArrayList在内存中的存放不一定连续。
posted on 2011-10-25 23:06 guowenhui 阅读(1981) 评论(0) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架