1. 程式人生 > >深度理解跳躍連結串列:一種基於概率選擇的平衡樹

深度理解跳躍連結串列:一種基於概率選擇的平衡樹

跳躍連結串列:一種基於概率選擇的平衡樹

作者:林子 時間:2014年9月
宣告:歡迎指出錯誤,轉載不要去掉出處

跳躍連結串列簡介

            二叉樹是一種常見的資料結構。它支援包括查詢、插入、刪除等一系列操作。但它有一個致命的弱點,就是當資料的隨機性不夠時,會導致其樹形結構的不平衡,從而直接影響演算法的效率。

  跳躍連結串列(Skip List)是1987年才誕生的一種嶄新的資料結構,它在進行查詢、插入、刪除等操作時的期望時間複雜度均為O(logn),有著近乎替代平衡樹的本領。而且最重要的一點,就是它的程式設計複雜度較同類的AVL樹,紅黑樹等要低得多,這使得其無論是在理解還是在推廣性上,都有著十分明顯的優勢。

  跳躍連結串列的最大優勢在於無論是查詢、插入和刪除都是O(logn),不過由於跳躍連結串列的操作是基於概率形成的,那麼它操作複雜度大於O(logn)的概率為,可以看出當n越大的時候失敗的概率越小。

  另外跳躍連結串列的實現也十分簡單,在平衡樹中是最易實現的一種結構。例如像複雜的紅黑樹,你很難在不依靠工具書的幫助下實現該演算法,但是跳躍連結串列不一樣,你可以很容易在半個小時內就完成其實現。

  跳躍連結串列的空間複雜度的期望為O(n),連結串列的層數期望為O(logn).

如何改進普通的連結串列?

 我們先看看一個普通的連結串列


可以看出查詢這個連結串列O(n),插入和刪除也是O(n).因此連結串列這種結構雖然節省空間,但是效率不高,那有沒有什麼辦法可以改進呢?

我們可以增加一條連結串列做為快速通道。這樣我們使用均勻分佈,從圖中可以看出L1層充當L0層的快速通道,底層的結點每隔固定的幾個結點出現在上面一層。


我們這裡主要以查詢操作來介紹,因為插入和刪除操作主要的複雜度也是取決於查詢,那麼兩條連結串列查詢的最好的時間複雜度是多少呢?

一次查詢操作首先要在上層遍歷<=|L1|次操作,然後在下層遍歷<=(L0/L1)次操作,至多要經歷


次操作,其中|L1|為L1的長度,n為L0的長度.

那麼最好的時間複雜度,也就怎麼設定間隔距離才能使查詢次數最少有

我們對|L1|的長度求導得


把上式代入函式,查詢次數最小也就是.這意味著下層每隔個結點在上層就有一個結點作為快速跑道。

那麼三條連結串列呢


同理那麼我們讓L2/L1=L1/L0,然後同樣列出方程,求導可得L2=,查詢次數為3*

........................................................................

第k條鏈條.....查詢次數為

我們這裡取k=logn,代入的查詢次數為2logn.

到此為主,我們應該知道了,期望上最好的層數是logn層,而且上下層結點數比為2,這樣查詢次數常數最小,複雜度保持在O(logn)。

跳躍連結串列的結構

跳躍表由多條鏈構成(L0,L1,L2 ……,Lh),且滿足如下三個條件:

  • 每條鏈必須包含兩個特殊元素:+∞ 和 -∞(其實不需要)
  • L0包含所有的元素,並且所有鏈中的元素按照升序排列。
  • 每條鏈中的元素集合必須包含於序數較小的鏈的元素集合。

結點結構原始碼
struct node
{
	int key;
	struct node *forward[MAXlevel];
};

MAXlevel可以是log(n)/log(2)

跳躍連結串列查詢操作

目的:在跳躍表中查詢一個元素x

   在跳躍表中查詢一個元素x,按照如下幾個步驟進行:

      1. 從最上層的鏈(Lh)的開頭開始

      2. 假設當前位置為p,它向右指向的節點為q(p與q不一定相鄰),且q的值為y。將y與x作比較

          (1) x=y  輸出查詢成功及相關資訊

          (2) x>y  從p向右移動到q的位置

          (3) x<y  從p向下移動一格 

      3. 如果當前位置在最底層的鏈中(L0),且還要往下移動的話,則輸出查詢失敗


元素53的查詢路徑

struct node* search(struct node*head, int key, int level)
{
	int i;
	struct node *p;
	p=head;
	for(i=level;i>=0;i--)
		while((p->forward[i]!=0)&&(p->forward[i]->key<key))
			p=p->foward[i];
	p==p->forward[0];//回到0級鏈,當前p或者空或者指向比搜尋關鍵字小
的前一個結點
	if(p==0)//這時若p為空則推出檢索,返回0
		return 0;
	else if(p->key==key)//找到,返回成功
		return p;
	else 
		return 0;//否則仍然檢索失敗,返回0
}


跳躍連結串列插入操作

目的:向跳躍表中插入一個元素x

     首先明確,向跳躍表中插入一個元素,相當於在表中插入一列從S0中某一位置出發向上的連續一段元素。有兩個引數需要確定,即插入列的位置以及它的“高度”。

     關於插入的位置,我們先利用跳躍表的查詢功能,找到比x小的最大的數y。根據跳躍表中所有鏈均是遞增序列的原則,x必然就插在y的後面。

     而插入列的“高度”較前者來說顯得更加重要,也更加難以確定。由於它的不確定性,使得不同的決策可能會導致截然不同的演算法效率。為了使插入資料之後,保持該資料結構進行各種操作均為O(logn)複雜度的性質,我們引入隨機化演算法(Randomized Algorithms)。

     我們定義一個隨機決策模組,它的大致內容如下:

 產生一個0到1的隨機數r如果r小於一個常數p(通常取0.25或0.5),則執行方案A,否則,執行方案B.

int randX(int &level)
{
	int i,j,t;
	t=rand();
	for(i=0;j=2;i<MAXlevel;i++,j+=j)
		if(t>RAND_MAX/j)
			break;
	if(i>level)
		level=i;
	return i;
}

RADN_MAX為隨機函式rand()能隨機到的最大值,每個結點的高度都由該隨機函式決定

初始時列高為1。插入元素時,不停地執行隨機決策模組。如果要求執行的是A操作,則將列的高度加1,並且繼續反覆執行隨機決策模組。直到第i次,模組要求執行的是B操作,我們結束決策,並向跳躍表中插入一個高度為i的列。

我們來看一個例子:

     假設當前我們要插入元素“40”,且在執行了隨機決策模組後得到高度為4

     步驟一:找到表中比40小的最大的數,確定插入位置


步驟二:插入高度為4的列,並維護跳躍表的結構


紫色的箭頭表示更新過的指標


void insert(struct node*head,int key,int &level)
{
	struct node*p,*update[MAXlevel];
	int i,newlevel;
	p=head;
	newlevel=randX(level);
	for(i=level;i>=0;i---)
	{
		while((p->forward[i]!=0)&&(p->forward[i]->key<key))
			p=p->forward[i];
		update[i]=p;//update[i]記錄了搜尋過程中在各層中走過的最大的結點位置
	}//設定新結點
	p=new(struct node);
	p->key=key;
	for(i=0;i<MAXlevel;i++)
		p->forward[i]=0;
	for(i=0;i<=newlevel;i++)//插入是從最高的newlevel層直至0層
	{
		p->forward[i]=update[i]->forward[i];//插入到分配到的層數
		update[i]->forward[i]=p;
	}
}


跳躍連結串列的刪除

目的:從跳躍表中刪除一個元素x

    刪除操作分為以下三個步驟:

在跳躍表中查詢到這個元素的位置,如果未找到,則退出 

將該元素所在整列從表中刪除 

將多餘的“空鏈”刪除 

刪除的過程即為插入的逆操作

int deletenode(struct node*head,int &level)
{
	int delkey;
	struct node*r;
	cout<<"請輸入要刪除的數字:";
	cin>>delkey;
	r=search(head,delkey,level);
	if(r)
	{
		int i=level;
		struct node*p,*q;
		while(i>=0)
		{
			p=q=head;
			while(p!=r&&p!=null)
			{
				q=p;
				p=p->forward[i];
			}
			if(p)
			{
				if(i==level&&q=head&&p->forward[i]==0)
					level--;
				else
					q->forward[i]=p->forward[i];
			}
			i--;
		}
		delete r;
		return 1;
	}
	else
		return 0;
}


跳躍連結串列的搜尋時間複雜度為O(logn)

定理:n個元素的跳躍連結串列的每一次搜尋的時間複雜度有很高的概率為O(logn).

高概率:事件E以很高的概率發生意味著對於a>=1,存在一個合適的常數使得事件E發生的概率Pr{E}>=1-O(1/n^a).

其中a是任意選擇的一個數,不同的a影響搜尋時間複雜度的常數,即a*O(logn),這個在後面介紹.

我們要證跳躍連結串列的時間複雜度,不能只是證明一次搜尋的複雜度為,是要證明全部的搜尋都是O(logn),因為這是基於概率的演算法,如果光一次有效率並沒有多大作用.

我們定義時間Ei為某一次搜尋失敗的概率,那麼假設k次搜素,我們先假定失敗的概率為O(1/n^a),其中至少有一次失敗的概率為


可以估算出k次有一次失敗的概率為1/n^(a-c),那麼我們只要讓a>=c+1或者a取無窮大,就可以證明每一次搜尋都具有高概率成功。


跳躍連結串列的層數有高概率為O(logn)

類似上面的方法,對於n個元素,如果有一個層數超過O(logn)就算失敗。那麼對於某一個元素超過clogn層,即失敗的概率為Pr{E}.那麼對於一次搜尋失敗的概率為

令a=c-1,則只要a>=1時,就有高概率的可能使得層數為O(logn)


跳躍連結串列單次查詢複雜度大於O(logn)的概率

每完成一次查詢,都肯定要從最頂層移動到最下面一層,這每改變一次層數是由概率選擇時候的p處於扔硬幣中的正面決定的。.既然上面知道層數高概率為clogn層,那麼扔正面的次數為clogn-1次.

我們假設扔了clogn個正面,超過10*clogn次是反面,則有


因為9-log(10e)中的9是線性增長,要遠遠大於log(10e)中的對數增長,因此超過10clogn的概率隨著10的增長變得越來越.所以全部的操作都在10logn以內,我們使用k替代10作為常數,即查詢次數為klogn,為O(logn).