本系列总计六篇文章,是 基于STL实现的笔试题常考七大基本数据结构 该文章在《代码随想录》和《labuladong的算法笔记》题目中的具体实践,每篇的布局是这样的:开头是该数据结构的总结,然后是在不同场景的应用,以及不同的算法技巧。本文是系列最后一篇,第六篇,介绍了树的相关题目,重点是要掌握二叉树、多叉树的构造、遍历(递归、非递归、层次),以及二叉树、二叉搜索树的属性,体会递归算法的本质是二叉树。
下面文章是在《代码随想录》和《labuladong的算法笔记》题目中的具体实践:
【笔记】数组
【笔记】链表
【笔记】哈希表
【笔记】字符串
【笔记】栈与队列
【笔记】二叉树
0、总结
- 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序遍历,都是先构造中节点。
- 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。
- 求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。
- 注意体会递归函数带返回值和不带返回值的区别
1、二叉树的构建
【ACM模式】根据输入的数组构造二叉树
力扣上如何自己构造二叉树输入用例? | 代码随想录 (programmercarl.com)
- 思路,借助顺序存储方式
1、把输入的 int 数组,先转化为二叉树节点数组,切记root绑定到新数组首元素
2、遍历,根据规则给左、右孩子赋值。节点 i 的左孩子下标 2 * i + 1,右孩子下标2 * i + 2。for中条件 i * 2 + 1 < vec.size(),不然会漏掉 i * 2 + 2节点值
3、层序遍历,打印
#include <bits/stdc++.h>
using namespace std;
struct TreeNode {
/* data */
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {
}
};
TreeNode* ConstructBinaryTree (const vector<int>& vec) {
if (vec.size() == 0) return nullptr;
vector<TreeNode*> vecTree (vec.size(), nullptr);
TreeNode* root = nullptr;
// 把输入的 int 数组,先转化为二叉树节点数组
for (int i = 0; i < vec.size(); i++) {
TreeNode* node = nullptr;
if (vec[i] != -1) {
node = new TreeNode(vec[i]);
}
vecTree[i] = node;
if (i == 0) {
root = node;
}
// cout << vec[i] << " ";
}
// cout << endl;
// 遍历,根据规则给左、右孩子赋值
for (int i = 0; i * 2 + 1 < vec.size(); i++) {
if (vecTree[i] != nullptr) {
vecTree[i]->left = vecTree[i * 2 + 1];
if (i * 2 + 2 < vec.size())
vecTree[i]->right = vecTree[i * 2 + 2];
}
}
return root;
}
// 层序遍历,按每层打印输出
void PrintBinaryTree (TreeNode* root) {
// 这一句多余了,遇见 -1 就直接返回了
// if (root = nullptr) return;
// 只有非空节点才入队列
queue<TreeNode*> que;
if (root != nullptr) que.push(root);
// 总结果集
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
// 每层的结果集
vector<int> level;
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
if (node != nullptr) {
level.push_back(node->val);
que.push(node->left);
que.push(node->right);
} else {
level.push_back(-1);
}
}
result.push_back(level);
}
for (int i = 0; i < result.size(); i++) {
for (int j = 0; j < result[i].size(); j++) {
cout << result[i][j] << " ";
}
cout << endl;
}
}
int main() {
// 用 -1 来表示nullptr
vector<int> vec = {
4,1,6,0,2,5,7,-1,-1,-1,3,-1,-1,-1,8};
TreeNode* root = ConstructBinaryTree(vec);
PrintBinaryTree(root);
}
// 输入结果如下
// 4,1,6,0,2,5,7,-1,-1,-1,3,-1,-1,-1,8
// 打印结果如下
// 4
// 1 6
// 0 2 5 7
// -1 -1 -1 3 -1 -1 -1 8
// -1 -1 -1 -1 这四个多余的 -1 是叶子节点3和8的左右孩子
2、二叉树的遍历
层序遍历
借助队列,见上段代码的 PrintBinaryTree()
函数
学会二叉树的层序遍历,可以一口气打完以下十题:
- 102.二叉树的层序遍历
- 107.二叉树的层次遍历II
- 199.二叉树的右视图
- 637.二叉树的层平均值
- 429.N叉树的层序遍历
- 515.在每个树行中找最大值
- 116.填充每个节点的下一个右侧节点指针
- 117.填充每个节点的下一个右侧节点指针II
- 104.二叉树的最大深度
- 111.二叉树的最小深度
【递归】前、中、后序遍历
- 最基础的递归,记住三部曲(后面会介绍回溯三步曲)
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
- 递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
二叉树前序遍历
根->左->右
/**
* Definition for a binary tree node.
* 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) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
preorder(root, result);
return result;
}
// 要修改原本的vec需要传入引用,否则只是对复制的vec进行修改,原本的vec依旧为空
void preorder(TreeNode* root, vector<int>& vec) {
if (root == nullptr) return;
// 前序位置
vec.push_back(root->val);
preorder(root->left, vec);
// 中序位置
preorder(root->right, vec);
// 后序位置
}
};
N叉树前序/后序遍历
class Solution {
private:
vector<int> result;
public:
vector<int> preorder(Node* root) {
if (root == NULL) return {
};
result.clear();
traverse(root);
return result;
}
void traverse(Node* root) {
if (root == NULL) return;
result.push_back(root->val);
for (Node* node : root->children) {
traverse(node);
}
}
};
中序遍历
左->根->右
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
inorder(root, result);
return result;
}
void inorder(TreeNode* cur, vector<int>& vec) {
if (cur == nullptr) return;
inorder(cur->left, vec);
// 中序位置
vec.push_back(cur->val);
inorder(cur->right, vec);
}
};
后序遍历
左->右->根
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
postorder(root, result);
return result;
}
void postorder(TreeNode* cur, vector<int>& vec) {
if (cur == nullptr) return;
postorder(cur->left, vec);
postorder(cur->right, vec);
// 后序位置
vec.push_back(cur->val);
}
};
【迭代】前、中、后序遍历
-
计算机处理递归用函数调用栈,因此理论上任何一个递归程序都可以用栈来迭代处理
-
会了前序就会后序(reverse),中序引入了指针,必须掌握。
前序遍历
先根入栈,然后注意入栈顺序是先右后左
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
if (root == nullptr) return{
};
vector<int> result;
stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
// 根
TreeNode* node = st.top();
result.push_back(node->val);
st.pop();
// 入栈是右左,才能保证输出是左右
if (node->right) st.push(node->right);
if (node->left) st.push(node->left);
}
return result;
}
};
中序遍历
与前后续遍历的逻辑不同,注意指针先一路向左,然后处理中,然后向右。
这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
if (root == nullptr) return {
};
stack<TreeNode*> st;
vector<int> result;
// 引入指针处理当前要输出的元素,栈则保存已遍历过的元素
TreeNode* cur = root;
while