代码随想录算法训练营第13天

代码随想录算法训练营第13天 | 二叉树理论基础篇,二叉树的递归遍历,二叉树的迭代遍历,102. 二叉树的层序遍历

一、刷题部分

1.1 二叉树理论基础篇

二叉树是面试的高频考点,普遍涉及递归和迭代的运用。

1.1.1 题目分类

1.1.2 二叉树的种类

满二叉树:

  • 深度为k,则必定有 2^k - 1 个节点。

完全二叉树

满二叉树从后往前去掉几个节点得到的树。堆也是一个完全二叉树。

二叉搜索树

有序树,其中序遍历一定有序。定义如下:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树

平衡二叉搜索树

AVL树,保证任何节点的左右子树高度差不超过 1 。且所有子树均是平衡二叉树。

  • map set multimap multiset 的底层实现都是平衡二叉搜索树。增删操作复杂度为 $\log{n}$,补充说明:unordered_map, unordered_set, unordered_map, unordered_set 的底层实现是哈希表。

1.1.3 二叉树的存储方式

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

链式存储中,最基本的结构就是 节点元素,左指针,右指针。

顺序存储就是当做满二叉树开辟空间,位置上有节点的就填入值,没有就留空,若节点下标是 x,那么左子树下标是 2*x + 1,右子树下标是 2*x + 2。下标从 0 开始。

实际用起来链式存储多一些。

1.1.4 二叉树的遍历方式

  • 深度优先遍历:先往深处走,遇到叶子节点再往回。
    • 前序遍历(递归法,迭代法)
    • 中序遍历(递归法,迭代法)
    • 后序遍历(递归法,迭代法)
  • 广度优先遍历:一层一层的去遍历。
    • 层次遍历(迭代法)

1.1.5 二叉树的定义

节点定义方式:

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

1.2 二叉树的递归遍历

这里重点想借最简单的递归案例把递归方法论整体确定下来。

首先注意递归三要素:参数与返回值、终止条件、单层逻辑。

  1. 首先确定递归函数的参数与返回值:确定哪些参数是递归过程中需要处理的,那么就在递归函数里面加上这个参数,并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止操作:写完了递归算法运行的时候,经常遇到栈溢出的错误,就是没有写终止条件或终止条件写错了。操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必定会溢出。
  3. 确定单层递归的逻辑:确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归过程。

以前序遍历实践上面方法:

  1. 确定递归函数的参数和返回值:本函数的数据交互只需要传入树以及用一个数组来保存结果即可。立即写出参数与返回值:
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);

1.3 二叉树的迭代遍历

迭代法模拟递归实际上就是自己维护一个栈来做同样的事情。理论上所有递归都可以用栈模拟,但是复杂的模拟起来很困难。本题的还都是很简单的,因此可以学习一下。

1.3.1 前序遍历 - 迭代法

思路:维护一个栈。初始化将根结点入栈。之后开始循环,只要栈不为空就出栈并且将该节点的右孩子,左孩子依次放入栈中。如此循环直到栈为空。

易写出代码:

vector<int> preorderTraversal(TreeNode* root) {
    stack<TreeNode*> st;
    vector<int> res;
    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);
        if(node->left) st.push(node->left);
    }
    return res;
}

1.3.2 中序遍历 - 迭代法

思路:首先,不可以像递归那样直接改一改顺序就能用了,中序的迭代法和前序的迭代法还是不一样的。

为什么呢?首先解释为什么前序遍历那么简洁,因为前序遍历访问节点的顺序就是遍历的顺序,因此很一致,但是对于中序遍历,其访问节点必须还是从根结点开始往下走,但是遍历的顺序又要求是左中右,这种不一致就会造成中序的迭代法不可能那么简洁。

这里就需要用栈来记录访问过的节点,数组用来记录遍历结果,还需要一个指针来访问节点,

处理的行为是这样的:从根结点开始,访问元素。若当前元素不为空,则入栈,接下来访问左孩子。若当前元素为空,则将栈顶元素弹出,放入结果数组,然后访问该元素的右孩子。这样的循环下来就完成了中序遍历。

vector<int> inorderTraversal(TreeNode* root) {
    //迭代法
    vector<int> res;
    TreeNode* cur = root;
    stack<TreeNode*> st;
    if (!root) return res;
    while (!(st.empty() && !cur)) {
        //当栈为空且当前指针已经指向空的时候结束
        if (cur != NULL) {
            st.push(cur);
            cur = cur->left;
        }
        else {
            TreeNode* node = st.top();
            st.pop();
            res.push_back(node->val);
            cur = node->right;
        }
    }
    return res;
}

1.3.3 后序遍历 - 迭代法

思路:后续遍历其实可以借用前序遍历的代码,因为如果将前序遍历入栈顺序反过来,就可以得到“中右左”的遍历序列,再 reverse 一下就是“左右中”的序列,即后续序列。

vector<int> postorderTraversal(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->left) st.push(node->left);
        if(node->right) st.push(node->right);
    }
    reverse(res.begin(), res.end());
    return res;
}

1.3.4 本题总结

实际上,这只是迭代法的一种,而且是技巧性比较强的一种。也因此造成了 递归法 逻辑很统一,但是迭代法逻辑不统一这样 反直觉 的情况出现。下一题就会介绍另一种统一的迭代法。


1.4 二叉树的统一迭代法

这部分这人判断可以先放着,不写了。


1.5 102. 二叉树的层序遍历

1.5.1 题目描述

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

示例 2:

输入:root = [1]
输出:[[1]]

示例 3:

输入:root = []
输出:[]

提示:

  • 树中节点数目在范围 [0, 2000]
  • -1000 <= Node.val <= 1000

1.5.2 初见思路

本科数据结构与算法课程学过,回忆一下思路大致如此:使用一个队列来做,根结点先入队。每次出队的时候,将该元素左右孩子分别入队。直到队空则结束。

仔细看了一下本题有点不一样,res 需要用一个二维数组来存放,也就是说层与层之间要分开。我想到一个方式就是使用计数器,由于队列里只可能存在 2 层的元素,所以就用 count1 表示 第一层元素个数,count2 表示第二层元素个数,每次出队 count1--,入队count2++,当 count1 减到 0 的时候就说明一层遍历完了。然后让count1 = count2,count2 = 0。

写一下代码:

vector<vector<int>> levelOrder(TreeNode* root) {
    queue<TreeNode*> q;
    int count1 = 1;//队列高层元素个数
    int count2 = 0;//队列低层元素个数
    vector<vector<int>> res;
    vector<int> level;
    if(!root) return res;
    q.push(root);
    while (!q.empty()) {
        TreeNode* node = q.front();
        q.pop();
        count1--;
        level.push_back(node->val);
        //将node的孩子放进来
        if (node->left) {
            q.push(node->left);
            count2++;
        }
        if (node->right) {
            q.push(node->right);
            count2++;
        }
        //高层元素遍历完毕
        if(!count1) {
            count1 = count2;
            count2 = 0;
            res.push_back(level);
            level.clear();
        }
    }
    return res;
}

顺利通过。

1.5.3 正式做题

明显第一眼就看到题解用来控制分层输出的逻辑和我的不一样,看一下题解是怎么做的。

看懂了,题目使用了一个变量暂存队列里高层元素个数,然后每次执行该次数循环就停下,此时队列里的元素个数恰好就是下一轮高层元素个数,从而少一个变量。

vector<vector<int>> levelOrder(TreeNode* root) {
    queue<TreeNode*> que;
    if (root != NULL) que.push(root);
    vector<vector<int>> res;
    while (!que.empty()) {
        int size = que.size();
        vector<int> vec;
        for (int i = 0; i < size; i++) {
            TreeNode* node = que.front();
            que.pop();
            vec.push_back(node->val);
            if(node->left) que.push(node->left);
            if(node->right) que.push(node->right);
        }
        result.push_back(vec);
    }
    return result;
}

1.5.4 遇到的困难

🈚️

1.5.5 本题总结

录里说还有 10 道题可以直接刷掉,这里就不占本文篇幅了,代码写到别的 md 里链接过去就好。


二、总结与回顾

今天算是对二叉树进行了一次初步的探索,研究了几种不同的二叉树,还练习了二叉树的遍历方式(递归法、迭代法)。很有收获,但是迫于时间问题没有完成所有的选学部分。

posted @ 2025-02-12 17:09  xc0208  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示