头文件: 用到的是前序遍历中的头文件,具体可参见 "二叉树的前序遍历"
一,后序遍历的概念
后序遍历
按照 左儿子-右儿子-根节点 的顺序访问二叉树
方式
1,递归遍历
2,栈迭代遍历 (借助栈结构)
3,Morris 遍历 (栈迭代的基础上优化空间复杂度)
二,递归遍历
/*
递归遍历
先访问当前节点的左子树,然后再访问当前节点的右子树,最后访问当前节点
T : O(n),n 是二叉树的节点数,每个节点恰好被遍历1次
S : O(n), 递归过程中的栈开销,平均情况下为 O(log n),最坏情况下树为链状,为 O(n)
*/
void postTraverse(TreeNode* root, vector<int>& vec) {
if (root) {
postTraverse(root->left, vec); // 访问当前节点的左子树
postTraverse(root->right, vec); // 访问当前节点的右子树
vec.emplace_back(root->val); // 访问当前节点
}
}
三,迭代遍历
/*
栈迭代遍历
后续遍历用栈来存储节点时,必须分清返回根节点时,是从左子树还是右子树返回的。
所以可以使用一个辅助指针,指向最近访问过得节点。(也可以每个节点加一个标志域,但会浪费空间)
时间复杂度:O(n),其中 n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。
空间复杂度:O(n),空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
*/
void postTraverseN(TreeNode* root, vector<int>& vec) {
stack<TreeNode*> stack;
TreeNode* prev = nullptr; // 指向最近访问过得节点
while (root || !stack.empty()) { // 当前节点不为空,或栈不为空
while (root) { // 若当前节点不为空,则加入到栈,然后让当前节点指向左子树
stack.emplace(root);
root = root->left;
}
// 当前节点为空,则需要获取栈顶元素(注意,不是弹出栈顶元素,因为此时的栈顶元素相当于一个没有左子树的根节点,而后续遍历是左右根,所以还需要判断右子树)
root = (TreeNode*)stack.top(); // 获取栈顶元素
if (root->right && root->right != prev) { // 若栈顶元素有右子树,且,右子树没有访问过 => 将当前节点指向其右子树
root = root->right;
}
else { // 若没有右子树,或者,右子树已经遍历过了,则此时需要 弹出并访问 该栈顶元素,然后将访问标志指向该元素,最后将当前节点置空
vec.emplace_back(root->val);
stack.pop();
prev = root;
root = nullptr; // 注意,必须将当前节点置空,否则会再次被加入栈
}
}
}
// 另一种栈迭代的方法,需要用到两个栈,但是特别容易理解
/*
1、申请一个栈s1,然后将头节点压入栈s1中。
2、从s1中弹出的节点记为cur,然后依次将cur的左孩子节点和右孩子节点压入s1中。
3、在整个过程中,每一个从s1中弹出的节点都放进s2中。
4、不断重复步骤2和步骤3,直到s1为空,过程停止。
5、从s2中依次弹出节点并打印,打印的顺序就是后序遍历的顺序。
*/
/*
void postTraverseN(TreeNode* root, vector<int>& vec) {
stack<TreeNode*> s1, s2;
s1.emplace(root);
while (!s1.empty()) {
TreeNode* top = (TreeNode*)s1.top();
s1.pop();
s2.emplace(top);
if (top->left) {
s1.emplace(top->left);
}
if (top->right) {
s1.emplace(top->right);
}
}
while (!s2.empty()) {
TreeNode* top = (TreeNode*)s2.top();
s2.pop();
vec.emplace_back(top->val);
}
}
*/
四,Morris 遍历
/*
Morris 遍历
Morris 遍历实现的原则:
1,记当前节点为 cur
2, 若当前节点 cur 没有左子树,则 cur 往右移。(cur = cur->right)
3,若当前节点 cur 有左子树,找到 "当前节点 cur 左子树" 的最右节点,记作 mostRight (最右节点的概念:就是当前节点不断往右儿子跑,所能到达的最靠右的节点)
3-1,若 mostRight 的右节点指向空,则让其指向 cur, cur 向左移。(cur = cur->left)
3-2, 若 mostRight 的右节点指向 cur,则让其指向空, cur 向右移。(cur = cur->right)
后续遍历中需要修改的点:
3-2, 若 mostRight 的右节点指向 cur,则让其指向空,记录从 mostRight -> cur.left 节点, cur 向右移。
......
T : O(n),其中 n 是二叉树的节点数。没有左子树的节点只被访问一次,有左子树的节点被访问两次。
S : O(1), 只操作已经存在的指针(树的空闲指针),因此只需要常数的额外空间。
*/
void addPath(TreeNode* node, vector<int>& vec) {
auto it = vec.end();
while (node != nullptr) {
it = vec.insert(it, node->val); // 相当于局部逆序插入,因为 insert 会在 it 所指向的元素前插入元素,并返回指向被插入元素的迭代器
node = node->right;
}
}
void postTraverseM(TreeNode* root, vector<int>& vec) {
TreeNode* cur = root;
TreeNode* mostRight = nullptr;
while (cur) {
mostRight = cur->left;
if (mostRight) {
while (mostRight->right && mostRight->right != cur) {
mostRight = mostRight->right;
}
if (mostRight->right == nullptr) {
mostRight->right = cur;
cur = cur->left;
}
else {
mostRight->right = nullptr;
addPath(cur->left, vec);// 倒序输出从当前节点左子节点到 mostRight 路径上的所有节点
/*
为何要倒序 ? 当 mostRight->right == cur 时,表明 cur 左子树的所有节点都已经遍历到了,此时应该输出左子树的所有节点。
而后序遍历是 左右中 的顺序,故输出顺序应是 mostRight 到 cur->left
注意, addPath() 该句只能放在这中间,因为它涉及到 cur 和 mostRight 的状态,若放在第一句,则 mostRight->right 会形成环,若放在最后,则 cur 所指向的节点就不对了
*/
cur = cur->right;
}
}
else {
cur = cur->right;
}
}
addPath(root, vec); // 原因:因为 while(cur) 循环结束后, root 到 root 最右孩子之间的节点都没有被打印出来
}
五,主函数
void postOrderTraversal(TreeNode* root) {
vector<int> ret;
postTraverse(root, ret);
cout << "后序遍历, 递归 : " << endl;
PRINT(ret);
ret.clear();
postTraverseN(root, ret);
cout << "后序遍历, 栈迭代 : " << endl;
PRINT(ret);
ret.clear();
postTraverseM(root, ret);
cout << "后序遍历, Morris : " << endl;
PRINT(ret);
}
#if POST_ORDER_TRAVERSAL
int main() {
TreeNode* root = getRandomTree();
postOrderTraversal(root);
return 0;
}
#endif