数据结构の学习(四):二叉树的可视化遍历

 笔者学习树的路线图是这样的。首先是树和节点的类定义,其次是用turtle实现了树的可视化,然后简单测试之后就进入到树的应用部分。一个经典的问题是利用树来计算表达式。按照工程化的思维,步骤应该是这样的:(1)给表达式加括号(2)创建解析树表达式(3)树的后序遍历,生成后缀表达式(4)后缀表达式求值。如果你很希望解决一个过程实际问题,那么请你备好笔记本和铅笔,带好小板凳,让我们开始吧!

 

如果你还是一位初学者,对二叉树具有浓厚的兴趣,推荐一套极好的学习视频:https://www.icourse163.org/course/PKU-1206307812。源自北大。本博客部分灵感亦来源于此,但要更加深入一些,原课程代码有一些缺陷,因此我手动实现全部代码。


不幸的是,笔者的博客将不会有完整代码(示范代码除外),所有代码均已上传到码云上。如果时间和精力允许的话,强烈建议你根据思路,手动写一遍,相信你会感觉到全身毛孔舒张而不是想砸电脑的快感。

前缀表达式:不含括号,将运算符写在前面,操作数写在后面。为纪念其发明者波兰数学家Jan Lukasiewicz,前缀表达式也称为“波兰式”。例如,- 1 + 2 3,它等价于1-(2+3)。
中缀表达式:我们习惯的表达式,如8+4-6*2,注意它也不含括号。
后缀表达式:又称逆波兰式(Reverse Polish notation,RPN,或逆波兰记法),不含括号,将运算符写在操作数之后,如123+4x+5-

树的遍历有三种:前序遍历、中序遍历和后序遍历。对于生成好的二叉树,我们将分别得到前缀表达式、中缀表达式、后缀表达式。可以说,树和表达式天然就是一个整体,只是表示方法不同而已。 相信在最初学习数据结构的时候(笔者也是这样),你永远在纠结一个问题,那就是前缀、后缀、中缀相互转换问题。这对计算机而言,小事一桩。对人类而言,我们不得不想一些技巧了。

一个很好的办法是给表达式加括号。比如中缀表达式1+(2+3)*4-5。我们给它加上括号,直到不能加为止。((1+((2+3)*4))-5)。然后将所有运算符移动到它所在括号的前面,也就是'('前面,注意不要移动数字。很棒,你一定很快速地在草稿纸上写出了

-(+(1*(+(23)4))5),然后我们将括号去掉,-+1*+2345,这就是前缀表达式。希望你不要太吃惊:-O。类似地,我们将运算符移动到括号后面,就得到了后缀表达式,123+4*+5-。

回到正轨上(如果还不是太晚),如下图是一棵树(忽略右下角的小乌龟)

前序遍历过程是:先遍历根节点,再是左子节点,最后是右子节点。

中序遍历过程是: 先遍历左子节点,再是根节点,最后是右子节点。

后序遍历过程是:先遍历左子节点,再是右子节点,最后是根节点。

对于计算机而言,只需要递归就行,这是非常神奇的操作。

def transversal(tree):
        transversal(tree.left_child)
        transversal(tree.right_child)

现在我们有了一个函数模板,只需要一句话就可以让它分别实现三种遍历,我们可以在函数外设计一个全局列表exp_list,来存储遍历值。现在请你思考,我们应该把exp_list .append(tree.val)添加在哪。

。。。loading

在你思考的间隙,我们需要来一点酷炫的操作。把这个过程可视化!天才需要一点想象力作催化剂不是吗(黑人问号?)

其实这也非常直观,我们只需要画一条线,让它从前一个节点指向下一个节点就行了。这个函数声明为draw_line

     t=turtle.turtle()   
     def draw_line(self,coord1, coord2,pensize=5,pen_color='red'): #from 1 to 2
        (x,y) = coord1  
        t.penup()
        t.pensize(pensize)
        t.pencolor(pen_color)
        t.setx(x);t.sety(y)
        t.pendown()
        t.goto(coord2[0],coord2[1])

这时候我们的遍历代码是这样的,对吧?

def preorder_transversal(tree):
    if tree:
        pre_exp.append(tree.data)
        preorder_transversal(tree.left_child)
        preorder_transversal(tree.right_child)
        
            
    
def inorder_transversal(tree):
    if tree:
        inorder_transversal(tree.left_child)
        in_exp.append(tree.data)
        inorder_transversal(tree.right_child)
        
       
def postorder_transversal(tree):
    if tree:
        postorder_transversal(tree.left_child)
        postorder_transversal(tree.right_child)
      
        post_exp.append (tree.data)

为了神秘的遍历过程展现在我们面前,我们得把draw_line函数设置在每一次append的位置。遍历函数不得不再添加一个参数last_tree。现在唯一的问题是,我们需要知道上个树节点的坐标,也就是圆心的坐标还有当前树节点的坐标。

如果你对前面的树画图代码还有印象的话。是的,我们对待自己的代码就像对待自己的孩子一样,数十年如一日,如果你很快就忘掉的话,那说明你不是一个称职的家长。我们在实现draw_node时候就是把节点圆心坐标作为参数输入的。你想到可以利用一个字典来存储坐标,而它的key 就是节点本身。然后我们再设计一个函数get_node_coord来获取节点坐标即可。

    def get_node_coord(self,node)->tuple:
        #return the coord of the node circle centre
        return self.node_memo[node]

接下来你迫不及待想看实现python绘画了。先别急,wait,我们inorder_transversal(tree,last_tree,draw = True): 如何初始化呢?last_tree用什么初始化?是一个空节点吗。ˇˍˇ||,很显然不是。你的大脑开始飞速运转,如同一台RTX2080。


答案是:和tree一样,都采用根节点初始化。这样我们第一条轨迹就是从根节点到根节点,不会对绘图有影响。

 

这是turtle跑完的后序遍历示意图。虽然每种遍历结果相同,在turtle运行之中,我们可以感受到遍历的过程。如果你觉得太快的话,可以设置:

t.speed('slowest')#'fastest' 'fast' 'normal' 'slow' 'slowest'

 它过程是1->2->+->3->4->/。看来笔者的图并不完善。这是因为递归过程没有左子节点指向右子节点。相信聪明的你可以进行完善。

希望这篇文章对你有帮助,相关代码均已上传至码云,如果你喜欢的话欢迎Fork或Star,甚至是请笔者喝咖啡!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值