二叉树的遍历
二叉树是一种常见的数据结构,对于这种结构的操纵一定要烂熟于心,这篇文章想记录一下的,是二叉树的遍历操作.
实现的方式
二叉树的遍历,事实上有两种是实现方法,一种是递归,这种方法过分简单,所以这里不会讲,第二种方式是非递归,你可能会问,既然递归实现这么简单,我们为什么还要非递归版本的实现,这个问题很好,这个实际上和递归的内存开销有关,每一次递归,程序便要堆栈,每次堆栈,便会占用比非递归昂贵得多的内存空间,一旦二叉树过分地大,很容易堆栈溢出,而非递归版本的实现内存消耗相对来说会小很多,所以这种方法的意义还是非常大的.
使用的数据结构
/*
* @struct
* @brief 二叉树节点
*/
struct treeNode {
treeNode *left;
treeNode *right;
int val; /* 卫星数据 */
treeNode(int x): val(x), left(nullptr), right(nullptr){}
};
先序遍历
/**
* @brief 先序遍历,非递归版本
*/
void preOrder(const treeNode *root, void(*visit)(const treeNode*)) {
const treeNode *p = root;
stack<const treeNode*> nodes;
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
递归版本的先序遍历
void preOrder(const treeNode *root, void (*visit)(const treeNode *)) {
if (root == nullptr) return;
visit(root);
if (root->left != nullptr) preOrder(root->left);
if (root->right != nullptr) preOrder(root->right);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
if (p != nullptr) nodes.push(p);
/* 非递归版本的先序遍历是递归版本的忠实模拟 */
while (!nodes.empty()) {
p = nodes.top(); nodes.pop();
visit(p);
if (p->right != nullptr) nodes.push(p->right);
if (p->left != nullptr) nodes.push(p->left);
}
}
该版本是先序遍历递归版本的忠实模拟.
中序遍历
其实有很多种实现非递归中序遍历的方式,下面是一种:
/**
* @brief 中序遍历,非递归版本
*/
void inOrder(const treeNode *root, void(*visit)(const treeNode*)) {
const treeNode *p = root;
stack<const treeNode*> nodes;
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
递归版本的中序遍历
void inOrder(treeNode *root, void (*visit)(const treeNode *)) {
if (root == nullptr) return;
if (root->left != nullptr) inOrder(root->left);
visit(root);
if (root->right != nullptr) inOrder(root->right);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
while (!nodes.empty() || p != nullptr) {
if (p != nullptr) { /* 先将左边部分入栈 */
nodes.push(p);
p = p->left;
}
else {
p = nodes.top(); nodes.pop();
visit(p);
p = p->right; /* 访问右子树 */
}
}
}
唯一不好的地方在于,这种方法,其实很巧妙,如果要你迅速写出上面的代码,在不是很熟悉的情况下,有非常大的难度,所以,有了下面这个对递归版本的模拟版本.
void inOrder2(const treeNode *root, void(*visit)(const treeNode*)) {
const treeNode *p = root;
stack<const treeNode*> nodes;
do {
while (p != nullptr) { /* 将左子树全部入栈 */
nodes.push(p);
p = p->left;
}
while (!nodes.empty()) {
p = nodes.top(); nodes.pop();
visit(p);
if (p->right != nullptr) { /* 左子树已经访问过了,右子树不为空 */
p = p->right;
break;
}
/* p不存在右子树,那么要退栈 */
}
} while (!nodes.empty());
}
这个版本好了一点,方便于理解和默写,这个版本和我们的平时思考的方式是一致的,我这里讲一下要点, do {expr;}while(condition)
这一轮迭代,相当于一次若干次递归,第一个 while
循环,将左子孙节点全部入栈,然后就开始退栈了,访问完栈顶元素之后,如果栈顶元素的右子节点不为空,那么要先访问右子树,这个时候只需要将 p 设定为右子节点,然后开始一轮新的do {expr;}while(condition)
迭代即可, 否则的话,因为栈顶元素的左子树已经访问过了,右子节点又为空,只能选择退栈.
后序遍历
后续遍历的非递归实现还是有相当大难度的.
/**
* @brief 后序遍历,非递归版本
*/
void postOrder(const treeNode* root, void *(*visit)(const treeNode*)) {
const treeNode *p = root, *q;
stack<const treeNode*> nodes;
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
递归版本的后续遍历
void postOrder(treeNode *root, void (*visit)(const treeNode *)) {
if (root == nullptr) return;
postOrder(root->left);
postOrder(root->right);
visit(root);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
do {
while (p != nullptr) { /* 往左下走 */
nodes.push(p);
p = p->left;
}
q = nullptr;
while (!nodes.empty()) {
p = nodes.top(); nodes.pop();
/* 右孩子不存在或者已经被访问过了,访问该节点 */
if (p->right == q) {
visit(p);
q = p; /* 保存刚访问过的节点 */
}
else {
/* 当前节点不能访问,需要第二次进栈 */
nodes.push(p);
/* 左子树已经遍历过了,根据后序遍历规则,先处理右子树 */
p = p->right;
break;
}
}
} while (!nodes.empty());
}
这个版本的实现和上面的中序遍历版本2有异曲同工之妙.每一次 do { expr; } while(condition);
循环都相当于递归版本的若干次递归. 首先让 p 节点所有的左子孙节点入栈,然后开始处理栈顶元素,如果栈顶元素的右子树还未被遍历过,让 p = p->right ,交由下一次 do { expr; } while(condition);
迭代的时候来处理. 如果已经处理过了,因为栈顶元素的左子树已经遍历过了,右子树也已经遍历过了,那么只能遍历栈顶元素,然后退栈,继续.
这里有一个和之前不太一样的点是,如果不采用辅助变量的话,我们是没法知道右子树是否已经被遍历过了的,因此需要构建一个辅助变量,记住前一次访问的节点.