如果你用递归实现树的遍历,那么他的空间复杂度一定是O(logN)的【树高】。
但是的的确确存在着神奇的O(1)空间复杂度的遍历方法-Morris遍历。
Morris的时间复杂度O(n),简略证明:对于一个节点x,我们需要去访问它左子树的右边界两次,而所有节点的右边界长度累加就是N,所以访问2*N次,复杂度为O(N)。
我们之所以要用递归的方式遍历,原因仅仅是我们没办法直接回到上一个节点,而递归所压的栈,可以帮助我们回退。但是,递归的代价往往是昂贵的,系统帮我们压的栈不仅仅是参数,还有当前句柄等等一堆信息,浪费的空间是很可观的,就算是手写的栈,依旧摆脱不了O(logN)的空间代价。如果有一条指向上面节点的指针就好了。。。。。。嗯,我们可以察觉,叶子节点的左孩子和有孩子都是指向null的,能不能利用起来呢。。。。。。这个很像一种数据结构-线索树。
Morris和线索树的思想很像,但是并没有线索化那么繁琐的代价。Morris的基本过程:
我们把当前正在访问的节点记为cur。
1)如果cur没有左孩子,那么cur到右孩子节点去cur=cur.right。
2)如果cur有左孩子,找到cur的左子树上的最右节点,记为mostright:
A.如果mostright.right==null 让mostright.right=cur,cur向其左孩子移动
B.如果mostright.right==cur 让mostright.right=null,cur向右移动
粗略地看完2)情况的A、B,大概可以知道mostright.t的指向就是指明cur的移动方向。
粗略证明:对于先序过程来说,是先根,再左树,最后右树,也就是说,当遍历了cur【旧】之后,我们去遍历cur【旧】的左子树并先将它的mostright.right=cur,那么那棵树中最后一个遍历的就是它的最右节点,此时cur=mostright且为叶子节点,根据之前给mostright.right设置的值跳转至cur【旧】,这时mostright.right==cur【旧】,指明了cur应该去遍历右子树,将这个mostright.right修正为null即可保持不变。
void Morris(Node* root){
if(!root)return;
node* cur=root,*mostRight;
while(cur!=NULL){
mostRight=cur.left;
if(mostRight!=NULL){
while(mostRight->right!=null&&mostRight->right!=root){//第一种对应了最右节点右孩子为空 第二种对应了最右节点右孩子有过修改
mostRight=mostRight->right;
}
if(mostRight.right==NULL){
mostRight.right=cur;
cur=cur.left;
continue;
}
else{
mostRight.right=NULL;
}
}
cur=cur.right;
}
}
Morris的实质:如果一个节点有左子树,会来到该节点两次,否则只来到一次。而递归的实质是,不管如何都访问一个节点三次。先序中序和后序也只是在哪一次访问时打印而已。
Morris改先序遍历:
void MorrisPre(Node* root){
if(!root)return;
Node* cur=root,*mostRight;
while(cur!=NULL){
mostRight=cur->left;
if(mostRight!=NULL){
while(mostRight->right!=NULL&&mostRight->right!=root){//第一种对应了最右节点右孩子为空 第二种对应了最右节点右孩子有过修改
mostRight=mostRight->right;
}
if(mostRight->right==NULL){//第一次访问cur
mostRight->right=cur;
printf("%d ",cur->value);
cur=cur->left;
continue;
}
else{
mostRight->right=NULL;
}
}
else{//第一次访问cur cur没有左子树 直接打印
printf("%d ",cur->value);
}
cur=cur->right;
}
}
Morris改中序遍历:【打印完了左子树再打印自身】
void MorrisMid(Node* root){
if(!root)return;
Node* cur=root,*mostRight;
while(cur!=NULL){
mostRight=cur->left;
if(mostRight!=NULL){
while(mostRight->right!=NULL&&mostRight->right!=root){//第一种对应了最右节点右孩子为空 第二种对应了最右节点右孩子有过修改
mostRight=mostRight->right;
}
if(mostRight->right==NULL){//第一次访问cur
mostRight->right=cur;
cur=cur->left;
continue;
}
else{//第二次访问cur
mostRight->right=NULL;
}
}
printf("%d ",cur->value);//cur即将往右 所以左子树要么访问结束要么没有左子树
cur=cur->right;
}
}
Morris改后序:仅仅关注可以来到自身两次的节点-也就是有左子树的节点,当第二次访问这些节点的时候,将他们的左子树的右边界的那条链逆序打印。
void printEdge(Node *root){
string str;
Node* p=root->left;
while(p->right!=NULL){
str=str+p->value;
p=p->right;
}
for(int i=str.length()-1;i>0;i--){
printf("%c ",str[i]);
}
}
void MorrisLater(Node* root){
if(!root)return;
Node* cur=root,*mostRight;
while(cur!=NULL){
mostRight=cur->left;
if(mostRight!=NULL){
while(mostRight->right!=NULL&&mostRight->right!=root){//第一种对应了最右节点右孩子为空 第二种对应了最右节点右孩子有过修改
mostRight=mostRight->right;
}
if(mostRight->right==NULL){//第一次访问cur
mostRight->right=cur;
cur=cur->left;
continue;
}
else{//第二次访问cur
mostRight->right=NULL;
printEdge(cur);
}
}
cur=cur->right;
}
printEdge(root);
}