数组
Contents
简介
数组(Array)是由相同类型的元素的集合所组成的数据结构。数组采用一块连续的内存空间来存储数据,利用数组下标索引可以随机存取对应的元素。数组元素的类型除了int、float、double和char等基本类型,还可以是类、枚举和结构体等复杂类型。借助数组可以实现线性表的顺序存储结构(即顺序表)。
在C#中,数组属于引用类型,数组变量本身是一个引用类型变量。**为了更好地学习后续的数据结构和算法,先介绍下C#中的变量类型。**C#变量类型分为值类型和引用类型,值类型包括基本类型(int、float、double、bool和char等类型)、结构体和枚举,引用类型包括数组、字符串、类、对象、接口和委托。
值类型变量直接存储变量的值,并将其存储在堆栈(Stack)中。引用类型变量实际的值则存放在托管堆(Heap)中(通过new关键字创建的实例变量的值都是存放在堆中),引用类型变量本身存储的是变量值在堆中存储的内存地址。但如果值类型嵌套进引用类型,即值类型作为引用类型的成员变量,则此时的值类型将作为引用类型的一部分存放在托管堆中。而引用类型即使嵌套进值类型,依然也是存放在托管堆中。
值类型由C#系统本身负责内存空间的分配和释放,当程序超过变量作用域范围后,就自动释放变量在栈中的内存空间。而引用类型由CLR中GC(Garbage Collector)负责内存空间的分配和释放,当对象不再使用或为null时,GC将会释放其在托管堆中的内存空间。栈的存储空间小,访问速度快;而堆的存储空间大,访问速度慢。
在C#中,所有的变量类型(值类型和引用类型)都间接或直接继承自System.Object类型,System.Object类型本身是引用类型。值类型继承自System.ValueType,而System.ValueType又继承自System.Object类型。System.ValueType重写了System.Object类型的Equals方法,用来比较两个变量的值是否相等。而引用类型几乎直接继承自System.Object类型,其Equals方法用来比较两个对象的引用地址是否相等。
值类型默认值为0,引用类型默认值为null。值类型转换为引用类型称为”装箱”,引用类型转换为值类型称为“拆箱”。由于值类型和引用类型之间的转换涉及到数据复制和类型转换,这些操作比较耗费性能,因此尽量避免频繁的"装箱"和"拆箱"操作。“装箱"和"拆箱"操作如下:
|
|
经过上面的介绍,相信对C#中的值类型和引用类型有了一定的理解。在下边的数组拓展中,我会讲到动态数组和泛型列表的区别和联系。
优点
数组可以根据下标索引随机存取(访问)对应的元素。
缺点
由于数组采用一块连续的内存空间,因此增加或删除数组中的元素会导致内存中其他元素位置跟着移动,这会产生额外的性能消耗。此外,数组声明时必须指定数组长度大小,如果实际用到的数组长度小于声明时指定的长度,会产生内存碎片,造成内存空间的浪费。
如果大于声明时指定的数组长度,会造成内存空间不够用,只能新建一个更大的数组,因为数组不支持动态地增加或减少数组大小。正因为如此,数组中没有直接增加或删除数组元素的函数,需要我们自己去写。
应用场景
当我们需要经常访问数组元素,而不需要频繁改动数组大小(即知道实际所用数组的大小)时,可以使用数组。
基本操作
|
|
拓展
多维数组
多维数组是数组的数组,最常用的多维数组是二维数组。多维数组中的每一个元素都是数组,直到最内层数组为止,最内层数组就是一个普通的一维数组。以二维数组为例,可以把二维数组看做是一个矩阵,系统默认采用行优先存储。每一行就是一个一维数组,多个一维数组组成二维数组。
但要注意C#中的二维数组和C/C++、JAVA不同,C#中二维数组只能访问矩阵中的单个值,无法访问每一行或者每一列对应的数组。C/C++和JAVA中的二维数组在C#里称为交错数组,即可以访问每一行的一维数组。下面是二维数组在C#中的实现:
|
|
动态数组
由于数组(静态数组)不能动态修改大小,因此动态数组应运而生。C#中的动态数组由ArrayList类型表示,位于System.Collections命名空间下,因此使用时必须引入System.Collections命名空间。ArrayList可以动态地增加或删除数组元素,因此使用时不需要指定其数组长度大小。此外,由于ArrayList元素类型是System.Object类型,因此动态数组中的元素可以是任何类型的元素。因为,在C#中,所有变量或对象都是Object类型。
任何事物都有两面性。由于动态数组所有元素类型均采用Object类型,没有明确指定元素具体的类型,这会带来类型安全问题。因为使用动态数组时,很可能造成类型不匹配的问题。此外,如果动态数组实际的元素类型是值类型,那么存储元素时动态数组内部产生“装箱”操作,索引取值时就会产生"拆箱"操作,这样频繁的"装箱"和"拆箱"操会带来额外的性能消耗。下面是动态数组的基本操作:
|
|
泛型列表
为了解决动态数组中频繁的"装箱"和"拆箱"操作,C#提供了泛型列表List来代替动态数组。**为了更好地理解泛型列表List,我们先介绍下什么是泛型。**泛型是指通过数据类型的替代参数来预先编写类或方法,等到类或方法实际使用时才去指定实际的数据类型。
换言之,泛型允许我们延迟绑定类或方法具体的数据类型,等到程序使用时才去指定具体的数据类型。这样可以最大程序的重用代码,保证数据的类型安全以及避免频繁的"装箱"和"拆箱"操作带来的性能开销问题。所以在实际开发中,不建议使用动态数组,而是用泛型列表代替。
泛型List位于System.Collections.Generic命名空间内,因此在使用时必须引用对应的命名空间。泛型List即结合了数组中利用下标索引随机存取元素的优点,又支持像动态数组一样动态地增加和删除数据元素,同时还避免了动态数组因频繁的"装箱"和"拆箱"操作带来的性能开销问题。下面是泛型List的基本操作:
|
|
哈希表
哈希表(Hash Table,又称散列表),是根据关键码值(Key-Value键值对)直接访问元素的数据结构。即通过把一个关键码值映射到表中的一个位置来访问记录(元素),以加快查找速度。这个映射函数称为哈希函数或散列函数,存放记录的数组称为哈希表或散列表,表中的这个位置称为哈希地址。
哈希表中一个元素关键字值为key,则通过哈希函数f(key)处理后可以得到这个元素在表中的地址,从而直接通过地址访问该元素,这样可以加快该元素的访问速度。理想情况下,哈希表访问某个元素的时间复杂度为O(1)。但如果通过不同的关键码值得到了相同的表中元素地址,就会造成地址冲突,这称为哈希冲突。因为我们希望元素和表中地址是一一对应的,这样就不会在查找时对同一表中地址的多个元素进行值比较的情况,从而降低查找效率。
因此,哈希表在查找元素过程中,关键码的比较次数至关重要。产生的哈希冲突越少,关键码的比较次数就比较少,查找效率就高。反之,产生的哈希冲突越多,关键码就需要与同一地址的其他元素多次比较值,这样时间复杂度就会提高,查找效率变低。
哈希表最常见的例子是用学号来检索学生的成绩。对于学生来说,学号是唯一的,所以不存在哈希冲突。但如果以学生出生年月日的后三位为键,就会产生哈希冲突。因为会存在同年同月同日生的学生,这时就比较学生的实际信息了。但我们可以通过哈希函数,在构造哈希表时,将学生的出生日期映射成不同的键,降低冲突的发生率,从而提高查找效率。
因此要想构造一个成功的哈希表,就必须降低或减少哈希冲突的发生率,因为冲突无法避免。而降低哈希冲突发生率的关键在于哈希函数,如果哈希函数选取不好就会经常发生哈希冲突,这会非常影响哈希表的性能。常用的哈希函数有直接寻址法、数字分析法、平方取中法和随机数法等,处理冲突的方法有开放寻址法和再散列法等。
哈希表实际上是一个一维数组,如果考虑到哈希冲突,也可视为二维数组。哈希表通过关键值映射为地址来存储元素,哈希地址很难像数组地址一样均匀地连续分布,必然会出现空缺的地址。如果哈希函数选取不当,哈希地址之间间隔增大,会让哈希表变成一个稀疏数组。因此,存储相同数量的元素,哈希表所需内存空间比数组更大。
但与数组相比,哈希表的效率更高。哈希表增删改查的时间复杂度都是O(1),而数组无法直接增加和删除元素,修改(修改之前要先查找)和查找元素的时间复杂度是O(n)。因为哈希表可以直接通过关键字的映射地址进行直接查找,而数组只能通过索引下标从前往后开始遍历查找,所以哈希表的效率更高。即哈希表是用空间换时间。
在C#中,哈希表是通过System.Collections命名空间下的HashTable类型来实现。但由于HashTable的元素类型为Object类型,使用时会造成频繁的”装箱“和”拆箱“操作,造成额外性能开销。因此C#推出了字典Dictionary<Tkey, TValue>,C#字典位于System.Collections.Generic命名空间下。
字典Dictionary<Tkey, TValue>是HashTable的泛型实现,效率更高。此外,字典是类型安全的,而HashTable会出现类型不匹配的情况。因此,在实际开发中,优先推荐使用字典Dictionary<Tkey, TValue>。C#字典中的键是唯一的,键和值都可以为任意类型,但键不能为空引用类型null。下面是C#字典的基本操作:
|
|