在数据结构中,集合的最基本的体现方式无外乎两种,一种是内存结构连在一起的数组的结构,一种是内存分散的通过指针连接的链表结构。
形式上,有两种存放方式,一种是排序的,一种是非排序的。排序的重要主要是为了检索快速使用的。如果对于集合中的元素很少,几个到100个,排序和非排序两种方式是没有区别的,全遍历也不会消耗多长时间。可是当集合中的个数特别多的时候,排序这个时候就相当的重要。比如如果对于排过序的数组结构,可以使用二分法进行查找,从遍历的复杂度O(n) 变成O(logN),效率大大提高。对于如果保存在hashmap中的数据结构,如果不考虑每个hash值的深度,那么复杂度是O(1),但是如果考虑每个hash节点的平均深度为x,那么时间复杂度就仅仅是O(1) + O(n)。
刚才说到HashMap结构,它的存储结构是这样的:
图中绿色框表示数组结构,白色框全部是链接块,通过链表的结构连接到数组的结构中,即HashMap的由两部分组成。数组结构,存放hash值,后续的链表存放每个值对应hash值的数据,如果对应同一个hash值那么后面的列表数越多,链表长度越长,由于这里的链表查找需要进行遍历,所以一个hash算法需要保证的是尽量均匀分布在每个hash值上,这样带来的每个链表也是最短的。
好了,非主题内容说了这么多,我们言归正传,开始说说跳表(skiplist)吧。
我们看名字,跳表这个词可能有点是链表结构,是的没错,就是全部的链表结构,而且是有序的链表结构。但是我们知道,即使对于排过序的链表,我们对于查找还是需要进行通过链表的指针进行遍历的,时间复杂度很高依然是O(n),这个显然是不能接受的。是否可以像数组那样,通过二分法进行查找呢,但是由于在内存中的存储的不确定性,不能这做。
但是我们可以使用二分法的思想,在链表结构中选择瞄点,这个瞄点和原始点使用指针连接,查找的时候,先查瞄点,找到合适的瞄点后,通过指针映射到原始数据节点,再往后查找,思想和二分法查找一摸一样。通过描述可能还不能完全理解,可以看下面的图:
我们简单分析一下上面的图,如果这个链表已经创建好了,比如查找字母g,现先在第三层进行查找,因为e<g<h,所以这里我们找到了第三层的e节点,e节点链接到下一层的e节点,再进行向后查找,发现f<g<h,于是定位到f节点并从f节点向下找到第一层的f节点,f节点向后,找到了数据的g节点。整个查找流程结束。
从图可以看出,如何选择“漏斗”算法(即选出瞄点得算法,瞄点即是第一层中的c如何被挑选为第二层中的c,这个c我们这暂且称为瞄点,同样每次向上挑选的店都一样)对于整个结构的优化很重要。其实如果漏斗算法挑选的合适,那么选出的瞄点正好像二分法中的每次中分点,当然这里的算法可以挑选的油很多,也可以根据数据分布进行挑选。
跳表的每一层保证得有第一个元素和最后一个元素。另外层数也不能过多。(其实在漏斗算法合理的情况下,层数也会受到限制,不可能无限度增加)。自此我们就讲明白了跳表是怎样一个结构。
但是有人疑问了,既然性能直抵二分法,为什么不直接用数组的方式呢。是二分法的确简单,但是我们需要注意的是,不能光看查找,还得看插入、删除等写入的操作,我们知道,数组的方式插入和删除需要重新分配大的整块资源,而且需要进行内存拷贝,移动数组,性能就比较低下了。链表的方式对于处理元素的插入具有非常明显的优势,大家再对照上面的图看看我们跳表如何实现元素的插入。需要注意的是元素的插入的时候,需要考虑新元素是否需要生成瞄点,这就根据之前的“漏斗”算法看看了。
好了,这里的理解到此结束,如果还有什么问题请留言。
本文章属于个人原创,转载请注明出处。