1. 程式人生 > >跳躍表(skip list)基本原理及C/C++實現

跳躍表(skip list)基本原理及C/C++實現

1、基本原理

不想好好寫字走的freestyle......其實可以直接看程式碼,我的註釋還是很詳細的,後面也有說明性的插圖。



後面關於時間複雜度的分析證明沒有貼(總之我們都知道它效能棒棒噠就行了)。

2、C/C++實現

比較符合MIT公開課的實現是這篇博文:跳躍表實現

而其他多數人都是抄襲這個實現:另一種實現

下面是自己的實現,因為最難的操作就是insert,所以目前只寫了insert:

#include<stdlib.h>
#include<time.h>
#include<iostream>
#include<vector>

using namespace std;

#define Tail false // 表示拋硬幣得到反面
#define Face true// 表示拋硬幣得到正面
typedef int ElementType;// 定義儲存元素型別

// 定義連結串列節點
typedef struct Node
{
	ElementType value;// 節點中儲存的資料
	// 三個方向的指標,橫向是單鏈表,縱向是雙鏈表
	Node* next;	
	Node* up;
	Node* down;
}Node;

// 跳躍表結構定義
typedef struct SkipList
{
	int level;// 記錄當前的跳躍表有幾層
	vector<Node*> heads; // 管理頭節點,便於以後從最高層往最底層查詢
}SkipList;

// 建立一個只含頭節點的表
SkipList* createSkipList()
{
	SkipList* ptrSkipList=new SkipList;
	ptrSkipList->level=1;
	// 建立第一個節點
	Node* head=new Node;
	head->value=INT_MIN;// 頭節點的資料統一設定為-∞
	// 初始化三個指標為空
	head->next=nullptr;
	head->up=nullptr;
	head->down=nullptr;
	ptrSkipList->heads.emplace_back(head);// 將節點指標加入vector
	srand(time(0));// 播撒隨機種子
	return ptrSkipList;
}

// 產生隨機數,決定插入的元素要不要往上走
bool flipCoin()
{
	if(rand()%2==1)
		return Face;
	else
		return Tail;
}

// 跳躍表的插入
void insert(SkipList* ptrSkipList,ElementType value)
{
	// 首先插入最底層的連結串列,注意插入時要保證有序性
	Node* ptrNewNode=new Node;
	Node* p,*q;// 輔助指標
	ptrNewNode->value=value;
	ptrNewNode->next=nullptr;
	ptrNewNode->up=nullptr;
	ptrNewNode->down=nullptr;
	// 尋找插入的合適位置
	for(p=ptrSkipList->heads[0]->next,q=ptrSkipList->heads[0];(p!=nullptr)&&(value>p->value);p=p->next,q=q->next)
	{
		// 注意不要把p=p->next;寫成p++;,會出事的
	}
	// 應該插在q後面
	q->next=ptrNewNode;
	ptrNewNode->next=p;
	// 拋硬幣,如果是反面,結束,否則向上走
	int currentLevel=0;// 記錄當前的層數,方便後面的連線,0在這裡是一個沒有意義的數,最小的level是1
	Node* move=ptrNewNode;// 輔助指標move,每次promote指向相同的節點
	// 開始拋硬幣,如果是正面就上升(promote)
	while(flipCoin()!=Tail)
	{	
		cout<<"插入"<<value<<"時得到正面"<<endl;
		currentLevel++;
		// 新建一個節點包含這個元素
		Node* promote=new Node;
		promote->value=value;
		// 和下面含有相同元素的節點互連
		move->up=promote;
		promote->down=move;
		promote->next=nullptr;
		move=promote;// 更新move
		// 如果沒有頭節點,新建一個頭節點,每次頭節點也要promote
		if(ptrSkipList->heads.size()<currentLevel+1)
		{
			Node* head=new Node;
			head->value=INT_MIN;
			head->next=nullptr;
			// 連線兩個頭節點
			ptrSkipList->heads[currentLevel-1]->up=head;
			head->down=ptrSkipList->heads[currentLevel-1];		
			ptrSkipList->heads.emplace_back(head);// 加入新的頭節點
			ptrSkipList->level++;// 更新層數
		}
		// 最後連線相同level的節點
		Node* r,*s;// 輔助指標
		for(r=ptrSkipList->heads[currentLevel]->next,s=ptrSkipList->heads[currentLevel];(r!=nullptr)&&(value>r->value);r=r->next,s=s->next)
		{
			// promote的時候也要保證元素之間的順序,從小到大排列
		}
		s->next=move;
		move->next=r;
	}
	cout<<value<<"插入完畢"<<endl;
}

int main(int argc, char* argv[])
{
	SkipList* mylist=createSkipList();
	insert(mylist,4);
	insert(mylist,3);
	insert(mylist,0);
	insert(mylist,13);
	insert(mylist,2);
	insert(mylist,50);
	insert(mylist,7);
	cout<<"跳躍表層數:"<<mylist->level<<endl;
	for(int i=mylist->level-1;i>=0;i--)
	{
		cout<<"第"<<i+1<<"層元素是:";
		for(Node* p=mylist->heads[i];p!=nullptr;p=p->next){
			cout<<p->value<<" ";
		}
		cout<<endl;
	}
}

寫完以後才發現橫向上其實也需要兩個指標。。。這樣查詢演算法就比較貼近公開課裡講的了,看起來更優雅,不過一個指標其實也是可以的,反正萬變不離其宗,將就吧。。

這是測試結果:


做了一個視覺化:


真美啊~