zoukankan      html  css  js  c++  java
  • 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 魔术方法指南
    转载请注明原文链接,对本文有任何建议和意见请在评论区讨论,谢谢!
  • 相关阅读:
    POJ 2342 树形DP入门题
    《4》CentOS7.0+OpenStack+kvm云平台部署—配置Nova
    GDI+ 填充背景时,非常多时候不起作用,GDI、GDI+配合运用
    Jave中System.getProperty()获取的值
    NYOJ128 前缀式计算(栈的运用)
    Web安全測试二步走
    一个SQL update语句
    C#时间格式之GMT时间的格式
    粘包的处理
    socket对于大数据的发送和接收
  • 原文地址:https://www.cnblogs.com/bjwu/p/9016566.html
Copyright © 2011-2022 走看看