Python數(shù)據(jù)結(jié)構(gòu)與算法之跳表詳解
0. 學(xué)習(xí)目標(biāo)
在諸如單鏈表、雙線鏈表等普通鏈表中,查找、插入和刪除操作由于必須從頭結(jié)點(diǎn)遍歷鏈表才能找到相關(guān)鏈表,因此時(shí)間復(fù)雜度均為O(n)。跳表是帶有附加指針的鏈表,使用這些附加指針可以跳過(guò)一些中間結(jié)點(diǎn),用以快速完成查找、插入和刪除等操作。本節(jié)將介紹跳表的相關(guān)概念及其具體實(shí)現(xiàn)。
通過(guò)本節(jié)學(xué)習(xí),應(yīng)掌握以下內(nèi)容:
- 跳表的基本概念及其與普通鏈表的區(qū)別
- 跳表及其操作的實(shí)現(xiàn)
1. 跳表的基本概念
跳表 (Skip List) 是一種可以快速進(jìn)行查找、插入和刪除等操作的有序鏈表(鏈表中的數(shù)據(jù)項(xiàng)按照某種順序進(jìn)行排列的鏈表稱為有序鏈表)。跳表的核心思想是以更大的跨度遍歷鏈表,以便可以跳過(guò)中間結(jié)點(diǎn)。
1.1 跳表介紹
首先,讓我們回想一下如何在以下單鏈表中查找數(shù)據(jù)元素,即使鏈表中的數(shù)據(jù)已經(jīng)排好序,我們也需要從頭結(jié)點(diǎn)開始遍歷鏈表。
例如要在以上單鏈表中查找 15,查找過(guò)程為: 0-->3-->5 -->7-->10-->15。
為了提高查找的效率,我們可以抽取鏈表中的一部分結(jié)點(diǎn)建立一個(gè)起“索引”作用的鏈,稱為“索引層”或簡(jiǎn)稱“索引”:
此時(shí)我們查找 15,首先在索引層中遍歷,當(dāng)我們掃描到值為 10 的結(jié)點(diǎn)時(shí),由于下一結(jié)點(diǎn)值為 27,因此我們知道 15 應(yīng)當(dāng)在這兩個(gè)結(jié)點(diǎn)之間,然后回到原鏈表中遍歷即可找到值為 15 的結(jié)點(diǎn),遍歷過(guò)程如下圖綠色箭頭所示:
可以看到通過(guò)建立索引鏈,需要遍歷的結(jié)點(diǎn)數(shù)得以減少,為了進(jìn)一步減少所需遍歷的結(jié)點(diǎn)數(shù),可以通過(guò)對(duì)索引鏈添加索引,跳表是對(duì)鏈表疊加多級(jí)索引的數(shù)據(jù)結(jié)構(gòu)。
1.2 跳表的性能
1.3 跳表與普通鏈表的異同
我們可以將跳表理解為排序后的鏈表,但與普通鏈表相比,有以下不同點(diǎn):
- 普通鏈表中的結(jié)點(diǎn)只有一個(gè) next 指針,跳表中的結(jié)點(diǎn)有多個(gè) next 指針;
- 給定結(jié)點(diǎn)的 next 指針的數(shù)量是隨機(jī)確定的。
跳表中結(jié)點(diǎn)的每個(gè) next 指針?lè)Q為一層,結(jié)點(diǎn)中的層數(shù)稱為結(jié)點(diǎn)的大小,其決定了結(jié)點(diǎn)在鏈表中的層級(jí);在普通的有序鏈表中,插入、刪除和查找操作需要對(duì)鏈表依次遍歷,因此每個(gè)操作的時(shí)間復(fù)雜度均為O(n),跳表允許在遍歷時(shí)跳過(guò)一些中間節(jié)點(diǎn),每個(gè)操作的時(shí)間復(fù)雜度為O(㏒n)。
2. 跳表的實(shí)現(xiàn)
跳表中的每個(gè)數(shù)據(jù)元素由一個(gè)結(jié)點(diǎn)表示,結(jié)點(diǎn)的層級(jí)在結(jié)點(diǎn)插入時(shí)隨機(jī)選擇,而不考慮數(shù)據(jù)結(jié)構(gòu)中數(shù)據(jù)元素的數(shù)量。第i級(jí)結(jié)點(diǎn)有i個(gè)next 指針(在結(jié)點(diǎn)實(shí)現(xiàn)時(shí)使用大小為i的數(shù)組 next 存儲(chǔ)i個(gè) next 指針),因此我們不需要在結(jié)點(diǎn)中存儲(chǔ)結(jié)點(diǎn)的層級(jí),使用最大層數(shù) max_level 來(lái)限制層級(jí)上限。跳表的頭結(jié)點(diǎn)具有從 1 級(jí)到 max_level 級(jí)的 next 指針。
2.1 跳表結(jié)點(diǎn)類
跳表的結(jié)點(diǎn)示意圖如下所示,每個(gè)結(jié)點(diǎn)都有一組用于指向其它層級(jí)結(jié)點(diǎn)的指針域列表 next:
class Node: def __init__(self, data, level=0): self.data = data self.next = [None] * level def __str__(self): return "Node({}, {})".format(self.data, len(self.next))
next 數(shù)組用于保存指向不同層級(jí)結(jié)點(diǎn)的指針,其中每個(gè)元素就是一個(gè) next 指針,而 data 變量用于存儲(chǔ)數(shù)據(jù),重載 __str__ 方法用于便于打印結(jié)點(diǎn)對(duì)象,len(self.next) 可以用于表示結(jié)點(diǎn)中的層數(shù)。
2.2 跳表的初始化
跳表的初始化是建立一個(gè)空的帶頭結(jié)點(diǎn)的跳表,其表長(zhǎng) length 初始化為 0,此時(shí)鏈表中沒(méi)有元素結(jié)點(diǎn),只有一個(gè)頭結(jié)點(diǎn):
class SkipList: def __init__(self, max_level=8): self.max_level = max_level node = Node(Node, max_level) self.head = node self.length = 0
創(chuàng)建跳表 SkipList 對(duì)象的時(shí)間復(fù)雜度為O(1)。
2.3 獲取跳表長(zhǎng)度
求取跳表長(zhǎng)度只需要重載 __len__ 從對(duì)象返回 length 的值,因此時(shí)間復(fù)雜度為O(1):
def __len__(self): return self.length
2.4 讀取指定位置元素
由于跳表中的每個(gè)結(jié)點(diǎn)的層級(jí)是在結(jié)點(diǎn)插入時(shí)隨機(jī)選擇的,因此讀取指定位置的數(shù)據(jù)元素和單鏈表類似,需要順序遍歷第0層結(jié)點(diǎn),操作的復(fù)雜度為O(n),如果索引不在可接受的索引范圍內(nèi),將引發(fā) IndexError 異常::
def __getitem__(self, index): if index > self.length - 1 or index < 0: raise IndexError("UnrolledLinkedList assignment index out of range") else: count = 0 node = self.head.next[0] while count < index: node = node.next[0] count += 1 return node.data
2.5 查找指定元素
跳表的查找操作從最上層開始,如果待查元素 value 小于當(dāng)前層的后繼結(jié)點(diǎn)的 data,則平移至當(dāng)前層的后繼結(jié)點(diǎn);否則下移進(jìn)入下一層,繼續(xù)比較,直至第一層。
def locate(self, value): node = self.head # 由于索引從0開始,因此第i層級(jí)的索引為i-1 level = self.max_level - 1 while level >= 0: while node.next[level] != None and node.next[level].data < value: # 平移到當(dāng)前層的后繼結(jié)點(diǎn) node = node.next[level] # 下移進(jìn)入下一層 level -= 1 if node.next[0].data == value: return node.next[0] else: raise ValueError("{} is not in skip list".format(value))
2.6 在跳表中插入新元素
與普通鏈表不同,在跳表中插入新元素不能指定位置(需要保證有序性),需要通過(guò)查找確定新數(shù)據(jù)元素的插入位置。
插入元素前,首先需要確定該元素所占據(jù)的層數(shù) level k,這是通過(guò)算法隨機(jī)生成的,這樣可以減少算法實(shí)現(xiàn)的復(fù)雜度,同時(shí)也可以保證跳表性能。
def random_level(self, max_level): num = random.randint(1, 2**max_level -1) lognum = math.log(num, 2) level = int(math.floor(lognum)) return max_level - level
然后需要在 level 1, level 2, ..., level k 各層的鏈表都插入新元素,為了達(dá)到此目的,我們需要在查找插入位置時(shí)維護(hù)一個(gè)列表 update,update 中的每個(gè)元素 update[i] 指向 level i 中小于插入元素 data 的最右邊結(jié)點(diǎn),即插入位置的左側(cè),如下圖所示,插入元素 data = 9,隨機(jī)層數(shù) k=3:
def update_list(self, data): update = [None] * self.max_level node = self.head level = self.max_level - 1 while level >= 0: while node.next[level] != None and node.next[level].data < data: node = node.next[level] update[level] = node level -= 1 return update
可以看到維護(hù) update 列表的過(guò)程與查找元素類似,根據(jù) update 列表可以判斷跳表中是否已存在元素 data,如果不存在將其插入跳表中:
def insert_node(self, data): # 產(chǎn)生隨機(jī) level,并構(gòu)造結(jié)點(diǎn) level = self.random_level(self.max_level) node = Node(data, level) # 獲取 update 列表 update = self.update_list(data) # 插入結(jié)點(diǎn) if len(update) == 0 or update[0].next[0] == None or update[0].next[0].data != data: self.length += 1 for i in range(level): node.next[i] = update[i].next[i] update[i].next[i] = node
2.7 刪除跳表中指定元素
刪除指定元素與插入操作類似:
def delete_node(self, data): # 獲取 update 列表 update = self.update_list(data) if len(update) > 0: node = update[0].next[0] # 刪除指定元素結(jié)點(diǎn) if node != None and node.data == data: self.length -= 1 for i in range(len(node.next)): update[i].next[i] = node.next[i] del node
2.8 其它一些有用的操作
1.跳表指定層元素輸出
將跳表指定層轉(zhuǎn)換為字符串以便進(jìn)行打印,編寫 print_level 方法打印指定層中數(shù)據(jù)元素:
def get_level(self, level): ? ? ? ? """輔助函數(shù),用于根據(jù)給定層構(gòu)造結(jié)果字符串""" ? ? ? ? node = self.head.next[level] ? ? ? ? result = '' ? ? ? ? while node: ? ? ? ? ? ? result = result + str(node.data) + '-->' ? ? ? ? ? ? node = node.next[level] ? ? ? ? result += 'None' ? ? ? ? return result ? ? def print_level(self, level): ? ? ? ? print('level {}'.format(level)) ? ? ? ? result = self.get_level(level) ? ? ? ? print(result)
2.跳表各層元素輸出
可以借助上述輔助函數(shù) get_level,使用 str 函數(shù)調(diào)用對(duì)象上的 __str__ 方法可以創(chuàng)建適合打印的字符串表示:
def __str__(self): result = '' for i in list(range(self.max_level))[self.max_level-1:0:-1]: result = result + self.get_level(i) + '\n' result += self.get_level(0) return result
3. 跳表應(yīng)用
接下來(lái),我們將測(cè)試上述實(shí)現(xiàn)的鏈表,以驗(yàn)證操作的有效性。
3.1 跳表應(yīng)用示例
首先初始化一個(gè)跳表 sl,并在其中插入若干元素:
sl = SkipList(4) for i in range(0, 30, 3): sl.insert_node(i)
我們可以直接打印跳表中的數(shù)據(jù)元素、鏈表長(zhǎng)度等信息:
print('跳表 sl 各層元素為:') print(sl) print('跳表 sl 長(zhǎng)度為:', len(sl)) print('跳表 sl 第 2 個(gè)元素為:', sl[2])
以上代碼輸出如下:
跳表 sl 各層元素為: 9-->None 0-->9-->18-->None 0-->9-->18-->27-->None 0-->3-->6-->9-->12-->15-->18-->21-->24-->27-->None 跳表 sl 長(zhǎng)度為: 10 跳表 sl 第 2 個(gè)元素為: 6
接下來(lái),我們將演示在添加/刪除元素、以及如何查找指定元素等:
# 插入元素 sl.insert_node(14) print('在跳表 sl 中插入數(shù)據(jù) 14 后:') print(sl) # 刪除元素 sl.delete_node(14) print('刪除跳表 sl 中數(shù)據(jù) 14 后:') print(sl) # 查找數(shù)據(jù)元素 15 print('查找跳表 sl 中數(shù)據(jù)元素 15:', sl.locate(15))
以上代碼輸出如下:
在跳表 sl 中插入數(shù)據(jù) 14 后: 9-->None 0-->9-->18-->None 0-->9-->14-->18-->27-->None 0-->3-->6-->9-->12-->14-->15-->18-->21-->24-->27-->None 刪除跳表 sl 中數(shù)據(jù) 14 后: 9-->None 0-->9-->18-->None 0-->9-->18-->27-->None 0-->3-->6-->9-->12-->15-->18-->21-->24-->27-->None 查找跳表 sl 中數(shù)據(jù)元素 15: Node(15, 1)
以上就是Python數(shù)據(jù)結(jié)構(gòu)與算法之跳表詳解的詳細(xì)內(nèi)容,更多關(guān)于Python跳表的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python Pandas list列表數(shù)據(jù)列拆分成多行的方法實(shí)現(xiàn)
這篇文章主要介紹了Python Pandas list(列表)數(shù)據(jù)列拆分成多行的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12python 獲取url中的參數(shù)列表實(shí)例
今天小編就為大家分享一篇python 獲取url中的參數(shù)列表實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-12-12Python模擬登錄requests.Session應(yīng)用詳解
這篇文章主要介紹了Python模擬登錄requests.Session應(yīng)用詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11python實(shí)現(xiàn)機(jī)器學(xué)習(xí)之元線性回歸
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)機(jī)器學(xué)習(xí)之元線性回歸,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09