代码随想录算法训练营第十四天|二叉树理论基础、递归遍历、迭代遍历、统一迭代
- ● 二叉树理论基础
- ● 1.基础理论
- (1)概念
- (2)性质
- ● 二叉树的分类
- (1)满二叉树
- (2)完全二叉树
- (3)二叉查找树(Binary Search Tree)
- (4)平衡二叉搜索树
- ● 二叉树的存储形式
- ● 二叉树的遍历
- 深度优先遍历
- 广度优先遍历
- ● 二叉树的定义
- ● 递归遍历
- ● 解题思路
- ● 注意
- ● 代码实现
- ● 迭代遍历/非递归遍历
- ● 解题思路
- ● 代码实现
- ● 统一迭代
- ● 解题思路
- ● 代码实现
● 二叉树理论基础
视频讲解:代码随想录|二叉树理论基础
● 1.基础理论
(1)概念
结点: 构成复杂数据结构的基本组成单位。
树: 是n(n >= 0)
个结点的有限集,当n == 0
时成为空树。对于任意非空树都满足:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、…、Tn,其中每一个集合本身又是一棵树,并且称为根的子树。
结点的度: 结点拥有子树数目成为结点的度,对于二叉树结点的度只有0, 1, 2
。
结点关系: 像B,C这样作为结点A的左孩子或者右孩子的结点称为孩子结点
,相应的A称为父亲结点
,而对于B, C(有共同的父亲结点)又互为对方的兄弟结点
。
树的度: 树中结点最大的度成为树的度。
层次: 根节点的层次为1,根的子节点层次为2,依次往下。
树的高度或深度: 树中结点的最大层次。
森林: 多棵互不相交的树的集合称为森林。
(2)性质
树的性质:
(1)树的节点数 = 所有结点度数 + 1;
(2)度为m的树第i曾最多有mi-1(i >= 1)个结点;
(3)高度为h的m茶树最多拥有(mh-1) / (m - 1)个结点;
(4)n个结点的m叉树最小高度为[ logm(n(m - 1) + 1)]。
● 二叉树的分类
在解题过程中二叉树有两种主要形式:满二叉树和完全二叉树。
(1)满二叉树
高度为h,并且由2{h} –1个结点的二叉树,被称为满二叉树。也就是说如果一个二叉树只有度为0的结点(叶子结点)和度为2的结点(分支结点),并且度为0的结点在同一层。
对于满二叉树而言,除了叶子结点,其余结点均达到最大子结点个数。
(2)完全二叉树
一颗二叉树中,只有最下面两层节点的度可以小于2,并且最下层的叶节点集中在靠左的若干位置上。
简单理解就是,在完全二叉树的基础上,除了最底层结点可能没有填满外,其余每层结点数都达到最大值。
对于完全二叉树,是在满二叉树的基础上,在最后一层从右向左依次缺少n(n >= 0)个元素,因此满二叉树是特殊的完全二叉树。
(3)二叉查找树(Binary Search Tree)
二叉查找树是一个有序树 ,每一个二叉查找树都满足:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)二叉查找树的左、右子树也分别为二叉排序树。
(4)平衡二叉搜索树
平衡二叉搜索树又称为AVL树,得名于它的发明者 G. M. Adelson-Velsky 和 Evgenii Landis,他们在1962年的论文《An algorithm for the organization of information》中公开了这一数据结构。AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树 。查找、插入和删除在平均和最坏情况下的时间复杂度都是O(logn)
,增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。
![平衡二叉搜索树![](https://img-blog.csdnimg.cn/direct/4f70b79d6d8a44f79036b297221ef168.png)
C++中map、set、multimap,multiset
的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn。
● 二叉树的存储形式
二叉树的存储形式包括链式存储和顺序存储。
我们常见的拥有左孩子指针和右孩子指针的形式就是链式存储 ,链式存储通过指针把分布在各个地址的节点串联一起;
而顺序存储 则依靠父亲结点和孩子结点之间所存在的数学关系,将其使用数组的方式存储在连续内存中
假设父亲结点序号为n,则左孩子结点序号为2n + 1,右孩子结点序号为2n + 2.
● 二叉树的遍历
二叉树主要有两种遍历方式:深度优先遍历和广度优先遍历。
深度优先遍历
从名字看,深度优先遍历就是先往深处走,当某一条路径走到头的时候,再返回上一个结点走另一条路径。
深度优先遍历包括前序遍历、中序遍历和后序遍历
,均可以使用迭代法和递归法实现;对于前序遍历、中序遍历和后序遍历:
前序遍历:中左右
中序遍历:左中右
后序遍历:左右中
我们只需要知道前中后指的是根(root)
的遍历顺序就很好理解。
广度优先遍历
广度优先遍历就是对树一层一层地进行遍历,包括了层次遍历。
● 二叉树的定义
链式存储
struct TreeNode{int val;TreeNode *left, *right;TreeNode(int x) : val(x), left(nullptr), right(nullptr){}
};
● 递归遍历
题目链接:
递归遍历|二叉树的前序遍历
递归遍历|二叉树的中序遍历
递归遍历|二叉树的后序遍历
视频讲解:代码随想录|递归遍历
● 解题思路
我们需要清楚递归算法的三要素,每次写递归的时候严格按照顺序做:
(1)确定递归函数的参数和返回值: 确定递归过程需要对哪些参数进行处理,就在递归函数传入该参数,并且还需要知道每次递归的返回值是什么;
(2)确定终止条件: 递归算法运行进场出现栈溢出,就是没写终止条件或者终止条件不正确。因为操作系统使用栈保存每一层的递归信息,如果递归没有种植,操作系统的内存栈必然溢出;
(3)确定单层递归逻辑: 就是需要清楚每一层递归对信息执行操作的顺序。
● 注意
对于traversal()
函数的传参需要注意:
在给函数传递参数时,如果不使用引用,那么参数会被传递给函数的副本。在这种情况下,vector参数vec会被复制到函数traversal的每个递归调用中。
由于vector对象在内部维护了动态数组,而数组的复制可能是一个昂贵的操作,因此在递归函数中多次复制vector对象可能会导致性能下降。
为了避免复制vector对象,使用引用是一个更好的选择。通过将vector&作为参数类型,你将在函数调用中传递vector对象的引用,而不是复制它的内容。这样,所有的递归调用都将使用同一个vector对象,避免了不必要的复制。
如果你不使用引用,而是将vector作为参数类型,你需要修改traversal函数的定义和递归调用,以便正确地传递和使用vector对象。
● 代码实现
二叉树的前序遍历
/*** 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:void traversal(TreeNode* cur, vector<int>& vec){if(cur == nullptr) return;vec.push_back(cur->val);traversal(cur->left, vec);traversal(cur->right, vec);}vector<int> preorderTraversal(TreeNode* root) {vector<int> res;traversal(root, res);return res;}
};
二叉树的中序遍历
/*** 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:void traversal(TreeNode* cur, vector<int>& vec){if(cur == nullptr) return;traversal(cur->left, vec);vec.push_back(cur->val);traversal(cur->right, vec);}vector<int> inorderTraversal(TreeNode* root) {vector<int> res;traversal(root, res);return res;}
};
二叉树的后序遍历
/*** 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:void traversal(TreeNode* cur, vector<int>& vec){if(cur == nullptr) return;traversal(cur->left, vec);traversal(cur->right, vec);vec.push_back(cur->val);}vector<int> postorderTraversal(TreeNode* root) {vector<int> res;traversal(root, res);return res;}
};
● 迭代遍历/非递归遍历
● 解题思路
在迭代过程中一共有两个操作:
处理: 将元素放进result数组中
访问: 遍历节点
二叉树的前序遍历
对于递归,其底层调用栈维护访问信息,因此我们在迭代实现的时候也需要借助栈完成。
二叉树前序遍历为根左右,我们先对根节点进行处理,也就是需要先将根节点的值放入返回容器中;因为栈是先进后出的数据结构,访问的时候需要先放入右孩子,再放入左孩子,才能让左孩子为栈顶元素,对其先进行处理,随后再对右孩子访问。
二叉树的后序遍历
后序遍历为左右根,结合调整前序遍历访问节点的顺序我们可以得到根右左,此时可以得到逆序的后序遍历结果,因此我们仍需要对其进行一步逆序操作得到正确顺序的后序遍历。
二叉树的中序遍历
中序遍历无法直接调整前序遍历/后序遍历访问结点的代码达到正确的输出效果,需要我们使用一个cur
遍历结点进行输出。
我们一路向左遍历完树的结点之后,此时cur
为nullptr
,中序遍历的顺序为左根右,此时我们可以对子树根节点进行处理,再将cur
往回倒一个结点查看其右孩子是否为空,循环往复。
● 代码实现
二叉树的前序遍历
/**
/*** 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> res;stack<TreeNode*> st;if(!root) return res;st.push(root);while(!st.empty()){//处理结点TreeNode* node = st.top();st.pop();res.push_back(node->val);//中//遍历结点if(node->right) st.push(node->right);//右;因为stack先进后出,所以需要先将right放入,才能先处理leftif(node->left) st.push(node->left);//左}return res;}
};
二叉树的后序遍历
/*** 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> postorderTraversal(TreeNode* root) {stack<TreeNode*> st;vector<int> res;if(root == nullptr) return res;st.push(root);while(!st.empty()){TreeNode* node = st.top();st.pop();res.push_back(node->val);if(node->left) st.push(node->left);if(node->right) st.push(node->right);}reverse(res.begin(), res.end());return res;}
};
二叉树的中序遍历
/*** 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> inorderTraversal(TreeNode* root) {vector<int> res;stack<TreeNode*> st;TreeNode* cur = root;while(cur || !st.empty() ){if(cur){st.push(cur);cur = cur->left;}else{cur = st.top();st.pop();res.push_back(cur->val); cur = cur->right;}}return res;}
};
● 统一迭代
● 解题思路
因为使用栈进行迭代遍历的时候无法同时解决访问结点和处理节点不一致的情况,因此我们需要将访问结点放入栈中,处理结点也放入栈中但需要标记,我们如何对处理结点进行标记呢?再处理节点放入栈之后,放入一个空指针作为标记即可。
● 代码实现
二叉树的前序遍历
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> 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> 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;}
};