3. 二叉树的后续遍历 (postOrder)

头文件: 用到的是前序遍历中的头文件,具体可参见 "二叉树的前序遍历"

一,后序遍历的概念

        后序遍历
                按照 左儿子-右儿子-根节点 的顺序访问二叉树

        方式
                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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以帮你建立一棵二叉树并输出其前序、中序和后序遍历序列。 这里我假设你想建立以下这棵二叉树: ``` 1 / \ 2 3 / \ 4 5 ``` 首先,我们需要定义二叉树的节点结构体: ```cpp struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; ``` 接下来,我们可以按照以下方法建立这棵二叉树: ```cpp TreeNode* buildTree() { // 创建各个节点 TreeNode* node1 = new TreeNode(1); TreeNode* node2 = new TreeNode(2); TreeNode* node3 = new TreeNode(3); TreeNode* node4 = new TreeNode(4); TreeNode* node5 = new TreeNode(5); // 建立节点之间的关系 node1->left = node2; node1->right = node3; node2->left = node4; node2->right = node5; return node1; // 返回根节点 } ``` 现在我们已经建立了这棵二叉树,接下来是前序、中序和后序遍历的代码: ```cpp // 前序遍历 void preOrder(TreeNode* root) { if (root == NULL) { return; } cout << root->val << " "; preOrder(root->left); preOrder(root->right); } // 中序遍历 void inOrder(TreeNode* root) { if (root == NULL) { return; } inOrder(root->left); cout << root->val << " "; inOrder(root->right); } // 后序遍历 void postOrder(TreeNode* root) { if (root == NULL) { return; } postOrder(root->left); postOrder(root->right); cout << root->val << " "; } ``` 最后,我们可以在 `main` 函数中调用上述函数输出遍历序列: ```cpp int main() { TreeNode* root = buildTree(); cout << "前序遍历:"; preOrder(root); cout << endl; cout << "中序遍历:"; inOrder(root); cout << endl; cout << "后序遍历:"; postOrder(root); cout << endl; return 0; } ``` 输出结果如下: ``` 前序遍历:1 2 4 5 3 中序遍历:4 2 5 1 3 后序遍历:4 5 2 3 1 ``` 希望能够帮助到你!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值