逃不掉的数据结构,知道一点就往这篇文章里面加一点,争取有一天干死面试官。
数据结构无处不在,放渲染专栏里面应该也没毛病吧。
如有理解有误的地方,还望指出
array
array是数组
- 需要一开始就指定长度,长度不会自己涨
- 值类型需要一开始就定义好,比如int[]
- 内存存储的排序,所以可以通过下标直接拿到值,查找时间复杂度O(1)
- 增删数据很麻烦,可以想象成排队,中间来个人,后面的人都得往后面挪一位,删除也是
arrayList
arrayList是数组
- 除了上一个的优点,arrayList还可以动态控制长度的数据,算是弥补了上一个的缺点之一
- 增加了一个新的特性,那就是值类型变成了object类型。有优点,也有缺点。优点就是我们可以存放任意类型的数据了,缺点就是多了装箱和拆箱的操作。
List<T>泛型List
list还是数组,我原来还以为是链表
- 上面的优点都有,主要解决arrayList的拆箱和装箱问题,直接一开始定死类型,就不用转来转去的了
LinkedList<T>
LinkedList是链表
![1ccbcc103fdd3edc189a95a6ca59397f.png](https://i-blog.csdnimg.cn/blog_migrate/68054508b1b2ff8b5b69775f254b81de.jpeg)
- 内存存储的排序上可能是不连续的,主要排列方式主要靠上一个元素来指定下一个元素,所以我们不能像数组那样直接通过下标去拿值,正常的查找就得遍历整个数据,查找复制度为O(n)
- 增删很简单,因为排序不需要一定是连续呢,所以增删很方便,假如我们想往AB直接加个C,那么只需要把A指向C,在把C指向B即可完成,删除同理
Queue<T>
Queue<T>是队列
- 先进先出,可以想象这是一个地铁通道,谁先走进去,出来的时候就是谁先走出来。一般用于处理一堆容易积累数据,而且谁先出现就先解决谁的情况,比如网络请求下载资源
- 初始容量为32, 增长因子为2.0。也就是说,如果超过32的长度,下一个长度就是64了,在继续就是128了,到后面岂不是吓死人
Stack<T>
Stack<T>是栈
- 先进后出,可以想象我们点击网页,比如从A网页跳转到B,B跳转到C,那后退的时候就是从C->B->A,用的正是这个结构
- 默认容量为10
哈希表
- HashTable不支持泛型,Hashtable中key-value键值对均为object类型,所以在存储或检索值类型时通常发生装箱和拆箱的操作,也就是它是类型不安全的。
- 哈希表是基于数组的,数组创建后难于扩展,需要提前定义好大小,一般来说其大小不能小于数据长度,所以能看出,这是一个空间换取时间的数据结构
- 我们定义一个很大的有序数组,想要得到位于该数组第n个位置的值,它的算法复杂度为O(1)。哈希表利用哈希函数将需要存储的内容的关键值转换为这个有序数组中的某个值(比如是位置信息),在被存储内容和有序数组之间建立了映射关系。这样,下次我们对这个值进行查找时只要使用同一个哈希函数对关键值进行转换,找到这个数组值就可以了。
- 但这样会有个问题,那就是key可能会重复,这就是哈希冲突,解决哈希冲突的方法很多
(1)开放地址法
简单来说就是计算出的哈希地址冲突了,就往附近找,找到就存入,发现还冲突就继续找。这种方法有一个通用的再散列函数形式:Hi=(H(key)+di)% m i=1,2,…,n
。其中di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。主要有以下三种
- 1:线性探测再散列 di=1,2,3,…,m-1。简单来说就是往下一个单元找,比如限制地址是1,你就往2找,2没找到,就往3找,一直+1这样找下去
- 2:二次探测再散列 di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 ) 。就是左右横跳,左边找一下没找到,就跑右边找一下
- 3伪随机探测再散列 di=伪随机数序列 。就给定一个随机函数,拿随机出来的值找
虽然开放地址法简单好用,但是因为它一开始的存放就是提前设置好的值,如果我们进行了增删,那原来的计算相当于无效了,我们得重新建立哈希表,就多了一些操作影响性能
(2)链地址法
提到增删,自然会想到链表,没错,这个方法就是用到了链表的特性,使得增删不会影响到原有的计算,变得可控
链地址法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
例如,已知一组关键字(32,40,36,53,16,46,71,27,42,24,49,64),哈希表长度为13,若哈希函数为:H(key)= key % 13,则用链地址法处理冲突的结果如下图所示:
![4de232bc9468eb1ff4b1787b3174651d.png](https://i-blog.csdnimg.cn/blog_migrate/6b45c9613729b1071e590cd437047b07.jpeg)
(3)再哈希法
这种方法是同时构造多个不同的哈希函数:Hi=RH1(key) i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
(4)建立一个公共溢出区
假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。
Dictionary<TKey,TValue>
Dictionary<TKey,TValue> 在哈希表基础上经行了优化
- 字典支持泛型,TKey,TValue都是指定好类型的,所以它是类型安全的,
- 采用了链接技术来处理哈希冲突。主要方法是通过采用额外的数据结构桶(bucket)。Dictionary<K,T> 中的每个位置(slot)都映射到了一个链表,当冲突发生时,冲突的元素将被添加到桶(bucket)列表中(像不像解决哈希冲突的第四个方法的具体实现)。
- 用其键检索值的速度非常快,时间复杂度为O(n/m),这里n为元素的总数量,m是桶的数量,但Dictionary几乎总是被实现为n=O(m),也就是说,元素的总数绝不会超过桶的总数,所以O(n/m)讲接近于O(1)。其检索的速度取决于为
TKey
指定的类型的哈希算法的质量。
![b4127e292141486cdecd1aeb1bc21dfc.png](https://i-blog.csdnimg.cn/blog_migrate/d9467d1c5ae589000398863ce4d31e3e.jpeg)
我们会发现字典会采用额外的数据结构来处理哈希冲突,这就是刚才提到的数组之一buckets桶了,buckets的长度就是字典的真实长度,因为buckets就是字典每个位置的映射,然后buckets中的每个元素都是一个链表,用来存储相同哈希的元素,然后再分配存储空间。
因此,我们面临的情况就是,即便我们新建了一个空的字典,那么伴随而来的是2个长度为3的数组。所以字典的运用场景应该那种大量数据的,如果数据量小,时间复杂度和其他数据结构差不多,却反而增加空间复杂度,就得不偿失了