日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深夜学算法之SkipList:让链表飞

發(fā)布時間:2024/1/23 编程问答 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深夜学算法之SkipList:让链表飞 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

1. 前言

上次寫Python操作LevelDB時提到過,有機(jī)會要實現(xiàn)下SkipList。摘錄下wiki介紹:

跳躍列表是一種隨機(jī)化數(shù)據(jù)結(jié)構(gòu),基于并聯(lián)的鏈表,其效率可比擬二叉查找樹。

我們知道對于有序鏈表,查找的時間復(fù)雜度為O(n),盡管真正的插入與刪除操作節(jié)點復(fù)雜度只有O(1),但都需要先查找到節(jié)點的位置,可以說是查找拉低了有序鏈表的性能。

簡單地講,SkipList采用“空間換時間”的思想,除了原始鏈表外還保存一些“跳躍”的鏈表,達(dá)到加速查找的效果。

我的實現(xiàn):https://github.com/liquidconv/DSAF

2. 感性認(rèn)識SkipList

bottom-up與top-down,我個人傾向后者。所以在給出SkipList里具體定義與算法前,先從問題出發(fā),研究一下SkipList的設(shè)計思路。

來看一個有序鏈表(這里H表示鏈表頭部,T表示鏈表尾部,不是有效節(jié)點):

1.png

假設(shè)我們要查找7,只能老老實實地按照1->2->3->…的順序走,忍受O(n)的效率;但如果是數(shù)組的話,可以使用二分查找達(dá)到O(lgn)。

可以在鏈表中使用二分查找嗎?

不可以,因為二分查找需要用到中間位置的節(jié)點,而鏈表不能隨機(jī)訪問。

——那么就把中間位置的節(jié)點單獨保存吧。

2.png

原來的鏈表寫成了三個鏈表,記從下到上的編號為0、1、2,可以發(fā)現(xiàn)0號鏈表就是原始鏈表,1號鏈表是原始鏈表四等分點,2號鏈表是原始鏈表的二等分點。

我們再來查找7,初始搜索范圍為(H, T):

  • 在2號鏈表中與4比較,7>4,更新搜索范圍為(4, T)
  • 在1號鏈表中與6比較,7>6,更新搜索范圍為(6, T)
  • 在0號鏈表中與7比較,7=7,查找成功。
  • 形象化地說,SkipList就是額外保存了二分查找的中間信息。不過SkipList中含有隨機(jī)化,生成的結(jié)構(gòu)不會像上面那樣完美,來看實際生成的一個SkipList:

    3.png

    之后會詳細(xì)討論隨機(jī)化的問題,現(xiàn)在先承上啟下地梳理下信息:

    • SkipList結(jié)合了鏈表和二分查找的思想
    • 將原始鏈表和一些通過“跳躍”生成的鏈表組成層
    • 第0層是原始鏈表,越上層“跳躍”的步距越大,鏈表元素越少
    • 上層鏈表是下層鏈表的子序列
    • 查找時從頂層向下,不斷縮小搜索范圍

    最后,可以利用“鏈”的性質(zhì),減少存儲空間:

    4.png

    3. 實現(xiàn)SkipList

    這里寫的SkipList是非常naive的,有許多可優(yōu)化之處。

    3.1 定義

    首先定義SkipList中的節(jié)點:

    typedef struct SkipListNode {int key;void *data;int level;SkipListNode **next_nodes; } SkipListNode;

    key是鍵,data是值,與標(biāo)準(zhǔn)鏈表中的節(jié)點一樣;區(qū)別在“鏈”的部分,level表示節(jié)點在第幾層中,next_nodes是每層上的后繼節(jié)點——比如上面那個例子里的節(jié)點4,在第2層是T,在第1層是6,在第0層是5。

    然后來定義SkipList:

    class SkipList {public:SkipList(int max_level);~SkipList(void);void insertNode(int key, void *data);void deleteNode(int key);void *getData(int key);void displayList(void);private:int MAX_LEVEL;int RandomLevel(void);SkipListNode *head;SkipListNode *tail; };

    接口的含義還是很清楚的。構(gòu)造SkipList時給定最大層數(shù)(其實是可以讓層數(shù)動態(tài)增長的),displayList用于打印整個SkipList。

    這里假設(shè)key是不重復(fù)的,所以insertNode實現(xiàn)了插入與修改,deleteNode實現(xiàn)了刪除,getData實現(xiàn)了查找。

    3.2 構(gòu)造與析構(gòu)

    首先來看構(gòu)造函數(shù)SkipList(int max_level):

    SkipList::SkipList(int max_level) {MAX_LEVEL = max_level > 0? max_level : 1;head = new SkipListNode;tail = new SkipListNode;head->next_nodes = new SkipListNode *[MAX_LEVEL];for(int i = 0; i < MAX_LEVEL; ++i)head->next_nodes[i] = tail; }

    首先確定SkipList的最大層數(shù)MAX_LEVEL,然后生成head與tail節(jié)點,head節(jié)點顯然必須是一個MAX_LEVEL層的節(jié)點,讓head在每一層上的后繼節(jié)點都是tail。

    用圖片來表示SkipLsit(3)的話,就是:

    5.png

    析構(gòu)函數(shù)~SkipList(void)也很簡單:

    SkipList::~SkipList(void) {SkipListNode *curr = nullptr;while(head->next_nodes[0] != tail) {curr = head->next_nodes[0];head->next_nodes[0] = curr->next_nodes[0];delete curr->next_nodes;delete curr;}delete head->next_nodes;delete head;delete tail; }

    第0層的鏈表是原始鏈表,上層鏈表的節(jié)點都來自第0層,所以可以利用這個性質(zhì),沿著第0層鏈表釋放節(jié)點,注意除了釋放SkipListNode還要釋放里面的next_nodes。

    3.3 插入、刪除與查找

    SkipList的插入、刪除與查找一脈相承,理解插入后刪除與查找都很簡單。但在給出插入算法的代碼前,先讓我們想想insertNode里需要做哪些工作:

    • 標(biāo)準(zhǔn)有序鏈表插入前需要定位,通常是確定新節(jié)點的前驅(qū)節(jié)點;SkipList中一個節(jié)點至多是MAX_LEVEL層的,需要插入到MAX_LEVEL個有序鏈表里,所以要確定每層的前驅(qū)節(jié)點
    • 構(gòu)造新節(jié)點,生成小于MAX_LEVEL的隨機(jī)數(shù)k,作為新節(jié)點的層數(shù)
    • 將新節(jié)點插入到第0層到第(k-1)層的鏈表中

    概括起來還是三步走:找前驅(qū),做節(jié)點,插入鏈表。

    第一步,找前驅(qū)

    SkipListNode *update[MAX_LEVEL];SkipListNode *curr = head;for(int i = MAX_LEVEL - 1; i >= 0; --i) {if(curr->next_nodes[i] == tail || curr->next_nodes[i]->key > key)update[i] = curr;else {while(curr->next_nodes[i] != tail && curr->next_nodes[i]->key < key)curr = curr->next_nodes[i];update[i] = curr;}}

    update是前驅(qū)節(jié)點數(shù)組,curr用來迭代,初始值為head。for循環(huán)的大結(jié)構(gòu)是自頂向下遍歷每層,找到該層上新節(jié)點的前驅(qū)節(jié)點。

    重點在于if-else結(jié)構(gòu),我們來看第i層。curr只有后繼節(jié)點不是tail,而且curr第i層后繼節(jié)點的key比新節(jié)點key小的時候才會更新,所以curr滿足性質(zhì):

    curr的后繼節(jié)點是tail,或者curr->key比key小。

    假如curr的后繼節(jié)點是tail,或者curr的key比新節(jié)點的key小,curr的后繼節(jié)點比新節(jié)點的key大的話,新節(jié)點的插入位置都正好在curr后面,也就是curr是新節(jié)點在第i層的前驅(qū)節(jié)點。

    否則就需要在第i層鏈表上向后移動curr,直到curr的后繼節(jié)點是tail,或者curr的后繼節(jié)點的key大于新節(jié)點的key,也就是回到之前的情形。

    假設(shè)要在下面的SkipList里插入5,來看update數(shù)組的計算過程:

    6.png

  • i = 2
    curr進(jìn)入循環(huán)時為head,第1層后繼節(jié)點為curr->next_nodes[2]
    curr->next_nodes[2]不是tail,而且key = 4 < 5
    進(jìn)入else部分,更新curr為4號節(jié)點
    update[2] = 4號節(jié)點

  • i = 1,搜索范圍為(4, tail)
    curr進(jìn)入循環(huán)時為4號節(jié)點,第1層后繼節(jié)點為curr->next_nodes[1]
    curr->next_nodes[1]不是tail但key = 6 > 5
    進(jìn)入if部分,不更新curr
    update[1] = 4號節(jié)點

  • i = 0,搜索范圍為(4, 6)
    curr進(jìn)入循環(huán)時為4號節(jié)點,后繼節(jié)點為6號節(jié)點
    進(jìn)入if部分,不更新curr
    update[0] = 4號節(jié)點

  • 繼續(xù)之前搜索范圍的說法,搜索的過程可以看做搜索范圍(curr, curr->next_nodes[i])的收緊。初始時為(head, tail),每層的while循環(huán)里收緊下界,curr遞增,在逐層下降的for循環(huán)里收緊上界,curr->next_nodes[i]遞減。

    這里為了清晰刪除了保證key不重復(fù)的代碼,后面有完整版。

    第二步,做節(jié)點

    int level = RandomLevel();SkipListNode *temp = new SkipListNode;temp->key = key;temp->data = data;temp->level = level;temp->next_nodes = new SkipListNode *[level + 1];

    內(nèi)容非常簡單,RandomLevel()之后討論隨機(jī)化時再說,總之就是產(chǎn)生一個0到MAX_LEVEL - 1之間的隨機(jī)數(shù)。唯一的坑就是生成next_nodes是要用(level+1)而不是level,考慮level = 0的情形就明白了。

    第三步,插入鏈表

    for(int i = 0; i <= level; ++i) {temp->next_nodes[i] = update[i]->next_nodes[i];update[i]->next_nodes[i] = temp;}

    來看完整的insertNode(int key, void *data):

    void SkipList::insertNode(int key, void *data) {SkipListNode *update[MAX_LEVEL];SkipListNode *curr = head;// 尋找每一層上待插入節(jié)點之前的節(jié)點for(int i = MAX_LEVEL - 1; i >= 0; --i) {if(curr->next_nodes[i] == tail || curr->next_nodes[i]->key > key)update[i] = curr;else {while(curr->next_nodes[i] != tail && curr->next_nodes[i]->key < key)curr = curr->next_nodes[i];if(curr->next_nodes[i] != tail && curr->next_nodes[i]->key == key) {curr->next_nodes[i]->data = data;return;}update[i] = curr;}}// 生成待插入節(jié)點int level = RandomLevel();SkipListNode *temp = new SkipListNode;temp->key = key;temp->data = data;temp->level = level;temp->next_nodes = new SkipListNode *[level + 1];// 在每層上的鏈表中插入節(jié)點for(int i = 0; i <= level; ++i) {temp->next_nodes[i] = update[i]->next_nodes[i];update[i]->next_nodes[i] = temp;} }

    刪除與插入完全是對稱的,直接來看代碼:

    void SkipList::deleteNode(int key) {SkipListNode *update[MAX_LEVEL];SkipListNode *curr = head;// 尋找每一層上待刪除節(jié)點之前的節(jié)點for(int i = MAX_LEVEL - 1; i >= 0; --i) {if(curr->next_nodes[i] == tail || curr->next_nodes[i]->key > key)update[i] = nullptr;else {while(curr->next_nodes[i] != tail && curr->next_nodes[i]->key < key)curr = curr->next_nodes[i];if(curr->next_nodes[i] != tail && curr->next_nodes[i]->key == key)update[i] = curr;elseupdate[i] = nullptr;}}SkipListNode *temp = nullptr;// 在每層上的鏈表中刪除節(jié)點for(int i = 0; i < MAX_LEVEL; ++i) {if(update[i]) {temp = update[i]->next_nodes[i];update[i]->next_nodes[i] = temp->next_nodes[i];}}// 最終釋放節(jié)點if(temp) {delete temp->next_nodes;delete temp;} }

    同樣先查找前驅(qū)數(shù)組,由于節(jié)點不一定在某層中出現(xiàn),找不到時就把前驅(qū)節(jié)點標(biāo)記為nullptr,在該節(jié)點出現(xiàn)的層的鏈表里刪除該節(jié)點,最終釋放節(jié)點。

    查找就更加簡單了,從上到下遍歷,找到就返回:

    void *SkipList::getData(int key) {SkipListNode* curr = head;for(int i = MAX_LEVEL - 1; i >= 0; --i) {if(curr->next_nodes[i] == tail || curr->next_nodes[i]->key > key)continue;else {while(curr->next_nodes[i] != tail && curr->next_nodes[i]->key < key)curr = curr->next_nodes[i];if(curr->next_nodes[i] != tail && curr->next_nodes[i]->key == key)return curr->next_nodes[i]->data;}}return nullptr; }

    3.4 隨機(jī)化

    SkipList是一種概率算法,非常依賴于生成的隨機(jī)數(shù)。這里不能用rand() % MAX_LEVEL的簡單做法,而要用滿足p=1/2幾何分布的隨機(jī)數(shù)。

    來看RandomLevel()的代碼:

    int SkipList::RandomLevel(void) {int level = 0;while(rand() % 2 && level < MAX_LEVEL - 1)++level;return level; }

    這里不做太多的數(shù)學(xué)分析,只做直觀解釋。考慮MAX_LEVEL = 4的情形,可能的返回值為0、1、2、3,顯然出現(xiàn)概率分別為:

    P(0) = (1/2)^0 * (1/2) = 1/2
    P(1) = (1/2)^1 * (1/2) = 1/4
    P(2) = (1/2)^2 * (1/2) = 1/8
    P(3) = 1 - P(0) - P(1) - P(2) = 1/8

    假設(shè)有16個元素的話,可以預(yù)計第0層有16個元素,第1層約有16 - 8 = 8個元素,第2層約有16 - 8 - 4 = 4個元素,第3層約有16 - 8 -4 -2 = 2個元素,從底向上每層元素數(shù)量大約減少一半。

    SkipList層數(shù)合適時自頂向下搜索,理想情況下每下降一層,搜索范圍減小一半,達(dá)到類似二分查找的效果,效率為O(lgn);最壞情況下也只是curr從head移動到tail,效率為O(n)。

    我的實現(xiàn)里最大層數(shù)是通過MAX_LEVEL靜態(tài)指定的,也可以讓最大層數(shù)動態(tài)增長——RandomLevel里不設(shè)置最大值,插入節(jié)點時得到的level比當(dāng)前SkipList層數(shù)大時就在頂上再加一層,刪除節(jié)點時如果只有這個節(jié)點在高層就去掉高層。

    4. 參考資料

    • Skip list:
      https://en.wikipedia.org/wiki/Skip_list
    • skiplist 跳表詳解及其編程實現(xiàn)
      http://www.tuicool.com/articles/J7rQRb



    作者:kophy
    鏈接:https://www.jianshu.com/p/fcd18946994e
    來源:簡書
    簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。

    總結(jié)

    以上是生活随笔為你收集整理的深夜学算法之SkipList:让链表飞的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。