算法学习记录~2023.4.28~二叉树Day1~144.二叉树的前序遍历 & 94.二叉树的中序遍历 & 145.二叉树的后序遍历 & 102.二叉树的层序遍历

算法学习记录| 2023.4.28| 二叉树Day1| 144.二叉树的前序遍历 & 94.二叉树的中序遍历 & 145.二叉树的后序遍历 & 102.二叉树的层序遍历

  • 一. 基础知识
    • 1. 二叉树的种类
    • 2. 二叉树的存储方式
    • 3. 二叉树的遍历方式
    • 4. 二叉树的定义
  • 二. 二叉树的递归遍历
    • 1. 递归三要素
    • 2. 以前序遍历的递归遍历为例的代码实现
  • 三. 二叉树的迭代遍历
    • 1. 前序遍历(后序类似)
    • 2. 中序遍历
  • 四. 迭代遍历通用写法
    • 1. 后序遍历(前序类似)
    • 2. 中序遍历
  • 五. 二叉树的层序遍历
    • 1. 思路
    • 2. 代码

一. 基础知识

1. 二叉树的种类

  1. 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。深度为k(从1开始算),有2^k-1个节点。
    算法学习记录~2023.4.28~二叉树Day1~144.二叉树的前序遍历 & 94.二叉树的中序遍历 & 145.二叉树的后序遍历 & 102.二叉树的层序遍历_第1张图片

  2. 完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
    算法学习记录~2023.4.28~二叉树Day1~144.二叉树的前序遍历 & 94.二叉树的中序遍历 & 145.二叉树的后序遍历 & 102.二叉树的层序遍历_第2张图片

  3. 二叉搜索树:有数值,是有序树

    • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
    • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
    • 它的左、右子树也分别为二叉排序树
      算法学习记录~2023.4.28~二叉树Day1~144.二叉树的前序遍历 & 94.二叉树的中序遍历 & 145.二叉树的后序遍历 & 102.二叉树的层序遍历_第3张图片
  4. 平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树。是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉搜索树,本身符合二叉搜索树
    C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树
    unordered_map、unordered_set的底层实现是哈希表
    算法学习记录~2023.4.28~二叉树Day1~144.二叉树的前序遍历 & 94.二叉树的中序遍历 & 145.二叉树的后序遍历 & 102.二叉树的层序遍历_第4张图片

2. 二叉树的存储方式

  1. 链式存储:使用指针。内存中非连续。
    算法学习记录~2023.4.28~二叉树Day1~144.二叉树的前序遍历 & 94.二叉树的中序遍历 & 145.二叉树的后序遍历 & 102.二叉树的层序遍历_第5张图片

  2. 顺序存储:使用数组。内存中连续。
    如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2
    算法学习记录~2023.4.28~二叉树Day1~144.二叉树的前序遍历 & 94.二叉树的中序遍历 & 145.二叉树的后序遍历 & 102.二叉树的层序遍历_第6张图片

3. 二叉树的遍历方式

主要有两种遍历方式:

  1. 深度优先遍历:先往深走,遇到叶子节点再往回走。
    - 实现方式:常用递归的方式;也可通过来实现,通常可以借助使用非递归的方式来实现递归,因为栈是递归的一种实现结构
    • 前序遍历(递归法,迭代法):中左右
    • 中序遍历(递归法,迭代法):左中右
    • 后序遍历(递归法,迭代法):左右中
  2. 广度优先遍历:一层一层遍历。
    - 实现方式:常用队列来实现。因为队列先进先出,所以才能一层一层遍历二叉树。
    • 层次遍历(迭代法)
      算法学习记录~2023.4.28~二叉树Day1~144.二叉树的前序遍历 & 94.二叉树的中序遍历 & 145.二叉树的后序遍历 & 102.二叉树的层序遍历_第7张图片

4. 二叉树的定义

和链表类似,但节点中多了两个指针,分别指向左子节点和右子节点

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) {}
  };

二. 二叉树的递归遍历

1. 递归三要素

  1. 确定递归函数的参数返回值:确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上该参数,并且还需明确每次递归的返回值是什么,进而确定递归函数的返回类型
  2. 确定终止条件:需要写出正确的终止条件来避免发生操作系统的内存栈溢出
  3. 确定单层递归的逻辑:确定每一层递归需要处理的信息

2. 以前序遍历的递归遍历为例的代码实现

  1. 确定递归函数的参数返回值:需要打印的只有遍历节点的数值,所以传入vector存放节点数值,没有其他数据需要处理了,而且也不需要有返回值,因此返回类型为void
void traversal (TreeNode* cur, vector<int>& vec)
  1. 确定终止条件:当当前遍历的节点为空时,说明递归结束
if (cur == NULL)
	return;
  1. 确定单层递归的逻辑:前序遍历为中左右的顺序,所以单层递归的逻辑中,要先取中节点的数值
vec.push_back(cur->val);		//中节点
traversal(cur->left, vec);		//左节点
traversal(cur->right, vec);		//右节点

完整前序遍历代码:

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);    // 中
}

三. 二叉树的迭代遍历

递归的实现:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,这就是递归可以返回上一层位置的原因。

迭代本质上是在模拟递归,因为在递归的过程中使用了系统栈,所以在迭代的解法中常用 Stack 来模拟系统栈

1. 前序遍历(后序类似)

力扣题目链接144.二叉树的前序遍历

前序遍历为中左右,所以每次先处理中节点,先把根节点入栈,然后将右子节点入栈,再左子节点。先将右子节点入栈是因为这样出栈时顺序才是和入栈时反过来的中左右。

具体思路结合下面的代码看:

class Solution {
public:

    struct TreeNode {       //定义二叉树
        int val;
        TreeNode *left;
        TreeNode *right;
        TreeNode(int val) : val(0), left(nullptr), right(nullptr) {}
    };

    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;        //用于迭代的栈
        vector<int> result;         //存储从栈中pop出来的正确顺序的结果

        if (root == NULL)           //空二叉树直接返回
            return result;
        st.push(root);              //根节点直接入栈。也方便下面循环终止条件的开启

        while(!st.empty()){
            TreeNode* node = st.top();          //中节点
            result.push_back(node->val);        //存已经是正确顺序的值,选择通过树的节点来取值,而不是直接从栈中取
            st.pop();                           //已经处理完的数据
            if (node->right)
                st.push(node->right);           //右子节点,空节点不入栈
            if (node->left)
                st.push(node->left);            //左子节点,空节点不入栈
        }
        return result;
    }
};

2. 中序遍历

力扣题目链接94.二叉树的中序遍历

中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进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;
    }
};

四. 迭代遍历通用写法

之前的方法不通用的原因是,在迭代法的中序遍历中,访问节点(遍历节点)和处理节点(将元素放进结果集)并不同步。
因此可以用标记法来进行统一,将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。

1. 后序遍历(前序类似)

详见代码注释(下一个中序遍历有动图演示)

#include
using namespace std;

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();               //除去NULL节点
                node = st.top();        //记录NULL前的数值
                result.push_back(node -> val);
                st.pop();               //弹出已经处理完的元素,开启下一个循环
            }
        }
        return result;
    }
};

2. 中序遍历

只需要改变一下左中右节点顺序处的代码。

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();               //除去NULL节点
                node = st.top();        //记录NULL前的数值
                result.push_back(node -> val);
                st.pop();               //弹出已经处理完的元素,开启下一个循环
            }
        }
        return result;
    }
};

五. 二叉树的层序遍历

属于广度优先遍历,需要借用一个辅助数据结构即队列来实现。
队列先进先出,符合一层一层遍历的逻辑;而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑

力扣题目链接102.二叉树的层序遍历

1. 思路

  • 通过队列来存储每一个节点
  • 队列中始终存储着一整层的节点,当处理当前层时,需要完成两步,存储当前层节点的数值,和添加下一层节点到队列中
  • 由于结果需要按层输出,因此result应该用vector
  • 具体实现和想法参考下面的代码

2. 代码

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;               //使用队列来保存节点
        vector<vector<int>> result;         //因为需要分别记录每一层的节点

        if ( root!= NULL ) que.push(root);

        while(!que.empty()){                //一层一层处理
            int size = que.size();          //记录当前层的节点个数
            vector<int> tmp;                //用于记录当前层的每个节点数值
            for (int i = 0; i < size; i++){
                TreeNode* node =que.front();  //每处理完当前层的一个节点就pop掉
                que.pop();
                tmp.push_back(node -> val); //保存处理完的节点的数值

                if (node -> left)           //判断当前处理的节点是否有子节点,如果有则加入队列,也就是加入到下一层
                    que.push(node -> left);
                if (node -> right)
                    que.push(node -> right);
            }
            result.push_back(tmp);          //把当前层的所有节点数值加入到最终结果中
        }
        return result;
    }
};

你可能感兴趣的:(算法记录,算法,学习,数据结构,c++)