代码随想录算法训练营第十四天 | 二叉树理论基础、递归遍历、迭代遍历、统一迭代

二叉树理论基础

要做到能写出二叉树的定义,笔试题才会写

种类

满二叉树

如果一棵二叉树只有度为 0 的节点和度为 2 的节点,并且为 0 的节点在同一层上,则这颗树为满二叉树,深度为 k,有 2 k − 1 2^k-1 2k1个节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZprGNTw-1665512002852)(assets/1665511941441-17.png)]

如图为满二叉树,深度为 4,有 15 个节点

完全二叉树

除了最底层没填满以外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置,从左到右是连续的。若最底层为第 h 层,则该层包含 1   ˜ 2 h − 1 1 \~\ 2^{h-1} 1 ˜2h1个节点

满二叉树一定是完全二叉树

在这里插入图片描述

优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系

二叉搜索树

二叉搜索树是一个有序树 ,是有数值的

  • 若左子树不为空,则左子树上所有节点的值均小于根节点的值

  • 若右子树不为空,则右子树上所有节点的值均大于根节点的值

  • 左右子树也分别为二叉搜索树

在这里插入图片描述

平衡二叉搜索树

AVM(adelson-Velsky and Landis) 树

性质:是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树

在这里插入图片描述

如图最后一棵不是平衡二叉树,因为左右两个子树的高度差绝对值超过 1

C++ 中 map、set、multimap、multiset 的底层实现都是平衡二叉搜索树 ,所以 map、set 的增删操作时间复杂度是 l o g n log n logn

unordered_map、unordered_set、unordered_map 底层实现是哈希表

💡注意理清楚自己使用的编程语言常用的容器底层是如何实现的,才能做好性能分析!

二叉树的存储方式

二叉树可以链式存储,也可以顺序存储

链式存储方式用指针,顺序存储的方式用数组

顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联在一起

下图为链式存储:

在这里插入图片描述

顺序存储就是用数组来存储二叉树,顺序存储方式如图:

在这里插入图片描述

如何遍历用数组存储的二叉树:

如果父节点的数组下标是 i,那么左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2

链式存储用得多

💡记住:用数组依然可以表示二叉树

二叉树的遍历方式

二叉树主要有两种遍历方式:

  1. 深度优先遍历:先往深走,遇到叶子节点再往回走

  2. 广度优先遍历:一层一层地去遍历

这两种遍历是图论中最基本地两种遍历方式

深度优先遍历和广度优先遍历进一步拓展有如下遍历方式:

  • 深度优先遍历

    • 前序遍历(递归法,迭代法)
    • 中序遍历(递归法,迭代法)
    • 后序遍历(递归法,迭代法)
  • 广度优先遍历

    • 层次遍历(迭代法)

深度优先遍历中的前中后,指的是中间节点的遍历顺序,即中间节点的顺序就是所谓的遍历方式

  • 前序遍历:中左右

  • 中序遍历:左中右

  • 后序遍历:左右中

在这里插入图片描述

栈是递归地一种实现结构 ,深度优先遍历可以借助栈使用非递归方式来实现

广度优先遍历一般使用队列来实现,需要先进先出的结构,才能一层一层地遍历二叉树

补充

深度优先遍历

从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路走到底,不断递归重复此过程,直到所有的顶点都遍历完成。

树是图的一种特例(连同无环的图就是树),用左图为例来深度优先遍历

在这里插入图片描述

  1. 从根节点 1 开始遍历,相邻节点有 2, 3, 4,先遍历节点 2,再遍历 2 的子节点 5,然后再遍历 5 的子节点 9

在这里插入图片描述

  1. 步骤 1 一条路走到底(9 是叶子节点,再无可遍历的节点),此时从 9 回退到上一个节点 5,看节点 5 是否有除了 9 以外的节点,没有就继续回退到 2,2 也没有再回退到 1。1 有除了 2 以外的节点 3,所以从节点 3 开始进行深度优先遍历

在这里插入图片描述

  1. 同理 10 开始往上回溯到 6,6 没有往上回溯到 3,3 有,所以遍历 7

在这里插入图片描述

  1. 7 往上回溯到 3、1,发现 1 还有节点 4 未遍历,所以沿着 4、8 进行遍历,遍历就完成了。完整的节点遍历顺序如图(蓝色数字)

在这里插入图片描述

广度优先遍历

从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再一次遍历每个相邻节点的相邻节点。

如动图所示,每个节点的值即为遍历顺序,所以广度优先遍历又叫层序遍历。先遍历第一层(节点 1),再遍历第二层(节点 2, 3, 4),第三层(5, 6, 7, 8),第四层(9, 10)

请添加图片描述

二叉树的定义

理解成是一个链表去定义,就简单得多

二叉树的存储方式分为链式存储和顺序存储。顺序存储就是用数组来存,链式存储的二叉树节点定义方式如下:

struct TreeNode
{
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL){}// 构造
}

相对于链表,二叉树的节点里多了一个指针,两个指针指向左右孩子

递归遍历(必须掌握)

递归三部曲:

  • 确定递归函数的参数和返回值

    • 确定哪些参数是递归过程中需要处理的,那么就在递归函数里加上这个参数,并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型
  • 确定终止条件

    • 运行递归算法经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写得不对。操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出
  • 确定单层递归的逻辑

    • 确定每一层递归需要处理的信息。这里会重复调用自己来实现递归的过程

前序遍历

  1. 确定参数和返回值

要打印出前序遍历节点的数值,参数里传入 vector 放节点的数值。不需要有返回值,因为都放参数里了,所以返回类型是 void。再传入一个根节点 cur

void traversal(TreeNode* cur, vector<int>& vec)
  1. 确定终止条件

当遍历的节点为空时,本层遍历结束,直接 return

if (cur == NULL) return;
  1. 确定单层递归的逻辑

前序遍历:中左右

vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); //右

完整代码

/**
 * 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 == NULL)return;
        vec.push_back(cur->val);
        traversal(cur->left, vec);
        traversal(cur->right, vec);
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        traversal(root, result);
        return result;
    }
};

中序遍历

中序遍历:左中右,同理完整代码如下

void traversal(TreeNode* cur, vector<int>& vec){
    if(cur == NULL)return;
    traversal(cur->left, vec);
    vec.push_back(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_back(cur->val);
}

迭代遍历(基础不好的录友,迭代法可以放过)

前序遍历

编程语言实现递归的逻辑,也是用,所以用栈去模拟递归的过程

请添加图片描述

如图,先把 5 放到栈里,再把 5 弹出,放到存放的数组里。

然后再把右孩子放进栈,再放左孩子。因为先进后出,先弹出来的才是左孩子,符合前序:中左右

对应的代码如下(注意代码中空节点不入栈

按前序的顺序,填满 result 这个数组

class Solution {
public:
    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;
    }
};

后序遍历

在这里插入图片描述

如图,只需要调整一下先序遍历的代码顺序,就能变成中右左的遍历顺序,然后再反转 result 数组,输出的结果顺序就是左右中了,代码如下,一共改了三行

class Solution {
public:
    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;
    }
};

中序遍历

前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点所以代码比较好处理,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点

中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是把节点的数值放到 result 数组中),这就造成了处理顺序和访问顺序是不一致的

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素

请添加图片描述

class Solution {
public:
    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;
        }
};

统一迭代(基础不好的录友,迭代法可以放过)

以中序遍历为例,之前的做法无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况

那么就将访问的节点放入栈中,把要处理的节点也放入栈中,但要做标记,即处理的节点放入栈之后,紧接着放入一个空指针作为标记,这种方法也叫标记法

中序遍历

动画中,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> 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;}
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值