代码随想录二叉树part01|二叉树的理论基础、递归遍历、迭代遍历、统一迭代
理论基础
种类
:
- 满二叉树,节点数量: 2 n − 1 2^n-1 2n−1
- 完全二叉树,从左到右是满的
- 二叉搜索树:左子树的所有节点都小于中间节点的值,右子树的所有节点都大于中间节点的值。(对树的结构没有要求,只要求节点的值)
- 平衡二叉搜索树:左子树和右子树高度的绝对值的差不超过1【map、set等树都是】
存储方式
:
- 链式存储
链表指向它的左右孩子 - 线式存储
下标设定一个数,作为数组的索引
给你一个节点的下标,找它左右孩子的下标: 2 i + 1 、 2 i + 2 2i+1、2i+2 2i+1、2i+2
二叉树的遍历
:
与图论中的两种遍历方式一致:深度优先搜索,广度优先搜索
- 深度优先:前、中、后序遍历
前序:根左右、后序:左右根、中序:左根右 - 广度优先:层序遍历
- 迭代法遍历二叉树:非递归的方法;上述两种遍历方式都存在递归形式和非递归形式
二叉树定义方式
:
struct TredNode{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
}
class TreeNode:
def __init__(self, val, left = None, right = None):
self.val = val
self.left = left
self.right = right
递归遍历
递归的三个要素:
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
递归的底层实现机制——栈
前序遍历 根左右
void traversal(TreeNode* cur, vector<int>& vec){
if(cur == NULL) return;
vec.push(cur->val); //根
traversal(cur->left, vec); //左
traversal(cur->right, vec);//右
}
中序遍历 左根右
void traversal(TreeNode* cur, vector<int>& vec){
if(cur == NULL) return;
traversal(cur->left, vec); //左
vec.push(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(cur->val); //根
}
LeetCode144 二叉树的前序遍历
==注意:==在写完dfs函数之后,不要忘记调用它,dfs(root)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
result = []
def dfs(node):
if node is None:
return
result.append(node.val)
dfs(node.left)
dfs(node.right)
dfs(root)
return result
LeetCode 145 后序遍历
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
def dfs(node):
if node is None:
return
dfs(node.left)
dfs(node.right)
res.append(node.val)
dfs(root)
return res
LeetCode 94 中序遍历
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
def dfs(node):
if node is None:
return
dfs(node.left)
res.append(node.val)
dfs(node.right)
dfs(root)
return res
迭代遍历【面试考察】
理论上,所有递归都可以用栈来模拟。
前序遍历:根左右
将根节点加入栈,弹出根节点,
将根节点的右孩子节点、左孩子节点放入栈注意顺序
分别弹出,弹出时,将其右孩子、左孩子加入栈
vector<int> function(TreeNode* root){
stack<node> st;
vector<int> vec;
st.push(root);
while(st.empty()){
node = st.top()
st.pop()
if(node != NULL) vec.push(node->val);
else continue;
if(node->right) st.push(node->right); //先放入右孩子
if(node->left) st.push(node->left); //再放入左孩子
}
return vec;
}
后序遍历:左右根
在前序遍历的基础上,交换左右孩子放入栈的顺序(根右左),最后反转数组(左右根)
反转数组,双指针法或者直接调用函数
vector<int> function(TreeNode* root){
stack<node> st;
vector<int> vec;
st.push(root);
while(st.empty()){
node = st.top()
st.pop()
if(node != NULL) vec.push(node->val);
else continue;
if(node->left) st.push(node->left); //再放入右孩子
if(node->right) st.push(node->right); //先放入左孩子
}
reverse(vec.begin(), vec.end());
return vec;
}
中序遍历:左根右 【不同】
中序遍历的与众不同之处,先访问的元素是根节点,但是先处理的元素应该是最左边的元素。
一路向左到叶子节点,指针为空,从栈中取元素并处理,看右孩子,存在则加入栈。
(用栈记录遍历过的节点)
vector<int> inorderTraversal(TreeNode* root){
vector<int> res;
stack<TreeNode*> st;
TreeNode* cur = root;
while(cur != NULL || !st.empty()){
if(cur != NULL){ // 一路向左
st.push(cur); // 左
cur = cur->left;
}else{ // 遇到空节点
cur = st.top();
st.pop();
res.push(cur->val); // 中
cur = cur->right; // 右
}
}
return res;
}
统一迭代法
以中序遍历为例,使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
中序遍历代码——左根右
class Solution {
public:
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;
}
};
前序遍历代码——根左右
相比中序遍历,只改变了两行代码的顺序
class Solution {
public:
vector<int> preorderTraversal(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); // 右
if (node->left) st.push(node->left); // 左
st.push(node); // 根
st.push(NULL);
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
后序遍历代码——左右根
class Solution {
public:
vector<int> postorderTraversal(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();
st.push(node); // 根
st.push(NULL);
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};