07_Python演算法+資料結構筆記-連結串列總結-雜湊表-樹-二叉樹-二叉搜尋樹
阿新 • • 發佈:2020-12-23
技術標籤:Python演算法+資料結構筆記python資料結構與演算法
b站視訊:路飛IT學城
https://www.bilibili.com/video/BV1mp4y1D7UP
文章目錄
個人部落格
https://blog.csdn.net/cPen_web
#61連結串列總結
###### 連結串列——複雜度分析
# 順序表(列表/陣列)與 連結串列
# 按元素值查詢 #注:複雜度都是O(n)
# 按下標查詢 #注:順序錶快 順序表O(1),連結串列O(n)
# 在某元素後插入 #注:順序表O(n) ,連結串列O(1)
# 刪除某元素 #注:順序表O(n) ,連結串列O(1)
#注:順序表:Python裡的列表 ,C裡的陣列 是 挨個存的
# 順序表(列表/陣列)與 連結串列
# 按元素值查詢
# 注:複雜度都是O(n),一個一個查詢,找到為止 (順序表 不考慮排序)
# 按下標查詢
# 注:順序錶快 順序表O(1),連結串列O(n)因為下標是10,得從頭開始數
# 在某元素後插入 順序表O(n) ,連結串列O(1)
# 刪除某元素 順序表O(n) ,連結串列O(1)
#注:連結串列相對於 順序表,在插入和刪除 2方面非常快
###### 連結串列與順序表
# 連結串列在插入和刪除的操作上明顯快於順序表
# 連結串列的記憶體可以更靈活的分配
#注:連結串列節點通過指標/next串起來,是分開的記憶體,而其他語言佇列滿了就沒辦法了
# 試利用連結串列重新實現棧和佇列
# 連結串列這種鏈式儲存的資料結構對樹和圖的結構有很大的啟發性
#62雜湊表
#注:雜湊表(散列表)
#注:Python的字典、集合 都是用雜湊表 實現的
###### 雜湊表
# 雜湊表一個通過雜湊函式來計算資料存 儲位置的資料結構,通常支援如下操作:
# insert(key, value):插入鍵值對(key,value)
# get(key):如果存在鍵為key的鍵值對則返回其value,否則返回空值
# delete(key):刪除鍵為key的鍵值對
#注:這個操作 就是字典的操作
#注:雜湊表有key、有value。沒有value 只有key 那就是集合(而且key不能重複)
###### 直接定址表
# 當關鍵字的全域U比較小時,直接定址是一種簡單而有效的方法。
#注:左邊的圓 是 U全域 和 key。根據這個U建了一個T 列表
#注:插入很快,查詢很快,刪除很快(把元素置成空)
# 直接定址技術缺點:
# ·當域U很大時,需要消耗大量記憶體,很不實際 #注:T列表的大小 根據U的,很大的U 列表很大 浪費記憶體
# ·如果域U很大而實際出現的key很少,則大量空間被浪費 #注:U很大,但字典值很少,浪費記憶體
# ·無法處理關鍵字不是數字的情況 #注:key是字串 有問題
#注:直接定址表 上面 + 雜湊函式,就成為 雜湊表
###### 雜湊
# 直接定址表:key為k的元素放到k位置上 #注:比如 key是2的 就放到2的位置上。雜湊不是
# 改進直接定址表:雜湊(Hashing)
# ·構建大小為m的定址表T
# ·key為k的元素放到h(k)位置上
# ·h(k)是一個函式,其將域U對映到表T[0,1,...,m-1] #注:即 輸出的值 是0到m-1,m是T列表的長度
#注:之前T 是根據U的大小建的,現在T 自己選個大小。之前 key是k的元素放到k的位置上,現在key為k的元素放到h(k)位置上
#注:h是個函式,能傳的引數 是U域裡所有的值,輸出的值是 0到m-1, m是T列表的大小
#注:h是雜湊函式
###### 雜湊表
# 雜湊表(Hash Table,又稱為散列表),是一種線性表的儲存結構。雜湊表由一個直接定址表和一個雜湊函式組成。雜湊函式h(k)將元素關鍵字k作為自變數,返回元素的儲存下標。
#注:雜湊表是線性的,不是圖 也不是樹
# 假設有一個長度為7的雜湊表,雜湊函式h(k)=k%7。元素集合{14,22,3,5}的儲存方式如下圖。
# 注:如果是直接定址表,14存不了,只能存0到6
# 14 % 7 = 0 存到 0這個位置
# 22 % 7 = 1 存到1這個位置
# 3……、5……
# #注:雜湊函式h(k)的取值範圍 只能是0到6 (因為取餘)
# #注:這就是非常經典的雜湊函式,除法雜湊
#注:再插入一個0 怎麼辦? 0 這個位置 已經有值了
###### 雜湊衝突
# 由於雜湊表的大小是有限的,而要儲存的值的總數量是無限的,因此對於任何雜湊函式,都會出現兩個不同元素對映到同一個位置上的情況,這種情況叫做雜湊衝突。
# 比如h(k)=k%7, h(0)=h(7)=h(14)=...
#注:雜湊衝突一定會存在,因為開的雜湊表 大小是有限的,肯定會有雜湊函式2個key 對應1個位置。
#注:雜湊函式 2個key 都到了一個位置上,這種現象叫做雜湊衝突
###### 解決雜湊衝突 -- 開放定址法
# 開放定址法:如果雜湊函式返回的位置已經有值,則可以向後探查新的位置來儲存這個值。
# 線性探查:如果位置i被佔用,則探查i+1, i+2,……
# 二次探查:如果位置i被佔用,則探查i+1^2 ,i-1^2 ,i+2^2 ,i-2^2 ,……
# 二度雜湊:有n個雜湊函式,當使用第1個雜湊函式h1發生衝突時,則嘗試使用h2, h3,……
#注:如果插入 有值 ,插入到其他地方。線性探查:i+1……;二次探查;二度雜湊
#-----------
### 注:開放定址法的 線性查詢
#注:雜湊表如何查值?比如:22,先找它的雜湊函式 h(22),22%7=1,那就到1的位置上去找,直接找 找到了
#注:現在 比如說找0,0%7=0,0 位置找不到,怎麼辦?先在這查,如果不是 往後找,如果不是,往後找,直到找到空位為止。找到空位如果還沒有找到,那麼這個雜湊表裡面肯定沒有0
#注:查詢也需要 按照 線性探查方式 查詢。插入的探查方式 和 查詢的探查方式一樣,到空位為止
#注:線性探查的效率並不是很高,它的裝載因子過大。它的好多東西都特別密,很多東西都在一塊
#-----------
### 注:開放定址表的 二次探查
#注:i+1, i-1, i+4, i-4. i+9, i-9……
#注:它是跳著的,越跳越多
#-----------
### 注:開放定址表 二度雜湊
#注:有好多個雜湊函式,如果一個雜湊函式衝突了,換另一個
#注:開放定址表 不太好,雜湊表可能滿了
###### 解決雜湊衝突 -- 拉鍊法
# 拉鍊法:雜湊表每個位置都連線一個連結串列,當衝突發生時,衝突的元素將被加到該位置連結串列的最後。
#注:拉鍊法:雜湊表的一個格子裡不是存一個元素,而是存一個連結串列
#注:衝突元素 如果發現衝突了,就放到這個連結串列後面 或者前面
#注:插入元素,如果位置是空的,就往後接一個;如果不是空,就在連結串列最後面或者最前面插一個 (頭插尾插 都可以)
#注:查詢的話 ,比如 查 155 在這個表裡是否有。首先帶入雜湊函式,發現值是11,那就到11這個位置上來找,然後 就遍歷這個連結串列, 91 不是 ,155 找到了
#注:刪除:刪除 先找到之後 再刪。找到155 後 把它刪除,連結串列的刪除 也是O(1)的
#注:拉鍊法很經典,雖然沒有直接定址錶快,但是直接定址表 可能實現不了
#注:查詢複雜度 不好說:如果雜湊函式 足夠好 ,節點足夠平均分到雜湊表上,整個的 有n個數,表長度是m。一個位置 有n/m個元素,那最多 只要查n/m次
#注:圖示的 雜湊函式 是對 16取餘數
###### 雜湊表 -- 常見雜湊函式 *
# 除法雜湊法:
# ·h(k) = k % m
# 乘法雜湊法:
# ·h(k) = floor(m*(A*key%1))
# 全域雜湊法:
# ·ha,b(k) = ((a*key + b) mod p) mod m a,b=1,2,...,p-1
#注:除法雜湊 很經典,最好理解的一種雜湊。直接對雜湊表的長度m 取餘數,拿到的值 就是它的下標
#注:乘法雜湊 k是 關鍵字,m是 雜湊表大小 ,A是一個值(A是一個小數), A*key%1, A乘以k對1取模,對1取模 就是取它的小數部分,再向下取整。floor是向下取整,比如說 3.6 向下取3
#注:全域雜湊 ha,b(k) = ((a*key + b) mod p) mod m a,b=1,2,...,p-1
#注:a、b 2個引數 a*key + b,mod p 對p取模 (%),括起來對 m取模
#63雜湊表實現
#注:寫雜湊表 之前,先寫個連結串列
#注:連結串列的建立
class LinkList: #注:連結串列類
class Node: #注:連結串列裡的節點
def __init__(self, item=None):
self.item = item
self.next = None
class LinkListIterator: #注:這個類是一個迭代器 因為 支援__next__
def __init__(self, node):
self.node = node
def __next__(self):
if self.node: #注:如果node不是空
cur_node = self.node
self.node = cur_node.next
return cur_node.item
else:
raise StopIteration
def __iter__(self):
return self
def __init__(self, iterable=None): #注:建構函式。傳一個列表
self.head = None
self.tail = None
if iterable: #注:如果有列表
self.extend(iterable)
#注:extend()接受一個列表引數 [1,2].extend([1,2,3]) [1,2,1,2,3]
#注:append()接受一個物件引數 [1,2].append([1,2,3]) [1,2,[1,2,3]]
def append(self, obj): #注:尾插
s = LinkList.Node(obj) #注:建立節點
if not self.head: #注:如果head是空
self.head = s
self.tail = s
else: #注:如果head不是空,插到尾巴上
self.tail.next = s
self.tail = s
def extend(self, iterable): #注:迴圈調appdend 就有extend了
for obj in iterable:
self.append(obj)
def find(self, obj): #注:在連結串列裡查詢,for迴圈查
for n in self: #注:self是linklist物件,self是迭代的支援這種寫法
if n == obj:
return True
else:
return False
def __iter__(self): #注:寫迭代器的 支援迭代
return self.LinkListIterator(self.head)
def __repr__(self): #注:轉換成字串
return "<<"+", ".join(map(str, self))+">>"
#注:map對於可迭代物件的每個元素 轉換成字串str
lk = LinkList([1,2,3,4,5]) #注:可迭代物件
for element in lk:
print(element)
#結果為
# 1
# 2
# 3
# 4
# 5
print(lk)
#結果 <<1, 2, 3, 4, 5>>
#連結串列建立 精簡程式碼
class LinkList:
class Node:
def __init__(self, item=None):
self.item = item
self.next = None
class LinkListIterator:
def __init__(self, node):
self.node = node
def __next__(self):
if self.node:
cur_node = self.node
self.node = cur_node.next
return cur_node.item
else:
raise StopIteration
def __iter__(self):
return self
def __init__(self, iterable=None):
self.head = None
self.tail = None
if iterable:
self.extend(iterable)
def append(self, obj):
s = LinkList.Node(obj)
if not self.head:
self.head = s
self.tail = s
else:
self.tail.next = s
self.tail = s
def extend(self, iterable):
for obj in iterable:
self.append(obj)
def find(self, obj):
for n in self:
if n == obj:
return True
else:
return False
def __iter__(self):
return self.LinkListIterator(self.head)
def __repr__(self):
return "<<"+", ".join(map(str, self))+">>"
#注:在這基礎上 寫 雜湊表
# 類似於集合的結構
class HashTable:
def __init__(self, size=101): #注:雜湊表的建構函式,size雜湊表的大小
self.size = size
self.T = [LinkList() for i in range(self.size)] #注:開一個T列表,每個位置都是一個連結串列(拉鍊法)
#注:剛開始T列表 每一個位置都是一個空連結串列 LinkList()
def h(self, k): #注:雜湊函式
return k % self.size #注:對self.size取模
def insert(self, k): #注:插入
#注:計算雜湊函式 返回的雜湊值
i = self.h(k)
#注:k這個元素 要插到i這個位置上去
#注:判斷這個元素在不在裡面
if self.find(k): #注:如果找到了,我就不插入。達到雜湊 去重的目的
print("Duplicated Insert.") #注:重複插入 提醒
else: #注:如果沒找到,插入
self.T[i].append(k) #注:插入
def find(self, k): #注:先寫 查詢函式
i = self.h(k) #注:先找到k的雜湊值
return self.T[i].find(k) #注:T[i] 是個連結串列
#注:雜湊表 刪除功能 沒寫。寫刪除的話 連結串列就得支援刪除的功能
ht = HashTable() #注:建立HashTable物件
ht.insert(0)
ht.insert(1)
# ht.insert(0)
# #注:輸入第3條語句時 ,提示 Duplicated Insert.
ht.insert(3)
ht.insert(102)
ht.insert(508)
print(",".join(map(str, ht.T)))
#注:列印這個雜湊表,1和102在一個連結串列裡,因為 雜湊表的長度是101,102對101取餘剩1
#注:508對101取餘剩3
# <<0>>,<<1, 102>>,<<>>,<<3, 508>>,<<>>,<<>>,<<>>,…………
print(ht.find(3))
#結果為 True
print(ht.find(102)) #注:也能找到,它是個連結串列, 先去1那個位置上找,發現1 不是,102 是,找到了
#結果為 True
print(ht.find(203))
#結果為 False #注:因為發現 1 不是,102 不是,沒了 返回一個false
#注:集合實現 跟它差不錯
#雜湊表 精簡程式碼
# 類似於集合的結構
class HashTable:
def __init__(self, size=101):
self.size = size
self.T = [LinkList() for i in range(self.size)]
def h(self, k):
return k % self.size
def insert(self, k):
i = self.h(k)
if self.find(k):
print("Duplicated Insert.")
else:
self.T[i].append(k)
def find(self, k):
i = self.h(k)
return self.T[i].find(k)
ht = HashTable()
ht.insert(0)
ht.insert(1)
ht.insert(3)
ht.insert(102)
ht.insert(508)
#print(",".join(map(str, ht.T)))
print(ht.find(203))
#64雜湊表應用
#注:雜湊表 的應用:Python裡的 集合和字典 底層就是雜湊表
###### 雜湊表的應用 -- 集合與字典
# 字典與集合都是通過雜湊表來實現的。
# ·a = {'name': 'Alex', 'age': 18, 'gender': 'Man'}
# 使用雜湊表儲存字典,通過雜湊函式將字典的鍵對映為下標。假設h('name') = 3, h('age') = 1, h('gender') = 4,則雜湊表儲存為[None, 18, None, 'Alex', 'Man'] #注:3 1 4 號位置
# 如果發生雜湊衝突,則通過拉鍊法或開發定址法解決
#注:用雜湊表來存,首先 把key傳到 雜湊函式裡去(比如說key='name'),得到一個值 比如說等於3 ( h('name')=3 ),把'Alex'放到3號位置上 ……
#注:之前講的雜湊函式 它的引數 都是整數,字串 也可以轉換成一個整數,以某種方式
a = {'name': 'Alex', 'age': 18, 'gender': 'Man'}
h('name') = 3, h('age') = 1, h('gender') = 4
[None, 18, None, 'Alex', 'Man'] #注:雜湊表儲存
#注:如果說 字典 條目比較少的話,雜湊函式夠好,一個記憶體裡 基本上不會拉鍊拉特別長,可能拉一個到兩個,這樣 你的按鍵查詢會非常快,包括集合的查詢
#注:一個列表 跟一個集合 去查,肯定是集合要快,因為這是 雜湊表的查詢速度指定的
#注:雜湊表的應用 -- md5演算法
#注:密碼學 主要2個方面:1、加密 解密 2、雜湊 比如說md5
#注:md5值 用的最多的是檔案,可以用md5值 判斷檔案是不是一樣的,如果這2個檔案雜湊值一樣 其實不能說 這2個檔案肯定一樣,只能說很大很大概率 這2個檔案一樣。
#注:因為 md5值 也是一個雜湊,有雜湊 就一定有雜湊衝突。檔案可以雜湊、字串也可以雜湊,什麼都可以md5,雜湊值一定是有限的,雖然是128位,2的128次方,但是必然會有2個檔案的雜湊值 是一樣的。
#注:但是 md5值 要求 不能 有限的時間 人工構造,偶然碰上 概率太小
#注:防止的是 人工構造 ,或者說是欺騙
#注:md5 已經被破解了。在安全的方面不是特別重要的 地方可以用:比如 不太重要的網站、比較檔案 ;但是 保密特別強的場合 不要用了
#注:2 雲端儲存服務商 比如說 百度雲 上傳 電影
#注:密碼學的雜湊函式
#注:SHA-224 就是224位 ,256 就是256 位
#注:SHA-2、MD5 也是 雜湊的一個應用,只不過 它的應用不是在儲存方面,主要 用在 安全方面的一些校驗 、比對 上
#注:雜湊值不能反解的,相當於 證明SHA2 安全性 是不可破解的
#注:Python hashlib庫裡 有對應的函式
#注:雜湊表 是一個 很高效的 做查詢的資料結構
#65樹的概念
#注:之前講了很多 線性的 資料結構:列表、連結串列、棧、佇列、雜湊表
#注:接下來看 樹狀資料結構
#注:樹是一種資料結構 比如:目錄結構
#注:樹是一種可以遞迴定義的資料結構
#注:樹是由n個節點組成的集合:
# 如果n=0,那這是一棵空樹 #注:相當於遞迴的終止條件
# 如果n>0,那存在1個節點作為樹的根節點,其他節點可以分為m個集合,每個集合本身又是一棵樹
#注:n > 0,A是根節點,下面分了m個,m是6。最後都遞迴完了 匯攏在一起 發現是樹
# 一些概念
# 根節點 #注:匯攏起來,最頭上那個節點
# 葉子節點 #注:沒有孩子的都叫做 葉子節點,因為葉子到頭了
# 樹的深度(高度) #注:示圖 是4 往下走幾層。A的深度是1,E的深度是2,J的深度是3,Q的深度是4
# 樹的度 #注:節點的度 就是這個節點 分幾個叉,比如E節點分2個叉 這個節點的度是2;F節點的度是3
#注:樹的度 是這個樹裡邊 所有節點的 最大的那個度。圖示 樹的度是 6
# 孩子節點/父節點 #注:A是D的父親節點,D是A的一個孩子節點
# 子樹 #注:E I J P Q 也是一棵樹,以E為根節點的一棵樹,這個樹 是整個樹的一棵子樹,因為它是遞迴定義的,所以任意一個節點也是一棵子樹 比如 節點 B
#66樹的例項:模擬檔案系統
#注:樹是由節點組成的,樹的核心是節點
class Node: #注:先定義樹的節點
def __init__(self, name, type='dir'):
self.name = name #注:檔名
self.type = type #"dir" or "file" #注:檔案型別 預設dir
#注:節點和節點之間的關聯
self.children = []
#注:連結串列有一個self.next 找它的下一個但是 樹 這個下面可能有很多 不是有一個
#注:所以搞一個列表,好多next都放到這個列表裡
self.parent = None #注:相當於 指向它的父母
# 鏈式儲存
def __repr__(self):
return self.name #注:只打印 檔名
n = Node("hello") #注:資料夾 hello
n2 = Node("world")
n.children.append(n2) #注:這樣 把hello資料夾下面的world資料夾 連起來
#注:這個操作 就相當於單鏈表,只不過 這裡有很多個next
#注:這就是從 hello 找到 world,children 把n 連到n2
n2.parent = n #注:父母只有一個,樹的結構決定的,所以不是列表
#注:這樣 n2 也可以往回找了
#注:這樣就有點像 雙鏈表了。children往下找,相當於 連結串列 往後找;parent往上找,相當於連結串列的往前找
#注:一般來說 實現樹的時候 children都是有的,parent屬性有沒有 看自己的需求。實現一棵只是往下鏈的樹也行,只是不能往回找。往回找他的父親的話,加一個parent
#注:linux檔案系統
# /abc.txt
# /var/
class FileSystemTree: #注:寫 樹的類
def __init__(self): #注:初始化
self.root = Node("/") #注:首先 維護一個根
self.now = self.root #注:還需要 存現在 在哪個目錄下
#注:模擬構造作業系統的命令
def mkdir(self, name): #注:傳一個name
# name 以 / 結尾
if name[-1] != "/": #注:如果name結尾不是斜槓
name += "/" #注:就補上 /
node = Node(name) #注:建立一個資料夾
#注:再把這個資料夾 跟現在這個資料夾 連起來
self.now.children.append(node) #注:正著連上
node.parent = self.now #注:反著連過去
def ls(self): #注:展示
return self.now.children #注:列印當前目錄的children
def cd(self, name): #注:切換目錄 絕對路徑與相對路徑。這裡 只可以進入下一級
"../var/python/"
#注:先按斜槓split 然後再一條一條進去,先執行.. 再迴圈執行這個函式
#注:如果支援絕對路徑,還是先按照斜槓split,然後 第一個斜槓 前面肯定是個空,判斷如果有空,你不是從now 走,而是從root開始走
#注:相對路徑是從now開始走,絕對路徑是從root開始走
#注:如果是檔案進來的話,還要控制檔案,不能讓他append東西
if name[-1] != "/":
name += "/"
if name == "../": #注:cd支援向上返回一級
self.now = self.now.parent
return
for child in self.now.children: #注:如果children裡有這個name
if child.name == name:
self.now = child #注:就切換目錄
return
raise ValueError("invalid dir") #注:如果沒有 就報錯
tree = FileSystemTree() #注:新建一個樹
tree.mkdir("var/")
tree.mkdir("bin/")
tree.mkdir("usr/")
print(tree.root.children)
#結果為 [var/, bin/, usr/]
print(tree.ls())
#結果為 [var/, bin/, usr/]
tree.cd("bin/") #注:進入 bin目錄
tree.mkdir("python/") #注:在bin目錄下建立 資料夾python
print(tree.ls())
#結果為 [python/]
tree.cd("../")
print(tree.ls()) #注:cd 回去
#結果為 [var/, bin/, usr/]
#注:樹 在絕大部分的儲存,都是跟連結串列一樣 鏈式儲存。通過往下走 往後指childre,往上 指parent。通過節點和節點之間相互連線的方式 組成樹的資料結構
#精簡程式碼
class Node:
def __init__(self, name, type='dir'):
self.name = name
self.type = type #"dir" or "file"
self.children = []
self.parent = None
# 鏈式儲存
def __repr__(self):
return self.name
class FileSystemTree:
def __init__(self):
self.root = Node("/")
self.now = self.root
def mkdir(self, name):
# name 以 / 結尾
if name[-1] != "/":
name += "/"
node = Node(name)
self.now.children.append(node)
node.parent = self.now
def ls(self):
return self.now.children
def cd(self, name):
if name[-1] != "/":
name += "/"
if name == "../":
self.now = self.now.parent
return
for child in self.now.children:
if child.name == name:
self.now = child
return
raise ValueError("invalid dir")
tree = FileSystemTree()
tree.mkdir("var/")
tree.mkdir("bin/")
tree.mkdir("usr/")
tree.cd("bin/")
tree.mkdir("python/")
tree.cd("../")
print(tree.ls())
#結果為 [var/, bin/, usr/]
#67二叉樹概念
#注:二叉樹 特殊的樹,度不超過2的樹,一個節點 最多分2個叉 有區別 左孩子和右孩子
#注:堆排序那裡講了 二叉樹的線性儲存方式,堆的儲存 把數存成一個列表 [0, 1, 2, 3, ……],父親找孩子 是 0 找 2i+1 ,2i+2。這種儲存方式比較適用於完全二叉樹(右邊少,左邊不會少東西)
#注:這個樹 A左邊少了好多節點,那這些節點就為空的,如果這個樹非常不完全 差很多,那這些空的地方 就浪費空間,所以用另一種儲存方式 鏈式儲存
#注:跟樹的儲存方式是一樣的。 只不過 剛才的樹 是self.children是個列表,這裡二叉樹 不用列表 lchid ,rchild 2孩子
#注: lchid ,rchild 2 孩子 左孩子 右孩子 ,往下指2個指標。如果還需要往上面的話,加一個self.parent,不需要 就不用了
#注:data用來存節點的資料
###### 二叉樹
# 二叉樹的鏈式儲存:將二叉樹的節點定義為一個物件,節點之間通過類似連結串列的連結方式來連線。
# 節點定義:
class BitreeNode: #注:二叉樹的節點
def __init__(self, data): #注:建構函式
self.data = data #注:存放資料
self.lchild = None # 左孩子
self.rchild = None # 右孩子
a = BitreeNode("A") #注:手動建立節點
b = BitreeNode("B")
c = BitreeNode("C")
d = BitreeNode("D")
e = BitreeNode("E")
f = BitreeNode("F")
g = BitreeNode("G")
e.lchild = a #注:連起來 ,e的左孩子
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f #注:這個樹連線完事了
root = e
print(root.lchild.rchild.data) #注:列印root左孩子的右孩子 其實就是節點
#結果為 C
#精簡程式碼
class BitreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
#68二叉樹遍歷
#注:線性的資料結構 都比較好遍歷 :列表 好遍歷 for迴圈,連結串列 也好遍歷 一直去next 取到空為止,棧、佇列都好遍歷
#注:二叉樹怎麼遍歷?注:二叉樹的遍歷方式 有4種
###### 二叉樹的遍歷方式:
# 前序遍歷:EACBDGF
# 中序遍歷:ABCDEGF
# 後序遍歷:BDCAFGE
# 層次遍歷:EAGCFBD
class BitreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
a = BitreeNode("A") #注:建立節點
b = BitreeNode("B")
c = BitreeNode("C")
d = BitreeNode("D")
e = BitreeNode("E")
f = BitreeNode("F")
g = BitreeNode("G")
e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f
root = e
######------注:二叉樹的 前序遍歷
#注:這個其實是 遞迴的定義的
def pre_order(root): #注:傳一個節點進去
if root: #注:如果root不是空 (是空 就不列印了)
#注:root是空 相當於 是遞迴的終止條件
print(root.data, end=',') #注:先列印根節點
pre_order(root.lchild) #注:然後訪問它的左子樹
pre_order(root.rchild) #注:然後訪問它的右子樹
#注:pre_order(root) 如果不是空,先訪問根,再訪問左子樹,再訪問右子樹
pre_order(root)
#結果為 E,A,C,B,D,G,F, #注:每個節點都列印一次
# 前序遍歷:EACBDGF
#注:pre_order(root) 如果不是空,先訪問根,再訪問左子樹,再訪問右子樹
#注:圖示裡面 先訪問E,訪問完了E, 再去訪問 左子樹A 遞迴的(A B C D) 再去訪問右子樹 (G F)
#注:在(A B C D) 裡 ,先訪問A ,然後遞迴左(沒有) 遞迴右(B C D)
#注:(B C D) 裡面 先訪問 C ,再遞迴左 B 遞迴右 D
#注:訪問G F ,先訪問G ,遞迴左(沒有),遞迴右(F)
#注:順序 EACBDGF 前序遍歷
######------注:二叉樹的 中序遍歷
#注:中序遍歷 和 前序遍歷不同的是 訪問次序不一樣
def in_order(root):
if root:
in_order(root.lchild) #注:先 遞迴左子樹 訪問左子樹
print(root.data, end=",") #注:然後訪問自己
in_order(root.rchild) #注:然後 遞迴右子樹
#注:先遞迴左子樹 訪問自己 遞迴右子樹
in_order(root)
#結果為 A,B,C,D,E,G,F,
#注:和前序遍歷 差別是 訪問的順序不一樣了。沒有前序遍歷那麼直觀
#注:中序遍歷不太直觀
#注:先遞迴左子樹 訪問自己 遞迴右子樹
#注:所以E在中間…… (先把根節點放中間) 如圖
######------注:二叉樹的 後序遍歷
#注:前序遍歷 訪問自己在最前面;中序遍歷 訪問自己在中間;後序遍歷 訪問自己在最後
def post_order(root):
if root:
post_order(root.lchild) #注:先 遞迴左
post_order(root.rchild) #注:然後 遞迴右
print(root.data, end=",") #注:最後 列印自己
#注:後序遍歷,先 遞迴左;然後 遞迴右;最後 列印自己
post_order(root)
#結果為 B,D,C,A,F,G,E,
#注:先左(A B C D) 後右(G F) 最後自己 E ……
#注:先確定列印的 即最後的 放最後面
#注:這三種遍歷 在面試中 被問到的比較多
#注:它還有一個別的型別的問題
#注:知道一棵二叉樹的 前序遍歷和 中序遍歷,確定這棵樹 並給出 後續遍歷
#注:一但給2個遍歷的序列,就能確定這個樹
# 前序遍歷:EACBDGF
# 中序遍歷:ABCDEGF
#注:前序遍歷 先訪問自己,第1個一定是根 :E是根
#注:中序序列 根在最中間:知道E是根了 對應的去看中序序列;A B C D 是E的左孩子,G F 是E的右孩子
#注:前序序列 :A B C D 裡,A是根 遞迴的
#注:再看 中序序列 :A B C D ,說明A 左子樹 是空的,右子樹 是 B C D
#注:接下來看B C D, 看前序序列 :C B D ,C是根
#注:再看中序序列, C是根 B C D ,B是C的左孩子,D是C的右孩子
#注:A、B、C、D 出來了,看G F
#注:看前序序列 G F,G是根
#注:F 看中序序列,G F ,F在右邊,所以F是G的右孩子,G的左孩子是空
#注:這就是 前序序列 和 中序序列 還原出來這棵樹,有了樹之後,就可以畫 後序序列了
#注:這是 給前中 可以推;同樣給後序和中序 也可以畫出來
#注:前序的時候 看第一個是根,後序的時候 最後一個是根,所以倒著看 同樣的道理
#注:面試:實現 二叉樹的 三種遍歷方式:遞迴的寫法。給2個序列,確定最後一個序列,或者說 給你2個序列,讓你寫函式 來構造出來這棵樹
######------注:二叉樹的 層次遍歷
#注:很好理解 按層來 E A G C F B D ,一層一層的 每一層 從左到右
#注:怎麼寫?用到 佇列 :剛開始訪問E 沒有問題,E出隊 接下來E的孩子們進隊 A G進隊,現在佇列裡是A G
#注:A出隊,A的孩子C進隊, 佇列裡剩下 G C;G出隊 ,G的孩子F進隊,佇列裡剩下 C F;接下來 C出隊 ,B D 進隊;F 出隊,沒有孩子進隊;B出隊 沒有孩子進隊;D出隊 沒有孩子進隊
#注:所以這個序列就是 層次遍歷的序列 :EAGCFBD
#注:層次遍歷 不光適用於 二叉樹 ,多叉樹 也可以。二叉樹 左孩子進隊 右孩子進隊;多叉樹 children 挨個進隊
from collections import deque #注:使用之前queue佇列
def level_order(root):
queue = deque() #注:首先建立空佇列
queue.append(root) #注:然後 root進隊
while len(queue) > 0: # 只要隊不空,一直訪問
#注:先出隊一個元素 ,然後把它的孩子進隊
node = queue.popleft() #注:出隊
print(node.data, end=',') #注:列印
if node.lchild: #注:判斷node的左孩子是不是有
queue.append(node.lchild) #注:如果有,就append node左孩子
if node.rchild: #注:如果node的右孩子有,它也進隊
queue.append(node.rchild)
level_order(root)
#結果為 E,A,G,C,F,B,D,
#精簡程式碼
class BitreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
a = BitreeNode("A") #注:建立節點
b = BitreeNode("B")
c = BitreeNode("C")
d = BitreeNode("D")
e = BitreeNode("E")
f = BitreeNode("F")
g = BitreeNode("G")
e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f
root = e
#------ 前序遍歷
def post_order(root):
if root:
post_order(root.lchild)
post_order(root.rchild)
print(root.data, end=",")
pre_order(root)
# 前序遍歷:EACBDGF
#------ 中序遍歷
def in_order(root):
if root:
in_order(root.lchild)
print(root.data, end=",")
in_order(root.rchild)
in_order(root)
# 中序遍歷:ABCDEGF
#------ 後序遍歷
def post_order(root):
if root:
post_order(root.lchild)
post_order(root.rchild)
print(root.data, end=",")
post_order(root)
# 後序遍歷:BDCAFGE
#------ 層次遍歷
from collections import deque
def level_order(root):
queue = deque()
queue.append(root)
while len(queue) > 0: # 只要隊不空
node = queue.popleft()
print(node.data, end=',')
if node.lchild:
queue.append(node.lchild)
if node.rchild:
queue.append(node.rchild)
level_order(root)
# 層次遍歷:EAGCFBD
#注:層次遍歷 不光適用於 二叉樹 還適用於 多叉樹
#69二叉搜尋樹的概念
#注:具體的 二叉樹 一種用法:二叉搜尋樹
###### 二叉搜尋樹 (binary search tree bst)
# 二叉搜尋樹是一棵 二叉樹 且滿足性質:設x是二叉樹的一個節點。如果y是x左子樹的一個節點,那麼y.key ≤ x.key;如果y是x右子樹的一個節點,那麼y.key ≥ x.key。
# 二叉搜尋樹的操作:查詢、插入、刪除
#注:y.key 就是y上存的那個值,可以存鍵值對 ,也可以只存一個鍵。左子樹上的 所有節點的值 都比它小,右子樹上的 所有節點的值 都比它大
#注:一個節點 左子樹上的 所有節點的值 都比它小,右子樹上的 所有節點的值 都比它大。如果所有節點 都滿足這個性質,那麼 他就是二叉搜尋樹
#注:有了這個性質後,可以做的操作:查詢、插入、刪除
#注:有了這個性質後,查詢 非常好查:比如說 查11 在不在這個樹裡面,先和根比,11比根小,所以如果11在的話 一定在根的左邊;那就到左邊查,跟5比,發現11比5大,如果11存在,那一定在5的右邊;然後發現11 找到了
#注:找7 ,跟17比 ,往左邊找;跟5比 往右邊找;跟11比 往左邊找;跟9比 往左邊找;跟8比往左邊找 但是8左邊是空了,沒有了 找不到了
#注:所以 查詢操作 只要執行 樹的深度次 就可以了
#注:插入操作: 插入32 ,插肯定往葉子節點插 ,往最下面這層插;32 跟17比 ,往右邊走,插到它的右子樹上來;跟35 比,插到它的左邊來;跟29比 ,插到它的右邊 ,右邊只要沒有 ,就插到這來
#注:插入和查詢差不多,反正就是 往深處走,走到一個地方,只要這個地方沒有了 ,那就插在這
#注:查詢 和 插入 複雜度 大概 logn 跟二分查詢不一樣 但是差不多,每次都少一半
#70二叉搜尋樹:插入
class BiTreeNode: #注:樹的節點
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None #注:查詢和插入操作 不用它也行,但刪除操作要用
#注:加了parent就是雙鏈表
class BST:
def __init__(self, li=None): #注:建構函式 可以傳一個列表進來
self.root = None #注:建構函式 先創造一個根節點
if li: #注:如果li不是None
for val in li: #注:就使用非遞迴的方法 把val插進來 。因為遞迴方法 慢
self.insert_no_rec(val)
#注:遞迴的寫法
def insert(self, node, val): #注:插入函式 ,node就是遞迴的插到哪個節點上去
if not node: #注:如果node是空,就找到這個位置了,把值插入
node = BiTreeNode(val) #注:相當於建立一個節點,但還沒和樹連起來
#注:如果node不為空,分3種情況
elif val < node.data: #注:如果val比node節點小,往左走,遞迴調左孩子
node.lchild = self.insert(node.lchild, val) #注:左孩子傳進來,遞迴左孩子。並把值傳給node.lchild 因為返回它自己
#注:這是 插到node的左孩子裡,讓它兩連起來;還要連parent
node.lchild.parent = node #注:它的左孩子的父親是它
elif val > node.data: #注:如果 val > node.data , 遞迴右孩子,從右孩子裡面找
node.rchild = self.insert(node.rchild, val)
node.rchild.parent = node #注:連parent
# else: #注:第3種情況 如果說 等於了
#注:如果說 等於的話,其實可以,但是一般情況下 預設鍵(值) 不能相等。等於 可以統一規定去右邊(或者左邊)。查詢的話 只能說查詢一個
#注:一般說 要求是 鍵值對,鍵不能重複。如果有這種情況 但是鍵會重複 ,可以給這個節點 加一個數據域,叫做count ,等於了 給這個節點count +1
return node
#注:這是 遞迴的寫法
#注:非遞迴的寫法 一般來說 非遞迴的比遞迴的快一點
def insert_no_rec(self, val):
#注:首先 一層層往下找(指標也好,其他也好),找一個p
p = self.root #注:p剛開始等於root
#注:如果來的節點 比它小,那p就往它左孩子走,……右孩子走
if not p: #注:空樹的情況下,特殊處理下
self.root = BiTreeNode(val) #注:直接給root賦值一個就可以了
return
while True: #注:如果不是空樹,迴圈 3種情況
if val < p.data: #注:往左子樹上走;需要判斷它左子樹上 有沒有
#注:如果左子樹上有,那麼p=左子樹;如果左子樹是None,就把val插到這
if p.lchild: #注:如果p的左子樹 不是None
p = p.lchild #注:p往左子樹上走一下
else: #注:左孩子不存在
p.lchild = BiTreeNode(val) #注:就把值插到這
p.lchild.parent = p #注:雙向的指定 ,連線parent
return
elif val > p.data: #注:第2種情況,和上面一樣的,把l換成r
if p.rchild: #注:如果 右孩子存在
p = p.rchild #注:p就往右孩子上走
else: #注:如果 右孩子不存在
p.rchild = BiTreeNode(val) #注:它的右孩子 就等於 一個新的節點
p.rchild.parent = p #注:再建立parent連線
return
else: #注:等於的情況,什麼都不幹
return
#注:Python的集合也是這樣,插入的時候 如果重複,什麼都不提示,但是它沒有插入
#注:前面講的 三種遍歷:前序遍歷、中序遍歷、後序遍歷
def pre_order(self,root):
if root:
print(root.data, end=",")
self.pre_order(root.lchild)
self.pre_order(root.rchild)
def in_order(self,root):
if root:
self.in_order(root.lchild)
print(root.data, end=",")
self.in_order(root.rchild)
def post_order(self,root):
if root:
self.post_order(root.lchild)
self.post_order(root.rchild)
print(root.data, end=",")
tree = BST([4,6,7,9,2,1,3,5,8])
tree.pre_order(tree.root)
print("")
tree.in_order(tree.root)
print("")
tree.post_order(tree.root)
#結果為
# 4,2,1,3,6,5,7,9,8,
# 1,2,3,4,5,6,7,8,9,
# 1,3,2,5,8,9,7,6,4,
#注:由前序和中序,可以畫出這棵樹,這肯定是一個滿足二叉搜尋樹的情況
#注:中序遍歷 1,2,3,4,5,6,7,8,9, 是排好序的,是巧合嗎?
import random
li = list(range(500))
random.shuffle(li)
tree = BST(li)
tree.in_order(tree.root) #注:中序序列 排好序的,不是巧合
#結果為 0,1,2,3,4,5,6,7,8,9,10
#注:二叉搜尋樹的 中序序列 一定是升序的
#注:中序序列 先左,再自己,再右。所以 先輸出的 一定是整個序列最小的,因為 左孩子永遠是最小的
#注:在 二叉搜尋樹裡,左比中小,右比中大;左先出去 小的先出,然後中 右,從小到大
#精簡程式碼
import random
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None
class BST:
def __init__(self, li=None):
self.root = None
if li:
for val in li:
self.insert_no_rec(val)
# 遞迴寫法
def insert(self, node, val):
if not node:
node = BiTreeNode(val)
elif val < node.data:
node.lchild = self.insert(node.lchild, val)
node.lchild.parent = node
elif val > node.data:
node.rchild = self.insert(node.rchild, val)
node.rchild.parent = node
return node
# 不遞迴的寫法
def insert_no_rec(self, val):
p = self.root
if not p: # 空樹
self.root = BiTreeNode(val)
return
while True:
if val < p.data:
if p.lchild:
p = p.lchild
else: # 左孩子不存在
p.lchild = BiTreeNode(val)
p.lchild.parent = p
return
elif val > p.data:
if p.rchild:
p = p.rchild
else:
p.rchild = BiTreeNode(val)
p.rchild.parent = p
return
else:
return
# 三種排序
def pre_order(self, root):
if root:
print(root.data, end=',')
self.pre_order(root.lchild)
self.pre_order(root.rchild)
def in_order(self, root):
if root:
self.in_order(root.lchild)
print(root.data, end=',')
self.in_order(root.rchild)
def post_order(self, root):
if root:
self.post_order(root.lchild)
self.post_order(root.rchild)
print(root.data, end=',')
li = list(range(500))
random.shuffle(li)
tree = BST(li)
tree.pre_order(tree.root)
print("")
tree.in_order(tree.root)
print("")
tree.post_order(tree.root)
#結果為
# 前序遍歷……
# 中序遍歷0,1,2,3,4,5,6,7,……
# 後序遍歷……