大家好~我叫「小鹿鹿鹿」,是本卖萌小屋的第二位签约作(萌)者(货)。和小夕一样现在在从事NLP相关工作,希望和大家分享NLP相关的、不限于NLP的各种小想法,新技术。这是我的第一篇试水文章,初来乍到,希望大噶多多滋辞(●'◡'●)。

冬天已经来了,秋招早已悄无声息的结束。作为一个已经工作了两年的老人,校招面试感觉就像高考一样遥远。但是呢,虽然工作了,还是要时刻保持危机感的呀,万一哪天就要跳槽了呢( ̄∇ ̄)。

遥想当年面试的时候,由于没有学过数据结构,在面试官出算法题之前就老实交待家底:“我的算法和数据结构不太行,树呀图呀都不太会(✿◡‿◡)"。但是经过两年断断续续的学习,发现其实树是一个套路非常明显的一类算法题,而遍历树是解决绝大多数树问题的基础(很多题目都是在树的遍历上扩展),下面小鹿就以树的遍历为例,解剖树里面深深的套路吧o(* ̄▽ ̄*)o。

树的基础回顾


二叉树长什么样子,小鹿这里就不上图啦,所谓的根节点、叶子节点也不介绍啦。我们知道,二叉树的遍历分为三种:前序、中序和后序。这三种序的不同主要就是在于什么时候访问根节点。以前序为例,在遍历一颗树的时候先访问根节点,再遍历其根节点的左子树,最后访问根节点的右子树。而中序遍历就是先遍历左子树,再访问根节点,最后遍历右子树;后序遍历小鹿就不重复啦。

树的递归


树有一个很好的特性,就是一棵树的根节点的左右子树仍然是一颗树

这好像是一句正确的废话╮( ̄▽ ̄"")╭

所以我们可以把一棵复杂的大树,分解成两颗稍微小一点的树,依次类推,最后变成最小的单元(只有一个节点的树)。不管怎么讲,处理只有一个节点的树是不是超级容易!这个呢就是递归的思想,所以说到这里,我们以后只要遇到关于树的问题,谁还不会用递归走一波呢!

下面就来开始实践!用递归的思想实现二叉树的前序、中序、后序遍历(遍历结果记录在self.ans的向量里)。小鹿这里就用python写啦(真的不要纠结编程语言噢)。遍历一个有n个节点的树,其时间复杂度和空间复杂度都是O(n)。

class Solution(object):def __init__(self):self.ans = []def preorderRecursive(self, root):if root:self.ans.append(root.val)self.preorderRecursive(root.left)self.perorderRecursive(root.right)def inorderRecursive(self, root):if root:self.inorderRecursive(root.left)self.ans.append(root.val)self.inorderRecursive(root.right)def postorderRecursive(self, root):if root:self.postorderRecursive(root.left)self.postorderRecursive(root.right)self.ans.append(root.val)

树和栈


用递归的思路解决树的遍历或者其他树的问题,思路非常清晰,代码也会非常的简单清楚。当我们在使用递归(函数的嵌套)的时候,其本质是在调用栈。我们可以用栈来实现树的遍历,进一步理解递归的思想。

class Solution(object):def __init__(self):self.ans = []def preorderStack(self, root):aStack = []if root:aStack.append(root)while aStack:p = aStack.pop()self.ans.append(p.val)if p.right:aStack.append(p.right)if p.left:aStack.append(p.left)return self.ansdef inorderStack(self, root):stack = []p = rootwhile p:stack.append(p)p = p.leftwhile stack:p = stack.pop()self.ans.append(p.val)p = p.rightwhile p:stack.append(p)p = p.leftreturn self.ansdef postorderStack(self, root)aStack = []prev = Nonep = rootwhile aStack or p:if p:aStack.append(p)p = p.leftelse:p = aStack[-1]if p.right == prev or p.right is None:self.ans.append(p.val)prev = paStack.pop()p = Noneelse:p = p.right return self.ans

用栈来实现树的遍历就稍微复杂一点啦。首先前序是最简单的,我们用一个栈(first-in-last-out)来维护树遍历的顺序。初始状态是把非空的根节点push到栈中,逐个从栈中取出节点,访问其值,然后先push右节点再push左节点到栈中,就保证访问顺序是 root->left->right 啦。超级简单有木有!

中序遍历就稍微难一些了,在访问一个节点之前需要访问其 left-most 节点,将其左节点逐个加入到栈中,直到当前节点为None。然后从栈中取出节点,由于栈的特性,在取出某个节点之前,其左节点已经被访问,所以就可以安心的访问该节点,然后把当前节点置为其右节点,依次类推。

后序遍历和中序遍历差不多,在中序遍历的基础上需要增加一个prev记录上一个访问的节点,确认在访问某个节点之前其右节点是否已经被访问。

用栈的方式遍历树,更加显式的告诉了大家树遍历的时候是怎么回溯的,其时间空间复杂度仍然是O(n)

Morris遍历


下面小鹿要给大家介绍一个神级树遍历方法Morris,使其空间复杂度从O(n)变成O(1)!

树的遍历一个难点就是确定什么时候以及回溯如何回溯(好像是两个点),第一种递归的方法通过函数的嵌套实现回溯,第二种方法通过栈存储节点的顺序实现回溯,而Morris则是利用树中空节点记录每个节点其回溯的节点,实现空间复杂度为O(1)的遍历。

具体来说,当我们访问了某个左子树最右的节点后需要回溯到其左子树的根节点,但是怎么回去呢,我们需要在之前加一个链接,将该根节点连到其左子树的最右节点的右节点上。是不是有点绕呢(╯﹏╰)b,不慌!小鹿带你一起做一遍中序遍历的例子就清楚啦~~

以上面的图为例做中序遍历,当前节点为6(curr=6)时,找到其左子树的最右节点5,添加链接备用,这样从节点5我们就可以回溯到节点6。更新当前节点,curr=2,重复相同的操作。当curr=1时,到达叶子节点,输出(节点变蓝),因为之前添加了链接,所以可以从节点1回溯到节点2,回溯删除链接。当前节点变成4,以此类推下去

已经懵逼的小伙伴们请仔细品味下面的代码( ̄▽ ̄)~

class Solution(object):def __init__(self):self.ans = []def inorderMorris(self, root):p = rootwhile p:if p.left is None:#left-most nodeself.ans.append(p.val)p = p.rightelse:#find prev, the right-most node of the left treeprev = p.leftwhile prev.right and prev.right != p:prev = prev.rightif prev.right is None:#first time to visit pprev.right = p #add linkp = p.leftelse:#second time to visit pself.ans.append(p.val)prev.right = Nonep = p.rightreturn self.ansdef preorderMorris(self, root):p = rootprev = Nonewhile p:if p.left is None:#left-most nodeself.ans.append(p.val)p = p.rightelse:#find right-most node of the left treeprev = p.leftwhile prev.right and prev.right != p:prev = prev.rightif prev.right is None:#first time to visit pprev.right = p #add linkself.ans.append(p.val)p = p.leftelse:#second time to visit pp = p.right #back to rootprev.right = None #delete the linkreturn self.ans

Morris前序遍历和中序遍历几乎一模一样,唯一的差别就是在第一次访问节点的时候就输出,还是第二次访问节点的时候输出。Morris后序遍历在此基础上还要稍微的复杂一丢丢。因为后续遍历根节点最后输出,需要增加一个dump节点作为假的根节点,使其的左子树right-most 指向原来的根节点。话不多说,我们来看下代码吧!

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = Noneclass Solution(object):def __init__(self):self.ans = []def postorderMorris(self, root):dump = TreeNode(0)dump.left = rootp = dumpwhile p:if p.left is None:p = p.rightelse:prev = p.leftwhile prev.right and prev.right != p:prev = prev.rightif prev.right is None:#first time to visit pprev.right = pp = p.leftelse:#second time to visit pself.singleLinkReverseTraversal(p.left, prev)prev.right = Nonep = p.rightreturn self.ansdef singleLinkReverseTraversal(self, start, end):#take the right branch from start to end as single link#travel reverselyif start == end:self.ans.append(start.val)returnself.reverse(start, end)curr = endwhile curr != start:self.ans.append(curr.val)curr = curr.rightself.ans.append(curr.val)self.reverse(end, start)def reverse(self, start, end):if start == end:returnprev = Nonecurr = startwhile curr != end:tmp = curr.rightcurr.right = prevprev = currcurr = tmpcurr.right = prev

眼尖的小伙伴看了代码之后就会发现,Morris后序遍历怎么多了两个函数,怎么和前序和中序不一样了呢ヽ(`Д´)ノ!这个Morris后序遍历确实比较挺难理解,我们还是用最简单的图示来走一遍代码的意思吧~~~

开始除了加了一个dump节点以外,都是一样的一路向下,到达left-most节点3,不输出(注意啦!不一样啦!)然后回溯到节点2,逆序输出从3到3的节点,删除链接。从节点2一路往右回溯到节点1,逆序输出从其左节点2到prev节点6,删除节点。以此类推,就噢啦ヽ( ̄▽ ̄)ノ。

现在大家都清楚了吧~~不清楚的地方欢迎在评论区提出噢,小鹿会尽最大努力为大家答疑解惑嗒( ̄▽ ̄)/后面小鹿鹿鹿还会持续推送关于算法和数据结构的小文章,大家有兴趣的话欢迎关注订阅哦(´▽`)ノ 。

感谢大家的阅读[]~( ̄▽ ̄)~*(鞠躬)

算法与数据结构--空间复杂度O(1)遍历树相关推荐

  1. 算法与数据结构(python):树与二叉树

    提示:专栏解锁后,可以查看该专栏所有文章. 文章目录 树和二叉树 树 二叉树 二叉树的表示 二叉树的遍历 树和二叉树 树 这里先简单讲讲树的概念.树结构是一种包括 节点(nodes)和边(edges) ...

  2. 算法与数据结构(三) 二叉树的遍历及其线索化(Swift版)

    前面两篇博客介绍了线性表的顺序存储与链式存储以及对应的操作,并且还聊了栈与队列的相关内容.本篇博客我们就继续聊数据结构的相关东西,并且所涉及的相关Demo依然使用面向对象语言Swift来表示.本篇博客 ...

  3. js 数组 实现 完全树_算法和数据结构 | 树状数组(Binary Indexed Tree)

    本文来源于力扣圈子,作者:胡小旭.点击查看原文 力扣​leetcode-cn.com 树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为 Fenwick 树.其初 ...

  4. 由任意二叉树的前序遍历序列和中序遍历序列求二叉树的思想方法_算法与数据结构基础 - 二叉树(Binary Tree)...

    二叉树基础 满足这样性质的树称为二叉树:空树或节点最多有两个子树,称为左子树.右子树, 左右子树节点同样最多有两个子树. 二叉树是递归定义的,因而常用递归/DFS的思想处理二叉树相关问题,例如Leet ...

  5. 算法与数据结构07:前缀树,计数排序与桶排序

    算法与数据结构07:前缀树,计数排序与桶排序 前缀树 计数排序 桶排序 前缀树 Trie 1.根据字符串数组中,每个字符串的字符作为路径,组成而成的一个多叉树结构 2.每个节点都有一个paths数组, ...

  6. 「算法与数据结构」时间与空间复杂度

    写在前面 可能有些人会吐槽,学算法有什么用,顶多就是去面试大厂的时候能用上,大厂面试算法也只是强中筛强的一个敲门砖而已,我又不去面大厂,不用学它,真的是这样吗? 肯定不是,在计算机行业发展,不管是前端 ...

  7. 数据结构思维 第六章 树的遍历

    第六章 树的遍历 原文:Chapter 6 Tree traversal 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 本章将介绍一个 Web 搜索引擎,我们将在本书其余部分开 ...

  8. 数据结构与算法之二叉树的先序遍历,中序遍历,后序遍历

    数据结构与算法之二叉树的先序遍历,中序遍历,后移遍历 目录 实现二叉树的先序,中序,后序遍历,包括递归方式和非递归方式 在二叉树中找到一个节点的后继节点 1. 实现二叉树的先序,中序,后序遍历,包括递 ...

  9. 数据结构与算法的时间空间复杂度

    提到数据结构与算法就不得不提时间复杂度和空间复杂度,本人看大部分文章介绍都比较晦涩难懂,就想着用简单的代码示例快速让你理解数据结构与算法的时间空间复杂度. 首先,时间复杂度表示的是使用某个数据结构或者 ...

最新文章

  1. mysql time_limit_mysql ---- limit使用方式
  2. jax-rs/jersey_在Oracle Cloud上的Prime-UI,JAX-RS和Jersey和Gson
  3. Python 中的range,以及numpy包中的arange函数
  4. 】.NET使用NPOI组件将数据导出Excel
  5. Scanner的用法强化
  6. Exact-k:组合推荐的秘密
  7. 标注工具——VoTT的调试笔记
  8. easypoi list中的map导出_EasyPOI简单用例,简单有效
  9. Pgsql登录连接失败原因及解决方法
  10. Java中快速生成UUID
  11. delphi 水晶按钮
  12. PKU红的HSB值,以及THU紫的HSB值。北大红,清华紫
  13. PHP+Mysql 实现最简单的注册登录
  14. 对数损失函数与最大似然损失函数
  15. Docker 制作带有中文字体的镜像
  16. 【多元统计分析】均值向量和协方差阵的检验——spss上机实验
  17. 唯样商城:电路板上的字母都代表什么意思?
  18. 想学机器学习吗?带坑的那种
  19. 树莓派4b安装Android10
  20. dynamic-datasource Please check the setting of primary解决方案

热门文章

  1. IEnumerable和IQueryable的区别
  2. Humble Numbers USCAO chapter 3.1
  3. 关于如何在BCB中使用CodeGuard
  4. 生产者-消费者模式的实现
  5. C语言定义:__DATE__和_TIME__
  6. axure弹窗关闭_干货来袭,Axure插入图标的几种办法
  7. 人工智能在语音和数字图像处理领域有哪些具体化应用_智能呼叫中心系统有哪些优势...
  8. smzdm 扫地机器人_堪称米家最值!米家系列扫地机器人选购须知
  9. 二十、PHP框架Laravel学习笔记——模型的作用域
  10. 二、PHP框架Laravel学习笔记——路由的定义和控制器