算法——樹和二叉樹

1、樹

一、什麼是樹?

  樹是一種數據結構,好比:目錄結構。html

  樹是一種能夠遞歸定義的數據結構。node

  定義:樹是由n個節點組成的集合python

    若是n=0,那這是一棵空樹;數據庫

    若是n>0,那存在1個節點做爲樹的根節點,其餘節點能夠分爲m個集合,每一個集合自己又是一棵樹。數據結構

  

二、相關概念

  根節點: 根節點(root)是樹的一個組成部分,也叫樹根。它是同一棵樹中除自己外全部節點的祖先,沒有父節點。app

  葉子節點(終端節點):一棵樹當中沒有子節點(即度爲0)的結點稱爲葉子結點,簡稱「葉子」。 葉子是指度爲0的結點,又稱爲終端結點。dom

  樹的深度(高度):樹中節點的最大層次。post

  節點的度:一個節點含有的子樹的個數稱爲該節點的度。spa

  樹的度:一棵樹中,最大的節點的度稱爲樹的度。3d

  父節點(雙親節點):若一個節點含有子節點,則這個節點稱爲其子節點的父節點;

  子節點(孩子節點):一個節點含有的子樹的根節點稱爲該節點的子節點;

  子樹:設T是有根樹,a是T中的一個頂點,由a以及a的全部後裔(後代)導出的子圖稱爲有向樹T的子樹。

三、樹的實例——模擬文件系統

class Node:
    def __init__(self, name, type='dir'):
        self.name = name
        self.type = type    # 類型能夠是"dir"或"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): """建立目錄""" 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/") print(tree.root.children) # [var/, bin/, usr/] print(tree.ls()) # [var/, bin/, usr/] tree.cd("bin/") tree.mkdir("python/") print(tree.ls()) # [python/] tree.cd("../") print(tree.ls()) # [var/, bin/, usr/]

  樹絕大多數的存儲都是和鏈表同樣鏈式存儲。日後指child;往前指parent。經過節點和節點間相互鏈接的關係來組成這麼一個數據結構。

2、二叉樹

  二叉樹:度不超過2的樹。以下所示:

  

  每一個節點最多有兩個孩子節點,兩個孩子節點被區分爲左孩子節點和右孩子節點。

一、特殊二叉樹——滿二叉樹

  一個二叉樹若是每一層的節點數都達到最大值,則這個二叉樹就是滿二叉樹。

二、特殊二叉樹——徹底二叉樹  

  葉節點只能出如今最下層和次下層,而且最下面一層的節點都集中在該層最左邊的若干位置的二叉樹。

  

  滿二叉樹必定是徹底二叉樹,但徹底二叉樹不必定是滿二叉樹。堆是一個特殊的徹底二叉樹。

3、二叉樹的存儲方式(表示方式)

  二叉樹這種數據結構在計算機中的存儲方法。

一、鏈式存儲方式

   二叉樹的鏈式存儲:將二叉樹的節點定義爲一個對象,節點之間經過相似鏈表的連接方式來鏈接。

 (1)節點定義

class BiTreeNode:
    def __init__(self, data):  # data就是傳進去的節點值
        self.data = data
        self.lchild = None
        self.rchild = None

(2)根據給定圖片生成二叉樹

  

  代碼以下:

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

print(root.lchild.rchild.data)  # C

二、順序存儲方式

  所謂順序存儲方式就是二叉樹用列表來存儲。以下圖所示就是用列表來存儲二叉樹。

  

  如上圖二叉樹標出了元素所對應的索引,則能夠有如下結論:

(1)父節點和左孩子節點的編號下標有什麼關係?

  父與左子下標關係:0-1  1-3 2-5 3-7 4-9

  i (父)——>2i+1 (子)

  若是已知父親節點爲i,那麼他的左孩子節點爲2i+1

(2)父節點和右孩子節點的編號下標有什麼關係?

  父與右子下標關係:0-2 1-4 2-6 3-8 4-10

  i (父)——>2i+2 (子)

  若是知道父親節點爲i,那麼他的右孩子節點爲2i+2

(3)知道孩子找父親規律?  

  知道左孩子求父節點:(n-1)/2=i

  知道右孩子求父節點:(n-2)/2=i 

4、二叉樹的遍歷方式

  

一、前序遍歷:EACBDGF

  訪問根節點操做發生在遍歷其左右子樹以前。

def pre_order(root):
    """前序遍歷"""
    if root:   # 若是不爲空(遞歸條件)
        print(root.data, end=',')   # 訪問本身
        pre_order(root.lchild)      # 遞歸左子樹
        pre_order(root.rchild)      # 遞歸右子樹

pre_order(root)   # E,A,C,B,D,G,F,

二、中序遍歷:ABCDEGF

  訪問根節點的操做發生在遍歷其左右子樹之間。

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,  

三、後序遍歷:BDCAFGE

  訪問根節點的操做發生在遍歷其左右子樹以後。

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,

四、層次遍歷:EAGCFBD

  層次遍歷很好理解,須要利用到隊列。不只適用二叉樹也適用多叉樹。

  用一個隊列保存被訪問的當前節點的左右孩子以實現層序遍歷。

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)   # E,A,G,C,F,B,D,

五、給定一個樹的兩種遍歷方式,就可推導出這個樹

  例如:前序遍歷——EACBDGF;中序遍歷——ABCDEGF。

  由此可知E是根節點,E的左邊包含ABCD,右邊包含GF。且A是根節點的左節點、G是根節點的右節點。

  BCD是A的子節點,因爲中序遍歷ABCD可知A的左節點是空的,右節點包含BCD,由前序ACBD可知C是A的右子節點。再由中序遍歷BCD可知B是C的左節點,D是C的右節點。

  GF是根節點右邊節點,G是右節點,F是G的子節點。由中序GF可知F是G節點的右節點。至此推導出整個樹。

5、二叉樹應用——二叉搜索樹

  二叉搜索樹是一顆二叉樹且知足性質:設x是二叉樹的一個節點。若是y是x左子樹的一個節點,那麼y.key <= x.key;若是y是x右子樹的一個節點,那麼y.key >= x.key。

  

  總結來講:二叉搜索樹的左子樹不空,則左子樹上全部節點的值均小於它的根節點的值;若它的右子樹不空,則右子樹上全部節點的值均大於它根節點的值;它的左右子樹也都是二叉搜索樹。

一、二叉搜索樹的插入

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:
            for val in li:
                self.insert_no_rec(val)

    def insert(self, node, val):
        """
        遞歸插入
        :param node: 節點
        :param val: 要插入的值
        :return:
        """
        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.lchild,val)
            node.rchild.parent = node
        # else:  # "="  else不用寫了
        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=",")  # 訪問本身


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,
"""

  能夠注意到中序遍歷輸出的是有序的,作以下驗證:

import random
li = list(range(500))
random.shuffle(li)
tree = BST(li)
tree.in_order(tree.root)  # 0,1,2,3,4,5,...,496,497,498,499

  這是由於二叉搜索樹的性質致使二叉搜索樹的左孩子必定是最小的,所以它的中序序列必定是升序的。

二、二叉搜索樹的查詢操做

class BST:
    """代碼省略"""

    def query(self, node, val):
        """
        遞歸查詢
        :param node: 要遞歸的節點
        :param val: 要查詢的值
        :return:
        """
        if not node:   # 若是node是空,則找不到
            return None   # 遞歸終止條件

        if val > node.data:   # 大於node的值往右邊找
            return self.query(node.rchild, val)
        elif val < node.data:  # 小於node的值往左邊找
            return self.query(node.lchild, val)
        else:
            return node    # 值相同返回當前節點

    def query_no_rec(self, val):
        """非遞歸查詢"""
        p = self.root
        while p:   # 若是樹不爲空
            if p.data < val:  # 大於p的值往右邊找
                p = p.rchild
            elif p.data > val:  # 小於p的值往左邊找
                p = p.lchild
            else:
                return p
        return None   # 樹爲空,遞歸終止條件

import random
li = list(range(0, 500, 2))
random.shuffle(li)

tree = BST(li)
print(tree.query_no_rec(3))  # None
print(tree.query_no_rec(6))  # <__main__.BiTreeNode object at 0x103d01cc0>
print(tree.query_no_rec(6).data)   # 6

三、二叉搜索樹的刪除操做

(1)若是要刪除的節點是葉子節點

  操做方法是:直接刪除

   

(2)若是要刪除的節點只有一個孩子

  操做方法是:將此節點的父親與孩子鏈接,而後刪除該節點。

  

(3)若是要刪除的節點有兩個孩子

  操做方法:將其右子樹的最小節點(該節點最多有一個右孩子)刪除,並替換當前節點。

  

(4)代碼實現以下所示:

class BiTreeNode:
    def __init__(self, data):
        self.data = data
        self.lchild = None   # 左孩子
        self.rchild = None   # 右孩子
        self.parent = None   # 加了parent就是雙鏈表


class BST:
    """代碼省略"""
    def __remove_node_1(self, node):
        """狀況1:node是葉子節點"""
        if not node.parent:   # 此葉子節點沒有父節點,說明樹中就這一個節點
            self.root = None   # 將這惟一的節點刪除

        if node == node.parent.lchild:   # node是父親的左孩子
            node.parent.lchild = None    # 父親與node斷聯繫
            node.parent = None           # node與父親斷聯繫(這句可寫可不寫)
        else:    # node是父親的右孩子
            node.parent.rchild = None    # # 父親與node斷聯繫

    def __remove_node_21(self, node):
        """狀況2-1:node只有一個左孩子"""
        if not node.parent:   # 若是node是根節點
            self.root = node.lchild    # 將node的左孩子置爲根節點
            node.lchild.parent = None  # 將新根節點的父親設爲空
        elif node == node.parent.lchild:  # 若是node是它父親的左孩子
            node.parent.lchild = node.lchild   # node父節點的左孩子設爲node的左孩子
            node.lchild.parent = node.parent   # node左孩子的父節點設爲node的父節點
        else:  # 若是node是它父親的右孩子
            node.parent.rchild = node.lchild   # node父節點的右孩子指向node的左孩子
            node.lchild.parent = node.parent   # node左孩子的父親指向node的父節點

    def __remove_node_22(self, node):
        """狀況2-2:node只有一個右孩子"""
        if not node.parent:   # 若是node是根節點
            self.root = node.rchild  # 將node的右孩子置爲根節點
            node.rchild.parent = None  # 將新根節點的父親設爲空

        elif node == node.parent.lchild:   # 若是node是父親的左孩子
            node.parent.lchild = node.rchild   # 將node父節點的左孩子指向node的右孩子
            node.rchild.parent = node.parent

        else:   # 若是node是父親的右孩子
            node.parent.rchild = node.rchild   # 將node父節點的右孩子指向node的右孩子
            node.rchild.parent = node.parent

    def delete(self, val):
        if self.root:   # 若是不是空樹
            node = self.query_no_rec(val)
            if not node:  # 若是node不存在
                return False
            if not node.lchild and not node.rchild:   # 若是node是葉子節點
                self.__remove_node_1(node)
            elif not node.rchild:    # 若是沒有右孩子(只有一個左孩子)
                self.__remove_node_21(node)
            elif not node.lchild:    # 若是沒有左孩子(只有一個右孩子)
                self.__remove_node_22(node)
            else:    # 若是兩個孩子都有
                min_node = node.rchild
                while min_node.lchild:   # 一直查找node右孩子的左子樹的左孩子,直到沒有爲止
                    min_node = min_node.lchild
                node.data = min_node.data   # 將min_node.data的值賦給node.data
                # 刪除min_node
                if min_node.rchild:   # 若是min_node只有右孩子
                    self.__remove_node_22(min_node)
                else:  # 若是min_node沒有孩子
                    self.__remove_node_1(min_node)


tree = BST([1,4,2,5,3,8,6,9,7])
tree.in_order(tree.root)   # 1,2,3,4,5,6,7,8,9,
print("")
tree.delete(4)
tree.in_order(tree.root)   # 1,2,3,5,6,7,8,9,
print("")
tree.delete(1)
tree.delete(8)
tree.in_order(tree.root)   # 2,3,5,6,7,9,

四、二叉搜索樹的效率  

  平均狀況下,二叉搜索樹進行搜索的時間複雜度爲O(logn)。

  最壞狀況下,二叉搜索樹可能很是偏斜,時間複雜度退化到O(n)。以下所示:

  

  解決方案:

  (1)隨機化的二叉搜索樹(打亂順序插入),有時是是否是插入的那打亂插入就很差用。

  (2)AVL樹

6、AVL樹

  AVL樹 

7、二叉搜索樹擴展應用——B樹

  B樹(B-Tree):B樹是一棵自平衡的多路搜索樹。經常使用於數據庫的索引,最經常使用數據庫的索引就是哈希表、B樹。

  以下所示,一個節點存了兩個值,分紅了三路。

  

相關文章
相關標籤/搜索