头文件: 用来定义 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