Python-資料結構與演算法(十一、字典(對映)——基於兩種不同的底層實現)
阿新 • • 發佈:2018-11-09
保證一週更兩篇吧,以此來督促自己好好的學習!程式碼的很多地方我都給予了詳細的解釋,幫助理解。好了,幹就完了~加油!
宣告:本python資料結構與演算法是imooc上liuyubobobo老師java資料結構的python改寫,並添加了一些自己的理解和新的東西,liuyubobobo老師真的是一位很棒的老師!超級喜歡他~
如有錯誤,還請小夥伴們不吝指出,一起學習~
No fears, No distractions.
一、字典簡述
- 字典(也叫對映)
- 結構特點:儲存(鍵,值)資料對的資料結構(Key,Value)。根據鍵(Key),來尋找值(Value)
- 應用:花名冊(學號 -> 人)、車輛管理(車牌號 -> 車)、資料庫(id -> 資訊)、詞頻統計(單詞 -> 頻率)
- 字典容易使用二分搜尋樹或者連結串列來實現,比如用二分搜尋樹來實現,相應的更改一下Node包含的元素就好了,比如:
用二分搜尋樹實現時:
# 這裡為了更清楚的展示Node類,用C++來寫的。
class Node{
K key;
V value;
Node* left;
Node* right
}
用連結串列實現時:
class Node{
K key;
V value;
Node* next;
}
- 字典介面:
void add(K, V) 新增元素
V remove(K) 刪除元素(Key充當索引的作用)
bool contains(K) 是否包含鍵值為K的元素
V get(K) 獲取鍵值為K的Value
int getSize() 獲取字典的大小
bool isEmpty() 判斷字典是否為空 - 基於二分搜尋樹的字典是有順序性的,而基於連結串列實現的字典是無順序性的,但是一般不用連結串列來實現無順序性的字典,而用雜湊表,雜湊表後面會講到。
二、基於二分搜尋樹的集合
1. 實現
# -*- coding: utf-8 -*-
# Author: Annihilation7
# Data: 2018-10-20 10:30am
# Python version: 3.6
from queue.loopqueue import LoopQueue # 會用到我們以前實現的迴圈佇列
class Node:
def __init__(self, key=None, value=None):
"""
Description: 節點類的建構函式
"""
self.key = key # 鍵
self.value = value # value
self.left = None # 指向左孩子的標籤
self.right = None # 指向右孩子的標籤
class BstDict:
def __init__(self):
"""
Description: 基於二分搜尋樹的字典類的建構函式
"""
self._root = None # 初始化根節點為None
self._size = 0 # 初始化self._size
def getSize(self):
"""
Description: 獲取字典內有效元素的個數
Returns:
有效元素的個數
"""
return self._size
def isEmpty(self):
"""
Description: 判斷字典是否為空
Returns:
bool值,空為True
"""
return self._size == 0
def add(self, key, value):
"""
Description: 向字典中新增鍵-值對,若鍵已經存在字典中,那就更新這個鍵所對應的value
時間複雜度:O(logn)
Params:
- key: 待新增的鍵
-value: 待新增的鍵所對應的value
"""
self._root = self._add(self._root, key, value) # 呼叫self._add方法,常規套路
def contains(self, key):
"""
Description: 檢視字典的鍵中是否包含key
時間複雜度:O(logn)
Params:
- key: 待查詢的鍵值
Returns:
bool值,存在為True
"""
return self._getNode(self._root, key) is not None # 呼叫self._getNode私有函式,看返回值是否是None
def get(self, key):
"""
Description: 得到字典中鍵為key的value值
時間複雜度:O(logn)
Params:
- key: 待查詢的鍵值
Returns:
相應的value值。若key不存在,就返回None
"""
node = self._getNode(self._root, key) # 拿到鍵為key的Node
if node: # 如果該Node不是None
return node.value # 返回對應的value
else:
return None # 否則(此時不存在攜帶key的Node)返回None
def set(self, key, new_value):
"""
Description: 將字典中鍵為key的Node的value設為new_value。注意,為防止與add函式發生混淆,
此函式預設使用者已經確信key在字典中,否則報錯。並不會有什麼新建Node的操作,因為這麼做為與add函式有相同的功能,就沒有意義了。
時間複雜度:O(logn)
Params:
- key: 將要被設定的Node的鍵
- new_value: 新的value值
"""
node = self._getNode(self._root, key)
if node is None:
raise Exception('%s doesn\'t exist!' % key) #報錯就完事了
node.value = new_value
def remove(self, key):
"""
Description: 將字典中鍵為key的Node刪除。注:若不存在攜帶key的Node,返回Node就好。這個remove函式和前面的二分搜尋樹的remove函式極為相似,不理解的可以去前面看一下
時間複雜度:O(logn)
Params:
- key: 待刪除的鍵
Returns:
被刪除節點的value
"""
ret = self._minimum(self._root) # 呼叫self._minimum函式儲存攜帶最小值的節點,便於返回
self._root = self._remove(self._root, key) # 呼叫self._remove函式
return ret.value # 返回攜帶最小值節點的value
def printBstDict(self):
"""對字典進行列印操作,這裡使用廣度優先遍歷,因為比較直觀"""
if self._root is None:
return
queue = LoopQueue()
queue.enqueue(self._root)
while not queue.isEmpty():
node = queue.dequeue()
print('[Key: %s, Value: %s]' % (node.key, node.value))
if node.left:
queue.enqueue(node.left)
if node.right:
queue.enqueue(node.right)
# private method
def _add(self, node, key, value):
"""
Description: 向以node為根的二叉搜尋樹中新增鍵-值對
Params:
- node: 根節點
- key: 待新增的鍵
-value: 待新增的鍵所對應的value
Returns:
新增後新的根節點
"""
getNode = self._getNode(self._root, key) # 先判斷字典中是否存在攜帶這個鍵的Node
if getNode is not None: # 如果已經存在
getNode.value = value # 將這個節點的value設為新傳入的value就好
return node # 返回根節點,不需要維護self._size哦
if node is None: # 遞迴到底的情況,該新增節點啦
self._size += 1 # 維護self._size
return Node(key, value) # 將新節點返回
if key < node.key: # 待新增key小於node的key,向左子樹繼續前進。這裡的操作在二叉搜尋樹中詳細講過,就不BB了
node.left = self._add(node.left, key, value)
elif key > node.key: # 待新增key大於node的key,向右子樹繼續前進。
node.right = self._add(node.right, key, value)
return node # 將node返回,這樣在遞迴的迴歸過程中逐級返回,最終返回的是根節點
def _remove(self, node, key):
"""
Description: 將以node為根節點的字典中鍵為key的Node刪除。注:若不存在攜帶key的Node,返回Node就好。
時間複雜度:O(logn)
Params:
- node: 以node為根節點的字典
- key: 待刪除的節點所攜帶的鍵
Returns:
刪除操作後的根節點
"""
if node is None: # 到頭了都沒找到key
return None # 直接返回None
if key < node.key: # 待刪除key小於當前Node的key
node.left = self._remove(node.left, key) # 往左子樹遞迴
return node
elif node.key < key: # 待刪除key大於當前Node的key
node.right = self._remove(node.right, key) # 網右子樹遞迴
return node
else: # 此時待刪除key和當前Node的key相等,別忘了有三種情況
if node.left is None: # 左子樹為空的情況
right_node = node.right # 記錄當前Node的右孩子
node.right = None # 當前Node的右孩子設為None,便於垃圾回收
self._size -= 1 # 維護self._size
return right_node # 將被刪除Node的右孩子返回,即用右孩子來代替被刪除節點的位置
elif node.right is None: # 右子樹為空的情況,大同小異,不再贅述
left_node = node.left
node.left = None
self._size -= 1
return left_node
else: # 這裡不再贅述了,不清楚的話回去看看二分搜尋樹那章,一樣的操作
successor = self._minimum(node.right)
successor.right = self._removeMin(node.right)
self._size += 1
successor.left = node.left
node.left = node.right = None
self._size -= 1
return successor
def _minimum(self, node):
"""
Description: 返回以node為根的二分搜尋樹的攜帶最小值的節點,下面的這些操作在二分搜尋樹中均有涉及,如果看不懂回到那一章再理解下就好了
Params:
- node: 根節點
Returns:
攜帶最小值的節點
"""
if node.left is None: # 遞迴到底的情況,node的左孩子已經為空了
return node # 返回node
return self._minimum(node.left) # 否則往左子樹繼續擼
def _removeMin(self, node):
"""
Description: 刪除以node為根的二分搜尋樹的攜帶最小值的節點,同理,返回刪除後的根節點
Params:
- node: 根節點
Returns:
返回刪除操作後的樹的根節點
"""
if node.left is None: # 遞迴到底的情況,node的左孩子已經為空
right_node = node.right # 記錄node的右孩子,為None也無所謂的
node.right = None # 將node的右孩子設為None,使其與樹斷絕聯絡,從而被垃圾回收
self._size -= 1 # 維護self._size
return right_node # 返回node的右孩子,即用右孩子來代替當前的node
node.left = self._removeMin(node.left) # 否則往node的左子樹繼續擼
return node # 返回node,從而最終返回根節點
def _getNode(self, node, key):
"""
Description: 設定一個根據key來找到以node為根的二叉搜尋樹的相對應的Node的私有函式,便於方便的實現其他函式
Params:
- node: 傳入的根節點
- key: 帶查詢的key
Returns:
攜帶key的節點Node, 若不存在攜帶key的節點,返回None
"""
if node is None: # 都到底了還沒有找到,直接返回None
return None
# 注意我們的二分搜尋樹依舊不包含重複的鍵哦~這也是字典的基本特點
if key < node.key: # 待查詢的key小於當前節點的key,向左子樹遞迴就完事了
return self._getNode(node.left, key)
elif node.key < key: # 待查詢的key大於當前節點的key,向右子樹遞迴就完事了
return self._getNode(node.right, key)
else: # 此時帶查詢的key和node.key是相等的,直接返回這個Node就好
return node
2. 測試
from DICT.bstdict import BstDict # 我們的基於二分搜尋樹的字典類寫在bstdict.py檔案中
test_bst = BstDict()
print('初始Size: ', test_bst.getSize())
print('是否為空?', test_bst.isEmpty())
add_list = [15, 4, 25, 22, 3, 19, 23, 7, 28, 24]
print('待新增元素:', add_list)
for add_elem in add_list:
test_bst.add(add_elem, str(add_elem) + 'x')
##################################################
# 15(15x) #
# / \ #
# 4(4x) 25(x) #
# / \ / \ #
# 3(3x) 7(7x) 22(22x) 28(28x) #
# / \ #
# 19(19x) 23(23x) #
# \ #
# 24(24x) #
##################################################
print('新增元素後列印集合:')
test_bst.printBstDict()
print('此時的Size: ', test_bst.getSize())
print('字典中是否包含鍵23?', test_bst.contains(23))
print('獲取鍵23的value:', test_bst.get(23))
print('將鍵為7的value設為"努力學習"並列印:')
test_bst.set(7, '努力學習')
test_bst.printBstDict()
print('刪除鍵22並列印:')
test_bst.remove(22)
test_bst.printBstDict()
print('此時的Size:', test_bst.getSize())
3. 輸出
初始Size: 0
是否為空? True
待新增元素: [15, 4, 25, 22, 3, 19, 23, 7, 28, 24]
新增元素後列印集合:
[Key: 15, Value: 15x]
[Key: 4, Value: 4x]
[Key: 25, Value: 25x]
[Key: 3, Value: 3x]
[Key: 7, Value: 7x]
[Key: 22, Value: 22x]
[Key: 28, Value: 28x]
[Key: 19, Value: 19x]
[Key: 23, Value: 23x]
[Key: 24, Value: 24x]
此時的Size: 10
字典中是否包含鍵23? True
獲取鍵23的value: 23x
將鍵為7的value設為"努力e學習"並列印:
[Key: 15, Value: 15x]
[Key: 4, Value: 4x]
[Key: 25, Value: 25x]
[Key: 3, Value: 3x]
[Key: 7, Value: 努力學習]
[Key: 22, Value: 22x]
[Key: 28, Value: 28x]
[Key: 19, Value: 19x]
[Key: 23, Value: 23x]
[Key: 24, Value: 24x]
刪除鍵22並列印:
[Key: 15, Value: 15x]
[Key: 4, Value: 4x]
[Key: 25, Value: 25x]
[Key: 3, Value: 3x]
[Key: 7, Value: 努力學習]
[Key: 23, Value: 23x]
[Key: 28, Value: 28x]
[Key: 19, Value: 19x]
[Key: 24, Value: 24x]
此時的Size: 9
三、基於連結串列的字典
1. 實現
# -*- coding: utf-8 -*-
# Author: Annihilation7
# Data: 2018-10-19 7:29 pm
# Python version: 3.6
class Node:
def __init__(self, key=None, value=None, next=None):
"""
Description: 節點的建構函式
Params:
- key: 傳入的鍵值,預設為None
- value: 傳入的鍵所對應的value值,預設為None
- next: 指向下一個Node的標籤,預設為None
"""
self.key = key
self.value = value
self.next = next # 下一個節點為None
class LinkedListDict:
"""以連結串列作為底層資料結構的字典類"""
def __init__(self):
"""
Description: 字典的建構函式
"""
self._dummyhead = Node() # 建立一個虛擬頭結點,前面講過,不再贅述
self._size = 0 # 字典中有效元素的個數
def getSize(self):
"""
Description: 獲取字典中有效元素的個數
Returns:
有效元素的個數
"""
return self._size
def isEmpty(self):
"""
Description: 判斷字典是否為空
Returns:
bool值,空為True
"""
return self._size == 0
def contains(self, key):
"""
Description: 檢視字典的鍵中是否包含key
時間複雜度:O(n)
Params:
- key: 待查詢的鍵值
Returns:
bool值,存在為True
"""
return self._getNode(key) is not None # 呼叫self._getNode私有函式,看返回值是否是None
def get(self, key):
"""
Description: 得到字典中鍵為key的value值
時間複雜度:O(n)
Params:
- key: 待查詢的鍵值
Returns:
相應的value值。若key不存在,就返回None
"""
node = self._getNode(key) # 拿到鍵為key的Node
if node: # 如果該Node不是None
return node.value # 返回對應的value
else:
return None # 否則(此時不存在攜帶key的Node)返回None
def add(self, key, value):
"""
Description: 向字典中新增key,value鍵值對。若字典中已經存在相同的key,更新其value,否咋在頭部新增Node,因為時間複雜度為O(1)
時間複雜度:O(n)
Params:
- key: 待新增的鍵值
- value: 待新增的鍵值的value
"""
node = self._getNode(key) # 先判斷字典中是否存在這個鍵
if node != None: # 已經存在
node.value = value # 更新這個Node的value
else:
self._dummyhead.next = Node(key, value, self._dummyhead.next) # 否則在頭部新增,新增操作連結串列那一章有講,這裡不再贅述
self._size += 1 # 維護self._size
def set(self, key, new_value):
"""
Description: 將字典中鍵為key的Node的value設為new_value。注意,為防止與add函式發生混淆,
此函式預設使用者已經確信key在字典中,否則報錯。並不會有什麼新建Node的操作,因為這麼做為與add函式有相同的功能,就沒有意義了。
時間複雜度:O(n)
Params:
- key: 將要被設定的Node的鍵
- new_value: 新的value值
"""
node = self._getNode(key) # 找到攜帶這個key的Node
if node is None: # 沒找到
raise Exception('%s doesn\'t exist!' % key) #報錯就完事了
node.value = new_value # 找到了就直接將返回節點的value設為new_value
def remove(self, key):
"""
Description: 將字典中鍵為key的Node刪除。注:若不存在攜帶key的Node,返回Node就好。
時間複雜度:O(n)
Params:
- key: 待刪除的鍵
Returns:
被刪除節點的value
"""
pre = self._dummyhead # 找到要被刪除節點的前一個節點(慣用手法,不再贅述)
while pre.next is not None: # pre的next只要不為空
if pre.next.key == key: # 如果找到了
break # 直接break,此時pre停留在要被刪除節點的前一個節點
pre = pre.next # 否則往後擼
if pre.next is not None: # 此時找到了
delNode = pre.next # 記錄一下要被刪除的節點,方便返回其value
pre.next = delNode.next # 不再贅述,如果不懂就去看看連結串列那節吧。O(∩_∩)O
delNode.next = None # delNode的下一個節點設為None,使delNode完全與字典脫離,便於垃圾回收器回收
self._size -= 1 # 維護self._size
return delNode.value # 返回被刪除節點的value
return None # 此時pre的next是None!說明並沒有找到這個key,返回None就好了。
def printLinkedListDict(self):
"""列印字典元素"""
cur = self._dummyhead.next
while cur != None:
print('[Key: %s, Value: %s]' % (cur.key, cur.value), end='-->')
cur = cur.next
print('None')
# private functions
def _getNode(self, key):
"""
Description: 一個輔助函式,是私有函式。功能就是返回要查詢的鍵的Node,若key不存在就返回None
時間複雜度:O(n)
Params:
- key: 要查詢的鍵值
Returns:
返回帶查詢key的節點,若不存在返回None
"""
cur = self._dummyhead.next # 記錄當前節點
while cur != None: # cur沒到頭
if cur.key == key: # 找到了
return cur