文章目录
一、二叉树的基本概念及实现
在介绍二叉树的基本概念与术语之前,需要先了解树的基本概念。
在了解了树的基本概念之后,二叉树的概念就很明了了,二叉树就是度不超过2的树。这里再次强调一下一些基本概念, 根结点、子结点、以及 叶子节点 。接下来将通过这张图来说明这些基本的概念。
图1 二叉树
1.1 根结点
根节点这一个“根”字,想必可以让大家联想到树根,这个根结点也就像是树根,由根生出后面的枝枝蔓蔓。图中的结点
A
\texttt{A}
A 就是一个根结点,这一棵二叉树就是从这个结点开始的。那还有没有其他的根结点呢?没有。这里的
B、C、E、F
\texttt{B、C、E、F}
B、C、E、F 结点也可以继续生成树,但是它们不是根结点。在一棵树中,根结点有且只能只有一个,它是所有除了它自身以外所有结点的祖先。
图中
B、C、E、F
\texttt{B、C、E、F}
B、C、E、F 这些点怎称呼呢?
1.2 父结点
父结点就是某个结点的上层,由父结点可以生成这个结点。
这里的
B、C、E、F
\texttt{B、C、E、F}
B、C、E、F 结点虽然不是根结点,但是它们却是父结点。比如
B
\texttt{B}
B 是结点
D、E
\texttt{D、E}
D、E 的父结点。
1.3 子结点
百度百科
在树形图中,当前结点的各个子树的根称为当前结点的 子结点。即当前结点能直接支配的结点。
上图中结点
B、C
\texttt{B、C}
B、C 是结点
A
\texttt{A}
A 的子结点,结点
E、F
\texttt{E、F}
E、F 分别是结点
B、C
\texttt{B、C}
B、C 的子结点。
1.4 二叉树数据结构的实现
class TreeNode {
public:
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {};
TreeNode(int v) : val(v), left(nullptr), right(nullptr) {};
TreeNode(int v, TreeNode *l, TreeNode *r) : val(v), left(l), right(r) {};
};
二、二叉树的遍历方法
在下方出现的动图中,绿色表示正在访问的结点,红色表示已经访问过的结点。
2.1 前序遍历
给定一棵二叉树,让你对这棵二叉树进行前序遍历的意思就是:按照先访问树的 根结点,再递归访问 左子树 最后再递归访问 右子树 的优先级顺序对一棵树进行访问遍历。遍历效果如图 2 所示。
在访问过程中,将结点记录下来,最后输出的结果就是二叉树的前序遍历结果。在递归遍历某个子树的过程中,也是将子树看作是一棵全新的树,按照上述顺序进行遍历。
这种先遍历根节点再递归遍历左子树最后递归遍历右子树的方法通常被简称为 根——左——右 的遍历方法。
图2 前序遍历
2.2 中序遍历
给定一棵二叉树,让你对这棵二叉树进行中序遍历的意思就是:按照先递归访问树的 左子树,再访问 根结点,最后再访问 右子树 的优先级顺序对一棵树进行访问。遍历效果如图 3 所示。
在访问过程中,将结点记录下来,最后输出的结果就是对二叉树的中序遍历结果。在递归遍历某个子树的过程中,也是将子树看作是一棵全新的树,按照上述顺序进行遍历。
中序遍历的方法通常被简称为 左——根——右 的遍历方法。
图3 中序遍历
2.3 后序遍历
给定一棵二叉树,让你对这棵二叉树进行后序遍历的意思就是:按照先访问树的 左子树,再访问 右子树,最后再访问 根结点 的优先级顺序对一棵树进行访问。遍历效果如图 4 所示。
在访问过程中,将结点记录下来,最后输出的结果就是对树的后序遍历。在递归遍历某个子树的过程中,也是将子树看作是一棵全新的树,按照上述顺序进行遍历。
后续遍历的方法通常被简称为 左——右——根 的说法。
图4 后序遍历
2.4 层序遍历
给定一棵二叉树,让你对这棵二叉树进行 层序遍历 的意思就是:按 层 对树进行访问。在访问过程中,将结点记录下来,最后输出的结果就是二叉树层序遍历的结果。
在图 5 中,对该二叉树进行层序遍历得到的第一层结点为 A \texttt{A} A,第二层为 BC \texttt{BC} BC,第三层为 DEF \texttt{DEF} DEF,第四层为 GHI \texttt{GHI} GHI。
最后,层序遍历的输出为 [A, B, C, D, E, F, G, H, I]
。
图5 层序遍历
实现代码
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if (root == NULL)
return res;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
vector<int> leave; // 每一层存放结点的容器
int n = q.size();
for (int i = 0; i < n; ++i) {
TreeNode *curr = q.front();
q.pop();
leave.push_back(curr->val);
if (curr->left != NULL)
q.push(curr->left);
if (curr->right != NULL)
q.push(curr->right);
}
res.push_back(leave);
}
return res;
}
以上代码实现二叉树的层序遍历,该实现将二叉树的每一层输出到一个容器中,最后返回的是包含每一层的一个容器。
在实现中用到了 队列 这种基本的数据结构,队列的特点是 先进先出,队尾入队,队头出队。
层序遍历要求按层从上到下,并且每一层中按从左到右的顺序进行遍历,于是选择队列这一基本的数据结构来实现层序遍历。
三、三种遍历的多种方法实现
3.1 递归方法实现
递归的实现方法比较简单,这里只介绍前序遍历的递归实现方法,其他两种遍历(中序和后续遍历)读者可以根据提示自行实现。
根据上述内容,我们知道前序遍历是按照 根——左——右 的顺序遍历结点的。在访问左子树和右子树的时候,同样也是按照 根——左——右
的顺序来访问。那么就可以通过递归来实现,同样的中序、后序遍历也可以这样实现。
前序遍历的递归实现
void process(TreeNode* root, vector<int> &res) {
if (root == nullptr) {
return;
}
res.push_back(root->val); // 三种遍历保存结点位置不同
process(root->left, res);
process(root->right, res);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
process(root, res);
return res;
}
3.2 迭代方法实现
3.2.1 前序遍历
原理
定义一个栈,二叉树非空就首先将树的 “根” 即 root \texttt{root} root 入栈,接着进行迭代,迭代的终止条件是 “栈为空”;
接着依次出栈,出栈即入答案 v e c t o r 容器 vector容器 vector容器 ,若当前结点有 右结点,入栈,接着若当前结点有 左结点,入栈;
实现代码
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode*> stk;
stk.push(root);
TreeNode* curr = nullptr;
while (!stk.empty()) {
curr = stk.top(); stk.pop();
res.push_back(curr->val);
if (curr->right != nullptr) {
stk.push(curr->right);
}
if (curr->left != nullptr) {
stk.push(curr->left);
}
}
return res;
}
};
3.2.2 后序遍历
原理
后序遍历是 “左——右——根” 的顺序,利用一次栈可以得到二叉树按照 “根——右——左” 顺序遍历得到的结点,即二叉树前序遍历的结果。恰好二叉树后续遍历的结果是前序遍历结果的逆序,因此二叉树后续遍历的迭代版本就在前序遍历迭代版本后增加一个逆序处理即可。
实现代码
以下给出两个实现后续遍历的迭代代码,分别是使用两个栈实现和使用一个栈实现。
// 两个栈实现后序遍历
vector<int> posOrderTraversal(TreeNode* root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode*> stk, stk2;
stk.push(root);
TreeNode* curr = nullptr;
while (!stk.empty()) {
curr = stk.top();
stk.pop();
stk2.push(curr);
if (curr->right != nullptr) {
stk.push(curr->right);
}
if (curr->left != nullptr) {
stk.push(curr->left);
}
}
while (!stk2.empty()) {
res.push_back(stk2.top()->val);
stk2.pop();
}
return res;
}
// 一个栈实现
vector<int> posOrderTraversal2(TreeNode* root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode*> stk;
stk.push(root);
TreeNode *curr = nullptr;
while (!stk.empty()) {
curr = stk.top();
if (curr->left != nullptr && root != curr->left && root != curr->right) {
stk.push(curr->left);
}
else if (curr->right != nullptr && root != curr->right) {
stk.push(curr->right);
}
else {
res.push_back(curr->val);
stk.pop();
root = curr;
}
}
return res;
}
3.2.3 中序遍历
原理
从根结点出发,沿着左子树一直扎下去,直到为空,在这过程中入栈经过的左子树,为空后,将栈顶元素存入 v e c t o r vector vector 容器,并继续沿着右子树一直走下去。
实现代码
vector<int> inOrderTraversal(TreeNode* root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode*> stk;
while (!stk.empty() || root) {
while (root) {
stk.push(root);
root = root->left;
}
root = stk.top(); stk.pop();
res.push_back(root->val);
root = root->right;
}
return res;
}
3.3 Morris方法实现
Morris算法相对于以上两种算法的优点是将空间复杂度优化到 O ( 1 ) O(1) O(1)。
Morris算法的实现过于复杂,这里只对前序遍历的实现进行介绍,感兴趣的读者可以自行研究其他两种遍历的Morris实现。
3.3.1 基本原则
①、如过当前结点 c u r r curr curr 没有左子结点,那么更新为右子结点即 c u r r = c u r r → r i g h t curr = curr \to right curr=curr→right ,并且 c u r r curr curr 更新为左指针;
②、如果 c u r r curr curr 有左子结点,那么首先找到左子结点中的最右子结点,记作 m o s t R i g h t mostRight mostRight ,并且 c u r r curr curr 更新为右指针:
- 对于结点 m o s t R i g h t mostRight mostRight ,如果它的右指针指向为空,那么更新它的右指针为 c u r r curr curr ,即 m o s t R i g h t → r i g h t = c u r r mostRight \to right = curr mostRight→right=curr;
- 对于结点 m o s t R i g h t mostRight mostRight ,如果它的右指针指向非空,那么更新它的右指针为 N U L L NULL NULL ,即 m o s t R i g h t → r i g h t = N U L L mostRight \to right = NULL mostRight→right=NULL 。
③、重复 ①、② 操作直至遍历完所有结点。
3.3.2 样例模拟与实现
明确了以上的基本原则之后,我们利用该原则进行模拟,结果如下:
①、当前结点 c u r r curr curr 是 A \texttt{A} A,是非空结点,有左子树,左子树中最右侧的结点为 H \texttt{H} H, H \texttt{H} H 的右指针指向为空,因此更新为指向 A \texttt{A} A, c u r r curr curr 更新为左指针 B \texttt{B} B ;
②、当前结点 c u r r curr curr 是 B \texttt{B} B,是非空结点,有左子树,左子树中最右侧的结点为 D \texttt{D} D, D \texttt{D} D 的右指针指向为空,因此更新为指向 B \texttt{B} B, c u r r curr curr 更新为左指针 D \texttt{D} D;
③、当前结点 c u r r curr curr 是 D \texttt{D} D,是非空结点,但是没有左子树,因此 c u r r curr curr 更新为 D \texttt{D} D 的右指针,由 ② 中知 D \texttt{D} D 的右指针是 B \texttt{B} B,因此 c u r r curr curr 更新为 B \texttt{B} B;
④、当前结点 c u r r curr curr 是 B \texttt{B} B,是非空结点,有左子树,左子树中最右侧的结点为 D \texttt{D} D, D \texttt{D} D 的右指针指向是非空的,因此更新为指向空。此时 c u r r curr curr 更新为它的右指针 E \texttt{E} E;
⑤、当前结点 c u r r curr curr 是 E \texttt{E} E,是非空结点,有左子树,左子树中最右侧的结点为 G \texttt{G} G, G \texttt{G} G 的右指针指向为空,因此更新为指向 E \texttt{E} E, c u r r curr curr 更新为左指针 G \texttt{G} G;
⑥、当前结点 c u r r curr curr 是 G \texttt{G} G,是非空结点,但是没有左子树,因此 c u r r curr curr 更新为 G \texttt{G} G 的右指针,由 ⑤ 中知 G \texttt{G} G 的右指针是E,因此 c u r r curr curr 更新为 E \texttt{E} E;
⑦、当前结点 c u r r curr curr 是 E \texttt{E} E,是非空结点,有左子树,左子树中最右侧的结点为 G \texttt{G} G, G \texttt{G} G 的右指针指向是非空的,因此更新为指向空。此时 c u r r curr curr 更新为它的右指针 H \texttt{H} H;
⑧、当前结点 c u r r curr curr 是 H \texttt{H} H,是非空结点,但是没有左子树,因此 c u r r curr curr 更新为 H \texttt{H} H 的右指针,由 ① 中知 H \texttt{H} H 的右指针是 A \texttt{A} A,因此 c u r r curr curr 更新为 A \texttt{A} A;
⑨、当前结点 c u r r curr curr 是 A \texttt{A} A,是非空结点,有左子树,左子树中最右侧的结点为 H \texttt{H} H, H \texttt{H} H 的右指针指向是非空的,因此更新为指向空。此时 c u r r curr curr 更新为它的右指针 C \texttt{C} C;
⑩、当前结点 c u r r curr curr 是 C \texttt{C} C,是非空结点,但是没有左子树,因此 c u r r curr curr 更新为 C \texttt{C} C 的右指针 F \texttt{F} F;
⑪、当前结点 c u r r curr curr 是 F \texttt{F} F,是非空结点,有左子树,左子树中最右侧的结点为 I \texttt{I} I, I \texttt{I} I 的右指针指向为空,因此更新为指向 F \texttt{F} F, c u r r curr curr 更新为左指针 I \texttt{I} I;
⑫、当前结点 c u r r curr curr 是 I \texttt{I} I,是非空结点,但是没有左子树,因此 c u r r curr curr 更新为 I \texttt{I} I 的右指针,由 ⑪ 中知H的右指针是 F \texttt{F} F,因此 c u r r curr curr 更新为 F \texttt{F} F;
⑬、当前结点 c u r r curr curr 是 F \texttt{F} F,是非空结点,有左子树,左子树中最右侧的结点为 I \texttt{I} I, I \texttt{I} I 的右指针指向是非空的,因此更新为指向空。此时 c u r r curr curr 更新为它的右指针 NULL \texttt{NULL} NULL;
⑭、 c u r r curr curr 为 NULL \texttt{NULL} NULL,退出循环,遍历结束。
实现代码
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if(root == nullptr){
return res;
}
TreeNode* curr = root;
TreeNode* mostRight = nullptr;
while(curr != nullptr){
mostRight = curr->left;
if(mostRight != nullptr){
while(mostRight->right != nullptr && mostRight->right != curr){
mostRight = mostRight->right;
}
if(mostRight->right == nullptr){
mostRight->right = curr;
res.emplace_back(curr->val);
curr = curr->left;
continue;
}
else{
mostRight->right = nullptr;
}
}
else{
res.emplace_back(curr->val);
}
curr = curr->right;
}
return res;
}
四、题目链接
题号 | 难度 |
---|---|
144. 二叉树的前序遍历 | Easy |
94. 二叉树的中序遍历 | Easy |
145. 二叉树的后序遍历 | Easy |
102. 二叉树的层序遍历 | Medium |
五、总结
以上介绍了二叉树几种常见的遍历方法,对于部分遍历方法介绍了多种的实现方法,诸如递归、迭代、Morris方法。
但是实际使用中只需要能够快速实现递归与迭代的方法就可以了。
Morris 算法,只是降低了空间复杂度,时间复杂度没有提高。该算法在数据结构的面试考察中基本不会涉及,也许会在工程问题有实际的应用场景,感兴趣的读者自行研究。
写在最后
如果文章内容有任何错误或者您对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。
如果大家有更优的时间、空间复杂度方法,欢迎评论区交流。
最后,感谢您的阅读,如果感到有所收获的话可以给博主点一个 👍 哦。