摘要:跳躍表的空間復(fù)雜度為。不過,二叉查找樹是有可能出現(xiàn)一種極端的情況的,就是如果插入的數(shù)據(jù)剛好一直有序,那么所有節(jié)點會偏向某一邊。例如這種接結(jié)構(gòu)會導(dǎo)致二叉查找樹的查找效率變?yōu)檫@會使二叉查找樹大打折扣。
假如我們要用某種數(shù)據(jù)結(jié)構(gòu)來維護一組有序的int型數(shù)據(jù)的集合,并且希望這個數(shù)據(jù)結(jié)構(gòu)在插入、刪除、查找等操作上能夠盡可能著快速,那么,你會用什么樣的數(shù)據(jù)結(jié)構(gòu)呢?
數(shù)組
一種很簡單的方法應(yīng)該就是采用數(shù)組了,在查找方面,用數(shù)組存儲的話,采用二分法可以在 O(logn) 的時間里找到指定的元素,不過數(shù)組在插入、刪除這些操作中比較不友好,找到目標位置所需時間為 O(logn) ,進行插入和刪除這個動作所需的時間復(fù)雜度為 O(n) ,因為都需要移動移動元素,所以最終所需要的時間復(fù)雜度為 O(n) 。 例如對于下面這個數(shù)組:
插入元素 3
鏈表
另外一種簡單的方法應(yīng)該就是用鏈表了,鏈表在插入、刪除的支持上就相對友好,當(dāng)我們找到目標位置之后,插入、刪除元素所需的時間復(fù)雜度為 O(1) ,注意,我說的是找到目標位置之后,插入、刪除的時間復(fù)雜度才為O(1)。
但鏈表在查找上就不友好了,不能像數(shù)組那樣采用二分查找的方式,只能一個一個結(jié)點遍歷,所以加上查找所需的時間,插入、刪除所需的總的時間復(fù)雜度為O(n)。
假如我們能夠提高鏈表的查找效率,使鏈表的查找的時間復(fù)雜度盡可能接近 O(logn) ,那鏈表將會是很棒的選擇。
提高鏈表的查找速度
那鏈表的查找速度可以提高嗎?
對于下面這個鏈表
假如我們要查找元素9,按道理我們需要從頭結(jié)點開始遍歷,一共遍歷8個結(jié)點才能找到元素9。能否采取某些策略,讓我們遍歷5次以內(nèi)就找到元素9呢?請大家花一分鐘時間想一下如何實現(xiàn)?
由于元素的有序的,我們是可以通過增加一些路徑來加快查找速度的。例如
通過這種方法,我們只需要遍歷5次就可以找到元素9了(紅色的線為查找路徑)。
還能繼續(xù)加快查找速度嗎?
答是可以的,再增加一層就行了,這樣只需要4次就能找到了,這就如同我們搭地鐵的時候,去某個站點時,有快線和慢線幾種路線,通過快線 + 慢線的搭配,我們可以更快著到達某個站點。
當(dāng)然,還能在增加一層,
基于這種方法,對于具有 n 個元素的鏈表,我們可以采取 ** (logn + 1) 層指針路徑的形式**,就可以實現(xiàn)在 O(logn) 的時間復(fù)雜度內(nèi),查找到某個目標元素了,這種數(shù)據(jù)結(jié)構(gòu),我們也稱之為跳躍表,跳躍表也可以算是鏈表的一種變形,只是它具有二分查找的功能。
插入與刪除
上面例子中,9個結(jié)點,一共4層,可以說是理想的跳躍表了,不過隨著我們對跳躍表進行插入/刪除結(jié)點的操作,那么跳躍表結(jié)點數(shù)就會改變,意味著跳躍表的層數(shù)也會動態(tài)改變。
這里我們面臨一個問題,就是新插入的結(jié)點應(yīng)該跨越多少層?
這個問題已經(jīng)有大牛替我們解決好了,采取的策略是通過拋硬幣來決定新插入結(jié)點跨越的層數(shù):每次我們要插入一個結(jié)點的時候,就來拋硬幣,如果拋出來的是正面,則繼續(xù)拋,直到出現(xiàn)負面為止,統(tǒng)計這個過程中出現(xiàn)正面的次數(shù),這個次數(shù)作為結(jié)點跨越的層數(shù)。
通過這種方法,可以盡可能著接近理想的層數(shù)。大家可以想一下為啥會這樣呢?
插入
例如,我們要插入結(jié)點 3,4,通過拋硬幣知道3,4跨越的層數(shù)分別為 0,2 (層數(shù)從0開始算),則插入的過程如下:
插入 3,跨越0層。
插入 4,跨越2層。
刪除
解決了插入之后,我們來看看刪除,刪除就比較簡單了,例如我們要刪除4,那我們直接把4及其所跨越的層數(shù)刪除就行了。
小結(jié)
跳躍表的插入與刪除至此都講完了,總結(jié)下跳躍表的有關(guān)性質(zhì):
(1). 跳躍表的每一層都是一條有序的鏈表.
(2). 跳躍表的查找次數(shù)近似于層數(shù),時間復(fù)雜度為O(logn),插入、刪除也為 O(logn)。
(3). 最底層的鏈表包含所有元素。
(4). 跳躍表是一種隨機化的數(shù)據(jù)結(jié)構(gòu)(通過拋硬幣來決定層數(shù))。
(5). 跳躍表的空間復(fù)雜度為 O(n)。
跳躍表 vs 二叉查找樹
有人可能會說,也可以采用二叉查找樹啊,因為查找查找樹的插入、刪除、查找也是近似 O(logn) 的時間復(fù)雜度。
不過,二叉查找樹是有可能出現(xiàn)一種極端的情況的,就是如果插入的數(shù)據(jù)剛好一直有序,那么所有節(jié)點會偏向某一邊。例如
這種接結(jié)構(gòu)會導(dǎo)致二叉查找樹的查找效率變?yōu)?O(n),這會使二叉查找樹大打折扣。
跳躍表 vs 紅黑樹
紅黑可以說是二叉查找樹的一種變形,紅黑在查找,插入,刪除也是近似O(logn)的時間復(fù)雜度,但學(xué)過紅黑樹的都知道,紅黑樹比跳躍表復(fù)雜多了,反正我是被紅黑樹虐過。在選擇一種數(shù)據(jù)結(jié)構(gòu)時,有時候也是需要考慮學(xué)習(xí)成本的。
而且紅黑樹插入,刪除結(jié)點時,是通過調(diào)整結(jié)構(gòu)來保持紅黑樹的平衡,比起跳躍表直接通過一個隨機數(shù)來決定跨越幾層,在時間復(fù)雜度的花銷上是要高于跳躍表的。
當(dāng)然,紅黑樹并不是一定比跳躍表差,在有些場合紅黑樹會是更好的選擇,所以選擇一種數(shù)據(jù)結(jié)構(gòu),關(guān)鍵還得看場合。
總上所述,維護一組有序的集合,并且希望在查找、插入、刪除等操作上盡可能快,那么跳躍表會是不錯的選擇。redis 中的數(shù)據(jù)數(shù)據(jù)便是采用了跳躍表,當(dāng)然,ridis也結(jié)合了哈希表等數(shù)據(jù)結(jié)構(gòu),采用的是一種復(fù)合數(shù)據(jù)結(jié)構(gòu)。
代碼如下
package skiplist; //節(jié)點 class Node{ int value = -1; int level;//跨越幾層 Node[] next;//指向下一個節(jié)點 public Node(int value, int level) { this.value = value; this.level = level; this.next = new Node[level]; } } //跳躍表 public class SkipList { //允許的最大層數(shù) int maxLevel = 16; //頭節(jié)點,充當(dāng)輔助。 Node head = new Node(-1, 16); //當(dāng)前跳躍表節(jié)點的個數(shù) int size = 0; //當(dāng)前跳躍表的層數(shù),初始化為1層。 int levelCount = 1; public Node find(int value) { Node temp = head; for (int i = levelCount - 1; i >= 0; i--) { while (temp.next[i] != null && temp.next[i].value < value) { temp = temp.next[i]; } } //判斷是否有該元素存在 if (temp.next[0] != null && temp.next[0].value == value) { System.out.println(value + " 查找成功"); return temp.next[0]; } else { return null; } } // 為了方便,跳躍表在插入的時候,插入的節(jié)點在當(dāng)前跳躍表是不存在的 //不允許插入重復(fù)數(shù)值的節(jié)點。 public void insert(int value) { int level = getLevel(); Node newNode = new Node(value, level); //update用于記錄要插入節(jié)點的前驅(qū) Node[] update = new Node[level]; Node temp = head; for (int i = level - 1; i >= 0; i--) { while (temp.next[i] != null && temp.next[i].value < value) { temp = temp.next[i]; } update[i] = temp; } //把插入節(jié)點的每一層連接起來 for (int i = 0; i < level; i++) { newNode.next[i] = update[i].next[i]; update[i].next[i] = newNode; } //判斷是否需要更新跳躍表的層數(shù) if (level > levelCount) { levelCount = level; } size++; System.out.println(value + " 插入成功"); } public void delete(int value) { Node[] update = new Node[levelCount]; Node temp = head; for (int i = levelCount - 1; i >= 0; i--) { while (temp.next[i] != null && temp.next[i].value < value) { temp = temp.next[i]; } update[i] = temp; } if (temp.next[0] != null && temp.next[0].value == value) { size--; System.out.println(value + " 刪除成功"); for (int i = levelCount - 1; i >= 0; i--) { if (update[i].next[i] != null && update[i].next[i].value == value) { update[i].next[i] = update[i].next[i].next[i]; } } } } //打印所有節(jié)點 public void printAllNode() { Node temp = head; while (temp.next[0] != null) { System.out.println(temp.next[0].value + " "); temp = temp.next[0]; } } //模擬拋硬幣 private int getLevel() { int level = 1; while (true) { int t = (int)(Math.random() * 100); if (t % 2 == 0) { level++; } else { break; } } System.out.println("當(dāng)前的level = " + level); return level; } //測試數(shù)據(jù) public static void main(String[] args) { SkipList list = new SkipList(); for (int i = 0; i < 6; i++) { list.insert(i); } list.printAllNode(); list.delete(4); list.printAllNode(); System.out.println(list.find(3)); System.out.println(list.size + " " + list.levelCount); } }
如果你覺得不錯,不妨點個贊讓更多人看到這篇文章,感激不盡。
最后推廣下我的公眾號:苦逼的碼農(nóng),文章都會首發(fā)于我的公眾號,公眾號已有100多篇原創(chuàng)文章,期待各路英雄的關(guān)注交流。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/6673.html
摘要:同時,在元類中,我們還需要加上一個判斷,只有在這個類創(chuàng)建時才需要控制其類的生成,其他的就不需要了。完整代碼后臺回復(fù)元類獲取原創(chuàng)不易,如果文章對你有用的話,點贊留言轉(zhuǎn)發(fā)是對我的最大支持日常學(xué)代碼不止,還有美和樂趣 我之前在深入理解python中的類和對象中說過,python中的類也是一個對象,可以說是類對象,可以由type來創(chuàng)建類對象的。有了這個知識我們先看看下面這個函數(shù): showIm...
摘要:所謂高并發(fā),就是同一時間有很多流量通常指用戶訪問程序的接口頁面及其他資源,解決高并發(fā)就是當(dāng)流量峰值到來時保證程序的穩(wěn)定性。索引多主多從分布式數(shù)據(jù)庫緩存連接池消息隊列等是從數(shù)據(jù)庫方便考慮如何優(yōu)化性能。 所謂高并發(fā),就是同一時間有很多流量(通常指用戶)訪問程序的接口、頁面及其他資源,解決高并發(fā)就是當(dāng)流量峰值到來時保證程序的穩(wěn)定性。 我們一般用QPS(每秒查詢數(shù),又叫每秒請求數(shù))來衡量程序的...
摘要:如果你只是簡單羅列出這幾個鉤子函數(shù)的名稱,不具體深入闡述的話,你這樣的回答很難令面試官滿意。那么接下來,閏土大叔將手摸手教你如何深入淺出地說出令面試官滿意的有亮點的回答。 當(dāng)面試官問:談?wù)勀銓ue的生命周期的理解,聽到這句話你是不是心里暗自竊喜:這也太容易了吧,不就是beforeCreate、created、beforeMount、mounted、beforeUpdate、updat...
摘要:不同目標的自動化測試有不同的測試工具,但是任何工具都無不例外的需要編程的過程,實現(xiàn)源代碼,也可以稱之為測試腳本。 寫在最前面:目前自動化測試并不屬于新鮮的事物,或者說自動化測試的各種方法論已經(jīng)層出不窮,但是,能夠在項目中持之以恒的實踐自動化測試的團隊,卻依舊不是非常多。有的團隊知道怎么做,做的還不夠好;有的團隊還正在探索和摸索怎么做,甚至還有一些多方面的技術(shù)上和非技術(shù)上的舊系統(tǒng)需要重構(gòu)……...
摘要:所以,雅虎的開發(fā)人員就試圖開發(fā)一個通用的無單點問題的分布式協(xié)調(diào)框架,以便讓開發(fā)人員將精力集中在處理業(yè)務(wù)邏輯上。在立項初期,考慮到之前內(nèi)部很多項目都是使用動物的名字來命名的例如著名的項目雅虎的工程師希望給這個項目也取一個動物的名字。 前言 提到ZooKeeper,相信大家都不會陌生。Dubbo,Kafka,Hadoop等等項目里都能看到它的影子。但是你真的了解 ZooKeeper 嗎?如...
閱讀 4151·2021-11-23 10:09
閱讀 1410·2021-11-23 09:51
閱讀 3041·2021-11-23 09:51
閱讀 1714·2021-09-07 09:59
閱讀 2438·2019-08-30 15:55
閱讀 2380·2019-08-30 15:55
閱讀 3026·2019-08-30 15:52
閱讀 2628·2019-08-26 17:04