二叉树的基本概念
二叉树是由左右子树构成的树,左右子树都可以为空,但是对于每个节点来说最多只有两个子树(也就是两个孩子节点)。
二叉树的三种遍历方式以及对应的三种算法去解决
三种遍历来源:三种遍历都是根据根节点遍历的先后顺序而命名的,对每个节点来说先访问他本身,再访问左子树,然后是右子树就是前序遍历。先访问他的左子树再访问它本身,再到它的右子树就是中序遍历。先访问它的左子树再访问它的右子树是后序遍历。
可能这里有的小伙伴会问为什么没有其他遍历了,按自由组合定律不应该有九种吗?其实先访问右子树和先访问左子树在本质上是一样的.为了符合人们的习惯,所以先访问左子树。
三种遍历的作用:是所有对二叉树结构操作的基础。
1.前序遍历
1.递归
思路和算法
对于每个节点来说先访问他本身,再访问左子树,然后是右子树就是前序遍历,这访问得公式,符合递推公式所以可以用递推函数,当是要用到递归函数,就便须要有回退的情况。所以这里的回退情况为节点所指向为空就回退。以下是具体实现。
这里假设树的结构为以下结构体
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode():val(0),left(NULL),right(NULL);//无参构造函数
TreeNode(int x):val(x),left(NULL),right(NULL);//初始化节点的值
TreeNode(int x,TreeNode* left,TreeNode* right)
:val(x),left(left),right(right){};//初始化节点和指针的值
}
在这里插入代码片
viod preorder(TreeNode* root,vector<int> p)
{
if(root==NULL)
{
return ;
}
else
{
p.push_back(root->val);
preordered(root->left);
preordered(root->right);
}
}
2.迭代
思路和算法
其实迭代和递推类似只不过这里用到自己的维护栈,其原理和递归函数一样。
在这里插入代码片
vector<int> preorder(TreeNode* root,vector<int> p)
{
if(root==NULL) return p;
stack<int> stk;
stk.push(root);//根节点入栈
while(!stk.empty())
{
while(root!=NULL)
{
p,push_back(root->val);//遍历每个节点
root=root->left;
}
TreeNode* T=stk.top();//栈顶元素出栈
stk.pop();//当节点为空时出栈
root=T->right;//遍历右子树
}
return p;
}
3.morris算法
思路和算法
Morris 遍历算法是另一种遍历二叉树的方法,它能将非递归的中序遍历空间复杂度降为 O(1)
**1.**如果 xx 无左孩子,先将 xx 的值加入答案数组,再访问 xx 的右孩子,即 x=x.right。
**2.**如果 xx 有左孩子,则找到 xx 左子树上最右的节点(即左子树中序遍历的最后一个节点,xx 在中序遍历中的前驱节点),我们记为 predecessor。根据 predecessor 的右孩子是否为空,进行如下操作。
1.如果 predecessor 的右孩子为空,则将其右孩子指向 xx,然后访问 xx 的左孩子,即 x=x.left。
2.如果 predecessor 的右孩子不为空,则此时其右孩子指向 xx,说明我们已经遍历完 xx 的左子树,我们将 predecessor 的右孩子置空,将 xx 的值加入答案数组,然后访问 xx 的右孩子,即 x=x.right。
3.重复上述操作,直至访问完整棵。
算法出现原因:这个算法最主要的添加前驱指针的指向,主要是为了回退。它跟递归比较像,递归储存访问过的节点。通过出栈,就可以逻辑上进行回退。但是这样会消耗空间。所以为了使空间复杂度为O(1),他不用栈去模拟回退。而是用前驱结点来代替。就像你走一条路。已经没有退路。但前驱指针。就是帮助你找退路。所以这就是这道算法的精妙之处,也是它与递推函数及其迭代法的不同之处。
简单描述算法:抓住的关系是,中序遍历。在前序遍历中运用到了中序遍历的相类似的一条性质。就是相对一个节点的左子树来说。就是他全部遍历左子树完他才遍历结点本身。而它的左子数的最右边节点。就是最后访问的,所以若让最后访问的这个结点去连接该结点本身,那么这个就可以有返回路线。每个结点都是这样建立前驱结点。可能你还有点懵,没事看一下代码就懂了。
在这里插入代码片
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
TreeNode *predecessor = nullptr;
while (root != nullptr) {
if (root->left != nullptr) {
// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止
predecessor = root->left;
while (predecessor->right != nullptr && predecessor->right != root) {
predecessor = predecessor->right;
}
// 让 predecessor 的右指针指向 root,继续遍历左子树
if (predecessor->right == nullptr) {
predecessor->right = root;
root = root->left;
}
// 说明左子树已经访问完了,我们需要断开链接
else {
res.push_back(root->val);
predecessor->right = nullptr;
root = root->right;
}
}
// 如果没有左孩子,则直接访问右孩子
else {
res.push_back(root->val);
root = root->right;
}
}
return res;
}
};
2.中序遍历
1.递归
思路和算法
按照访问左子树——根节点——右子树的方式遍历这棵树,而在访问左子树或者右子树的时候,我们按照同样的方式遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质,我们可以直接用递归函数来模拟这一过程。
思路
在这里插入代码片
viod preorder(TreeNode* root,vector<int> p)
{
if(root==NULL)
{
return ;
}
else
{
preordered(root->left);
p.push_back(root->val);
preordered(root->right);
}
}
2.迭代
思路和算法
其实迭代和递推类似只不过这里用到自己的维护栈,其原理和递归函数一样。
在这里插入代码片
vector<int> preorder(TreeNode* root,vector<int> p)
{
if(root==NULL) return p;
stack<int> stk;
stk.push(root);//根节点入栈
while(!stk.empty())
{
while(root!=NULL)
{//遍历每个节点
root=root->left;
}
TreeNode* T=stk.top();//栈顶元素出栈
p.push_back(T->val);//遍历
stk.pop();//当节点为空时出栈
root=T->right;//遍历右子树
}
return p;
}
3.morris算法
思路和算法
Morris 遍历算法是另一种遍历二叉树的方法,它能将非递归的中序遍历空间复杂度降为 O(1)。
Morris 遍历算法整体步骤如下(假设当前遍历到的节点为 x):
如果 x 无左孩子,先将 x 的值加入答案数组,再访问 x 的右孩子,即 x=x.right。
如果 x 有左孩子,则找到 x 左子树上最右的节点(即左子树中序遍历的最后一个节点,x 在中序遍历中的前驱节点),我们记为predecessor。根据predecessor 的右孩子是否为空,进行如下操作。
如果 predecessor 的右孩子为空,则将其右孩子指向 x,然后访问 xxx 的左孩子,即 x=x.leftx 。
如果 predecessor 的右孩子不为空,则此时其右孩子指向 x,说明我们已经遍历完 xxx 的左子树,我们将 predecessor\textit{predecessor}predecessor 的右孩子置空,将 x 的值加入答案数组,然后访问 xxx 的右孩子,即 x=x.right。
重复上述操作,直至访问完整棵树。
简单概过:就是建立回路,有回路就可以保证可以遍历完整个二叉树。而morris算法就是这作用,这个建立回路的原则就是利用中序遍历的性质(对于一个节点来说,遍历完它的左子树的的最后一个元素(它的左子树的最右边节点)就到它)。可以这里有的小伙伴会问,为什么不栈进行记忆保存。因为栈需要空间,而morris空间复杂度为O(1),它只是对指针的指向进行改变。
在这里插入代码片
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
TreeNode *predecessor = nullptr;
while (root != nullptr) {
if (root->left != nullptr) {
// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止
predecessor = root->left;
while (predecessor->right != nullptr && predecessor->right != root) {
predecessor = predecessor->right;
}
// 让 predecessor 的右指针指向 root,继续遍历左子树
if (predecessor->right == nullptr) {
predecessor->right = root;
root = root->left;
}
// 说明左子树已经访问完了,我们需要断开链接
else {
res.push_back(root->val);
predecessor->right = nullptr;
root = root->right;
}
}
// 如果没有左孩子,则直接访问右孩子
else {
res.push_back(root->val);
root = root->right;
}
}
return res;
}
};
3.后序遍历
1.递归
思路和算法
后序遍历:按照访问左子树——右子树——根节点的方式遍历这棵树,而在访问左子树或者右子树的时候,我们按照同样的方式遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质,我们可以直接用递归函数来模拟这一过程。
在这里插入代码片
viod preorder(TreeNode* root,vector<int> p)
{
if(root==NULL)
{
return ;
}
else
{
preordered(root->left);
preordered(root->right);
p.push_back(root->val);
}
}
2.迭代
思路和算法
和前面两种算法类似这里就不做过多讲解直接上代码
在这里插入代码片
class Solution {
public:
vector<int> postorderTraversal(TreeNode *root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode *> stk;
TreeNode *prev = nullptr;
while (root != nullptr || !stk.empty()) {
while (root != nullptr) {
stk.emplace(root);
root = root->left;
}
root = stk.top();
stk.pop();
if (root->right == nullptr || root->right == prev) {
res.emplace_back(root->val);
prev = root;
root = nullptr;
} else {
stk.emplace(root);
root = root->right;
}
}
return res;
}
};
3.morris算法
思路和算法
新建临时节点,令该节点为 root;
如果当前节点的左子节点为空,则遍历当前节点的右子节点;
如果当前节点的左子节点不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点;
如果前驱节点的右子节点为空,将前驱节点的右子节点设置为当前节点,当前节点更新为当前节点的左子节点。
如果前驱节点的右子节点为当前节点,将它的右子节点重新设为空。倒序输出从当前节点的左子节点到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右子节点。
重复步骤 2 和步骤 3,直到遍历结束。
这样我们利用 Morris 遍历的方法,后序遍历该二叉搜索树,即可实现线性时间与常数空间的遍历。
这里同学们可以自己试写一下代码。这样更容易掌握。
以上算法小伙伴们可以思考一下各个算法复杂度,及优缺点。若有问题可以留言评论区,我看到一定会及时回复。
下周我会细节讲以下内容,请小伙伴们持续关注!
三种算法本质区别以及优缺点;递归算法的优点和缺点;迭代算法的优点和缺点;morris算法的优点和缺点;二叉树在解决实际问题中的运用;其他数据结构和二叉树的联系。