鏈表的“二分查找”--跳躍表

二分查找是一種效率很高的查找方法,但是二分查找要求數據結構必須是順序表,也就是類似於數組的連續存儲,因爲只有這樣才能一下定位出數組的中間位置(直接使用類似a[len / 2]),然後就可以把數組一分爲二,進行後面的操作。
但是對於鏈表,由於存儲是離散的,不能像數組一樣,快速定位中間位置,來把鏈表一分爲二,所以一般的二分查找不能直接應用於鏈表。

本文要說的跳躍表其實也可以算是一種新的數據結構,採用空間換時間的方式來達到近似二分查找的效率。
其中存放的數據也是有序的。

平衡二叉樹也是一種查找方式,但是這種查找方式爲了維持樹的平衡,需要進行復雜的操作(旋轉等),學習和使用難度很大。

而本文要說的跳躍表就是一種較爲簡單的方式來達到替代平衡二叉樹的效果。
對於平衡二叉樹不多做介紹。

先複習一下二分查找的過程,一個簡單的順序表如下:
在這裏插入圖片描述
如何查找93這個元素呢?(當然這是沒有什麼意義的,只討論算法)
先取數組中間元素比較,如果比中間元素大,就與右邊比較,重複本步驟,直到取出。

分析:

那麼對於離散存放的數據怎麼辦呢?我們無法直接取出中間元素。
在這裏插入圖片描述
不能直接找到19的位置(除非從頭遍歷,但是時間複雜度達不到要求)

這簡單,我們找個變量把中間的位置存起來不就行了嗎?如下:
在這裏插入圖片描述
但是還是有問題,當比較完一次後,要進行第二次比較時(左:11-18或右23-121)比較時,怎麼取出它們的中間元素呢? 答案是跟上邊一樣,再找變量存起來他們的位置。

在這裏插入圖片描述
很明顯第一次查找時與19 copy比較,第二次查找時與13 copy或19copy比較,那麼怎麼知道第一次要與19 copy,而不是13 copy?也就是如何區分19 copy與13 copy, 93 copy, 其實從圖中也可以看出來了–分層。
把19 copy放在第一層, 13 copy, 93 copy放在第二層,那麼在比較時先取第一層的元素比較,然後跟第二層中某個元素比較就,來確定最終位置就可以了。
在這裏插入圖片描述

還需要把同一層的元素鏈接起來(不然怎麼說是同一層呢),如果與本層第一個元素比較後,大於第一個元素,那麼就可以取後面的元素來繼續比較。
最終情況類似:
在這裏插入圖片描述

實現:

根據上面的描述,我們先分析一下跳躍表節點需要哪些成員,存放數據的區域(value,可以是需要的數據類型,這裏使用int方便演示), 一個分值(score, 用於排序),還需要一個向右指的指針(right,來尋找本層下一個元素),還需要一個向下的指針(down,確定本元素在下一層的位置,在下一層查詢過程中,直接以本元素的位置爲基準)。

有一些實現是使用上面這幾個成員,但是這裏我們直接採用論文中節點的形式:
在這裏插入圖片描述
可以看到,跟我們上面的描述還是有區別的,value, score域肯定是相同的,但是並不是一個節點只有一個right域,而是一個節點可能有多個right, 如6節點就有4個right, 而且這四個right是以數組的方式放在數據後面的,這樣也有好像,就是不需要down節點來指向下一層對應自己的元素的。
比如上上圖中的19 copy2要根據down來找到19 copy1, 然後19copy1根據down來找到19.
但是這裏就不需要down指針了,因爲只有一個節點。

所以這裏我們需要的成員有value, score, 還有一堆right, 如下
在這裏插入圖片描述
爲了簡單,所有節點都有20個right, 而不動態伸縮。

跳躍表本身的結構直接給出:
在這裏插入圖片描述
header就是最左側的節點,他的right指向本層第一個數據節點,max_level表示當前已使用的最大層數。

初始化:
在這裏插入圖片描述
比較簡單,初始化完成後如下圖:
在這裏插入圖片描述
right0-right19就表示level0-level19, 共20層。

查找
僞代碼如下:
在這裏插入圖片描述
先取header, 主要是爲了使用right域(也就是僞代碼中的forward),
x = list->header, 然後取x->forward[i], 相當於我們的x->right[i], 就是取第i層(right0-19表示了level0-19層)
先看外層循環,是從list->level,相當於我們代碼中的list->max_level, 由最外層開始,這個雙重循環我們直接看個例子來解釋。
還是這個圖
在這裏插入圖片描述
比如我們查找12這個元素,外層循環第一次(i=最外層),然後內層while循環x->forward[i]->key, 其實就是取了 6這個元素節點的score來與要查找的score比較(原文中叫key), 6比12小,x=x->forward[i], 這個時候x就指向了6節點(剛纔指向header),那個外層循環i就遞減第第二層, 再比較時就是x->forward[i], 這個時候i爲第二層,x爲節點6,那麼就是比較25節點了(6的forward在第二層的指向),依次進行。
經過上面雙重循環後x應該指向的是9這個結點,這個時候已經到最下邊一層了。

然後下一行代碼 x = x->forward[1], 就是把最後一層的下一元素(大於等於待查找數值的,當前元素是小於待查找數值的),賦值給x,進行最後的比較。

可以看到這種查找是類似於二分查找的過程的。
C++代碼如下:儘可能與僞代碼描述符合。
在這裏插入圖片描述
插入:
僞代碼:
在這裏插入圖片描述
只解釋兩個地方:
1.使用與查找算法相同的方式找到每一層要更新的位置,記錄到update數組中,最下邊的循環會一起處理。
2. randomLevel()可以說是算法最重要的地方了,上面例圖中的6有4層,9有2層是怎麼確定的呢?答案是隨機確定的,也正是由於使用這個概率的方式,查詢操作的時間複雜度可以達到O(logN)。所以說中間的節點不一定是最高層(最先被查找(順序表是中間的最先被查找)),而是完全隨機的。
在這裏插入圖片描述

刪除:
沒有看,如果讀者想實現的話,可以參考文末的論文鏈接,自行實現。
主要是光寫查詢,插入就花了很長時間,還有bug,實在是沒精力去研究刪除了。

本文代碼:

#include <iostream>
#include <cstdlib>
#include <ctime>

#define MAX_LEVEL 20

struct Node {
	Node* right[20];
	int score;
	int value;
};

class SkipList {
 private:
	Node *header;
	int max_level;
 public:
	SkipList()
	{
		max_level = -1;
		header = new Node;
		for (int i=0; i<MAX_LEVEL; i++)
			header->right[i] = nullptr;
	}

	Node* search(int score_search)
	{
		std::cout << "search path is ";
		Node *x = header;
		for (int i=max_level; i>=0; i--) {
			while (x->right[i]->score < score_search) {
				std::cout << " " << x->right[i]->score;
				if ((nullptr == x->right[i]) || (nullptr == x->right[i]->right[i]))
					break;
				x = x->right[i];
			}
			std::cout << std::endl;
		}
		if (nullptr != x->right[0])
			x = x->right[0];
		if (x->score == score_search)
			return x;
		return nullptr;
	}

	void insert(int score, int value)
	{
		Node *update[MAX_LEVEL];
		int new_level = random_level();
		Node *x = header;

		for (int i=max_level; i>=0; i--) {
			while ((nullptr != x->right[i]) && (x->right[i]->score < score))
				x = x->right[i];	
			update[i] = x;
		}

		if (nullptr != x)
			x = x->right[0];

		if (nullptr != x) {
			if ((x->score == score)) {
				x->value = value;
				return;
			}
		}

		if (new_level > max_level) {
			for (int i=max_level; i<=new_level; i++)
				update[i] = header;
			max_level = new_level;
		}

		x = new Node;
		x->value = value;
		x->score = score;
		
		for (int i=0; i<=new_level; i++) {
			x->right[i] = update[i]->right[i];
			update[i]->right[i] = x;
		}
	}

	int random_level()
	{
		/* srand不能放在這裏 */
		int r = 0;
		while (rand() % 2)
			++r;
		return r;
	}

	void traverse()
	{
		Node *t = nullptr;
		for (int i=max_level; i>=0; i--) {
			t = header->right[i];
			if (nullptr != t) {
				std::cout << "level" << i << ": ";
				while (t) {
					std::cout << t->score << " ";
					t = t->right[i];
				}
				std::cout << std::endl;
			}
		}
	}
};

int main()
{
	srand((unsigned int)time(0));

	SkipList *skip_list = new SkipList;

	skip_list->insert(9, 99);
	skip_list->insert(7, 77);
	skip_list->insert(8, 88);
	skip_list->insert(6, 66);
	skip_list->insert(3, 33);

	skip_list->traverse();

	Node *f = skip_list->search(8);
	std::cout << "find element is " << f->value << std::endl;

	return 0;
}


注:本文中代碼有bug, 可用於學習算法,不能在生產環境中直接使用。


參考:
論文鏈接:https://www.cl.cam.ac.uk/teaching/0506/Algorithms/skiplists.pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章