代码随想录day13-二叉树(1)
二叉树的基本知识
二叉树的基本性质
经过前人的总结,二叉树具有以下几个性质:
- 二叉树中,第i层最多有 2 i − 1 2^{i-1} 2i−1个结点。
- 如果二叉树的深度为k,那么此二叉树最多有 2 k − 1 2^{k}-1 2k−1个结点。
- 二叉树中,终端结点数(叶子结点数)为 n 0 n_0 n0),度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1。
完全二叉树和满二叉树
满二叉树以及完全二叉树的定义以及区别。
满二叉树一定是完全二叉树,反之则不一定。
满二叉树示意图:
完全二叉树示意图:
二叉树的存储方式
顺序表的方式存储
二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。需要注意的是顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。堆就是典型的使用顺序表存储的一种完全二叉树。
完全二叉树的顺序存储,仅需从根节点开始,按照层次依次将树中节点存储到数组即可。
如上面所示的完全二叉树,存储状态如图所示:
不仅如此,从顺序表中还原完全叉树也很简单。我们知道,完全二叉树具有这样的性质,将树中节点按照层次并从左到右依次标号(1,2,3…)若节点i有左右孩子,则其左孩子节点为2*i,右孩子节点为2*i+1。比性质可用于还原数组中存储的完全二叉树。(注意,这里的i是从1开始的,如果是数组的下标,左孩子结点为2*i+1,右孩子结点为2*i+2)。
链表的方式存储
由于使用顺序表存储二叉树只能存储完全二叉树,存储其他的树需要先转换成完全二叉树,比较麻烦,所以,常用的存储二叉树的方式为链表的方式进行存储。如图所示:
表示一个结点的代码为:
struct TreeNode{
int val; // 结点的数值
TreeNode* left; // 左孩子结点
TreeNode* right; // 右孩子结点
TreeNode() {};
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}; // 初始化列表的构造函数
};
二叉树的遍历方式
二叉树的遍历方式主要有两种:
- 深度优先遍历:先往深走,遇到叶子节点再往回走;
- 广度优先遍历:一层一层的去遍历。
深度优先遍历:
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
广度优先遍历
- 层序遍历(递归法,迭代法)
这个前中后,其实就是指的是中间结点的遍历顺序。
递归
实现递归需要注意以下几个步骤:
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
这也是递归最重要的几步,此外,根据一些题目,我对递归做了一点点自己的理解,主要是方便自己理解,在之后进行补充。
1、LeetCode 144 二叉树的前序遍历
题目分析:
对于二叉树的遍历既可以使用递归的方式,也可以采用非递归,使用栈或者队列的方式。
递归法: 注意递归的三要素
class Solution {
public:
void traversal(TreeNode* node, vector<int>& vec) {
if (!node) return; // 递归的中止条件
// 每一层递归需要执行的逻辑
vec.emplace_back(node->val); // 先保存当前结点的值
traversal(node->left, vec); // 再遍历左树
traversal(node->right, vec); // 最后遍历右树
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
traversal(root, ans);
return ans;
}
};
非递归法:
class Solution {
public:
// 使用非递归的(迭代)的方法进行前序遍历,注意栈的操作是先弹出再存放新的
vector<int> preorderTraversal(TreeNode* root) {
if (root == nullptr) return {};
vector<int> ans;
stack<TreeNode*> stk;
stk.push(root); // 先将根结点压进去
while (!stk.empty()) { // 直至栈为空,表示遍历结束
TreeNode* node = stk.top();
ans.emplace_back(stk.top()->val);
stk.pop();
// 由于栈是先进后出,所以先将右子树压进栈
if (node->right != nullptr) stk.push(node->right);
if (node->left != nullptr) stk.push(node->left);
}
return ans;
}
};
注意由于栈是先进后出的,所以要先压右结点,再压左结点。
2、LeetCode 145 二叉树的后序遍历
递归法:
class Solution {
public:
void traversal(TreeNode* node, vector<int>& vec) {
if (!node) return; // 递归的中止条件
// 每一层递归需要执行的逻辑
traversal(node->left, vec); // 先遍历左树
vec.emplace_back(node->val); // 再保存当前结点的值
traversal(node->right, vec); // 最后遍历右树
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
traversal(root, ans);
return ans;
}
};
非递归法:
class Solution {
public:
// 使用非递归的(迭代)的方法进行前序遍历,注意栈的操作是先弹出再存放新的
vector<int> postorderTraversal(TreeNode* root) {
if (root == nullptr) return {};
vector<int> ans;
stack<TreeNode*> stk;
stk.push(root); // 先将根结点压进去
while (!stk.empty()) { // 直至栈为空,表示遍历结束
TreeNode* node = stk.top();
ans.emplace_back(stk.top()->val);
stk.pop();
// 注意这里是左子树先进去
if (node->left != nullptr) stk.push(node->left);
if (node->right != nullptr) stk.push(node->right);
}
// 需要将ans反转
reverse(ans.begin(), ans.end());
return ans;
}
};
3、LeetCode 94 二叉树的中序遍历
递归法:
class Solution {
public:
void traverse(TreeNode* node, vector<int>& ans) {
if (node == nullptr) return;
traverse(node->left, ans); // 左
ans.emplace_back(node->val); // 中
traverse(node->right, ans); // 右
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
traverse(root, ans);
return ans;
}
};
迭代法:注意中序遍历的迭代法和前序遍历以及后序遍历的迭代法的不同之处
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
if (root == nullptr) return {};
// 使用迭代法来实现
vector<int> ans;
stack<TreeNode*> stk;
TreeNode* node = root;
// 注意循环中的判断条件,当stk为空的时候,此时的node不一定是空,所以得考虑两个条件
while (node || !stk.empty()) {
while (node) {
stk.push(node);
node = node->left; // 首先将所有的左节点全部入栈
}
// 此时已经到了最左边的结点了,所以肯定是最左边的结点就是第一个遍历到的
node = stk.top();
ans.emplace_back(node->val);
stk.pop();
node = node->right;
}
return ans;
}
};
注意这里的循环的判断条件是node || stk.empty()
,是这样理解的,二叉树如图所示:
最开始node=root,然后进入循环,进入大循环之后,再进入内部的循环,最终出来后node为空,然后获得了相应的值后,将栈顶元素弹出,此时node = node->right
。这个时候栈已经为空了,但是node不为空,所以是可以继续进行循环的,因为树还没有遍历完。直到node为空同时栈为空才保证了树遍历完毕。