数据结构笔记

来源:互联网 发布:linux snmptrap接收 编辑:程序博客网 时间:2024/05/22 05:10

今天发现一篇一年前的笔记,放上来备录(/ω·\*)


1. 这种数据结构特点是什么,适合用来做什么,在什么样的场景下适合用这种数据结构

2. 这种数据结构的增删改查,时间复杂度是怎么样的

3. 这种数据结构不适合做什么,为啥不适合


【数组排序】

冒泡排序(bubble)

1、比较相邻的元素,如果第一个比第二个大,就交换他们两个

2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,在这一点,最后的元素应该会是最大的数

3、针对所有的元素重复以上的步骤,除了最后一个

4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较


分析:最好时间复杂度O(n)

最坏时间复杂度O(n^2) 

平均复杂度O(n^2)


选择排序(select)

1、在待排序记录r[1]~r[n]中选出最小的记录,将它与r[1]交换

2、在待排序记录r[2]~r[n]中选出最小的记录,将它与r[2]交换

3、以此类推,第i趟在待排序记录r[i]~r[n]中选出最小的记录,将它与r[i]交换,使有序序列不断增长直到全部排序完毕


分析:交换操作介于0和(n-1)次之间,比较操作为n(n-1)/2之间,赋值操作介于0和3(n-1)次之间

最好情况是,已经有序,交换0次,最坏情况是,逆序,交换n-1次,交换次数比冒泡排序较少,由于交换所需cpu比比较所需的cpu时间多,n值较小时,选择比冒泡快。

原地操作,当方度(space complexity)要求较高,可以采用,实际适用场景罕见


插入排序(insert)

1、从第一个元素开始,该元素可以认为已经被排序

2、取出下一个元素,在已经排序的元素序列中从后向前扫描

3、如果该元素(已排序)大于新元素,将该元素移到下一位置

4、重复步骤3 直到找到已排序的元素小于或等于新元素的位置

5、将新元素插入该位置后

6、重复步骤2~5


分析:时间复杂度:T(n)=O(n^2)

适用于少量数据的排序


归并排序(merge)

 归并排序是一种分治法,它反复将两个已经排序的序列合并成一个序列(平均时间复杂度O(nlog 的n次方),最好时间复杂度O(n))

1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

2、设定两个指针,最初位置分别为两个已经排序序列的起始位置

3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

4、重复步骤直到某一指针达到序列尾

5、将另一序列剩下的所有元素直接复制到合并序列尾


分析:

最佳情况 T(n)=O(n)

最差情况 T(n)=O(nlogn)

平均情况 T(n)=O(nlogn)

分析:时间复杂度: T(n)=O(nlogn)   

基于比较模型的,如果输入的数据基本有序,不适用



快速排序(quick)

1、从数列中挑出一个元素,称为‘基准’(pivot)

2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准大的摆放在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作

3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列值排序


分析:时间复杂度: T(n)=O(nlogn)  


R-QUICK快递排序

与上面quick排序不同在于随机选择基准数


计数排序(count)

1、找出待排序的数组中最大和最小的元素

2、统计数组中每个值为i 的元素出现的次数,存入数组c的第i项

3、对所有的计数累加(从c中的第一个元素开始,每一项和前一项相加)

4、反向填充目标数组:将每个元素i 放在新数组的第c[i] 项,每放一个元素就将c[i] 减去1


分析:当输入的元素是n个0到k之间的整数时,它的运行时间是O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。


基数排序(radix)

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零,然后,从最低位开始,依次进行一次排序,这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列


分析:基数排序的时间复杂度是O(k·n) 其中n是排序元素个数,k是数字位数。k决定了进行多少轮处理,而n是每轮处理的操作数目    



【链表和数组对比】

链表:

1、不能执行随机索引查找,只能顺序查找

2、查找一个元素的时间复杂度为线性级O(n)

3、在已知待操作的节点时,插入和删除操作的时间复杂度为常数级O(1)

4、由于每个节点都需要存储下一个节点的索引,所以更加耗费空间

数组:

1、能够执行随机查找,查找耗费时间复杂度为O(1)

2、插入和删除操作的时间复杂度为线性级O(n)

3、需要开辟一整块连续的内存空间


综上:链表不能进行随机索引查找,查找速度慢,而插入和删除的速度快,适用于不需要随机查找,而是频繁插入和删除的数据集。另外,由于链表的每个节点需要耗费额外空间,当数据集很大时,空间问题将会是一个可能的隐患


【链表】


链表(linked list)

链表是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(pointer),由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快很多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表响应的时间复杂度分别是O(logn)和O(1)


栈(stack)

栈在计算机中限定仅在表尾进行插入或删除操作的线性表,栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据,栈是只能在某一段插入和删除的特殊线性表。


队列(queue)

队列,是先进先出(FIFO,First-In-First-Out)的线性表。只允许在后端进行插入操作,在前端进行删除操作。队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端添加


双向链表(doubly link list)

双向链表也叫双链表,是链表的的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱,所以,从双向链表中的任意一个结点开始,都可以很方便的访问它的前驱结点和后继结点,一般我们都构造双向循环链表


双端队列(deque)

双端队列是一种具有队列和栈性质的数据结构,双端队列中的元素可以从两端弹出,插入和删除操作限定在队列的两边进行



【哈希表(hash table)】

散列表(也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构,也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(hash)表。函数f(key)为哈希函数


哈希表也有一些缺点它是基与数组的,数组创建后难于扩展某些哈希表被基本填满时,性能下降得非常严重,所以程序虽必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。

而且,也没有一种简便的方法可以以任何一种顺序〔例如从小到大)遍历表中的数据项。如果需要这种能力,就只能选择其他数据结构。

然而如果不需要有序遍历数据,井且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。


哈希表是一种动态集合数据结构,在一些合理的假设下,在哈希表中查找一个元素的期望时间是O(1)

linear pribing 线性探查

quadratic probing 二次探查

rehashing 二度哈希/double hashing 双重哈希


处理哈希冲突(hash collisions)的方式有两种,即冲突避免机制(collision avoidance)和冲突解决机制(collision resolution)


避免哈希冲突的一个方法就是选择合适的哈希函数,哈希函数中的冲突发生的几率与数据的分步有关。

在处理冲突时,有很多策略可以实施,这些策略称为冲突解决机制,其中一种方法就是将要插入的元素放到另外一个块空间中,因为相同的哈希位置已经被占用


哈希冲虚解决策略 开放寻址法(open addressing)

即将所有的元素都存放在哈希表被的数组中,不使用额外的数据结构


线性探查(linear probing)

1、当插入新的元素时,使用哈希函数在哈希表中定位元素位置

2、检查哈希表中该位置是否已经存在元素,如果该位置内容为空,则插入并返回,否则转向步骤3

3、如果该位置为i,则检查i+1是否为空,如果已被占用,则检查i+2 依次类推,直到找到一个内容为空的位置

线性探查方式虽然简单,但并不是解决冲突的最好的策略,因为它会导致同类哈希的聚集(primary clustering)这导致搜索哈希表时,冲突依然存在。


二次探查(quadratic probing)

即每次检查位置空间的步长为平方倍数,也就是说,如果位置s被占用,则首先检查s+1²处,然后检查s-1²,s+2²,s-2²,s+3²依次类推,而不是像线性探查那样以s+!,s+2的方式增长,尽管如此,二次探查同样也会导致同类哈希聚集问题。


二度哈希(rehashing)/双重哈希(double hashing)

有一个包含一组哈希函数H1...Hn的集合,当需要从哈希表中添加或获取元素时,首先使用哈希函数H1,如果导致冲突,则尝试使用Hn。所有的哈希函数都与H1十分相似,不同的是它们选用的乘法因子(multiplicative factor)


当使用二度哈希时,重要的是在执行了hashsize次探查后,哈希表中的每一个位置都有且只有一次被访问到,也就是说,对于给定的key,对哈希表中的同一位置不会同时用用Hi和Hj。


二度哈希使用了Θ(m²)种探查序列,而线性探查和二次探查使用了Θ(m)种探查序列,故二度哈希提供了更好的冲突避免的策略。


哈希表中添加新元素时,需要检查以保证元素与空间大小的比例不会超过最大比例,如果超过了,哈希表空间将被扩充,步骤如下

1、哈希表的位置空间几乎被翻倍,准确的说,位置空间值从当前的素数值增加到下一个最大的素数值

2、因为二度哈希时,哈希表中的所有元素值将依赖于哈希表的位置空间值,所以表中所有的值也需要重新二度哈希

由此看出,对哈希表的扩充将是以性能损耗为代价,因此,我们应该预先估计哈希表中最有可能容纳的元素数量,在初始化哈希表时给予合适的值进行构造,以避免不必要的扩充。


哈希冲突解决策略 链接技术(chaining)

链接技术是一种冲突解决策略,在链接法中,把哈希到同一个槽中的所有元素都放到一个链表中


使用探查技术时,如果发生冲突,则将尝试列表中的下一个位置,如果使用二度哈希,则将导致所有的哈希被重新计算,而链接技术将采用额外的数据结构来处理冲突,其将哈希表中每个位置都映射到一个链表,当冲突发生时,冲突的元素将被添加到桶(bucket)列表中,而每个桶都包含了一个链表以存储相同哈希的元素


使用链接技术添加元素的操作涉及到哈希计算和链表操作,但其仍为常量,渐进时间为O(1),而进行查询和删除操作时,其平均时间取决于元素的数量和桶的数量,具体的说就是运行时间为O(n/m),这里n为元素的总数量,m是桶的数量,但通常对哈希表的实现几乎总是使n=O(m),也就是说,元素的总数绝不会超过桶的总数,所以O(n/m)也变成了常量O(1)


哈希函数的设计

一个好的哈希函数应满足假设:每个关键字都等可能地被哈希到m个槽位的任何一个之中,并且与其他的关键字已被哈希到哪一个槽位中无关,不幸的是,通常情况下不太可能检查这一条件是否成立,因为人们很少能知道关键字所符合的概率分布,而各关键字可能并不是完全互相独立的,在实践中,常常运用启发式技术来构造好的哈希函数,比如在设计中,可以利用有关关键字分布的限制性信息等


除法哈希法(the division methed)和乘法哈希法(the multiplication method)属于启发式的方法,而全域哈希法(universal hashing)则采用了随机化技术来获取良好的性能


除法哈希法 

一个好的哈希做法是以独立于数据中可能存在的任何模式的方式导出哈希值,例如,除法哈希法用一个特定的质数来除所给的关键字,所得的余数即为该关键字的哈希值

hash(key) = key med m


乘法哈希值

hash(key) = floor(m * (A * key mod 1))

其中floor表示对表达式进行下取整,常数A取值范围为(0<A<1),m表示哈希表的大小,mod为取余操作,[A * key mod 1]表示将key乘上某个在0~1之间的数并取乘积的小数部分,该表达式等价于[A * key - floor(A * key)]

乘法哈希法的一个优点是对m的选择没有什么特别的要求,一般选择它为2的某个幂次,这是因为我们可以在大多数计算机上更方便的实现该哈希函数

虽然这个方法对任何的A值都适用,但对某些值效果更好,最佳的选择与待哈希的数据的特征有关,don knuth 认为A≈(√5-2)/2 = 0.618033988……比较好,可称为黄金分割点


全域哈希表

在向哈希表中插入元素时,如果所有的元素全部被哈希到同一个桶中,此时数据的存储实际上就是一个链表,那么平均的查找时间为Θ(n),而实际上,任何一个特定的哈希函数都有可能出现这种最坏情况,唯一有效的改进方法就是随机地选择哈希函数,使之独立于要存储的元素,这种方法称作全域哈希