Python數據結構與算法之跳表詳解
0. 學習目標
在諸如單鏈表、雙線鏈表等普通鏈表中,查找、插入和刪除操作由於必須從頭結點遍歷鏈表才能找到相關鏈表,因此時間復雜度均為O(n)。跳表是帶有附加指針的鏈表,使用這些附加指針可以跳過一些中間結點,用以快速完成查找、插入和刪除等操作。本節將介紹跳表的相關概念及其具體實現。
通過本節學習,應掌握以下內容:
- 跳表的基本概念及其與普通鏈表的區別
- 跳表及其操作的實現
1. 跳表的基本概念
跳表 (Skip List) 是一種可以快速進行查找、插入和刪除等操作的有序鏈表(鏈表中的數據項按照某種順序進行排列的鏈表稱為有序鏈表)。跳表的核心思想是以更大的跨度遍歷鏈表,以便可以跳過中間結點。
1.1 跳表介紹
首先,讓我們回想一下如何在以下單鏈表中查找數據元素,即使鏈表中的數據已經排好序,我們也需要從頭結點開始遍歷鏈表。
例如要在以上單鏈表中查找 15,查找過程為: 0–>3–>5 –>7–>10–>15。
為瞭提高查找的效率,我們可以抽取鏈表中的一部分結點建立一個起“索引”作用的鏈,稱為“索引層”或簡稱“索引”:
此時我們查找 15,首先在索引層中遍歷,當我們掃描到值為 10 的結點時,由於下一結點值為 27,因此我們知道 15 應當在這兩個結點之間,然後回到原鏈表中遍歷即可找到值為 15 的結點,遍歷過程如下圖綠色箭頭所示:
可以看到通過建立索引鏈,需要遍歷的結點數得以減少,為瞭進一步減少所需遍歷的結點數,可以通過對索引鏈添加索引,跳表是對鏈表疊加多級索引的數據結構。
1.2 跳表的性能
1.3 跳表與普通鏈表的異同
我們可以將跳表理解為排序後的鏈表,但與普通鏈表相比,有以下不同點:
- 普通鏈表中的結點隻有一個 next 指針,跳表中的結點有多個 next 指針;
- 給定結點的 next 指針的數量是隨機確定的。
跳表中結點的每個 next 指針稱為一層,結點中的層數稱為結點的大小,其決定瞭結點在鏈表中的層級;在普通的有序鏈表中,插入、刪除和查找操作需要對鏈表依次遍歷,因此每個操作的時間復雜度均為O(n),跳表允許在遍歷時跳過一些中間節點,每個操作的時間復雜度為O(㏒n)。
2. 跳表的實現
跳表中的每個數據元素由一個結點表示,結點的層級在結點插入時隨機選擇,而不考慮數據結構中數據元素的數量。第i級結點有i個next 指針(在結點實現時使用大小為i的數組 next 存儲i個 next 指針),因此我們不需要在結點中存儲結點的層級,使用最大層數 max_level 來限制層級上限。跳表的頭結點具有從 1 級到 max_level 級的 next 指針。
2.1 跳表結點類
跳表的結點示意圖如下所示,每個結點都有一組用於指向其它層級結點的指針域列表 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 數組用於保存指向不同層級結點的指針,其中每個元素就是一個 next 指針,而 data 變量用於存儲數據,重載 __str__ 方法用於便於打印結點對象,len(self.next) 可以用於表示結點中的層數。
2.2 跳表的初始化
跳表的初始化是建立一個空的帶頭結點的跳表,其表長 length 初始化為 0,此時鏈表中沒有元素結點,隻有一個頭結點:
class SkipList: def __init__(self, max_level=8): self.max_level = max_level node = Node(Node, max_level) self.head = node self.length = 0
創建跳表 SkipList 對象的時間復雜度為O(1)。
2.3 獲取跳表長度
求取跳表長度隻需要重載 __len__ 從對象返回 length 的值,因此時間復雜度為O(1):
def __len__(self): return self.length
2.4 讀取指定位置元素
由於跳表中的每個結點的層級是在結點插入時隨機選擇的,因此讀取指定位置的數據元素和單鏈表類似,需要順序遍歷第0層結點,操作的復雜度為O(n),如果索引不在可接受的索引范圍內,將引發 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 小於當前層的後繼結點的 data,則平移至當前層的後繼結點;否則下移進入下一層,繼續比較,直至第一層。
def locate(self, value): node = self.head # 由於索引從0開始,因此第i層級的索引為i-1 level = self.max_level - 1 while level >= 0: while node.next[level] != None and node.next[level].data < value: # 平移到當前層的後繼結點 node = node.next[level] # 下移進入下一層 level -= 1 if node.next[0].data == value: return node.next[0] else: raise ValueError("{} is not in skip list".format(value))
2.6 在跳表中插入新元素
與普通鏈表不同,在跳表中插入新元素不能指定位置(需要保證有序性),需要通過查找確定新數據元素的插入位置。
插入元素前,首先需要確定該元素所占據的層數 level k,這是通過算法隨機生成的,這樣可以減少算法實現的復雜度,同時也可以保證跳表性能。
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 各層的鏈表都插入新元素,為瞭達到此目的,我們需要在查找插入位置時維護一個列表 update,update 中的每個元素 update[i] 指向 level i 中小於插入元素 data 的最右邊結點,即插入位置的左側,如下圖所示,插入元素 data = 9,隨機層數 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
可以看到維護 update 列表的過程與查找元素類似,根據 update 列表可以判斷跳表中是否已存在元素 data,如果不存在將其插入跳表中:
def insert_node(self, data): # 產生隨機 level,並構造結點 level = self.random_level(self.max_level) node = Node(data, level) # 獲取 update 列表 update = self.update_list(data) # 插入結點 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] # 刪除指定元素結點 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.跳表指定層元素輸出
將跳表指定層轉換為字符串以便進行打印,編寫 print_level 方法打印指定層中數據元素:
def get_level(self, level): """輔助函數,用於根據給定層構造結果字符串""" 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.跳表各層元素輸出
可以借助上述輔助函數 get_level,使用 str 函數調用對象上的 __str__ 方法可以創建適合打印的字符串表示:
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. 跳表應用
接下來,我們將測試上述實現的鏈表,以驗證操作的有效性。
3.1 跳表應用示例
首先初始化一個跳表 sl,並在其中插入若幹元素:
sl = SkipList(4) for i in range(0, 30, 3): sl.insert_node(i)
我們可以直接打印跳表中的數據元素、鏈表長度等信息:
print('跳表 sl 各層元素為:') print(sl) print('跳表 sl 長度為:', len(sl)) print('跳表 sl 第 2 個元素為:', 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 長度為: 10 跳表 sl 第 2 個元素為: 6
接下來,我們將演示在添加/刪除元素、以及如何查找指定元素等:
# 插入元素 sl.insert_node(14) print('在跳表 sl 中插入數據 14 後:') print(sl) # 刪除元素 sl.delete_node(14) print('刪除跳表 sl 中數據 14 後:') print(sl) # 查找數據元素 15 print('查找跳表 sl 中數據元素 15:', sl.locate(15))
以上代碼輸出如下:
在跳表 sl 中插入數據 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 中數據 14 後: 9-->None 0-->9-->18-->None 0-->9-->18-->27-->None 0-->3-->6-->9-->12-->15-->18-->21-->24-->27-->None 查找跳表 sl 中數據元素 15: Node(15, 1)
以上就是Python數據結構與算法之跳表詳解的詳細內容,更多關於Python跳表的資料請關註WalkonNet其它相關文章!