这是跟着代码随想录的顺序学习算法的第十五天。
以下是学习时自己的一些理解与笔记,如有错误欢迎指正与讨论。
二叉树的统一迭代法
参考相关链接:
笔记
(算是根据结论来反向思考过程的合理性吧或者为了自圆其说而不断补充,通过这样的思考来理解=-=)
第一次通过边想边写的方式来理一下脑袋里冒出来的想法,顺便用这样的方法来尝试理清核心的思想与逻辑关系,但感觉还是得做更多的题才能理解的更清楚,姑且算是做个标记,二刷时候再来看看怎么修改补充。
前序(后序)
之前的前序遍历的迭代法(其实后序用的思想也可以算作前序,后序只是在此基础上改了左右顺序,最后结果进行翻转)核心思想在于首先操作的就是中间节点,每次操作的时候直接可以先出栈,接着,再去操作左右节点。
// 前序
while(stack.length) {
cur = stack.pop();
res.push(cur.val);
cur.right && stack.push(cur.right);
cur.left && stack.push(cur.left);
}
需要补充说明的是,这里可以把二叉树中的每个节点都看作一颗小的二层满二叉树(叶子节点可能为空),然后每次都只在这样一个小二叉树中考虑遍历和栈的使用。
接着,在这个小二叉树中再分层考虑,先考虑顶层,前序遍历是中左右,这里用栈存储的话,我们本来是应该把顶层的中间节点压入栈中,为了栈里面的节点顺序是右左中嘛。
但是,因为这里的小二叉树中的顶层节点(微观)实际上也是更高层的小二叉树的叶子节点(宏观),所以在上一层的小二叉树的遍历中,这一层的小二叉树的顶层节点其实已经在栈中了,所以直接输出就可以了。(追溯到最高层时,其实是在定义的时候就把根节点给提前压入栈中了,不然没有更高层的小二叉树来操作这个根节点)
思考到这个地方,我不禁冒出一个问题:
那是怎么保证这时候栈里面的最外层节点就是需要的节点的呢?
把小二叉树的顶层节点输出后,先继续往下走,边走边找答案。
按照上面的思路,为了让下一层的小二叉树能够直接输出它们的顶层节点,所以这里就需要把这一层的小二叉树的两个叶子节点按照右左的顺序压入栈中,这个时候在本层的遍历就完成了,接下来,再去下一层的小二叉树中开始新的遍历。
走到这里,发现答案出来了,因为小二叉树的叶子节点是按照右左的顺序压入栈中的,所以每次先进入的下一层的小二叉树的顶层节点一定是栈中最外面的上一层的左叶子节点。
这是主体的循环操作的前后关系,或者说整个算法的中间部分。
那开头和结尾又和这个中间部分关系是什么呢,怎么开始的,怎么结束的呢?
先来看看开始,发现其实就是前面想到的【(追溯到最高层时,其实是在定义的时候就把根节点给提前压入栈中了,不然没有更高层的小二叉树来操作这个根节点)】,相当于手动给这循环操作加了个开始键。
再来想想结尾的部分是怎么样的,从上一层的左叶子节点往下遍历着一层层的小二叉树,到达整个二叉树最左边的最深处的小二叉树的时候,这个时候已经没有给下一层的左叶子节点了(先假设此时也没有右叶子节点了),那么这个时候栈中最外面的元素就是这一层的右兄弟节点,也就是开始了右边的小二叉树的遍历了,注意到,此时的栈中,存的全都是每一层小二叉树的右叶子节点,所以能够借助栈的先进后出的特性会慢慢的往上去遍历右边的节点,直到栈为空即全部遍历。
通过从后往前思考把大部分的点都找了出来,接着,再试着通过整理的方式,从前往后把逻辑理一下就是核心代码了,先出顶层节点再存右左节点。
中序
按照这样的方式来迭代,中序遍历则需要做一些特殊处理,为什么呢?
因为在前面的方法中,中间节点历遍的次序是在两端的(最开始或最后面),在末尾的也可转成最开始的,也就是说,小二叉树中最开始处理的就是中间节点,处理完之后再直接通过这个中间节点去获取和存储左右叶子节点。而中序遍历时,需要的顺序是左中右,中间节点的次序是在中间的,如果按照上面的方法来处理,则无法获取到右边节点的信息,所以需要进行一些特殊的处理。
主要思路是,先一路将小二叉树的左叶子节点压入栈中,直到左边的最深处,此时,该层的小二叉树的左叶子节点已经为空(不妨先假设右叶子节点也为空),先将此时该层的顶层节点弹出,也就是栈的最外层元素。
此时,获取到的是该层小二叉树的顶层节点,也就是该层的中间节点,而此时我们需要输出的是上一层的中间节点,而此时这个节点正是栈的最外层元素,所以我们需要让栈再弹一次最外层元素,而不做别的操作。此时可以先去找右边的节点,发现也为空,做这样一次"无效操作",让我们能够获取到栈的最外层元素,此时再通过这更高一层的小二叉树去找右边的节点,此时就成了"有效操作",即可以获取到右边的节点信息。
这样的方法和之前的区别主要在于一个是先弹出再下一层,一个是先下一层再弹出,弹出的是中间节点,下一层的是去左边节点。
两个方法对于栈的使用也有明显的区别,前者一路存的是小二叉树的右边叶子节点(左边的在下一层马上就用掉了),通过"右左"来从深层回到顶层;后者一路存的是小二叉树的左边叶子节点,同时搭配一步"无效操作"去获得中间的节点,通过这左边叶子节点兼中间节点来返回顶层。
// 中序
while(stack.length || cur) {
if(cur) {
stack.push(cur);
// 左
cur = cur.left;
} else {
// --> 弹出 中
cur = stack.pop();
res.push(cur.val);
// 右
cur = cur.right;
}
核心就在于怎么拿到中间的节点,二叉树的特点就在是只有通过中间节点才能获取到左右两边的节点,不能通过左节点之间获取右节点。
统一
所以如果想让这三种方式形式统一,那要考虑的问题就是**如何认出来这是中间节点**,**如何处理中间节点**。
这里的处理方法是,给加入栈中的中间的节点后添加一个空节点做为一种标识符,同时换一种使用栈的方式,即将小二叉树中的三个节点都按倒序的方式存入栈中,而不是向前面的只主要存一部分,再通过这部分边输出边回到顶层,这里是通过空节点标识符来直接判断到这是该层小二叉树的中间节点。
那么如何处理节点呢?
每次都先将栈的最外层元素拿出来,即获取到该层的中间节点,再根据需要来按一定顺序将其压入栈中,也就是先把中间节点拿出来,不输出,再放回去,到最后再根据标识符来判断输出中间节点。(到叶子节点的时候会做一次"无效操作"来使得此时的叶子节点是小二叉树的中间节点)
// 前序
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
if (node.right) stack.push(node.right); // 右
if (node.left) stack.push(node.left); // 左
stack.push(node); // 中
stack.push(null);
};
// 中序
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
if (node.right) stack.push(node.right); // 右
stack.push(node); // 中
stack.push(null);
if (node.left) stack.push(node.left); // 左
};