欢迎访问我的博客首页。
二叉树和普通树
1. 定义结点
二叉树结点和普通树结点的定义:
struct binaryTreeNode {
binaryTreeNode(int x = 0) :data(x), lchild(nullptr), rchild(nullptr) {}
binaryTreeNode *lchild, *rchild;
int data;
};
struct treeNode {
treeNode(int x = 0) :data(x) {}
vector<treeNode*> children;
int data;
};
二叉树每个结点最多有两个子结点,结点的指针域使用两个结点指针就行。普通树每个结点的子结点数不确定,我们使用 vector 存放指向子结点的指针,这样就可以使用 vector 的迭代器遍历子结点,当然普通树的指针域也可以使用链表等其它方式实现。
2. 创建树
创建图 1 所示的二叉树和普通树。普通树的 a 结点值为 1,b 结点值为 2,以此类推。
图
1
图 1
图1
下面是创建二叉树和普通树的代码。
binaryTreeNode* build_binary_tree() {
const int n = 12;
int a[n] = { 1,2,4,5,7,8,10,11,15,16,17,19 };
binaryTreeNode* nodes[n];
for (int i = 0; i < n; i++)
nodes[i] = new binaryTreeNode(a[i]);
nodes[3]->lchild = nodes[2];
nodes[1]->lchild = nodes[0];
nodes[1]->rchild = nodes[3];
nodes[4]->lchild = nodes[1];
nodes[4]->rchild = nodes[8];
nodes[8]->lchild = nodes[6];
nodes[8]->rchild = nodes[10];
nodes[6]->lchild = nodes[5];
nodes[6]->rchild = nodes[7];
nodes[10]->lchild = nodes[9];
nodes[10]->rchild = nodes[11];
return nodes[4];
}
treeNode* build_tree() {
treeNode* a = new treeNode(1);
treeNode* b = new treeNode(2);
treeNode* c = new treeNode(3);
a->children.push_back(b);
a->children.push_back(c);
treeNode* d = new treeNode(4);
treeNode* e = new treeNode(5);
b->children.push_back(d);
b->children.push_back(e);
treeNode* f = new treeNode(6);
treeNode* g = new treeNode(7);
d->children.push_back(f);
d->children.push_back(g);
treeNode* h = new treeNode(8);
treeNode* i = new treeNode(9);
treeNode* j = new treeNode(10);
e->children.push_back(h);
e->children.push_back(i);
e->children.push_back(j);
return a;
}
3. 层序遍历
1. 层序遍历
层序遍历很简单,可以使用队列实现:根节点先入队列,当队列不为空时:队首结点出队列,访问出队列的结点并让它的左右子节点依次进队列。
void layer_traverse_binary_tree(binaryTreeNode* tree) {
if (tree == nullptr)
return;
queue<binaryTreeNode*> qu;
qu.push(tree);
while (qu.empty() != true) {
tree = qu.front();
qu.pop();
cout << tree->data << " ";
if (tree->lchild != nullptr)
qu.push(tree->lchild);
if (tree->rchild != nullptr)
qu.push(tree->rchild);
}
cout << endl;
}
void layer_traverse_tree(treeNode* tree) {
if (tree == nullptr)
return;
queue<treeNode*> qu;
qu.push(tree);
while (qu.empty() != true) {
tree = qu.front();
qu.pop();
cout << tree->data << " ";
for (auto it : tree->children)
qu.push(it);
}
cout << endl;
}
上面是二叉树和普通树的层序遍历。二叉树和普通树其实就是有向图,而层序遍历就是有向图的广度优先遍历。
2. 利用层序遍历创建二叉树
利用层序遍历,可以根据一个数组创建一颗二叉树。这个数组用于创建一颗完全二叉树。使用指定的数值标记空结点,可以创建非完全二叉树。比如我们想创建一个包含两个结点的二叉树,根结点的值是 1 且它只有一个值为 2 的右子结点。如果用 -1 标记空结点,则数组应该是 {1, -1, 2}。
binaryTreeNode* create_node(int x, int empty) {
if (x == empty)
return nullptr;
return new binaryTreeNode(x);
}
// 1.使用层序遍历创建二叉树。
binaryTreeNode* create_binary_tree(vector<int>& vec, int empty) {
if (vec.size() == 0)
return nullptr;
binaryTreeNode* root = new binaryTreeNode(vec[0]);
queue<binaryTreeNode*> qu;
qu.push(root);
int index = 1;
while (index < vec.size()) {
binaryTreeNode* temp = qu.front();
qu.pop();
if (temp == nullptr) {
index += 2;
qu.push(nullptr);
qu.push(nullptr);
continue;
}
temp->lchild = create_node(vec[index++], empty);
temp->rchild = create_node(vec[index++], empty);
qu.push(temp->lchild);
qu.push(temp->rchild);
}
return root;
}
// 2.使用递归算法创建二叉树。
binaryTreeNode* create_binary_tree_recurse(vector<int>& vec, int empty, int index = 0) {
if (index >= vec.size() || vec[index] == empty)
return nullptr;
binaryTreeNode* node = create_node(vec[index], empty);
node->lchild = create_binary_tree_recurse(vec, empty, 2 * index + 1);
node->rchild = create_binary_tree_recurse(vec, empty, 2 * index + 2);
return node;
}
上面不仅给出了使用层序遍历创建二叉树的方法,还给出了一种递归创建二叉树的方法,两者的使用方法一样。使用 { 1, 2, -1, 4, 3, -1, -1, -1, 6, 5, 0, -1, -1, -1, -1, -1, -1, 7, -4, 8, 9 } 创建的二叉树如图 2 。
图
2
图 2
图2
3. 分行打印树的每一层
问题:分行打印树,树的每层结点占一行。
分析:打印当前层结点时统计下一层的结点数。层序遍历的过程中,第 n 层第 1 个结点出队列时下一层的第一个结点入队列,第 n 层最后 1 个结点出队列后下一层所有结点都进入队列。所以我们可以在打印前一层时统计下一层的结点数,而第 1 层结点数是 1,所以我们总是可以知道下一层有多少结点。知道了有多少个结点就知道什么时候应该打印换行符。下面以二叉树为例,分行打印普通树的算法和该算法相同,只是访问子结点有差异。
void layer_traverse(binaryTreeNode* tree) {
if (tree == nullptr)
return;
queue<binaryTreeNode*> qu;
qu.push(tree);
int this_layer = 1, next_layer = 0;
while (qu.empty() != true) {
tree = qu.front();
qu.pop();
this_layer--;
cout << tree->data << " ";
if (tree->lchild != nullptr) {
qu.push(tree->lchild);
next_layer++;
}
if (tree->rchild != nullptr) {
qu.push(tree->rchild);
next_layer++;
}
if (this_layer == 0) {
cout << endl;
this_layer = next_layer;
next_layer = 0;
}
}
}
this_layer 是当前层尚未打印的结点数,next_layer 是下一层的结点总数。
4. 左视图与右视图
1. 非递归算法
分行打印树的算法已经能知道每一层的结点数,根据这个算法可以获取树的左视图与右视图。获取右视图很简单,只需在分行打印树算法的第 11 行前加上条件语句 if(this_layer == 0)。获取左视图也很简单,我们要知道什么时候遍历到的是每一层的第一个结点,所以我们可以用一个变量 this_count 记录每一层的结点数。当 this_count == this_layer 时打印结点,如第 10 行:
void layer_traverse(binaryTreeNode* tree) {
if (tree == nullptr)
return;
queue<binaryTreeNode*> qu;
qu.push(tree);
int this_layer = 1, this_count = 1, next_layer = 0;
while (qu.empty() != true) {
tree = qu.front();
qu.pop();
if (this_count == this_layer)
cout << tree->data << " ";
this_layer--;
if (tree->lchild != nullptr) {
qu.push(tree->lchild);
next_layer++;
}
if (tree->rchild != nullptr) {
qu.push(tree->rchild);
next_layer++;
}
if (this_layer == 0) {
cout << endl;
this_layer = next_layer;
this_count = next_layer;
next_layer = 0;
}
}
}
2. 递归算法
void left_view(binaryTreeNode* tree, vector<int>& path, int level = 0) {
if (tree == nullptr)
return;
if (path.size() == level)
path.push_back(tree->data);
left_view(tree->lchild, path, level + 1);
left_view(tree->rchild, path, level + 1);
}
void right_view(binaryTreeNode* tree, vector<int>& path, int level = 0) {
if (tree == nullptr)
return;
if (path.size() == level)
path.push_back(tree->data);
right_view(tree->rchild, path, level + 1);
right_view(tree->lchild, path, level + 1);
}
5. 先序、中序、后序遍历
先序遍历算法从上到下遍历树左侧的结点,然后从下到上以同样的方法处理每个结点的右子树。中序遍历算法从下到上遍历树左侧的结点,每遍历一个结点之后以同样的方法处理它的右子树。后续遍历算法从下到上遍历树左侧的结点,每遍历一个结点之前以同样的方法处理它的右子树。
因为都有从下到上的过程,所以都要使用栈先保存从上到下的路径:递归算法使用函数调用栈,非递归算法需要自己定义栈。
只有二叉树有中序遍历的概念,普通树因每个结点的子结点数目不确定而没有中序遍历的概念。
5.1 递归算法
递归遍历的算法比较简单,而且遍历二叉树和普通树的算法步骤相同。
// 1. 先序递归遍历
void pre_recurse_traverse_binary_tree(binaryTreeNode* tree) {
if (tree == nullptr)
return;
cout << tree->data << ' ';
pre_recurse_traverse_binary_tree(tree->lchild);
pre_recurse_traverse_binary_tree(tree->rchild);
}
void pre_recurse_traverse_tree(treeNode* tree) {
if (tree == nullptr)
return;
cout << tree->data << " ";
for (auto it : tree->children)
pre_recurse_traverse_tree(it);
}
// 2. 中序递归遍历
void in_recurse_traverse_binary_tree(binaryTreeNode* tree) {
if (tree == nullptr)
return;
in_recurse_traverse_binary_tree(tree->lchild);
cout << tree->data << ' ';
in_recurse_traverse_binary_tree(tree->rchild);
}
// 3. 后序递归遍历
void post_recurse_traverse_binary_tree(binaryTreeNode* tree) {
if (tree == nullptr)
return;
post_recurse_traverse_binary_tree(tree->lchild);
post_recurse_traverse_binary_tree(tree->rchild);
cout << tree->data << ' ';
}
void post_recurse_traverse_tree(treeNode* tree) {
if (tree == nullptr)
return;
for (auto it : tree->children)
post_recurse_traverse_tree(it);
cout << tree->data << " ";
}
5.2 适合二叉树的非递归算法
二叉树每个结点最多有两个子结点,而普通树则不确定,所以遍历二叉树更简单。
void preOrder(binaryTreeNode* tree) {
stack<binaryTreeNode*> st;
while (tree != nullptr || st.empty() != true) {
while (tree != nullptr) {
st.push(tree);
cout << tree->data << ' ';
tree = tree->lchild;
}
tree = st.top();
st.pop();
tree = tree->rchild;
}
}
void inOrder(binaryTreeNode* tree) {
stack<binaryTreeNode*> st;
while (tree != nullptr || st.empty() != true) {
while (tree != nullptr) {
st.push(tree);
tree = tree->lchild;
}
tree = st.top();
st.pop();
cout << tree->data << ' ';
tree = tree->rchild;
}
}
void postOrder(binaryTreeNode* tree) {
binaryTreeNode* lastVisit = nullptr;
stack<binaryTreeNode*> st;
while (tree != nullptr || st.empty() != true) {
while (tree != nullptr) {
st.push(tree);
tree = tree->lchild;
}
tree = st.top();
if (tree->rchild == nullptr || tree->rchild == lastVisit) {
st.pop();
cout << tree->data << ' ';
lastVisit = tree;
tree = nullptr;
}
else
tree = tree->rchild;
}
}
这三个算法步骤相同,分为两步:
- 第一步是第 2 层的 while 循环,它以 tree 为根结点,不停地找结点的左结点入栈直到左结点为空。以图 1 为例,进栈的结点依次是 7、2、1。如果是先序遍历,进栈的时候访问结点,如第 5、6 行。
- 第二步紧接着第一步,处理栈顶结点。当我们处理栈顶结点时,它要么没有左子树要么左子树已被访问,所以处理栈顶结点就是处理该结点和它的右子树。如果是中序遍历,先处理栈顶结点再处理它的右子树。如果是后续遍历,先处理右子树再处理这个栈顶结点。处理右子树只需把工作指针 tree 指向它的右子树,然后从第一步开始处理,如第 11、25、45 行。
后续遍历需要用一个标记位记录右子结点是否被访问,如第 41 行。而且遍历完右子树后工作指针要置空,如第 42 行,这是为了避免第 33 行让工作指针 tree 指向的结点再次进栈。
这些算法虽然更适合二叉树,但稍加改造也能遍历普通树。比如先序遍历算法,只要在第 11 行能获取普通树结点 tree 的下一个子结点,就可以遍历普通树。
5.3 适合所有树的非递归算法
下面介绍适合所有树的先序遍历算法和后序遍历算法。
1. 先序遍历
上面的算法只在栈中保存树左侧的结点,根据出栈结点的右指针获取每个结点的右子结点,进而处理右子树。因为二叉树才有左右指针的概念,所以这种算法不太适合普通树。如果把右子结点也入栈,就不用通过右指针获取右子结点了,这样的算法也就可以方便地处理所有树了。以先序遍历为例:
先序遍历时,根结点入栈,遍历到根结点时根结点出栈,它的右子结点、左子结点依次进栈。然后以同样的方法处理左子结点。之所以右子结点先进栈,是因为它晚于左子结点被遍历。这样的先序遍历算法和层序遍历算法很相似,只有两点差异:
- 先序遍历使用栈,层序遍历使用队列。
- 先序遍历中每个结点的右子结点先入栈,层序遍历中每个结点的左子结点先入队列。这是因为栈和队列的访问顺序不同,而两种遍历都要保证左子结点先被遍历。
2. 后序遍历
后续遍历和先序遍历很相似:假如先序遍历时,遍历完一个结点后,先遍历它的右子结点再遍历它的左子结点,这样的先序遍历序列就和后序遍历序列正好相反。
图
3
图 3
图3
如图 3,后序遍历序列是 B->C->D->A,右侧子结点优先的先序遍历序列是 A->D->C->B,它们正好是相反的。我们把右侧子结点优先的先序遍历序列存储在额外空间中,然后返回额外空间内序列的反序列就是后序遍历序列了。下面先实现先序遍历算法,然后用先序遍历算法实现后序遍历算法。
// 1. 二叉树的先序非递归遍历
void pre_traverse_binary_tree(binaryTreeNode* tree) {
if (tree == nullptr)
return;
stack<binaryTreeNode*> st;
st.push(tree);
while (st.empty() != true) {
tree = st.top();
st.pop();
cout << tree->data << " ";
if (tree->rchild != nullptr)
st.push(tree->rchild);
if (tree->lchild != nullptr)
st.push(tree->lchild);
}
cout << endl;
}
// 2. 普通树的先序非递归遍历
void pre_traverse_tree(treeNode* tree) {
if (tree == nullptr)
return;
stack<treeNode*> st;
st.push(tree);
while (st.empty() != true) {
tree = st.top();
st.pop();
cout << tree->data << " ";
for (auto it = tree->children.rbegin(); it != tree->children.rend(); it++)
st.push(*it);
}
cout << endl;
}
// 3. 二叉树的后序非递归遍历
vector<int> pose_traverse_binary_tree(binaryTreeNode* tree) {
if (tree == nullptr)
return{};
vector<int> res;
stack<binaryTreeNode*> st;
st.push(tree);
while (st.empty() != true) {
tree = st.top();
st.pop();
res.push_back(tree->data);
if (tree->lchild != nullptr)
st.push(tree->lchild);
if (tree->rchild != nullptr))
st.push(tree->rchild);
}
reverse(res.begin(), res.end());
return res;
}
// 4. 普通树的后序非递归遍历
vector<int> post_traverse_tree(treeNode* tree) {
if (tree == nullptr)
return{};
vector<int> res;
stack<treeNode*> st;
st.push(tree);
while (st.empty() != true) {
tree = st.top();
st.pop();
res.push_back(tree->data);
for (auto &x : tree->children)
st.push(x);
}
reverse(res.begin(), res.end());
return res;
}
总结:这里的先序遍历算法和后序遍历算法即适合二叉树又适合普通树。先序遍历算法类似于层序遍历算法,后序遍历算法类似于先序遍历算法。只是后序遍历算法需要额外空间。
6. 路径
6.1 递归算法
1. 使用值传递的参数存放路径
下面是使用递归的遍历算法实现的一个通用的获取二叉树路径的算法,它几乎可以解决任何获取二叉树路径的问题。
void get_path(binaryTreeNode* tree, int total, vector<vector<int>>& paths, vector<int> path = {}, int sum = 0) {
if (tree == nullptr)
return;
path.push_back(tree->data);
sum += tree->data;
if (tree->lchild == nullptr && tree->rchild == nullptr && sum == total)
paths.push_back(path);
get_path(tree->lchild, total, paths, path, sum);
get_path(tree->rchild, total, paths, path, sum);
}
// 调用:
vector<vector<int>> paths;
get_path(binaryTree, 20, paths);
该算法的优点是简洁,缺点是需要较大的栈内存,原因是参数 path 不是引用类型。因为 path 不是引用类型,如果树有 n 个结点,函数调用栈就有 n 个 path 的副本。这 n 个副本使递归调用时 path 相互不影响,这样的代码简洁但需要更大栈内存。该算法的通用性如下:
- 该算法用于获取根结点到所有叶结点和为指定值的路径。
- 第 2 个 if 语句中,不考虑 sum,可以获取根结点到所有叶结点的路径。
- 第 2 个 if 语句中,不考虑左右子树是否为空,可以获取根结点到所有结点和为指定值的路径。
- 第 2 个语句中,只考虑结点值是否与指定值相等,可以获取根结点到所有与指定值相等的结点的路径。
- 对该算法处理左右子树的地方稍加修改,即可处理普通树。
2. 使用引用传递的参数存放路径
void get_path(binaryTreeNode* tree, int total, vector<vector<int>>& paths, vector<int>& path = vector<int>{}, int sum = 0) {
if (tree == nullptr)
return;
path.push_back(tree->data);
sum += tree->data;
if (tree->lchild == nullptr && tree->rchild == nullptr && sum == total)
paths.push_back(path);
get_path(tree->lchild, total, paths, path, sum);
get_path(tree->rchild, total, paths, path, sum);
sum -= path.back();
path.pop_back();
}
// 调用:
vector<vector<int>> paths;
get_path(binaryTree, 20, paths);
这个代码和上面的代码功能完全一样,只是参数 path 是引用类型。path 是引用类型时,需要有第 10、11 行。这两行是为了避免遍历左子树对遍历右子树产生影响。比如遍历完结点 A 时 path = [root, …, A],第 8 行遍历完 A 的左子树 B 时 path = [root, …, A, B, …]。第 9 行去遍历 A 的右子树 C,这时 path 中显然不能有左子树 B 的结点。因此遍历完的结点要及时从 path 中移除。
移除的顺序要与遍历的顺序相反,即遍历顺序是 root -> … -> A -> B,接下来应该移除 B 才能保证遍历 C 时从 root 到 A 的路径还在 path 中。所以第 10、11 行的代码在遍历左右子树之后。
图
4
图 4
图4
图 4 是递归函数的调用栈示意图,函数 get_path 的第 8、9 行是递归调用部分,get_path 函数调用自身。假如 get_path1 调用 get_path2,第 2 至 7 行递归调用前的部分 get_path1 先执行,而第 10 至 11 行递归调用后的部分 get_path2 先执行。以图 4 为例,A 结点在 B 结点前进入 path,B 结点在 A 结点前被移出 path。
3. 使用递归算法获取路径的例题
下面是具体的例子:从二叉树和普通树中找出一条满足条件的路径,这个路径的终点与指定值相等,即获取根结点到指定结点的路径。
bool get_path_binary_tree(binaryTreeNode* tree, int node, list<binaryTreeNode*>& path) {
if (tree == nullptr)
return false;
path.push_back(tree);
if (tree->data == node)
return true;
// found = true:子树包含结点node。
bool found = false;
found = get_path_binary_tree(tree->lchild, node, path);
if (found == false)
found = get_path_binary_tree(tree->rchild, node, path);
if (found == false)
path.pop_back();
return found;
}
bool get_path_tree(treeNode* tree, int node, list<treeNode*>& path) {
if (tree == nullptr)
return false;
path.push_back(tree);
if (tree->data == node)
return true;
// found = true:子树包含结点node。
bool found = false;
for (auto it : tree->children) {
found = get_path_tree(it, node, path);
if (found == true)
break;
}
if (found == false)
path.pop_back();
return found;
}
上面二叉树和普通树的算法相同,仅仅因为二叉树结点和普通树结点的子结点数目不同引起查找子树有略微差异,如第 10 至 12 行与第 27 至第 31 行分别是在二叉树结点 tree 的子树中查找 node 和在普通树结点 tree 的子树中查找 node。
函数的第一个参数是根结点 tree,返回值为 true 说明以 tree 为根结点的子树中包含 node 结点。
以二叉树为例,第 2 行和第 5 行是递归终止条件。第 2 行:遍历到叶结点还没有找到 node,则 node 不在以 tree 为根结点的子树中,返回 false;第 5 行:找到了 node 结点返回 true。没有到达终止条件时,先暂且认为 tree 是路径上的结点,如第 9 行。判断 tree 的子树中有没有 node 结点,found 为 true 说明 tree 的子树有 node 结点,则以 tree 为根结点的子树也有 node,返回 true。found 为 false 说明没有,则 tree 不在路径上,把它从路径中移除,如第 14 行。
6.2 非递归算法
1. 非递归遍历算法
观察 7.2 节的算法可以发现,这个算法的形式就是二叉树的先序、中序、后序递归遍历算法的形式。它主要有三部分:
// 第一部分。
path.push_back(tree->data);
sum += tree->data;
if (tree->lchild == nullptr && tree->rchild == nullptr && sum == total))
paths.push_back(path);
// 第二部分。
get_path(tree->lchild, total, paths, path, sum);
get_path(tree->rchild, total, paths, path, sum);
// 第三部分。
sum -= path.back();
path.pop_back();
第一部分和先序遍历算法中访问结点的位置相同,第二部分是递归调用,第三部分和后序遍历算法中访问结点的位置相同。回想 6.1 节适合二叉树的非递归遍历算法,它们也有相似的算法结构,区别也仅仅是访问结点的位置不同。所以能否用非递归算法实现这样通用的获取路径的算法?
下面我们用适合二叉树的后序遍历算法实现通用的获取路径的算法。为什么不用先序遍历算法而用后序遍历算法?和 7.2 节的算法一致,查找路径的算法即需要在先序遍历时把结点加入 path,又需要在后序遍历时把结点及时从 path 移除。后序遍历算法很容易改成先序遍历算法,而先序遍历算法要修改更多才能变成后序遍历算法。
void get_path_binary_tree(binaryTreeNode* tree, int total, vector<vector<int>>& paths, vector<int>& path = vector<int>{}, int sum = 0) {
binaryTreeNode* lastVisit = nullptr;
stack<binaryTreeNode*> st;
while (tree != nullptr || st.empty() != true) {
while (tree != nullptr) {
// 查找路径部分。
path.push_back(tree->data);
sum += tree->data;
if (tree->lchild == nullptr && tree->rchild == nullptr && sum == total)
paths.push_back(path);
// 遍历部分。
st.push(tree);
tree = tree->lchild;
}
tree = st.top();
if (tree->rchild == nullptr || tree->rchild == lastVisit) {
// 查找路径部分。
sum -= path.back();
path.pop_back();
// 遍历部分。
st.pop();
lastVisit = tree;
tree = nullptr;
}
else
tree = tree->rchild;
}
}
这个算法和上面的算法一样,可以处理几乎任何路径问题。对比这个非递归的算法与二叉树的非递归后序遍历算法,对比 7.2 节递归的路径算法与二叉树的递归遍历算法,前者都是在后者的基础上稍加修改而来,而且修改的方式一样。
2. 非递归算法例题
例题:求根结点到指定结点的路径
如果二叉树有多个节点的值都是 x,我们要找出根节点到所有值为 x 的节点的路径,使用上面的算法很容易实现。如果只需找出一条满足条件的路径,我们也可以不用额外的空间存储路径,而直接从二叉树后序遍历时使用的栈中获取路径,其中栈顶是路径终点:
bool get_path_binary_tree(binaryTreeNode* tree, int node, stack<binaryTreeNode*>& st) {
binaryTreeNode* lastVisit = nullptr;
while (tree != nullptr || st.empty() != true) {
while (tree != nullptr) {
st.push(tree);
tree = tree->lchild;
}
tree = st.top();
if (tree->rchild == nullptr || tree->rchild == lastVisit) {
if (tree->data == node)
return true;
st.pop();
lastVisit = tree;
tree = nullptr;
}
else
tree = tree->rchild;
}
return false;
}
6.3 路径问题总结
二叉树特有的三个遍历算法都使用了栈。为什么只有后续遍历的栈中保存的才是根结点到搜索结点的路径:
- 先序遍历和中序遍历时,遍历到结点 p 的右子树时结点 p 已出栈。也就是说如果要查找的结点 node 在结点 p 的右子树中,经过结点 p 到达结点 node 时结点 p 已被移出栈。所以先序遍历和中序遍历的栈不能保存完整的搜索路径。
- 后序遍历时,当结点 p 的左右子树都被遍历后才会遍历结点 p。也就是说从结点 p 的子树中找到结点 node 时结点 p 还在栈中。因此只有后序遍历的栈可以保存完整的搜索路径。
求路径的算法基于先序、后序遍历算法。无论是递归算法还是非递归算法,都在先序遍历访问结点的时候进栈,后序遍历访问结点的时候出栈。这是因为先序遍历时遍历到一个结点,栈中存放的就是根节点到该结点的路径,后序遍历到该结点时出栈是为了清楚栈中无关的结点以便寻找根节点到下一个结点的路径。