理论基础
需要了解 二叉树的种类,存储方式,遍历方式 以及二叉树的定义
文章讲解:代码随想录
二叉树有两种主要的形式:满二叉树和完全二叉树。
优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
二叉搜索树要保证左根右的大小关系。
平衡二叉树是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
遍历方法——
- 深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
前中后序指的就是中间节点的遍历顺序。
深度优先用栈,广度优先用队列。
递归遍历 (必须掌握)
二叉树的三种递归遍历掌握其规律后,其实很简单
题目链接/文章讲解/视频讲解:代码随想录
首先明确递归三要素:
-
确定递归函数的参数和返回值。
-
确定终止条件。
-
确定单层递归的逻辑。
用递归来实现遍历,逻辑上来讲比较好理解,这里只举中序遍历:
class Solution {
public:
void traversal(TreeNode* cur,vector<int> & res){
if(cur==NULL) return ;
traversal(cur->left,res);
res.push_back(cur->val);
traversal(cur->right,res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
traversal(root,res);
return res;
}
};
迭代遍历 (基础不好的录友,迭代法可以放过)
题目链接/文章讲解/视频讲解:代码随想录
前序遍历——
精髓在于访问到哪个节点,就要处理哪个节点,于是就比较好写了,就是先将get到要访问的元素(直接就是栈顶元素),直接处理,然后再将右左子节点分别压入栈中,注意顺序不能反(因为先入后出的属性),这样下个要处理的元素就会是当前访问到的元素的左子节点,没有的话就是右子节点,再没有的话,就是它的兄弟节点。
vector<int> preorderTraversal(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->right) st.push(node->right); // 右(空节点不入栈)
if (node->left) st.push(node->left); // 左(空节点不入栈)
}
return result;
}
中序遍历——
实际上是和前序遍历有很大不同的,因为中序遍历意味着不能即时处理当前访问的元素,而是要先将其压入栈中,这就有些不同了。要先判断当前访问节点是否为空,如果不为空,就先压入栈中,试图访问它的左子节点,如果为空,就证明它的上一个节点(也就是当前栈顶元素)没有左子节点,那就可以直接退回到上个节点,并处理那个节点了,处理之后,就要试图访问它的右子节点,也就开启了新一轮循环,这时候,如果右子节点存在,那就压入栈中,再试图访问它的左子节点,若不存在,此时栈顶元素是当前访问节点的父节点的父节点,循环也依旧成立。
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
后序遍历——
后序遍历其实是在前序遍历的基础上改动而成的,就是直接将前序遍历中右左子结点压入栈的顺序改成了左右子节点压入栈的顺序,这样处理的顺序就变成了根右左,正好和左右根是对称的,直接将数组反过来就行了。至于为什么不直接进行左右根的处理顺序,我尝试了直接按照这个顺序处理,但是没想到对的方式,关键是因为如果当前节点不为空的话,其实是不知道究竟要访问它的左子节点还是右子节点的,毕竟处理过后没有留下标志,这样就行不通了,所以只能间接。
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;
}
统一迭代 (基础不好的录友,迭代法可以放过)
这是统一迭代法的写法, 如果学有余力,可以掌握一下
题目链接/文章讲解:代码随想录
本质上,是用后面放一个NULL,来证明访问到的节点已经被找过它的左右子节点了,这样遍历完就会保证所有节点后面都一定有一个NULL,这样首先就可以保证一定是遍历了所有元素。
其次通过改变在栈中存储该节点的左右子节点和自己本身的顺序,就可以保证遍历的顺序(提示一点:栈是先进后出,顺序要反过来)。
如果遇到前面没有NULL的节点,就说明,该节点的左右子节点没有被存储进去,这样就要先将该节点弹出,再按照遍历的反顺序存储该节点及其左右子节点;如果遇到前面有NULL的,就说明找过了,这样直接存入数组就行。
循环的条件是栈不空,每次要取栈顶元素来处理。
中序遍历——
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}
只要替换这一部分就可以写出前序遍历和后序遍历——
//前序遍历:
if (node != NULL) {
st.pop();
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
st.push(node); // 中
st.push(NULL);
}
//中序遍历:
if (node != NULL) {
st.pop();
if (node->right) st.push(node->right);
st.push(node);
st.push(NULL);
if (node->left) st.push(node->left);
}
//后序遍历:
if (node != NULL) {
st.pop();
st.push(node); // 中
st.push(NULL);
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
}