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

大家好~我叫「小鹿鹿鹿」,是本卖萌小屋的第二位签约作(萌)者(货)。和小夕一样现在在从事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,删除节点。以此类推,就噢啦ヽ( ̄▽ ̄)ノ。

      

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/480995.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PCA主成分分析学习总结

大概主成分分析(Principal components analysis,以下简称PCA)是最重要的降维方法之一。在数据压缩消除冗余和数据噪音消除等领域都有广泛的应用。一般我们提到降维最容易想到的算法就是PCA,下面我们就对PCA的原理做一个总结。首先…

技术动态 | 大规模中文概念图谱CN-Probase正式发布

本文转载自公众号知识工场。 历时多年的研发,复旦大学知识工场实验室正式推出大规模中文概念图谱——CN-Probase,用于帮助机器更好的理解人类语言。概念图谱中包含实体(比如“刘德华”)、概念(比如“演员”&#xff09…

生产Docker应用重启排查经历

一、现象描述 近期,生产云平台监控发生Docker应用重启次数过多事故报警,经观察发现某些Docker应用不定期地出现重启现象,已严重影响服务正常提供 生产应用重启的判断条件:健康检查连续3次检查不通过 生产健康检查间隔时间设置为…

【Java】深入理解Java线程

1 相关概念 并发:两个或多个事件在同一时间段内发生【多个任务交替执行】 并行:两个或多个事件在同一时刻发生【多个任务同时执行】 进程:进入内存的程序 内存:所有应用程序都要进入到内存中执行 临时存储RAM 线程:进…

机器学习——多元线性回归分析(multiple regression)及应用

版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主允许不得转载。 https://blog.csdn.net/loveliuzz/article/details/78006493 </div><link rel"stylesheet" href"https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_vi…

肖仰华 | 知识图谱研究的回顾与展望

本文转载自公众号知识工场。 本文整理自2017年10月19日肖仰华教授在知识图谱前沿技术课程&#xff08;华东师范大学站&#xff09;所做的报告&#xff0c;报告的题目为《知识图谱研究的回顾与展望》。 大家好&#xff0c;很多人在对知识图谱的研究或者落地方面都表现出了极大的…

Java应用性能调优工具介绍及实践

一、背景 &#xff08;1&#xff09;、随着微服务架构的逐渐推广&#xff0c;一个大型的单个应用程序被拆分为数个微服务系统&#xff0c;这为研发人员的本地调试跟踪带来困难 &#xff08;2&#xff09;、在微服务架构中&#xff0c;由于业务的复杂性&#xff0c;常常一个业务…

Google、MS和BAT教给我的面试真谛

大家好呀&#xff0c;我是「小鹿鹿鹿」&#xff0c;我又来啦&#xff5e;&#xff5e;趁大家还有依稀印象赶紧乘热打铁&#xff5e;&#xff5e;这次聊一聊关于面试的一些小想法&#xff0c;希望和大家交流交流&#xff5e;&#xff5e;虽然资历尚浅&#xff0c;但是也面过不少…

AAAI 2018 论文 | 蚂蚁金服公开最新基于笔画的中文词向量算法

导读&#xff1a;词向量算法是自然语言处理领域的基础算法&#xff0c;在序列标注、问答系统和机器翻译等诸多任务中都发挥了重要作用。词向量算法最早由谷歌在2013年提出的word2vec&#xff0c;在接下来的几年里&#xff0c;该算法也经历不断的改进&#xff0c;但大多是仅适用…

甲子光年 | 为什么知识图谱终于火了?

本文转载自公众号甲子光年。如果知识是人类进步的阶梯&#xff0c;知识图谱就是AI进步的阶梯。作者&#xff5c;金丝猴编辑&#xff5c;甲小姐设计&#xff5c;孙佳栋微信&#xff5c;甲子光年 (ID:jazzyear)“知识图谱”相较于AI其他分支&#xff0c;似乎是最后一个热起来的赛…

Java多线程并发编程

一、线程池 1.1、什么是线程池 线程池是一种多线程的处理方式&#xff0c;利用已有线程对象继续服务新的任务&#xff08;按照一定的执行策略&#xff09;&#xff0c;而不是频繁地创建销毁线程对象&#xff0c;由此提高服务的吞吐能力&#xff0c;减少CPU的闲置时间。具体组成…

Step-by-step to Transformer:深入解析工作原理(以Pytorch机器翻译为例)

大家好&#xff0c;我是青青山螺应如是&#xff0c;大家可以叫我青青&#xff0c;工作之余是一名独立摄影师。喜欢美食、旅行、看展&#xff0c;偶尔整理下NLP学习笔记&#xff0c;不管技术文还是生活随感&#xff0c;都会分享本人摄影作品&#xff0c;希望文艺的技术青年能够喜…

知识图谱与智能问答基础理解

什么是知识图谱&#xff1f; 知识图谱本质上是语义网络&#xff0c;是一种基于图的数据结构&#xff0c;由节点(Point)和边(Edge)组成。在知识图谱里&#xff0c;每个节点表示现实世界中存在的“实体”&#xff0c;每条边为实体与实体之间的“关系”。知识图谱是关系的最有效的…

鲍捷 | 知识图谱从 0 级到 10 级简化版

本文转载自公众号&#xff1a;文因学堂。文因学以前写过几个进阶指南&#xff0c;可能都太难&#xff0c;不接地气。这里重新写一个更实事求是、更便于工程落地的版本0级&#xff1a;掌握正则表达式、SQL、JSON和一门支持if-then-else的高级语言 —— 是的&#xff0c;这些不是…

Java并发优化思路

一、并发优化 1.1、Java高并发包所采用的几个机制 &#xff08;1&#xff09;、CAS&#xff08;乐观操作&#xff09; jdk5以前采用synchronized&#xff0c;对共享区域进行同步操作&#xff0c;synchronized是重的操作&#xff0c;在高并发情况下&#xff0c;会引起线…

他与她,一个两年前的故事

“ 有没有那个Ta&#xff0c;值得你一生去守护”1她能力出众&#xff0c;业务能力无人能出其左右&#xff1b;他资质平庸&#xff0c;扮演一个很不起眼的角色&#xff1b;她国色天香&#xff0c;是整个公司上上下下关注的焦点&#xff1b;他其貌不扬&#xff0c;甚至很多人根本…

科普 | 知识图谱相关的名词解释

知识图谱&#xff08;Knowledge Graph&#xff09;是谷歌于2012年提出。企业通常出于商业目的去设计新的概念和名词。但每一个概念的提出都有其历史渊源和本质内涵。下面列举了知识图谱相关的几个概念&#xff0c;并简要阐明了它们与知识图谱的关系和区别。Knowledge Base&…

搜索引擎核心技术与算法 —— 倒排索引初体验

今天开启一个新篇章——智能搜索与NLP。本篇章将由羸弱菜鸡小Q和大家一同学习与智能搜索相关的知识和技术&#xff0c;希望能和大家一同学习与进步&#xff0c;冲鸭&#xff01;&#xff01;这里首先区分两个概念&#xff1a;搜索和检索检索&#xff1a;数据库时代的概念&#…

微服务设计原则和解决方案

一、微服务架构演进过程 近年来我们大家都体会到了互联网、移动互联带来的好处&#xff0c;作为IT从业者&#xff0c;在生活中时刻感受互联网好处的同时&#xff0c;在工作中可能感受的却是来自自互联网的一些压力&#xff0c;那就是我们传统企业的IT建设也是迫切需要转型&…

技术动态 | 知识可视化,连接和探究知识之间的联系!

本文转载自公众号&#xff1a;东湖大数据交易中心。大数据百人会线上沙龙 第八期3月1日晚八点&#xff0c;大数据百人会沙龙第八期主讲嘉宾——北京知珠传媒科技有限公司CEO郝庆一先生&#xff0c;分享他对可视化的理解&#xff0c;以及连接知识、探究知识之间的关系。1可视化…