1. 二叉树的前序遍历 (preOrder)

头文件: 用来定义 TreeNode 以及随机获取一个二叉树,该头文件也用于后面的 中序 / 后序 / 层序 遍历。

#ifndef __TREE_NODE_H__
#define __TREE_NODE_H__

#include <stdlib.h>
#include <time.h>

#define MIN_NUM 1
#define MAX_NUM 10
#define NUM_SIZE 10

#define PRINT(vec) \
	do { \
		for (auto it = (vec).begin(); it != (vec).end(); it++) { \
			std::cout << *it << " "; \
		} \
		std::cout << std::endl << std::endl; \
	} while(0)

#define PRE_ORDER_TRAVERSAL 0
#define IN_ORDER_TRAVERSAL 0
#define POST_ORDER_TRAVERSAL 0
#define LEVEL_ORDER_TRAVERSAL 1

struct TreeNode{
	int val;
	TreeNode* left;
	TreeNode* right;
	TreeNode() : val(0), left(nullptr), right(nullptr) {}
	TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
	TreeNode(int x, TreeNode* left, TreeNode* right) : val(x), left(left), right(right) {}
};

static TreeNode* getRandomTree();

TreeNode* getRandomTree() {
	srand((unsigned)time(NULL));

	TreeNode* root = new TreeNode(0);
	TreeNode* tmp = root;

	for (int i = 0; i < NUM_SIZE; i++) {
		int val = rand() % (MAX_NUM - MIN_NUM + 1) + MIN_NUM;		// [MIN_NUM, MAX_NUM]
		TreeNode* node = new TreeNode(val);
		
		int seed = rand() % 2;
		if (seed == 0) {
			tmp->left = node;
		}
		else {
			tmp->right = node;
		}
		tmp = node;
	}
	return root;
}

#endif

一,前序遍历的概念

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

        方式
                1,递归遍历
                2,栈迭代遍历 (借助栈结构)
                3,Morris 遍历 (栈迭代的基础上优化空间复杂度)

二,递归遍历

/*
递归遍历
    先访问当前节点,然后再访问当前节点的左儿子和右儿子
    T : O(n),n 是二叉树的节点数,每个节点恰好被遍历1次
    S : O(n), 递归过程中的栈开销,平均情况下为 O(log n),最坏情况下树为链状,为 O(n)
*/
void preTraverse(TreeNode* root, vector<int>& ret) {
    if (root) {
        ret.emplace_back(root->val);		// 访问当前元素
        preTraverse(root->left, ret);		// 递归的访问左子树
        preTraverse(root->right, ret);		// 递归的访问右子树
    }
}

三,栈迭代遍历

/*
栈迭代遍历
    该方法与递归遍历类似,区别在于递归遍历隐式的维护了一个栈,而迭代则显示的将这个栈模拟出来
    因为栈的构造是 后进先出(LIFO),所以需要先将右孩子入栈,再将左孩子入栈,则左孩子会先出栈被访问。
    T : O(n),n 是二叉树的节点数,每个节点恰好被遍历1次
    S : O(n), 递归过程中的栈开销,平均情况下为 O(log n),最坏情况下树为链状,为 O(n)
*/
void preTraverseN(TreeNode* root, vector<int>& ret) {
    if (root) {
        stack<TreeNode*> stack;
        stack.emplace(root);

        while (!stack.empty()) {
            TreeNode* node = (TreeNode*)stack.top();
            stack.pop();
            ret.emplace_back(node->val);	// 在该节点出栈时,访问它

            if (node->right) {		// 先放右孩子
                stack.emplace(node->right);
            }

            if (node->left) {		// 再放左孩子
                stack.emplace(node->left);
            }
        }
    }
}

四,Morris 遍历

/*
Morris 遍历
    这种方法由 J. H. Morris 在 1979 年的论文「Traversing Binary Trees Simply and Cheaply」中首次提出,因此被称为 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) [此时,相当于 cur 节点的左子树节点都遍历到了]

自己动手画一画就知道为什么啦~

    T : O(n),其中 n 是二叉树的节点数。没有左子树的节点只被访问一次,有左子树的节点被访问两次。
    S : O(1), 只操作已经存在的指针(树的空闲指针),因此只需要常数的额外空间。

举例:
                   1
                /    \
              2     3
             /  \    / \
            4	 5  6   7
                  \
                   8

当前节点 cur 为 1, 且 1 有左子树, 1 的左子树节点为 2。
则 当前节点左子树 的 最右节点为 8, 所以 8 为 mostRight。(因为从 2 往右跑,最多可以跑到 8)


           1
         /   \
        2   3
       /
    4

当前节点 cur 为 1, 且 1 有左子树, 1 的左子树节点为 2。
则 当前节点左子树 的 最右节点为 2, 所以 2 为 mostRight。(最右节点为当前节点本身)

模板:
    这种模版是 Morris 遍历二叉树的通用模版,至于前序、中序和后续,需要在不同的点对节点进行访问

void Morris(TreeNode* root) {
    TreeNode* cur = root;
    TreeNode* mostRight = nullptr;

    while (cur) {
        mostRight = cur->left;	// 当前节点 cur 的左子树

        if (mostRight) {	// 若当前节点有左子树,则寻找 cur 左子树的最右节点
            while (mostRight->right && mostRight->right != cur) { // 若有右孩子,且右孩子不指向当前节点
                mostRight = mostRight->right;
            }

            // 此时 mostRight 为当前节点左子树的最右节点
            if (mostRight->right == nullptr) {	// 若最右节点的右儿子为空,则让其指向当前节点 cur
                mostRight->right = cur;
                cur = cur->left;		// 让当前节点往左移动
            }
            else {		// 这种情况等同于 mostRight->right == cur,此时,相当于 cur 节点的左子树都遍历完了
                mostRight->right = nullptr;
                cur = cur->right;
            }
        }
        else {	// 若没有左子树,则当前节点往右移动
            cur = cur->right;
        }
    }
}
*/

五,用 Morris 方法对二叉树进行前序遍历

void preTraverseM(TreeNode* root, vector<int>& ret) {
    TreeNode* cur = root;
    TreeNode* mostRight = nullptr;
	
    while (cur) {
        mostRight = cur->left;			// 当前节点 cur 的左子树

        if (mostRight) {	// 若当前节点有左子树,则寻找 cur 左子树的最右节点
            while (mostRight->right && mostRight->right != cur) {		// 若有右孩子,且有孩子不指向当前节点
                mostRight = mostRight->right;
            }

            // 此时 mostRight 为当前节点左子树的最右节点
            if (mostRight->right == nullptr) {	// 若最右节点的右儿子为空,则让其指向当前节点 cur
                ret.emplace_back(cur->val);		// ## 前序遍历,需要在此访问当前节点
                mostRight->right = cur;
                cur = cur->left;		// 让当前节点往左移动
            }
            else {		// 这种情况等同于 mostRight->right == cur
                mostRight->right = nullptr;
                cur = cur->right;
            }
        }
        else {	// 若没有左子树,则当前节点往右移动
            ret.emplace_back(cur->val);		// ## 前序遍历,需要在此访问当前节点
            cur = cur->right;
        }
    }
}

六,主函数

void preOrderTraversal(TreeNode* root) {
    vector<int> ret;

    preTraverse(root, ret);
    cout << "前序遍历, 递归 : " << endl;
    PRINT(ret);

    ret.clear();
    preTraverseN(root, ret);
    cout << "前序遍历, 栈迭代 : " << endl;
    PRINT(ret);

    ret.clear();
    preTraverseM(root, ret);
    cout << "前序遍历, Morris : " << endl;
    PRINT(ret);
}

#if PRE_ORDER_TRAVERSAL

int main() {
    TreeNode* root = getRandomTree();

    preOrderTraversal(root);

    return 0;
}

#endif

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值