算法第14天| (二叉树part01)理论基础、递归遍历、迭代遍历、统一迭代
文章目录
一、 理论基础
1. 二叉树的种类
满二叉树、完全二叉树、二叉搜索树、平衡二叉树
面试会考察你对所使用容器的理解程度,会问你map的key或者set里的元素是不是有序的,答案是肯定的。因为它的底层实现是平衡二叉搜索树,它是要按元素的值进行排序的,所以map的key或者set里的元素都是有序的。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,但是unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。(所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!)
2. 二叉树的存储方式
链式存储、线性存储(很少用)
链式存储:每个节点有两个指针,一个指向左孩子,一个指向右孩子;
线性存储:找到节点i的左右孩子的下标:左孩子:2i+1; 右孩子:2i+2;
构造二叉树(二叉树就是一个链表):
3. 遍历方式
深度优先搜索:用递归实现,一条路走到黑,然后再回头换方向。前中后序遍历都是深度优先搜索。可以用递归法和迭代法实现这三种遍历。
广度优先搜索:一层一层遍历,层序遍历,层序遍历就是迭代法,是用一个队列实现对二叉树一层一层的搜索。因为队列的先进先出的规则符合一层一层遍历的需求。
递归法和迭代法:编程语言在实现递归的时候,就是用栈来实现。
面试有可能给你一道比较简单的二叉树的题目,让你用非递归的方式(也就是迭代法)来实现。
前中后序遍历指的是父节点。
前序遍历:中左右
中序遍历:左中右
后序遍历:左右中
4. 定义方式
leetcode是核心代码模式,不需要你定义二叉树or处理输入输出。但是你自己要能手写出来一些基本数据结构的定义。(在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。)
二叉树其实就是链表,所以链式存储的二叉树的节点定义:
// 构造一个结构体
struct TreeNode {
int val;
// 定义指向左右孩子的指针
TreeNode *left;
TreeNode *right;
// 构造函数,传下来一个值,默认左右节点为空,方便对新节点进行初始化
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
二、递归遍历
144. 二叉树的前序遍历
145. 二叉树的后序遍历
94. 二叉树的中序遍历
代码随想录链接
递归三部曲:
1.确定递归函数的参数和返回值
递归函数的参数大多数是一个根节点和一个数组,返回值一般是void,因为直接把想要的结果放进参数了。
2.确定终止条件
3.确定单层递归的逻辑
以下以前序遍历为例:
- 确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入vector来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:
void traversal(TreeNode* cur, vector<int>& vec)
- 确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
if (cur == NULL) return;
- 确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
前序遍历完整代码:
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
// 递归遍历左、右子树
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
中序遍历
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}
后序遍历
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
三、迭代法实现前中后序遍历
- 前序遍历
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
class Solution {
public:
// 因为要返回前序遍历之后的数组,就是把元素放进数组里,vector是C++里的一种数组,也可以用普通的数组;把二叉树传进来,只需传入根节点即可
vector<int> preorderTraversal(TreeNode* root) {
// 定义一个栈st 栈里存放的元素是二叉树中的节点node,node是一个结构体
stack<TreeNode*> st;
// 定义数组result存放遍历的结果
vector<int> result;
if (root == NULL) return result;
// 根节点入栈
st.push(root);
// 只要栈不为空,循环处理栈
while (!st.empty()) {
// 把根节点弹出栈
// 先获取元素
TreeNode* node = st.top(); // 中
// 弹出
st.pop();
result.push_back(node->val);
if (node->right) st.push(node->right); // 先放节点的右孩子(空节点不入栈)
if (node->left) st.push(node->left); // 再放节点的左孩子(空节点不入栈)
}
return result;
}
};
- 后序遍历
后序遍历是左右中,只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:
代码如下:
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
if (node->right) st.push(node->right); // 空节点不入栈
}
reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
return result;
}
};
- 中序遍历:
左中右,不能直接在前序遍历基础上改;
因为前序遍历中,遍历的节点和要处理的节点是一个顺序,所以才能写出以上较为简洁的代码。
虽然后序遍历 遍历节点和处理节点也不同,但是可以通过改变左右节点的入栈顺序,以及反转reverse前序遍历得到的result来获得后序遍历结果。而在中序遍历中,则没有办法以类似的方式获得。【中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的】
思路:
因为遍历节点和处理节点不同,所以要定义一个指针来帮助遍历二叉树,如果是要处理的元素,就把它加入到数组里。遍历过程中,要用栈记录遍历过的顺序,因为处理元素的时候其实是按照遍历的顺序逆序输出的。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
// 指针用于遍历二叉树,初始为根节点
TreeNode* cur = root;
// 进入处理二叉树的逻辑
// while循环终止条件:当我们当前遍历的指针为空,或者 栈为空,遍历终止
// 中间是 或 的逻辑
while (cur != NULL || !st.empty()) {
// 如果当前节点不为空,就把当前访问的元素加入到栈里
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
// 指针继续往左走,一路向左,直到遇到空节点
cur = cur->left; // 左
} else {
// 因为栈记录的是访问过的节点,那么从栈顶弹出的元素就是最近访问过的元素,将其弹出,即为要处理的第一个元素,处理元素就是把此元素加入到遍历的数组里 result.push_back(cur->val);
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
// 接着访问当前指针的右孩子,如果右孩子不为空,就将其加入到栈,然后继续遍历它的左孩子,如果左孩子为空,则弹出此元素(栈顶元素),然后继续看它的右孩子,如果右孩子为空,则继续走else的逻辑,继续从栈里弹出元素,加入到数组里。
cur = cur->right; // 右
}
}
return result;
}
};
统一迭代法统一迭代法的写法, 如果学有余力,可以掌握一下
参考链接:
作者:力扣官方题解
链接:
https://leetcode.cn/problems/binary-tree-preorder-traversal/solutions/461821/er-cha-shu-de-qian-xu-bian-li-by-leetcode-solution/
https://leetcode.cn/problems/binary-tree-postorder-traversal/solutions/431066/er-cha-shu-de-hou-xu-bian-li-by-leetcode-solution/
https://leetcode.cn/problems/binary-tree-inorder-traversal/solutions/412886/er-cha-shu-de-zhong-xu-bian-li-by-leetcode-solutio/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。