Python數據結構應用6——樹


  • 數據結構中的樹的結點和機器學習中決策樹的結點有一個很大的不同就是,數據結構中的樹的每個葉結點都是獨立的。

  • 樹的高度(Height)指葉結點的最大層樹(不包含根結點)

一、樹的建立

樹可以這樣定義:一棵樹由一系列結點和一系列連接結點的邊組成

樹也可以這樣定義: 一棵樹有根和其他子樹組成,這些子樹也是樹

在python,使用的定義都是后者。

1.1.list of lists

對於一個list:['q',[],[]],代表的是一棵樹(子樹),’q’是根結點,[],[]分別左右兩個子結點。所以,有一個左結點為'a'的樹可以寫為['q',['a',[],[]],[]]

my_tree = ['a', ['b', ['d',[],[]], ['e',[],[]] ], ['c',['f',[],[]], []] ]

在這種定義的情況下,根結點為my_tree[0],左結點為my_tree[1],右結點為my_tree[2]

def binary_tree(r):
    return [r, [], []]
def insert_left(root, new_branch):
    t = root.pop(1)   # The left child position
    if len(t) > 1:   # if not empty
        # The origin left child turn to be the left child of new_branch
        root.insert(1, [new_branch, t, []])
    else:
        root.insert(1, [new_branch, [], []])
    return root
def insert_right(root, new_branch):
    t = root.pop(2)
    if len(t) > 1:
        root.insert(2, [new_branch, [], t])
    else:
        root.insert(2, [new_branch, [], []])
    return root
def get_root_val(root):
    return root[0]
def set_root_val(root, new_val):
    root[0] = new_val
def get_left_child(root):
    return root[1]
def get_right_child(root):
    return root[2]
x = binary_tree('a')
insert_left(x,'b')
insert_right(x,'c')
insert_right(get_right_child(x), 'd')  # important!
insert_left(get_right_child(get_right_child(x)), 'e')
['d', ['e', [], []], []]

1.2 Nodes and References

這種思想主要是構建一個類,並采用嵌套的方法,生成左右結點(子樹)

class BinaryTree:
    def __init__(self, root):
        self.key = root
        self.left_child = None
        self.right_child = None
    def insert_left(self, new_node):
        if self.left_child == None:
            self.left_child = BinaryTree(new_node)
        else:
            t = BinaryTree(new_node)
            t.left_child = self.left_child
            self.left_child = t
    def insert_right(self, new_node):
        if self.right_child == None:
            self.right_child = BinaryTree(new_node)
        else:
            t = BinaryTree(new_node)
            t.right_child = self.right_child
            self.right_child = t
    def get_right_child(self):
        return self.right_child
    def get_left_child(self):
        return self.left_child
    def set_root_val(self,obj):
        self.key = obj
    def get_root_val(self):
        return self.key
x = BinaryTree('a')
x.insert_left('b')
x.insert_right('c')
x.get_right_child().insert_right('f')
x
<__main__.BinaryTree at 0x105930278>

這兩種構造方法都沒有辦法返回到父結點,具體見 三、解析樹

二、基於二叉堆的優先隊列

之前的隊列(queue)都是先進先出的數據結構。然而在優先隊列中,在隊列里的數據都有一個優先級標簽。 在優先級隊列中,隊列的項的邏輯順序由他們的優先級決定。最高優先級在隊列的前面,最低優先級在項的后面。

實現優先級隊列的經典方法是使用成為二叉堆的數據結構,它可以在\(O(log(n))\)時間中排隊和取出隊列

PS:二叉堆(binary heap)和stack的那個堆不是同一回事,這個堆(heap)在結構上是一棵完全二叉樹,在實現上使用的數組list,采用數組的index值表示父結點及子結點。二叉堆有兩個常見的變體:最小堆(其中最小的鍵總是在前面)和最大堆。

由於二叉堆要是一個完全二叉樹,所以為了保證log(n)的時間量,必須保證樹是平衡的。

如圖是一個二叉堆,堆的結構類似樹,實現方式由list實現。list的第一個元素是0,至於為什么要這樣呢?因為樹上的每一個元素都有對應的index,要滿足左右結點分別是2*p&2*p+1,如果索引index是從0開始,則不滿足。

二叉堆的屬性是一個很重要的東西,二叉堆的所有操作基本都基於它的屬性:每個結點的父結點都比它本身要小 (最小堆)

class BinHeap:
    def __init__(self):
        self.heap_list = [0]
        self.current_size = 0

2.1 插入(insert)操作

在二叉堆中插入一個項,過程是:將這個要插入的項置於二叉堆list的最后一項,由於二叉堆要保持它的屬性(每個結點的父結點都比它本身要小),這個項與其父結點進行比較,小的在上,大的在下進行可能的交換,這種比較要一直持續到根結點。

這種交換由於是將待插入的項從下往上交換,所以叫做percolate_up

    def perc_up(self, i):
        while i//2 > 0:
            if self.heap_list[i] < self.heap_list[i//2]:
                self.heap_list[i],self.heap_list[i//2]=self.heap_list[i//2],self.heap_list[i]
            i = i//2
    def insert(self, k):
        self.heap_list.append(k)
        self.current_size += 1
        self.perc_up(self, self.current_size)

2.2 刪除(出隊)操作

由於我們研究的是最小堆,所以出隊操作是移出值最小的那個結點,即根結點。

具體操作:移除根結點后,將list的最后一個項放置在根結點的位置上,然后根據二叉堆的屬性進行percolate_down操作(即根結點的項依次向下進行比較)

    def perc_down(self, i):
        while (i * 2) <= self.current_size:
            mc = self.min_child(i)
            if self.heap_list[i] > self.heap_list[mc]:
                self.heap_list[i],self.heap_list[mc]=self.heap_list[mc],self.heap_list[i]
            i = mc
    # 返回左右結點小的那一個
    def min_child(self, i):
        if i*2+1 > self.current_size:
            return i*2
        else:
            if self.heap_list[i*2] < self.heap_list[i*2+1]:
                return i*2
            else:
                return i*2+1
    def del_min(self):
        # The first element is 0
        ret_val = self.heap_list[1]
        self.heap_list[1] = self.heap_list[self.current_size]
        self.current_size = self.current_size - 1
        self.heap_list.pop()
        self.perc_down(1)
        return ret_val

2.3 建立二叉堆

兩種想法:

1.可以將list直接進行排序,最低花費\(O(nlogn)\),得出的list肯定是個二叉堆

2.對於一個list的每一個項從左往右進行percolate_down操作,時間復雜度\(O(n)\)

    def build_heap(self, a_list):
        i = len(a_list)//2
        self.current_size = len(a_list)
        self.heap_list = [0] + a_list[:]
        while(i > 0):
            self.perc_down(i)
            i = i - 1

三、解析樹(parse tree)

把解析表達式表示成樹的形式,如:\(((7+3)*(5-2))\)可以表示成

解析樹的形成可以定義為以下四個規則:

1.如果遇到(,意味着有一個新的表達式,則創建一棵新子樹(添加一個新子結點,且再創建這個新結點的左孩子,將處理目標移到這個左孩子上)

2.如果遇到[+,-,*,/]操作符(operators),將此操作符置於現結點,同時創建一個右孩子,將處理目標移到這個右孩子上

3.如果遇到一個number,將這個number置於到現結點,將處理目標返回到父結點

4.如果遇到),將處理目標返回到父結點。

def build_parse_tree(fp_exp):
    fp_list = fp_exp.split()
    p_stack = Stack()
    e_tree = BinaryTree('')
    p_stack.push(e_tree)
    current_tree = e_tree
    for i in fp_list:
        if i == '(':
            current_tree.insert_left('')
            p_stack.push(current_tree)
            current_tree = current_tree.get_left_child()
        elif i not in ['+','-','*','/',')']:
            current_tree.set_root_val(int(i))
            parent = p_stack.pop()
            current_tree = parent
        elif i in ['+','-','*','/']:
            current_tree.set_root_val(i)
            current_tree.insert_right('')
            p_stack.push(current_tree)
            current_tree = current_tree.get_right_child()
        elif i == ')':
            current_tree = p_stack.pop()
        else:
            raise ValueError
    return e_tree

由於之間構造的二叉樹類無法訪問其父結點,所以這里要新建一個堆在儲存當前的父結點。

在遇到(和operaters的這兩個操作都需要將當前操作結點移到左右孩子上,所以這兩個操作需要將父結點入堆。

四、樹的遍歷

三種遍歷方式:先序遍歷(preorder,中左右), 中序遍歷(inorder,左中右), 后序遍歷(postorder,左右中),他們的不同在於每個結點訪問的順序。

先,中,后都是對於根結點來說的。

在python中兩種方法進行遍歷:

1.外界函數

2.類中函數

事實證明外界函數處理遍歷更加有效率,因為在大多數情況下,遍歷都會跟別的操作混合在一起,所以外界函數修改更方便。

def preorder(tree):
    if tree:
        print(tree.get_root_val())
        preorder(tree.get_left_child())
        preorder(tree.get_right_child())
def preorder(self):   # 類中函數
    print(self.key)
    if self.left_child:
        self.left.preorder()
    if self.right_child:
        self.right.preorder()
def postorder(tree):
    if tree:
        postorder(tree.get_left_child())
        postorder(tree.get_right_child())
        print(tree.get_root_val)
def inorder(tree):
    if tree:
        inorder(tree.get_left_child())
        print(tree.get_root_val)
        inorder(tree.get_right_child())

4.1 解析樹evalute

import operator
def postorder_eval(tree):
    opers = {'+':operator.add,'-':operator.sub,'*':operator.mul,
      '/':operator.truediv}
    res1 = None
    res2 = None
    if tree:
        res1 = postorder_eval(tree.get_left_child())
        res2 = postorder_eval(tree.get_right_child())
        if res1 and res2:
            return opers[tree.get_root_val()](res1,res2)
        else:
            return tree.get_root_val()

五、二叉搜索樹(BST)

二叉搜索樹的實現主要依賴於它的性質(BST property):parent的key值比左結點大,比右結點小。

class TreeNode:
    def __init__(self, key, val, left=None, right=None, parent=None):
        self.key = key
        self.payload = val
        self.left_child = left
        self.right_child = right
        self.parent = parent
    def has_left_child(self):
        return self.left_child
    def has_right_child(self):
        return self.right_child
    def is_left_child(self):
        return self.parent and self.parent.left_child == self
    def is_right_child(self):
        return self.parent and self.parent.right_child == self
    def is_root(self):
        return not self.parent
    def is_leaf(self):
        return not (self.right_child or self.left_child)
    def has_any_children(self):
        return self.right_child or self.left_child
    def has_both_children(self):
        return self.right_child and self.left_child
    def replace_node_data(self, key, value, lc, rc):
        self.key = key
        self.payload = value
        self.left_child = lc
        self.right_child = rc
        if self.has_left_child():
            self.left_child.parent = self
        if self.has_right_child():
            self.right_child.parent = self

BinarySearchTree 這個類很復雜:

class BinarySearchTree:
    def __init__(self):
        self.root = None
        self.size = 0
    def length(self):
        return self.size
    def __len__(self):
        return self.size
    def __iter__(self):
        return self.root.__iter__()

下面的put()函數在BinarySearchTree中,他的主要功能是插入一個新node。如果樹沒有根結點,即將待插入結點為根結點,如果樹有根結點,即執行該類的私有函數_put()

    def put(self, key, val):
        if self.root:
            self._put(key, val, self.root)
        else:
            self.root = TreeNode(key ,val)
        self.size = self.size + 1
    def _put(self, key, val, current_node):
        if key < current_node.key:
            if current_node.has_left_child():
                self._put(key, val, current_node.left_child)
            else:
                current_node.left.child = TreeNode(key, val, parent=current_node)
        else:
            if current_node.has_right_child():
                self._put(key, val, current_node.right_child)
            else:
                current_node.right_child = TreeNode(key, val, parent=current_node)

下面是python的魔術方法,能輕易的進行賦值操作:(下列函數生成后即可以執行 my_zip_tree[32]=='hello' 這種賦值操作)

    def __setitem__(self, k, v):
        self.put(k, v)

get()操作是put()的反操作,用來找到結點

    def get(self, key):
        if self.root:
            res = self._get(key, self.root)
            if res:
                return res.payload
            else:
                return None
        else:
            return None
    def _get(self, key, current_node):
        if not current_node:
            return None
        elif current_node.key == key:
            return current_node
        elif key < current_node.key:
            return self._get(key, current_node.left_child)
        else:
            return self._get(key, current_node.right_child)
    def __getitem(self, key):
        return self.get(key)

使用get()函數,我們還可以創建in操作,即創建__contains__函數:

    def __contains__(self, key):
        if self._get(key, self.root):
            return True
        else:
            return False
if 'Northfield' in my_zip_tree:
    print("oom ya ya")

刪除結點操作是二叉搜索樹中最復雜的部分,主要過程分為兩步:首先,使用get()操作找到要刪除的node;然后,刪除node且保證其他子樹連接仍然保持二叉樹屬性。

def delete(self, key):
    if self.size > 1:
        node_to_remove = self._get(key, self.root)
        if node_to_remove:
            self.remove(node_to_remove)
            self.size = self.size - 1
        else:
            raise KeyError('Error, key not in tree')
    elif self.size == 1 and self.root.key == key:
        self.root = None
        self.size = self.size - 1
    else:
        raise KeyError('Error, key not in tree')

def __delitem__(self, key):
    self.delete(key)

remove()操作分為三種情況:

第一種:刪除的結點沒有孩子,這種情況很直接,直接刪除就ok

if current_node.is_leaf():
    if current_node == current_node.parent.left_child:
        current_node.parent.left_child = None
    else:
        current_node.parent.right_child = None

第二種:刪除的結點有一個孩子(這里只考慮這個孩子是左孩子的情況,右孩子的情況是對稱的)

1.如果要刪除的結點是左孩子,那么我們只要更新它的左孩子與他的父結點對接即可

2.如果要刪除的結點是右孩子,那么我們只要更新它的右孩子與他的父結點對接即可

3.如果刪除的結點沒有父母,那么他肯定是根結點

else: # this node has one child
    if current_node.has_left_child():
    if current_node.is_left_child():
        current_node.left_child.parent = current_node.parent
        current_node.parent.left_child = current_node.left_child
    elif current_node.is_right_child():
        current_node.left_child.parent = current_node.parent
        current_node.parent.right_child = current_node.left_child
    else:
        current_node.replace_node_data(current_node.left_child.key,
                      current_node.left_child.payload,
                      current_node.left_child.left_child,
                      current_node.left_child.right_child)
    else:
        if current_node.is_left_child():
            current_node.right_child.parent = current_node.parent
            current_node.parent.left_child = current_node.right_child
        elif current_node.is_right_child():
            current_node.right_child.parent = current_node.parent
            current_node.parent.right_child = current_node.right_child
        else:
            current_node.replace_node_data(current_node.right_child.key,
                          current_node.right_child.payload,
                          current_node.right_child.left_child,
                          current_node.right_child.right_child)

第三種:刪除的結點有兩個孩子。這時,並不能直接用他的孩子替代,這個時候需要找一個繼任者(successor),這個繼任者滿足兩個條件(1.在刪除結點中的子孫中第一個比他大的結點。2.這個繼任者最多有一個孩子),繼任者接替刪除結點的位置。

elif current_node.has_both_children(): #interior
    succ = current_node.find_successor()
    succ.splice_out()
    current_node.key = succ.key
    current_node.payload = succ.payload
def find_successor(self):
    succ = None
    if self.has_right_child():
        succ = self.right_child.find_min()
    else:
        if self.parent:
            if self.is_left_child():
                succ = self.parent
            else:
                self.parent.right_child = None
                succ = self.parent.find_successor()
                self.parent.right_child = self
return succ
  • Reference:
  1. Problem Solving with Algorithms and Data Structures, Release 3.0
  2. Python 魔術方法指南


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM