gh_mirrors/leet/leetcode项目:树的遍历算法题解汇总
你还在为二叉树遍历算法发愁吗?递归解法太简单,面试官要求非递归实现怎么办?Morris遍历看不懂?本文汇总了gh_mirrors/leet/leetcode项目中树的遍历算法题解,涵盖先序、中序、后序、层序等多种遍历方式,提供栈实现和Morris遍历等高效解法,助你轻松应对各类树遍历问题。读完本文,你将掌握不同遍历算法的实现原理、时间空间复杂度分析及实际应用场景。
树的基本概念与节点定义
树是一种重要的数据结构,在计算机科学中有着广泛的应用。二叉树(Binary Tree)是树的一种特殊形式,每个节点最多有两个子节点,通常称为左子节点和右子节点。
在LeetCode题解中,二叉树的节点定义如下:
// 树的节点
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) { }
};
上述代码定义了一个简单的二叉树节点结构,包含节点值(val)、左子节点指针(left)和右子节点指针(right)。该定义位于项目的C++/chapTree.tex文件中,是所有树相关算法的基础。
二叉树的遍历方式
树的遍历是指按照一定的顺序访问树中的所有节点。常见的树遍历方式可以分为两大类:深度优先遍历(Depth-First Search, DFS)和宽度优先遍历(Breadth-First Search, BFS)。
深度优先遍历
深度优先遍历是沿着树的深度优先访问节点,尽可能深地搜索树的分支。根据节点访问顺序的不同,深度优先遍历又可分为以下三种:
- 先序遍历(Preorder Traversal):访问顺序为 根节点 -> 左子树 -> 右子树
- 中序遍历(Inorder Traversal):访问顺序为 左子树 -> 根节点 -> 右子树
- 后序遍历(Postorder Traversal):访问顺序为 左子树 -> 右子树 -> 根节点
宽度优先遍历
宽度优先遍历,也称为层序遍历(Level Order Traversal),是从树的根节点开始,逐层访问树的节点,同一层的节点按照从左到右的顺序访问。
先序遍历算法
先序遍历(Preorder Traversal)的访问顺序是:先访问根节点,然后递归地先序遍历左子树,最后递归地先序遍历右子树。
栈实现
栈是实现非递归先序遍历的常用数据结构。其基本思路是:
- 将根节点入栈
- 当栈不为空时,弹出栈顶节点并访问
- 将右子节点入栈(注意:先右后左,因为栈是后进先出的)
- 将左子节点入栈
- 重复步骤2-4,直到栈为空
以下是使用栈实现的先序遍历代码:
// LeetCode, Binary Tree Preorder Traversal
// 使用栈,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
vector<int> preorderTraversal(TreeNode *root) {
vector<int> result;
stack<const TreeNode *> s;
if (root != nullptr) s.push(root);
while (!s.empty()) {
const TreeNode *p = s.top();
s.pop();
result.push_back(p->val);
if (p->right != nullptr) s.push(p->right);
if (p->left != nullptr) s.push(p->left);
}
return result;
}
};
上述代码实现了二叉树的非递归先序遍历,时间复杂度为O(n),空间复杂度为O(n),其中n是二叉树的节点数。完整代码可参考C++/chapTree.tex。
Morris先序遍历
Morris遍历算法是一种空间效率更高的遍历方法,它可以在O(1)的空间复杂度下完成树的遍历。Morris先序遍历的核心思想是利用树的空指针来存储遍历过程中的前驱节点,从而避免使用栈。
以下是Morris先序遍历的实现代码:
// LeetCode, Binary Tree Preorder Traversal
// Morris先序遍历,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
vector<int> preorderTraversal(TreeNode *root) {
vector<int> result;
TreeNode *cur = root, *prev = nullptr;
while (cur != nullptr) {
if (cur->left == nullptr) {
result.push_back(cur->val);
prev = cur; /* cur刚刚被访问过 */
cur = cur->right;
} else {
/* 查找前驱 */
TreeNode *node = cur->left;
while (node->right != nullptr && node->right != cur)
node = node->right;
if (node->right == nullptr) { /* 还没线索化,则建立线索 */
result.push_back(cur->val); /* 仅这一行的位置与中序不同 */
node->right = cur;
prev = cur; /* cur刚刚被访问过 */
cur = cur->left;
} else { /* 已经线索化,则删除线索 */
node->right = nullptr;
/* prev = cur; 不能有这句,cur已经被访问 */
cur = cur->right;
}
}
}
return result;
}
};
Morris先序遍历的时间复杂度为O(n),空间复杂度为O(1),是一种空间效率极高的遍历算法。完整代码可参考C++/chapTree.tex。
中序遍历算法
中序遍历(Inorder Traversal)的访问顺序是:先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。对于二叉搜索树(BST),中序遍历可以得到一个递增的有序序列。
栈实现
使用栈实现中序遍历的基本思路是:
- 将当前节点的所有左子节点入栈
- 弹出栈顶节点并访问
- 将当前节点指向弹出节点的右子节点
- 重复步骤1-3,直到栈为空且当前节点为nullptr
以下是使用栈实现的中序遍历代码:
// LeetCode, Binary Tree Inorder Traversal
// 使用栈,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
vector<int> inorderTraversal(TreeNode *root) {
vector<int> result;
stack<const TreeNode *> s;
const TreeNode *p = root;
while (!s.empty() || p != nullptr) {
if (p != nullptr) {
s.push(p);
p = p->left;
} else {
p = s.top();
s.pop();
result.push_back(p->val);
p = p->right;
}
}
return result;
}
};
上述代码实现了二叉树的非递归中序遍历,时间复杂度为O(n),空间复杂度为O(n)。完整代码可参考C++/chapTree.tex。
Morris中序遍历
与先序遍历类似,中序遍历也可以使用Morris算法实现,以达到O(1)的空间复杂度。
// LeetCode, Binary Tree Inorder Traversal
// Morris中序遍历,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
vector<int> inorderTraversal(TreeNode *root) {
vector<int> result;
TreeNode *cur = root, *prev = nullptr;
while (cur != nullptr) {
if (cur->left == nullptr) {
result.push_back(cur->val);
prev = cur;
cur = cur->right;
} else {
/* 查找前驱 */
TreeNode *node = cur->left;
while (node->right != nullptr && node->right != cur)
node = node->right;
if (node->right == nullptr) { /* 还没线索化,则建立线索 */
node->right = cur;
/* prev = cur; 不能有这句,cur还没有被访问 */
cur = cur->left;
} else { /* 已经线索化,则访问节点,并删除线索 */
result.push_back(cur->val);
node->right = nullptr;
prev = cur;
cur = cur->right;
}
}
}
return result;
}
};
Morris中序遍历的时间复杂度为O(n),空间复杂度为O(1)。完整代码可参考C++/chapTree.tex。
后序遍历算法
后序遍历(Postorder Traversal)的访问顺序是:先递归地后序遍历左子树,然后递归地后序遍历右子树,最后访问根节点。后序遍历的实现相对复杂一些,因为根节点需要在左右子树都访问完毕后才能访问。
栈实现
使用栈实现后序遍历的基本思路是:
- 使用一个栈存储节点
- 使用一个辅助指针记录刚刚访问过的节点
- 将当前节点的所有左子节点入栈
- 当栈顶节点的右子节点为空或已被访问时,弹出栈顶节点并访问
- 否则,将右子节点入栈,并继续处理右子树
以下是使用栈实现的后序遍历代码:
// LeetCode, Binary Tree Postorder Traversal
// 使用栈,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
vector<int> postorderTraversal(TreeNode *root) {
vector<int> result;
stack<const TreeNode *> s;
/* p,正在访问的结点,q,刚刚访问过的结点*/
const TreeNode *p = root, *q = nullptr;
do {
while (p != nullptr) { /* 往左下走*/
s.push(p);
p = p->left;
}
q = nullptr;
while (!s.empty()) {
p = s.top();
s.pop();
/* 右孩子不存在或已被访问,访问之*/
if (p->right == q) {
result.push_back(p->val);
q = p; /* 保存刚访问过的结点*/
} else {
/* 当前结点不能访问,需第二次进栈*/
s.push(p);
/* 先处理右子树*/
p = p->right;
break;
}
}
} while (!s.empty());
return result;
}
};
上述代码实现了二叉树的非递归后序遍历,时间复杂度为O(n),空间复杂度为O(n)。完整代码可参考C++/chapTree.tex。
Morris后序遍历
Morris后序遍历的实现更为复杂,需要对树的结构进行更多的调整和恢复操作。
// LeetCode, Binary Tree Postorder Traversal
// Morris后序遍历,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
vector<int> postorderTraversal(TreeNode *root) {
vector<int> result;
TreeNode dummy(-1);
TreeNode *cur, *prev = nullptr;
std::function < void(const TreeNode*)> visit =
&result{
result.push_back(node->val);
};
dummy.left = root;
cur = &dummy;
while (cur != nullptr) {
if (cur->left == nullptr) {
prev = cur; /* 必须要有 */
cur = cur->right;
} else {
TreeNode *node = cur->left;
while (node->right != nullptr && node->right != cur)
node = node->right;
if (node->right == nullptr) { /* 还没线索化,则建立线索 */
node->right = cur;
prev = cur; /* 必须要有 */
cur = cur->left;
} else { /* 已经线索化,则访问节点,并删除线索 */
visit_reverse(cur->left, prev, visit);
prev->right = nullptr;
prev = cur; /* 必须要有 */
cur = cur->right;
}
}
}
return result;
}
private:
// 逆转路径
static void reverse(TreeNode *from, TreeNode *to) {
TreeNode *x = from, *y = from->right, *z;
if (from == to) return;
while (x != to) {
z = y->right;
y->right = x;
x = y;
y = z;
}
}
// 访问逆转后的路径上的所有结点
static void visit_reverse(TreeNode* from, TreeNode *to,
std::function< void(const TreeNode*) >& visit) {
TreeNode *p = to;
reverse(from, to);
while (true) {
visit(p);
if (p == from)
break;
p = p->right;
}
reverse(to, from);
}
};
Morris后序遍历的时间复杂度为O(n),空间复杂度为O(1)。完整代码可参考C++/chapTree.tex。
层序遍历算法
层序遍历(Level Order Traversal)是从树的根节点开始,逐层访问树的节点,同一层的节点按照从左到右的顺序访问。层序遍历通常使用队列来实现。
递归版
层序遍历的递归实现思路是:使用一个辅助函数,记录当前遍历的层数,将同一层的节点值存储在同一个数组中。
// LeetCode, Binary Tree Level Order Traversal
// 递归版,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
vector<vector<int> > levelOrder(TreeNode *root) {
vector<vector<int>> result;
traverse(root, 1, result);
return result;
}
void traverse(TreeNode *root, size_t level, vector<vector<int>> &result) {
if (!root) return;
if (level > result.size())
result.push_back(vector<int>());
result[level-1].push_back(root->val);
traverse(root->left, level+1, result);
traverse(root->right, level+1, result);
}
};
上述代码实现了层序遍历的递归版本,时间复杂度为O(n),空间复杂度为O(n)。完整代码可参考C++/chapTree.tex。
迭代版
层序遍历的迭代实现通常使用两个队列,一个队列存储当前层的节点,另一个队列存储下一层的节点。
// LeetCode, Binary Tree Level Order Traversal
// 迭代版,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
vector<vector<int> > levelOrder(TreeNode *root) {
vector<vector<int> > result;
queue<TreeNode*> current, next;
if(root == nullptr) {
return result;
} else {
current.push(root);
}
while (!current.empty()) {
vector<int> level; // elments in one level
while (!current.empty()) {
TreeNode* node = current.front();
current.pop();
level.push_back(node->val);
if (node->left != nullptr) next.push(node->left);
if (node->right != nullptr) next.push(node->right);
}
result.push_back(level);
swap(next, current);
}
return result;
}
};
上述代码实现了层序遍历的迭代版本,使用两个队列分别存储当前层和下一层的节点,时间复杂度为O(n),空间复杂度为O(n)。完整代码可参考C++/chapTree.tex。
变种:之字形层序遍历
之字形层序遍历(Zigzag Level Order Traversal)是层序遍历的一种变种,要求奇数层从左到右遍历,偶数层从右到左遍历,或者反之。
// LeetCode, Binary Tree Zigzag Level Order Traversal
// 广度优先遍历,用一个bool记录是从左到右还是从右到左,每一层结束就翻转一下。
// 迭代版,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
vector<vector<int> > zigzagLevelOrder(TreeNode *root) {
vector<vector<int> > result;
queue<TreeNode*> current, next;
bool left_to_right = true;
if(root == nullptr) {
return result;
} else {
current.push(root);
}
while (!current.empty()) {
vector<int> level; // elments in one level
while (!current.empty()) {
TreeNode* node = current.front();
current.pop();
level.push_back(node->val);
if (node->left != nullptr) next.push(node->left);
if (node->right != nullptr) next.push(node->right);
}
if (!left_to_right) reverse(level.begin(), level.end());
result.push_back(level);
left_to_right = !left_to_right;
swap(next, current);
}
return result;
}
};
之字形层序遍历的实现思路是在普通层序遍历的基础上,增加一个标志位(left_to_right),当标志位为false时,将当前层的节点值数组反转。完整代码可参考C++/chapTree.tex。
树遍历算法的应用
树遍历算法在实际应用中非常广泛,以下是一些常见的应用场景:
恢复二叉搜索树
二叉搜索树(BST)的中序遍历是一个递增的有序序列。如果二叉搜索树中的两个节点被错误地交换,可以通过中序遍历找到这两个节点,并将它们交换回来,恢复二叉搜索树的性质。
// LeetCode, Recover Binary Search Tree
// Morris中序遍历,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
void recoverTree(TreeNode* root) {
pair<TreeNode*, TreeNode*> broken;
TreeNode* prev = nullptr;
TreeNode* cur = root;
while (cur != nullptr) {
if (cur->left == nullptr) {
detect(broken, prev, cur);
prev = cur;
cur = cur->right;
} else {
auto node = cur->left;
while (node->right != nullptr && node->right != cur)
node = node->right;
if (node->right == nullptr) {
node->right = cur;
//prev = cur; 不能有这句!因为cur还没有被访问
cur = cur->left;
} else {
detect(broken, prev, cur);
node->right = nullptr;
prev = cur;
cur = cur->right;
}
}
}
swap(broken.first->val, broken.second->val);
}
void detect(pair<TreeNode*, TreeNode*>& broken, TreeNode* prev,
TreeNode* current) {
if (prev != nullptr && prev->val > current->val) {
if (broken.first == nullptr) {
broken.first = prev;
} //不能用else,例如 {0,1},会导致最后 swap时second为nullptr,
//会 Runtime Error
broken.second = current;
}
}
};
上述代码使用Morris中序遍历实现了二叉搜索树的恢复,时间复杂度为O(n),空间复杂度为O(1)。完整代码可参考C++/chapTree.tex。
判断对称二叉树
对称二叉树是指二叉树的左子树和右子树镜像对称。可以通过层序遍历或递归的方式判断一棵二叉树是否对称。
// LeetCode, Symmetric Tree
// 递归版,时间复杂度O(n),空间复杂度O(logn)
class Solution {
public:
bool isSymmetric(TreeNode *root) {
if (root == nullptr) return true;
return isSymmetric(root->left, root->right);
}
bool isSymmetric(TreeNode *p, TreeNode *q) {
if (p == nullptr && q == nullptr) return true; // 终止条件
if (p == nullptr || q == nullptr) return false; // 终止条件
return p->val == q->val // 三方合并
&& isSymmetric(p->left, q->right)
&& isSymmetric(p->right, q->left);
}
};
上述代码使用递归的方式判断二叉树是否对称,时间复杂度为O(n),空间复杂度为O(logn)。完整代码可参考C++/chapTree.tex。
将二叉树展开为链表
可以使用后序遍历的思想将二叉树展开为单链表,使得展开后的链表中节点的顺序与二叉树的先序遍历顺序相同。
// LeetCode, Flatten Binary Tree to Linked List
// 递归版1,时间复杂度O(n),空间复杂度O(logn)
class Solution {
public:
void flatten(TreeNode *root) {
if (root == nullptr) return; // 终止条件
flatten(root->left);
flatten(root->right);
if (nullptr == root->left) return;
// 三方合并,将左子树所形成的链表插入到root和root->right之间
TreeNode *p = root->left;
while(p->right) p = p->right; //寻找左链表最后一个节点
p->right = root->right;
root->right = root->left;
root->left = nullptr;
}
};
上述代码使用递归的方式将二叉树展开为链表,时间复杂度为O(n),空间复杂度为O(logn)。完整代码可参考C++/chapTree.tex。
总结与展望
本文详细介绍了gh_mirrors/leet/leetcode项目中树的遍历算法题解,包括先序遍历、中序遍历、后序遍历和层序遍历等多种遍历方式,以及栈实现和Morris遍历等不同实现方法。每种算法都提供了详细的代码示例和复杂度分析,并介绍了树遍历算法在恢复二叉搜索树、判断对称二叉树和将二叉树展开为链表等实际问题中的应用。
树遍历是数据结构和算法中的基础内容,掌握树遍历算法对于解决复杂的树相关问题至关重要。希望本文能够帮助读者深入理解树遍历算法,并在实际应用中灵活运用。
更多树相关的算法题解可以参考项目中的C++/chapTree.tex文件,该文件包含了151道LeetCode题解中的树相关题目,是学习树算法的宝贵资源。
点赞收藏本文,关注作者,获取更多优质算法题解和编程资源!下期我们将介绍图算法的核心思想和应用,敬请期待!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



