一、初步了解Morris 算法思想
以二叉树前序遍历为例:
Morris 遍历的核心思想是利用树的大量空闲指针,实现空间开销的极限缩减。其前序遍历规则总结如下:
新建临时节点,令该节点为 root;
如果当前节点的左子节点为空,将当前节点加入答案,并遍历当前节点的右子节点;
如果当前节点的左子节点不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点:
如果前驱节点的右子节点为空,将前驱节点的右子节点设置为当前节点。然后将当前节点加入答案,并将前驱节点的右子节点更新为当前节点。当前节点更新为当前节点的左子节点。
如果前驱节点的右子节点为当前节点,将它的右子节点重新设为空。当前节点更新为当前节点的右子节点。
重复步骤 b 和步骤 c,直到遍历结束。
这样我们利用 Morris 遍历的方法,前序遍历该二叉树,即可实现线性时间与常数空间的遍历。
二、图例分析
刚接触Morris 遍历算法,仅仅根据上述核心思想也许并不能融会贯通,个人经验是了解核心思想后自己试试能完成几步Morris 算法的核心编程,然后再自己制作图例或网上寻找资源,通过对整个过程图的一步步分析,理清哪里不懂,哪里有问题······
里面内容非常充实。
三、代码实践
以力扣144. 二叉树的前序遍历为例(分享下个人第一次的书写过程):
(1)建立连接
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> temp;
if(root==nullptr)
{
return temp;
}
TreeNode* p1 = root, * p2 = nullptr;//分别用2个指针记录节点位置
//第一步,建立连接
while()//循环条件暂且不知
{
p2=p1->left;
if(p2)//p2不为空指针
{
while(p2->right)//找到以p2为根的最右侧子节点
{
p2=p2->right;
}
if(p2->right==nullptr)//若p2->right为空指针,则将p2的右节点指向p1,建立连接
{
p2->right=p1;//建立连接
temp.push_back(p1->val);//趁机将p1节点的值加入temp容器
p1=p1->left;//p1左移
continue;//建立该节点好连接后,返回至p1位移后的位置进行其他点的连接
}
}
else//p2是空指针
{
}
}
}
};
当连接过程进行到最后一个节点时,p1->right指向p1的根节点,p2为空指针,连接已经建好,接下来该进行断开连接的操作了。
(2)断开连接
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> temp;
if(root==nullptr)
{
return temp;
}
TreeNode* p1 = root, * p2 = nullptr;//分别用2个指针记录节点位置
//第一步,建立连接
while()//循环条件暂且不知
{
p2=p1->left;//若是p1沿着连接返回自身的根节点处后,则下面需要p2置空操作,以防止无限循环
if(p2)//p2不为空指针
{
while(p2->right && p2->right != p1)//找到以p2为根的最右侧子节点
//断开连接操作时,因为所有节点已经连好,所以这步操作需要添加条件
{
p2=p2->right;
}
if(p2->right==nullptr)//若p2->right为空指针,则将p2的右节点指向p1,建立连接
{
p2->right=p1;//建立连接
temp.push_back(p1->val);//趁机将p1节点的值加入temp容器
p1=p1->left;//p1左移
continue;//建立该节点好连接后,返回至p1位移后的位置进行其他点的连接
}
else//只有p2->right == p1这种情况了
{
p2->right=nullptr;//将p2置空,置空后,明显应将p1指向右子节点,而外层else时有同样操作,优化下重复代码
}
}
else//p2是空指针,表示所有连接已经连好,该进行断开连接的流程了
{
temp.push_back(p1->val);//趁机将p1节点的值加入temp容器
}
p1=p1->right;
}
}
};
当断开最后一个有连接的最右侧节点后,因为最后是p1=p1->right;操作,故p1为空指针,从这里不难得出外层while循环的进行条件是p1 != nullptr;
(3)最终代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> temp;
if(root==nullptr)
{
return temp;
}
TreeNode* p1 = root, * p2 = nullptr;//分别用2个指针记录节点位置
//第一步,建立连接
while(p1)//p1不为空才进行下列代码
{
p2=p1->left;//若是p1沿着连接返回自身的根节点处后,则下面需要p2置空操作,以防止无限循环
if(p2)//p2不为空指针
{
while(p2->right && p2->right != p1)//找到以p2为根的最右侧子节点
//断开连接操作时,因为所有节点已经连好,所以这步操作需要添加条件
{
p2=p2->right;
}
if(p2->right==nullptr)//若p2->right为空指针,则将p2的右节点指向p1,建立连接
{
p2->right=p1;//建立连接
temp.push_back(p1->val);//趁机将p1节点的值加入temp容器
p1=p1->left;//p1左移
continue;//建立该节点好连接后,返回至p1位移后的位置进行其他点的连接
}
else//只有p2->right == p1这种情况了
{
p2->right=nullptr;//将p2置空,置空后,明显应将p1指向右子节点,而外层else时有同样操作,优化下
}
}
else//p2是空指针,表示所有连接已经连好,该进行断开连接的流程了
{
temp.push_back(p1->val);//趁机将p1节点的值加入temp容器
}
p1=p1->right;
}
return temp;
}
};
建议多常识写几次,加深印象。
(4)错误笔记
==与=不要写错,否则会爆栈;
注意指针相关操作;
出错时,先自己浏览整个流程,尝试找出错因所在点,如果长时间未找出错因,建议在编译器上调试;
欢迎探讨!