博主在学习跳跃表的时候,在网上看了好几篇博客,内容和方法都有着差别,甚至有的博客还有明显的错误,耽误了很多时间。其间自己也产生了许多疑惑,然后对比了好几篇博客,结合跳跃表的目的(减少链表查询时间)自己思考了一下,慢慢解开了这些疑惑。这里会把跳跃表的思路已经刚看跳跃表的人需要直到和注意的点写下来。
一、为什么要用跳跃表
我们知道单链表在存储长度未知的动态数据是非常合适的选择(数组长度在定义时旧的确定),但是它在查询时,由于我们只有链表的头指针,因此只能从第一个元素依次遍历,直到找到要查找的元素,它的时间复杂度是O(n)。
比如这里我们有下图所示的单链表,我们要查询元素为55的结点,必须从头结点,循环遍历到最后一个节点,不算-INF(负无穷)一共查询8次。那么用什么办法能够用更少的次数访问55呢?最直观的,当然是新开辟一条捷径去访问55。
如下图,我们要查询元素为55的结点,只需要在L2层查找4次即可。在这个结构中,查询结点为46的元素将耗费最多的查询次数5次。即先在L2查询46,查询4次后找到元素55,因为链表是有序的,46一定在55的左边,所以L2层没有元素46。然后我们退回到元素37,到它的下一层即L1层继续搜索46。非常幸运,我们只需要再查询1次就能找到46。这样一共耗费5次查询。
那么,如何才能更快的搜寻55呢?有了上面的经验,我们就很容易想到,再开辟一条捷径。
这里的思想参考了下面这篇博客:
https://blog.csdn.net/weixin_33949359/article/details/94700153
如果有n个元素,因为是2分,所以层数就应该是log n层 (本文所有log都是以2为底),再加上自身的1层。以上图为例,如果是4个元素,那么分层为L3和L4,再加上本身的L2,一共3层;如果是8个元素,那么就是3+1层。最耗时间的查询自然是访问所有层数,耗时logn+logn,即2logn。为什么是2倍的logn呢?我们以上图中的46为例,查询到46要访问所有的分层,每个分层都要访问2个元素,中间元素和最后一个元素。所以时间复杂度为O(logn)。
至此为止,我们引入了最理想的跳跃表,但是如果想要在上图中插入或者删除一个元素呢?比如我们要插入一个元素22、23、24……,自然在L1层,我们将这些元素插入在元素21后,那么L2层,L3层呢?我们是不是要考虑插入后怎样调整连接,才能维持这个理想的跳跃表结构。我们知道,平衡二叉树的调整是一件令人头痛的事情,左旋右旋左右旋……一般人还真记不住,而调整一个理想的跳跃表将是一个比调整平衡二叉树还复杂的操作。幸运的是,我们并不需要通过复杂的操作调整连接来维护这样完美的跳跃表。有一种基于概率统计的插入算法,也能得到时间复杂度为O(logn)的查询效率,这种跳跃表才是我们真正要实现的。
这里相应提高动态数据的查询效率,我们当然也可以用红黑树存储数据,但是红黑树的实现复杂度是远远超过跳跃表的,也就是说,跳跃表是我们很快就能实现的一种方法,这就是他的优势。
二、跳跃表的查找
在说插入之前,首先要知道跳跃表是怎么查找元素的,因为在插入元素时,是需要先查找到插入元素的位置,才可以进行插入。
这里要先写几点注意:
1.跳跃表可以有很多层,具体几层是由扔硬币决定,也就是利用随机函数决定,反面就增加层数,然后继续扔,直到正面就结束增加层数。
2.跳跃表每一层都有一个头节点,头节点的值为INT_MIN,也就是最小值,而跳跃表的头指针是指向最上一层的头节点。有篇博客将每个节点都实现了最大层数的节点,指针域值定义Node *node[MAX_LEVEL],我个人认为这样实现不太好。
3.跳跃表是有序的!!每一层都是按照从小到大的顺序排列!这一点一定要知道!
4.建议大家看了几篇博客,知道思路之后,可以试着自己实现,思想就是让查询和插入效率尽量低就好了。
接下来说一下,查找的思路:
比如这里有一个如上图所示的无序表,我们向查找元素46
1.我们先从最上层开始往右找,只要发现右边节点的数值大于46,我们就结束这一层的查找,进入当前指针位置的下一层,如果小于46,我们就向右移动指针,如果等于46,直说明查找成功,如下右边没有节点即为NUL,我们也是结束这一层的查找,进入当前指针位置的下一层。
2.头节点是指向L4的头节点位置,一开始我们指针是停留在L4头部,发现右边节点是55大于46,这时,我们往下移动指针到L3的头部。
3.当前指针指向L3的头部,指针右边节点是21,小于46,因此我们往右移动指针,这时指针的右边节点是55,大于46,我们将当前指针下移。
4.此时指针是指向L2层21的位置,指针右边节点是37,小于46,因此我们往右移动指针,这时指针的右边节点是55,大于46,我们将当前指针下移。
5.此时指针是指向L2层37的位置,指针右边节点是46,等于46,因此找到了该元素。
代码如下:
bool find_value(int value)
{
Node *node = m_head;
//从最上层开始找,直到找到最下层为止
while (node->m_down != nullptr)
{
//一层层找,找到该层大于插入值的节点,跳出此次循环
while (node->m_right != nullptr&&value > node->m_right->m_value)
{
if (node->m_right->m_value>value) break;
else if (node->m_right->m_value == value)return true;
node = node->m_right;
}
node = node->m_down; //继续去下一层查找
}
//这时最下一层还没查找,这里查询最下一层
while (node->m_right != nullptr)
{
if (node->m_right->m_value > value) return false;
else if (node->m_right->m_value == value) return true;
node = node->m_right;
}
return false;
}
二、跳跃表的插入
跳跃表的插入操作是最麻烦的操作,只要会插入元素,其他操作基本都没问题了。
博主在看其他博客时,发现有的访问量很大的博客在进行跳跃表插入时,没有按照跳跃表正常的查找方式去找待插入的位置,反而用了直接遍历最下一层的方式找到插入位置,那么插入的效率就会很低了!
因此这里跳跃表的插入位置的查询一定要按照查找的思路进行插入。
接下来说一下,插入的思路:
1.首先找到最下一层插入的位置,注意,是最下层,L1层:
(1)我们先从最上层开始往右找,只要发现右边节点的数值大于待插入值,我们就结束这一层的查找,进入当前指针位置的下一层,如果小于插入值,我们就向右移动指针,直到找到大于待插入值的节点或者下一节点为空的情况,我们就结束这一层的查找,进入当前指针位置的下一层。
(2)这时我们的指针已经在L1层(最下一层)待插入的位置了,如果指针右边没有元素,说明待插入的元素是表种最大的元素,这时候,在该位置的右边插入顺序表即可。如果指针边没有元素,将新节点的插入到当前节点和当前节点的右节点的中间即可。
2.到这里我们已经完成了L1层(最下一层)的插入操作,接下来需要通过随机函数,知道在硬币出现证明前,一共出现了几次反面,几次反面就说明该次插入的节点有几层。
3.这里L1层已插入的节点我们记作newNode,它上层待插入的节点记作newNodeUp,当前指针所在位置为node,node的右边节点是newNode。
接下来我们进行newNode的上层节点newNodeUp的插入:
这里效率最高的插入思路应该是,首先查看当前要插入的层数,如果大于跳跃表目前的最高层,则新建一个头部节点,将头节点指针指向新增头部节点位置。L1层开始,查看当前指针所在位置node的上面是否有节点,如果上面有节点,将上层待插入的节点newNodeUp插入到node的上层节点后面即可。如果没有节点,将node向左移动,直到node移动到头部节点。
代码如下:
void add_val(int value)
{
Node *node = m_head;
//从最上层开始找,直到找到最下层为止
while (node->m_down!=nullptr)
{
//一层层找,找到该层大于插入值的节点,跳出此次循环
while (node->m_right != nullptr&&value > node->m_right->m_value)
{
node = node->m_right;
}
node = node->m_down; //继续去下一层查找
}
//这时最下一层还没查找,这里查询最下一层
while (node->m_right!=nullptr&&value > node->m_right->m_value)
{
node = node->m_right;
}
Node *newNode = new Node();
newNode->m_value = value;
//此时node已经在最下面一层了,如果这时候右边为空的话,说明该元素比跳跃表中所有元素都要大
//这时候,在该位置的右边插入顺序表即可
if (node->m_right==nullptr)
{
node->m_right = newNode;
newNode->m_lift = node;
}
else
{//将新节点的插入到当前节点和当前节点的右节点的中间即可
newNode->m_right = node->m_right;
node->m_right = newNode;
newNode->m_lift = node;
}
//第一层插入完成中,开始确定该节点的层数
int level = this->randomLevel(); //获取此次插入的随机层数
for (int i=1;i<=level;i++)
{
Node *newNodeUp = new Node();
newNodeUp->m_value = value;
newNodeUp->m_down = node->m_right;
node->m_right->m_up = newNodeUp;
//如果当前层大于现在的最大层数,则将层数+1,新增一个头节点的下部节点,将头节点上移
if (m_level<i)
{
m_level = i;
Node *newHead = new Node();
newHead->m_value = INT_MIN;
newHead->m_right = newNodeUp;
newNodeUp->m_lift = newHead;
newHead->m_down = m_head;
m_head->m_up = newHead;
m_head = newHead;
}
//如果node上方没有节点,就依次往左查看node左边节点的上方是否有节点
//如果有则将新插入节点的上层节点插入到该位置
while (node->m_lift!=nullptr)
{
if (node->m_up!=nullptr)
{
Node *upNode = node->m_up; //node的上节点
newNodeUp->m_lift = upNode;
//如果上节点右边不为空,将newNodeUp的右指针指向上节点右边节点即可
if (upNode->m_right != nullptr) {
newNodeUp->m_right = upNode->m_right;
}
upNode->m_right = newNodeUp;
break;
}
node = node->m_lift; //当前节点上层没有数据,将当前指针向左移动,再查看上方有没有节点
}
node=node->m_up;
//newNodeUp左边为空,说明node左边是空,即上面的while循环没有执行,node现在再每行的头节点位置
}
}
整体工程代码如下,博主写了一个按照跳跃表格式打印表数据的函数,这个函数只是验证测试用,因此没有注重效率问题。
#include <iostream>
#define MAX_LEVEL 100
class Node
{
public:
Node():m_lift(nullptr), m_down(nullptr), m_right(nullptr),m_up(m_up)
{
}
int m_value;
Node *m_lift,*m_down,*m_right,*m_up; //定义四个方向的指针域
};
//跳跃链表类
class skList
{
public:
skList():m_level(0)
{
m_head = new Node();
m_head->m_value = INT_MIN;
}
~skList()
{
Node *node = m_head;
//从左到右、从上到下释放空间
while (node->m_down!=nullptr)
{
Node *rowNode = node;
node = node->m_down;
while (rowNode->m_right!=nullptr)
{
Node *pNo = rowNode->m_right;
delete rowNode;
rowNode = pNo;
}
delete rowNode;
}
//释放最下一层空间
while (node->m_right!=nullptr)
{
Node *rowNode = node;
node = node->m_right;
delete rowNode;
}
delete node;
}
//插入元素的时候元素所占有的层数完全是随机算法
int randomLevel()
{
int level = 0;
while (rand() % 2)
level++;
level = (MAX_LEVEL > level) ? level : MAX_LEVEL;
return level;
}
skList& operator<<(const int value)
{
this->add_val(value);
return *this;
}
//插入的时候按照查找的方法去找
void add_val(int value)
{
Node *node = m_head;
//从最上层开始找,直到找到最下层为止
while (node->m_down!=nullptr)
{
//一层层找,找到该层大于插入值的节点,跳出此次循环
while (node->m_right != nullptr&&value > node->m_right->m_value)
{
node = node->m_right;
}
node = node->m_down; //继续去下一层查找
}
//这时最下一层还没查找,这里查询最下一层
while (node->m_right!=nullptr&&value > node->m_right->m_value)
{
node = node->m_right;
}
Node *newNode = new Node();
newNode->m_value = value;
//此时node已经在最下面一层了,如果这时候右边为空的话,说明该元素比跳跃表中所有元素都要大
//这时候,在该位置的右边插入顺序表即可
if (node->m_right==nullptr)
{
node->m_right = newNode;
newNode->m_lift = node;
}
else
{//将新节点的插入到当前节点和当前节点的右节点的中间即可
newNode->m_right = node->m_right;
node->m_right = newNode;
newNode->m_lift = node;
}
//第一层插入完成中,开始确定该节点的层数
int level = this->randomLevel(); //获取此次插入的随机层数
for (int i=1;i<=level;i++)
{
Node *newNodeUp = new Node();
newNodeUp->m_value = value;
newNodeUp->m_down = node->m_right;
node->m_right->m_up = newNodeUp;
//如果当前层大于现在的最大层数,则将层数+1,新增一个头节点的下部节点,将头节点上移
if (m_level<i)
{
m_level = i;
Node *newHead = new Node();
newHead->m_value = INT_MIN;
newHead->m_right = newNodeUp;
newNodeUp->m_lift = newHead;
newHead->m_down = m_head;
m_head->m_up = newHead;
m_head = newHead;
}
//如果node上方没有节点,就依次往左查看node左边节点的上方是否有节点
//如果有则将新插入节点的上层节点插入到该位置
while (node->m_lift!=nullptr)
{
if (node->m_up!=nullptr)
{
Node *upNode = node->m_up; //node的上节点
newNodeUp->m_lift = upNode;
//如果上节点右边不为空,将newNodeUp的右指针指向上节点右边节点即可
if (upNode->m_right != nullptr) {
newNodeUp->m_right = upNode->m_right;
}
upNode->m_right = newNodeUp;
break;
}
node = node->m_lift; //当前节点上层没有数据,将当前指针向左移动,再查看上方有没有节点
}
node=node->m_up;
//newNodeUp左边为空,说明node左边是空,即上面的while循环没有执行,node现在再每行的头节点位置
}
}
bool find_value(int value)
{
Node *node = m_head;
//从最上层开始找,直到找到最下层为止
while (node->m_down != nullptr)
{
//一层层找,找到该层大于插入值的节点,跳出此次循环
while (node->m_right != nullptr&&value > node->m_right->m_value)
{
if (node->m_right->m_value>value) break;
else if (node->m_right->m_value == value)return true;
node = node->m_right;
}
node = node->m_down; //继续去下一层查找
}
//这时最下一层还没查找,这里查询最下一层
while (node->m_right != nullptr)
{
if (node->m_right->m_value > value) return false;
else if (node->m_right->m_value == value) return true;
node = node->m_right;
}
return false;
}
//按照跳跃表格式打印跳跃表
void printAll()
{
Node *node = m_head;
//移动到最下一层
while (node->m_down!=nullptr)
{
node = node->m_down;
}
int size = 0;
Node *firstNode = node->m_right; //最下层的第一个元素
//获取最下层链表长度
while (node->m_right != nullptr)
{
size++;
node = node->m_right;
}
//每个元素一个数组,数组长度为表的最大高度
int **valueList = new int *[size+1];
for (int i = 0; i < size +1; i++)
{
valueList[i] = new int[m_level + 1];
}
for (int i=0;i<size;i++)
{
Node *rowNode = firstNode;
valueList[i][0] = rowNode->m_value;
for (int j=1;j<=m_level;j++)
{
if (rowNode->m_up!=nullptr)
{
valueList[i][j] = rowNode->m_up->m_value;
rowNode = rowNode->m_up;
}
else valueList[i][j] = 0;
}
//valueList[i][m_level] = rowNode->m_value;
firstNode = firstNode->m_right;
}
for (int i= m_level;i >= 0;i--)
{
for (int j= 0;j<size;j++)
{
if (valueList[j][i]== 0)std::cout << "---";
else std::cout << valueList[j][i] << "--";
}
std::cout << std::endl;
}
for (int i = 0; i < size; i++) {
delete[]valueList[i];
}
delete[]valueList;
}
private:
Node *m_head;
int m_level;
};
int main()
{
skList list;
list.add_val(1);
list.add_val(2);
list.add_val(3);
list.add_val(4);
list.add_val(8);
list.add_val(6);
list << 43 << 222 << 11 << 23;
list.printAll();
system("pause");
return 0;
}
测试结果: