1. 程式人生 > 其它 >07_Python演算法+資料結構筆記-連結串列總結-雜湊表-樹-二叉樹-二叉搜尋樹

07_Python演算法+資料結構筆記-連結串列總結-雜湊表-樹-二叉樹-二叉搜尋樹

技術標籤: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值 用的最多的是檔案,可以用md5值 判斷檔案是不是一樣的,如果這2個檔案雜湊值一樣 其實不能說 這2個檔案肯定一樣,只能說很大很大概率 這2個檔案一樣。
#注:因為 md5值 也是一個雜湊,有雜湊  就一定有雜湊衝突。檔案可以雜湊、字串也可以雜湊,什麼都可以md5,雜湊值一定是有限的,雖然是128位,2的128次方,但是必然會有2個檔案的雜湊值 是一樣的。
#注:但是 md5值 要求 不能 有限的時間 人工構造,偶然碰上 概率太小
#注:防止的是 人工構造 ,或者說是欺騙
#注:md5 已經被破解了。在安全的方面不是特別重要的 地方可以用:比如 不太重要的網站、比較檔案  ;但是  保密特別強的場合  不要用了

md5

#注:2 雲端儲存服務商  比如說 百度雲 上傳 電影

#注:密碼學的雜湊函式

sha2

#注:SHA-224 就是224位 ,256  就是256 位
#注:SHA-2、MD5 也是 雜湊的一個應用,只不過 它的應用不是在儲存方面,主要 用在 安全方面的一些校驗 、比對 上

sha2

#注:雜湊值不能反解的,相當於 證明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,……
# 後序遍歷……