只要翻开任意一本《数据结构》书,翻到二叉树那一章,你可以看到一个典型的二叉树中序遍历算法形如:
代码1: 经典的二叉树中序遍历算法
typedef struct _Node {
struct _Node *left, *right;
char value;
} Node;
void valueInMiddle(Node *root) { // 中序遍历二叉树
while(1) {
while(root != NULL) {
push(root);
root = root->left;
}
if(isEmpty())
return;
root = pop();
printf("%c", root->value);
root = root->right;
}
}
算法中巧妙地使用了堆栈这一数据结构,其中push()和pop()就是对堆栈的压入和弹出操作。这区区10行代码是不是很神奇?能把它彻底看懂看明白的同学请举手,......,天哪!一个都没有。
不要着急,不要上火。方老师当学生时也是通过极其曲折复杂的过程才彻底搞明白这个算法的。让我们从递归程序出发,突破重重困难,彻底搞明白这个算法以及递归程序非递归化的本质。
1. 递归的中序遍历算法
除了上述非递归的中序遍历算法之外,还存在一个十分简洁的递归中序遍历算法:
代码2: 递归中序遍历算法
void recursive(Node *root) {
if(root != NULL) {
recursive(root->left);
printf("%c", root->value);
recursive(root->right);
}
}
递归算法更加简洁。但是递归存在两个问题,第一,由于需要把参数、返回地址等压入堆栈,所以一般来说,在编译环境下递归程序要比非递归程序要稍慢一些。第二,递归受到操作系统、运行环境和高级语言对递归深度的限制。所以,我们很有必要掌握递归程序非递归化的方法。
2. 递归程序非递归化
不熟悉如何进行递归程序非递归化的学员请参见方老师博客《计算机内部如何实现递归和函数调用:计算机组成基本知识之二》https://fanglin.blog.csdn.net/article/details/119202980,下面给出对上述递归代码进行非递归化所得到的结果:
代码3: 递归中序遍历算法非递归化后的结果
void nonRecursion(Node *root) {
push(0); // 返回地址,0表示退出程序
push((long)root); // 参数入栈
long addr;
ADDR_10: // 递归函数recursive()的入口
root = (Node *)(top(0)); // 从栈顶获取参数root,0表示偏移
if(root == NULL)
goto ADDR_200; // 跳转到末尾
push(100); // 返回地址入栈
push((long)root->left); // 参数入栈
goto ADDR_10; // 跳转到递归函数入口, 等价于递归调用
// recursive(root->left)
ADDR_100:
root = (Node *)(top(0)); // 从栈顶获取参数root
printf("%c", root->value);
push(200); // 返回地址入栈
push((long)root->right); // 参数入栈
goto ADDR_10; // 跳转到递归函数入口, 等价于递归调用
// recursive(root->right)
ADDR_200: // 函数末尾处理:
addr = pop(2); // 弹出参数和返回地址
if(addr == 100)
goto ADDR_100; // 根据返回地址跳转到相应位置
if(addr == 200)
goto ADDR_200; // 根据返回地址跳转到相应位置
}
这个代码应该说还是很好理解的。现在请比较一下代码1和代码3,注意两者的代码量、风格、可读性,然后我告诉你,两者完全等价,你相信吗?
3. 逐步优化非递归化的代码
现在让我们一步一步地转化代码3,最终你会得到代码1的结果。
3.1 消除尾递归
第一步,消除尾递归。看代码3,你会发现在函数尾部,当addr==200时,实际上是在不停地执行pop(2)操作。这说明,200这个返回地址及其相应的参数其实不必压入堆栈。我们试着把这个返回地址删除:
代码 4-1: 删除返回地址200
void nonRecursion_1(Node *root) {
push(0);
push((long)root);
long addr;
ADDR_10:
root = (Node *)(top(0));
if(root == NULL)
goto ADDR_200;
push(100);
push((long)root->left);
goto ADDR_10;
ADDR_100:
root = (Node *)(top(0));
printf("%c", root->value);
pop(1);
// push(200);
push((long)root->right);
goto ADDR_10;
ADDR_200:
addr = pop(2);
if(addr == 100)
goto ADDR_100;
// if(addr == 200)
// goto ADDR_200;
}
3.2 消除另一个返回地址100
既然消除了返回地址200,那么堆栈中除了最早的一个返回地址0之外,只有100这个返回地址了。返回地址0比较特殊,你可以暂时忽略它。这样,堆栈中就可以认为只有一个返回地址100。唯一的返回地址是没有必要保存在堆栈中的,所以应该消除它。同时消除返回地址0,方法是调用判断当前堆栈是否为空的函数isEmpty(),如果堆栈为空,表示当前返回地址就是0,也就是说应该退出程序。否则就按返回地址100考虑:
代码 4-2: 删除返回地址100
void nonRecursion_2(Node *root) {
// push(0);
push((long)root);
// long addr;
ADDR_10:
root = (Node *)(top(0));
if(root == NULL)
goto ADDR_200;
// push(100);
push((long)root->left);
goto ADDR_10;
ADDR_100:
root = (Node *)(top(0));
printf("%c", root->value);
pop(1);
push((long)root->right);
goto ADDR_10;
ADDR_200:
// addr = pop(2);
pop(1);
// if(addr == 100)
if(!isEmpty())
goto ADDR_100;
}
3.3 移动函数的尾部
现在,我们发现,地址ADDR_200只在第6行被用到,我们可以把相应的代码全部移到那里去:
代码 4-3: 移动函数的尾部
void nonRecursion_3(Node *root) {
push((long)root);
ADDR_10:
root = (Node *)(top(0));
if(root == NULL) {
// goto ADDR_200;
pop(1);
if(isEmpty())
return;
goto ADDR_100;
}
push((long)root->left);
goto ADDR_10;
ADDR_100:
root = (Node *)(top(0));
printf("%c", root->value);
pop(1);
push((long)root->right);
goto ADDR_10;
//ADDR_200:
// pop(1);
// if(!isEmpty())
// goto ADDR_100;
}
3.4 移动goto之上的三条语句
现在可以看到,程序的上半部分是一个循环,由goto ADDR_100跳出循环,所以可以把这一语句之上的三条语句移动到循环之外:
代码 4-4: 三条语句移动到循环之外
void nonRecursion_4(Node *root) {
push((long)root);
ADDR_10:
root = (Node *)(top(0));
if(root == NULL) {
// pop(1);
// if(isEmpty())
// return;
goto ADDR_100;
}
push((long)root->left);
goto ADDR_10;
ADDR_100:
pop(1);
if(isEmpty())
return;
root = (Node *)(top(0));
printf("%c", root->value);
pop(1);
push((long)root->right);
goto ADDR_10;
}
3.5 消除最后一个goto语句
现在只剩下最后一个goto语句了,可以消除它:
代码 4-5: 消除最后一个goto语句
void nonRecursion_5(Node *root) {
push((long)root);
while(1) {
// ADDR_10:
while(1) {
root = (Node *)(top(0));
if(root == NULL) {
// goto ADDR_100;
break;
}
push((long)root->left);
// goto ADDR_10;
}
//ADDR_100:
pop(1);
if(isEmpty())
return;
root = (Node *)(top(0));
printf("%c", root->value);
pop(1);
push((long)root->right);
// goto ADDR_10;
}
}
3.6 合并连续的top和pop操作
连续的top和pop操作可以合并:
代码 4-6: 合并连续的top和pop操作
void nonRecursion_6(Node *root) {
push((long)root);
while(1) {
while(1) {
root = (Node *)(top(0));
if(root == NULL) {
break;
}
push((long)root->left);
}
pop(1);
if(isEmpty())
return;
// root = (Node *)(top(0));
root = (Node *)pop(1);
printf("%c", root->value);
// pop(1);
push((long)root->right);
}
}
3.7 合并循环内外的相同操作
外循环开始之前有一个push操作,循环的最后一句也是push操作,这两个操作可以合并:
代码 4-7: 合并循环内外的相同操作
void nonRecursion_7(Node *root) {
// push((long)root);
while(1) {
push((long)root);
while(1) {
root = (Node *)(top(0));
if(root == NULL) {
break;
}
push((long)root->left);
}
pop(1);
if(isEmpty())
return;
root = (Node *)pop(1);
printf("%c", root->value);
// push((long)root->right);
root = root->right;
}
}
3.8 再次合并循环内外的相同操作
内循环中也有类似现象,即循环开始之前有一个push操作,循环的最后一句也是push操作,这两个操作可以合并:
代码 4-8: 再次合并循环内外的相同操作
void nonRecursion_8(Node *root) {
while(1) {
// push((long)root);
while(1) {
push((long)root);
// root = (Node *)(top(0));
if(root == NULL) {
break;
}
// push((long)root->left);
root = root->left;
}
pop(1);
if(isEmpty())
return;
root = (Node *)pop(1);
printf("%c", root->value);
root = root->right;
}
}
3.9 消除多余的push和pop操作
现在你会发现内循环的退出条件是堆栈中压入了一个空指针NULL,内循环结束后该指针又被弹出了,既然如此,那就不要把NULL压入堆栈:
代码 4-9 消除多余的push和pop操作
void nonRecursion_9(Node *root) {
while(1) {
// while(1) {
while(root != NULL) {
push((long)root);
// if(root == NULL) {
// break;
// }
root = root->left;
}
// pop(1);
if(isEmpty())
return;
root = (Node *)pop(1);
printf("%c", root->value);
root = root->right;
}
}
把上述代码整理以后即得代码1。是不是很神奇?
现在你对递归、递归程序非递归化,非递归程序的优化,二叉树中序遍历算法是不是有了更加深刻的认识?
如果觉得方老师写得好,请加我粉丝,向你的同学、同事、老师、学生、朋友、亲友中热爱计算机科学的人推荐方老师的博客:
http://fanglin.blog.csdn.net