【算法导论】 第十二课 跳跃表

本节课介绍了一种全新的数据结构——跳跃表

跳跃表是一种简单又有趣的动态搜索数据结构,其主要优点在于其易于实现,而且很好的保证了其具有高效的性能,即2*O(lgn)的搜索性能

在此之前我想首先谈谈链表,链表的优点在于其插入和删除只需要常数项的时间(加上查找该元素需要额外的O(n)时间),但是其查找效率只有O(n),这里顺带补充一下链表类的问题,以下先给出两个BAT公司面试时热衷于考的两个链表经典问题:

1.如何快速查找单向链表倒数第m个元素

2.如何快速判断一个单向链表是否存在环

对于链表类问题,其核心思想不外乎两点,1是开双指针(甚至多指针),2是开双链表(甚至多链表),其实以上两个问题开双指针便能巧妙地解决,第一个问题,先开一个指针走m步,然后再开一个指针同步走,当前一个指针走到链表末端时,后一个指针就正好指向倒数第m个元素了,第二个问题,开一个快指针和一个慢指针,快指针每次移动两步,慢指针每次移动一步,如果存在环,那么快指针一定会追到慢指针,可以想象两个人在操场赛跑,快的人跑了很久之后会超慢的人一圈。

接下来我们继续谈跳跃表,其实跳跃表用到的就是第二个思路,开双链表甚至多个链表

首先考虑建立两个链表L1和L2,L1为快表,即只保存部分元素,L2为慢表保存全部元素,注意,以下提到的链表均为排好序的链表

当我们要搜索某一元素时,我们先走快表,因为快表只保留了部分元素,所以是跳跃前进的,直到快表走过该元素,我们再退回快表前一个结点换到慢表继续走,这样效率显然比在慢表上进行线性查找要好一些,这里的快慢表就像美国地铁的快慢线地铁一样,快线的地铁只在几个站停顿,而慢线会在所有站停顿,乘客可以先乘快线到一个最接近目的地的前一个站,再转乘慢线到达该地

那么问题来了?

如何建表L1和 L2呢?L2无疑是一条包含所有结点的单向链表,那么L1应该设置多少个结点最为合理呢,直观上感受L1应该是均匀分布最好,那么是以怎样的密度分布最合适呢?

我们不难得出查找时间上界为|L1|+|L2|/|L1|+换乘的常数,这里|L1|表示L1的长度(最坏情况就是L1走到末端后,走回一个节点然后进入L2,因为L2可以看做被L1分成了L1个段,所以每段长度为|L2|/|L1|,所以为|L1|+|L2|/|L1|+换乘的常数),因为L2长度为n(包括整个链表),换乘即链表L1向下走到L2的时间,为常数,所以我们的目标是要使得|L1|+n/|L1|最小,即|L1|为sqrt(n)时最优(可以求导得出或者通过其他数学方法,证明略),此时时间消耗为2*sqrt(n),即每隔sqrt(n)设立一个快表的结点,共sqrt(n)个快表结点

什么?sqrt(n)还不过瘾?

那么我们还能做怎样的优化呢?答案是加更多的链表,我们看看三条链表应是多少,直觉告诉我们是3*n的1/3次方

其实,可以证明k条链表的时候为k*n的1/k次方

因为n是常数,那么k多大比较合适呢?lgn! 让我们看看k取lgn的时候为多少,即lgn*n^(1/lgn)为多少,即求lgn*n^logn(2),还记得我们计算递归时间复杂度时的换底公式吗?这里n^logn(2)即2^logn(n)即2^1,即2,所以整个时间复杂度为2*lgn,这是一个非常好的性能。

这种情况下的跳跃表称为理想跳跃表,每一层数量减少一半,总共lgn层链表,从最上级链表开始搜,搜不到就向下,最多下logn层,每层最多搜2个元素,所以搜索复杂度为O(2lgn)

那么问题又来了,如何动态维护这样一个跳跃表呢?

先看删除功能,删除功能只要从上级链表搜到之后,就可以直接删除,并向下将所有链表的该结点都删除,这个比较简单,那么插入呢?

插入(x) 先search(x)在底表的位置然后插入该元素,是否结束了呢?不,因为在某一段连续插入若干个结点后,这一段会变得非常长,整个跳跃表的平衡结构无疑会被打破,那么如何维护理想线段表的结构呢?

1.保持每段之间的理想距离,如果距离过大,就从中间分割,然后将中点上升一层结点

这个方法从直观上看非常巧妙,但是实行起来却有一定难度,因为你必须实时记录每一段的长度

2.采用我们最喜欢的随机化算法,抛硬币 如果正面,就把这个结点提升一个level(即把该结点也加入上一级的链表中),再抛硬币(看是否持续提升level),因为两个相邻链表的长度之比为1:2,而硬币出正面的概率也是50%,事实证明这样做是可行的,这里值得一提的是,老师在这节课发了两个硬币给同学,一个利用抛硬币产生随机数,一个利用抛硬币决定当前插入结点是否需要提升level,在课堂上直接做起了实验,整个课程氛围也很好,也让同学们都对该算法有了直观的理解,这一种教学方式很值得借鉴

注意,这里需要考虑一个特殊情况,就是当我们插入的元素为最小的元素时,如果它没有提升一个level,那么上级链表的开头就不是第一个元素,这样也会打乱整个跳跃表的理想结构,因此我们需要打个补丁:即把一个负无穷值插到所有链表头,这样就算插入了一个最小的元素也能保证每个表是以负无穷开始,即每个链表都可以从最左边开始。

在课堂上,通过实验表明算法2似乎在平均情况下可以得到一个很好的跳跃表,其实不仅仅是平均情况可以得到一个好的跳跃表,在绝大多数情况都可以得到一个好的跳跃表.

可以证明,得到一个好的跳跃表的概率P>=1-O(1/n^a)  这里a是一个介于0到1的参数,与n有关,在课堂的最后,老师花了尽20分钟的时间来证明,具体证明方式我们这里略过(其实是我根本没看懂其证明过程 逃~~)

时间: 2024-10-05 06:20:51

【算法导论】 第十二课 跳跃表的相关文章

算法导论第十二章__二叉搜索数

package I第12章__二叉搜索树; //普通二叉树 public class BinaryTree<T> { // -----------------------数据结构--------------------------------- private int height = 0; private Node<T> rootNode; class Node<T> { T t; int key; Node left; Node right; public Node

算法导论第十二章 二叉搜索树

一.二叉搜索树概览 二叉搜索树(又名二叉查找树.二叉排序树)是一种可提供良好搜寻效率的树形结构,支持动态集合操作,所谓动态集合操作,就是Search.Maximum.Minimum.Insert.Delete等操作,二叉搜索树可以保证这些操作在对数时间内完成.当然,在最坏情况下,即所有节点形成一种链式树结构,则需要O(n)时间.这就说明,针对这些动态集合操作,二叉搜索树还有改进的空间,即确保最坏情况下所有操作在对数时间内完成.这样的改进结构有AVL(Adelson-Velskii-Landis)

算法导论 第二十二章:图的搜索

图有两种标准的表示方法,即邻接矩阵和邻接表(通常邻接矩阵用于稠密图,邻接表用于稀疏图).如下: 对于图的搜索有两种方法:深度优先搜索 & 广度优先搜索. 广度优先搜索(Breadth-first search) 广度优先搜索是将已发现和未发现顶点之间的边界沿其广度方向向外扩展.亦即算法首先会发现和s距离为k的所有点,然后才会发现和s距离为k+1的其他顶点. 伪代码: EG: 运行时间:O(V+E). 深度优先遍历(Depth-first search) 在深度优先搜索中,对于最新发现的顶点,如果

Python学习第二十二课——Mysql 表记录的一些基本操作 (增删改)

记录基本操作: 增:(insert into) 基本语法: insert into 表名(字段) values(对应字段的值): 例子1: insert into employee(id,name,age,salary) values(1,"憨憨",24,5000); 指定添加insert into employee values(2,"憨憨",0,24,"技术部",5000); 对应添加 insert into employee set nam

算法导论 第二十二章:拓扑排序

拓扑排序(针对有向无回路图DAG)是深度优先搜索的一个应用,其结果图中所有顶点的一个线性排列. 伪代码如下: EG: 拓扑排序完整代码如下: #include<iostream> #include<iomanip> #include<string> #include<algorithm> using namespace std; #define UDG 0 #define DG 1 #define WHITE 0 #define GRAY 1 #define

算法导论 第十二章:二叉查找树(Binary Search Trees)

二叉查找树具有如下性质: x是二叉查找树中的一个节点,如果y是x左子树中的一个节点,则y.key ≤ x.key ; 如果 y 是 x 右子树中的一个节点,则 x.key ≥ y.key. 在二叉树上执行的基本操作的时间与树的高度成正比.当这棵树是完全二叉树时,这些操作的最坏情况运行时间为Θ(lgn);如果该树是含n个节点的线性链,则这些操作的最坏情况的运行时间为Θ(n).我们可以通过随机构造二叉查找树(期望高度:E(h)=O(lgn)),从而使得在这种树上基本动态集操作的平均时间为Θ(lgn)

NeHe OpenGL教程 第二十二课:凹凸映射

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第二十二课:凹凸映射 凹凸映射,多重纹理扩展: 这是一课高级教程,请确信你对基本知识已经非常了解了.这一课是基于第六课的代码的,它将建立一个非常酷的立体纹理效果. 这一课由Jens Schneider所写,它基本上是由第6课改写而来

Kali Linux Web 渗透测试— 第十二课-websploit

Kali Linux Web 渗透测试— 第十二课-websploit 文/玄魂 目录 Kali Linux Web 渗透测试— 第十二课-websploit............................................... 1 Websploit 简介........................................................................................... 2 主要功能...........

NeHe OpenGL教程 第四十二课:多重视口

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第四十二课:多重视口 多重视口 画中画效果,很酷吧.使用视口它变得很简单,但渲染四次可会大大降低你的显示速度哦:) 欢迎来到充满趣味的另一课.这次我将向你展示怎样在单个窗口内显示多个视口.这些视口在窗口模式下能正确的调整大小.其中有