跳表的特点,可以概括如下。•跳表是多层(level)链表结构;•跳表中的每一层都是一个有序链表,并且按照元素升序(默认)排列;•跳表中的元素会在哪一层出现是随机决定的,但是只要元素出现在了第 k 层,那么 k 层以下的链表也会出现这个元素;•跳表的底层的链表包含所有元素;•跳表头节点和尾节点不存储元素,且头节点和尾节点的层数就是跳表的最大层数;•跳表中的节点包含两个指针,一个指针指向同层链表的后一节点,一个指针指向下层链表的同元素节点。以上图中的跳表为例,如果要查找元素 71,那么查找流程如下图所示。
从顶层链表的头节点开始查找,查找到元素71的节点时,一共遍历了4个节点,但是如果按照传统链表的方式(即从跳表的底层链表的头节点开始向后查找),那么就需要遍历7个节点,所以跳表以空间换时间,缩短了操作跳表所需要花费的时间。跳跃列表的算法有同平衡树一样的渐进的预期时间边界,并且更简单、更快速和使用更少的空间。这种数据结构是由William Pugh(音译为威廉·普)发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。 谷歌上找到一篇作者关于跳表的论文,感兴趣强烈建议下载阅读:https://epaperpress.com/sortsearch/download/skiplist.pdf跳表在动态查找过程中使用了一种非严格的平衡机制来让插入和删除都更加便利和快捷,这种非严格平衡是基于概率的,而不是平衡树的严格平衡。说到非严格平衡,首先想到的是红黑树RbTree,它同样采用非严格平衡来避免像AVL那样调整树的结构,这里就不展开讲红黑树了,看来跳表也是类似的路子,但是是基于概率实现的。public class SkiplistNode {
public int data;
public SkiplistNode next;
public SkiplistNode down;
public int level;
public SkiplistNode(int data, int level) {
this.data = data;
this.level = level;
}
跳表初始化的相关代码如下所示。public LinkedList<SkiplistNode> headNodes;
public LinkedList<SkiplistNode> tailNodes;
public int curLevel;
public Random random;
public Skiplist() {
random = new Random();
//headNodes用于存储每一层的头节点
headNodes = new LinkedList<>();
//tailNodes用于存储每一层的尾节点
tailNodes = new LinkedList<>();
//初始化跳表时,跳表的层数随机指定
curLevel = getRandomLevel();
//指定了跳表的初始的随机层数后,就需要将每一层的头节点和尾节点创建出来并构建好关系
SkiplistNode head = new SkiplistNode(Integer.MIN_VALUE, 0);
SkiplistNode tail = new SkiplistNode(Integer.MAX_VALUE, 0);
for (int i = 0; i <= curLevel; i++) {
head.next = tail;
headNodes.addFirst(head);
tailNodes.addFirst(tail);
SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
headNew.down = head;
tailNew.down = tail;
head = headNew;
tail = tailNew;
}
}
现在要往跳表中添加元素 120,并且随机指定的层数为 3,大于了当前跳表的层数 2,此时需要先扩大跳表的层数,2如 下图所示。
将元素 120 插入到跳表中时,从顶层开始,逐层向下插入,如下图所示。
跳表的添加方法的代码如下所示。public void add(int num) {
//获取本次添加的值的层数
int level = getRandomLevel();
//如果本次添加的值的层数大于当前跳表的层数
//则需要在添加当前值前先将跳表层数扩充
if (level > curLevel) {
expanLevel(level - curLevel);
}
//curNode表示num值在当前层对应的节点
SkiplistNode curNode = new SkiplistNode(num, level);
//preNode表示curNode在当前层的前一个节点
SkiplistNode preNode = headNodes.get(curLevel - level);
for (int i = 0; i <= level; i++) {
//从当前层的head节点开始向后遍历,直到找到一个preNode
//使得preNode.data < num <= preNode.next.data
while (preNode.next.data < num) {
preNode = preNode.next;
}
//将curNode插入到preNode和preNode.next中间
curNode.next = preNode.next;
preNode.next = curNode;
//如果当前并不是0层,则继续向下层添加节点
if (curNode.level > 0) {
SkiplistNode downNode = new SkiplistNode(num, curNode.level - 1);
//curNode指向下一层的节点
curNode.down = downNode;
//curNode向下移动一层
curNode = downNode;
}
//preNode向下移动一层
preNode = preNode.down;
}
}
private void expanLevel(int expanCount) {
SkiplistNode head = headNodes.getFirst();
SkiplistNode tail = tailNodes.getFirst();
for (int i = 0; i < expanCount; i++) {
SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
headNew.down = head;
tailNew.down = tail;
head = headNew;
tail = tailNew;
headNodes.addFirst(head);
tailNodes.addFirst(tail);
}
}
•跳表的搜索的代码如下所示。public boolean search(int target) {
//从顶层开始寻找,curNode表示当前遍历到的节点
SkiplistNode curNode = headNodes.getFirst();
while (curNode != null) {
if (curNode.next.data == target) {
//找到了目标值对应的节点,此时返回true
return true;
} else if (curNode.next.data > target) {
//curNode的后一节点值大于target
//说明目标节点在curNode和curNode.next之间
//此时需要向下层寻找
curNode = curNode.down;
} else {
//curNode的后一节点值小于target
//说明目标节点在curNode的后一节点的后面
//此时在本层继续向后寻找
curNode = curNode.next;
}
}
return false;
}
•跳表的删除的代码如下所示。public boolean erase(int num) {
//删除节点的遍历过程与寻找节点的遍历过程是相同的
//不过在删除节点时如果找到目标节点,则需要执行节点删除的操作
SkiplistNode curNode = headNodes.getFirst();
while (curNode != null) {
if (curNode.next.data == num) {
//preDeleteNode表示待删除节点的前一节点
SkiplistNode preDeleteNode = curNode;
while (true) {
//删除当前层的待删除节点,就是让待删除节点的前一节点指向待删除节点的后一节点
preDeleteNode.next = curNode.next.next;
//当前层删除完后,需要继续删除下一层的待删除节点
//这里让preDeleteNode向下移动一层
//向下移动一层后,preDeleteNode就不一定是待删除节点的前一节点了
preDeleteNode = preDeleteNode.down;
//如果preDeleteNode为null,说明已经将底层的待删除节点删除了
//此时就结束删除流程,并返回true
if (preDeleteNode == null) {
return true;
}
//preDeleteNode向下移动一层后,需要继续从当前位置向后遍历
//直到找到一个preDeleteNode,使得preDeleteNode.next的值等于目标值
//此时preDeleteNode就又变成了待删除节点的前一节点
while (preDeleteNode.next.data != num) {
preDeleteNode = preDeleteNode.next;
}
}
} else if (curNode.next.data > num) {
curNode = curNode.down;
} else {
curNode = curNode.next;
}
}
return false;
}public class Skiplist {
public LinkedList<SkiplistNode> headNodes;
public LinkedList<SkiplistNode> tailNodes;
public int curLevel;
public Random random;
public Skiplist() {
random = new Random();
//headNodes用于存储每一层的头节点
headNodes = new LinkedList<>();
//tailNodes用于存储每一层的尾节点
tailNodes = new LinkedList<>();
//初始化跳表时,跳表的层数随机指定
curLevel = getRandomLevel();
//指定了跳表的初始的随机层数后,就需要将每一层的头节点和尾节点创建出来并构建好关系
SkiplistNode head = new SkiplistNode(Integer.MIN_VALUE, 0);
SkiplistNode tail = new SkiplistNode(Integer.MAX_VALUE, 0);
for (int i = 0; i <= curLevel; i++) {
head.next = tail;
headNodes.addFirst(head);
tailNodes.addFirst(tail);
SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
headNew.down = head;
tailNew.down = tail;
head = headNew;
tail = tailNew;
}
}
public boolean search(int target) {
//从顶层开始寻找,curNode表示当前遍历到的节点
SkiplistNode curNode = headNodes.getFirst();
while (curNode != null) {
if (curNode.next.data == target) {
//找到了目标值对应的节点,此时返回true
return true;
} else if (curNode.next.data > target) {
//curNode的后一节点值大于target
//说明目标节点在curNode和curNode.next之间
//此时需要向下层寻找
curNode = curNode.down;
} else {
//curNode的后一节点值小于target
//说明目标节点在curNode的后一节点的后面
//此时在本层继续向后寻找
curNode = curNode.next;
}
}
return false;
}
public void add(int num) {
//获取本次添加的值的层数
int level = getRandomLevel();
//如果本次添加的值的层数大于当前跳表的层数
//则需要在添加当前值前先将跳表层数扩充
if (level > curLevel) {
expanLevel(level - curLevel);
}
//curNode表示num值在当前层对应的节点
SkiplistNode curNode = new SkiplistNode(num, level);
//preNode表示curNode在当前层的前一个节点
SkiplistNode preNode = headNodes.get(curLevel - level);
for (int i = 0; i <= level; i++) {
//从当前层的head节点开始向后遍历,直到找到一个preNode
//使得preNode.data < num <= preNode.next.data
while (preNode.next.data < num) {
preNode = preNode.next;
}
//将curNode插入到preNode和preNode.next中间
curNode.next = preNode.next;
preNode.next = curNode;
//如果当前并不是0层,则继续向下层添加节点
if (curNode.level > 0) {
SkiplistNode downNode = new SkiplistNode(num, curNode.level - 1);
//curNode指向下一层的节点
curNode.down = downNode;
//curNode向下移动一层
curNode = downNode;
}
//preNode向下移动一层
preNode = preNode.down;
}
}
public boolean erase(int num) {
//删除节点的遍历过程与寻找节点的遍历过程是相同的
//不过在删除节点时如果找到目标节点,则需要执行节点删除的操作
SkiplistNode curNode = headNodes.getFirst();
while (curNode != null) {
if (curNode.next.data == num) {
//preDeleteNode表示待删除节点的前一节点
SkiplistNode preDeleteNode = curNode;
while (true) {
//删除当前层的待删除节点,就是让待删除节点的前一节点指向待删除节点的后一节点
preDeleteNode.next = curNode.next.next;
//当前层删除完后,需要继续删除下一层的待删除节点
//这里让preDeleteNode向下移动一层
//向下移动一层后,preDeleteNode就不一定是待删除节点的前一节点了
preDeleteNode = preDeleteNode.down;
//如果preDeleteNode为null,说明已经将底层的待删除节点删除了
//此时就结束删除流程,并返回true
if (preDeleteNode == null) {
return true;
}
//preDeleteNode向下移动一层后,需要继续从当前位置向后遍历
//直到找到一个preDeleteNode,使得preDeleteNode.next的值等于目标值
//此时preDeleteNode就又变成了待删除节点的前一节点
while (preDeleteNode.next.data != num) {
preDeleteNode = preDeleteNode.next;
}
}
} else if (curNode.next.data > num) {
curNode = curNode.down;
} else {
curNode = curNode.next;
}
}
return false;
}
private void expanLevel(int expanCount) {
SkiplistNode head = headNodes.getFirst();
SkiplistNode tail = tailNodes.getFirst();
for (int i = 0; i < expanCount; i++) {
SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
headNew.down = head;
tailNew.down = tail;
head = headNew;
tail = tailNew;
headNodes.addFirst(head);
tailNodes.addFirst(tail);
}
}
private int getRandomLevel() {
int level = 0;
while (random.nextInt(2) > 1) {
level++;
}
return level;
}
}typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
在Redis中对跳表的实现基本上也是遵循这个思想的,只不过有微小差异,看下Redis关于跳表层数的随机源码src/z_set.c:/* Returns a random level for the new skiplist node we are going to create.
* The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
* (both inclusive), with a powerlaw-alike distribution where higher
* levels are less likely to be returned. */
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */(random()&0xFFFF) < (ZSKIPLIST_P*0xFFFF)
可以看到0.5的32bit转换16进制结果为0x3f000000,如果与0xFFFF做与运算结果还是0,不符合预期。实际应用时对于随机层数的实现并不统一,重要的是随机数的生成,在LevelDB中对跳表层数的生成代码是这样的:template <typename Key, typename Value>
int SkipList<Key, Value>::randomLevel() {
static const unsigned int kBranching = 4;
int height = 1;
while (height < kMaxLevel && ((::Next(rnd_) % kBranching) == 0)) {
height++;
}
assert(height > 0);
assert(height <= kMaxLevel);
return height;
}
uint32_t Next( uint32_t& seed) {
seed = seed & 0x7fffffffu;
if (seed == 0 || seed == 2147483647L) {
seed = 1;
}
static const uint32_t M = 2147483647L;
static const uint64_t A = 16807;
uint64_t product = seed * A;
seed = static_cast<uint32_t>((product >> 31) + (product & M));
if (seed > M) {
seed -= M;
}
return seed;
}如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。幂次定律的表现是少数几个事件的发生频率占了整个发生频率的大部分, 而其余的大多数事件只占整个发生频率的一个小部分。幂次定律应用到跳表的随机层数来说就是大部分的节点层数都是黄色部分,只有少数是绿色部分,并且概率很低。定量的分析如下:•节点层数至少为1,大于1的节点层数满足一个概率分布。•节点层数恰好等于1的概率为p^0(1-p)•节点层数恰好等于2的概率为p^1(1-p)•节点层数恰好等于3的概率为p^2(1-p)•节点层数恰好等于4的概率为p^3(1-p)依次递推节点层数恰好等于K的概率为p^(k-1)(1-p)因此如果我们要求节点的平均层数,那么也就转换成了求概率分布的期望问题了:
表中P为概率,V为对应取值,给出了所有取值和概率的可能,因此就可以求这个概率分布的期望了。方括号里面的式子其实就是高一年级学的等比数列,常用技巧错位相减求和,从中可以看到结点层数的期望值与1-p成反比。对于Redis而言,当p=0.25时结点层数的期望是1.33。总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此
我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r
刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr