xym-ee
  • 计算机与嵌入式开发学习
  • 1-1.编程基础
    • C 语言
      • C 中的数据
      • C 语言基础
      • 字符输入输出
      • 函数
      • 数组和指针
      • 字符串处理
      • 存储类别
      • 文件 I/O
      • 复杂数据类型
      • 位操作
      • 预处理和 C 库
    • 数据结构和算法入门
    • leetcode 刷算法题
      • 递归与栈
      • 二叉树与递归
      • 回溯问题
      • 动态规划 1
    • 基本工具和使用
      • shell
      • shell 脚本
      • vim 编辑器
      • 命令行数据整理
      • 命令行环境和配置
  • 1-2.计算机系统基础(CSAPP)
    • 1.计算机基础
    • 2.数据的表示
    • 3.加减运算
    • 4.乘除运算
    • 5.程序的表示转换和链接
    • 6.IA32指令
    • 7.过程调用
    • 10.程序的链接
  • 1-3.数字电路、计算机组成
    • 1.数字电路、virtual circuit board
    • 2.计算机组成/steam:Turing Complete
    • 3.微机原理与接口技术(8086)
  • 1-4.计算机网络
    • 1.从浏览器开始
    • 2.协议栈和网卡
    • 3.网络设备
    • 4.运营商、接入网
    • 5.服务器
    • 6.数据返回浏览器
    • socket编程
  • 1-5.操作系统
    • 0.绪论
      • 1.应用视角的操作系统
      • 2.硬件视角的操作系统
      • 3.数学视角的操作系统
      • 4.状态机模型的应用
    • 1.并发
      • 1.并发 bug 的解决思路
      • 2.互斥
      • 3.同步
      • 4.信号量
      • 5.真实并发
      • 6.调试技巧
      • 7.os kernel 实现
    • 2.虚拟化
      • 1.操作系统上的进程
      • 2.进程的地址空间
      • 3.系统调用和unix shell
      • 4.C 标准库的实现
      • 5.linux 操作系统
      • 6.可执行文件和加载
      • 7.动态链接和加载
      • 8.内核的实现
      • 9.fork 的应用
    • 3.持久化
      • 1.存储设备的原理
      • 2.输入输出设备模型
      • 3.设备驱动程序
      • 4.文件系统 API
      • 5.fat 和 unix 文件系统
      • 6.持久数据的可靠性
    • 总结
  • 2-1.嵌入式裸机开发
    • 嵌入式系统通信接口与协议
    • cortex-m 内核芯片裸机开发
    • MPU
  • 2-2.中等规模系统开发
    • LVGL 图形库
    • 裸机开发的软件框架
    • 基于 rtos 开发
  • 2-3.armv7-m架构与 rtos 原理
    • armv7-m 架构
    • rt-thread 内核实现
    • rt-thread 驱动开发
  • 3-1.linux 应用开发基础
  • 3-2.linux 镜像构建
    • uboot 使用
    • uboot 适配
    • uboot 启动分析
    • uboot 自定义命令
    • linux 内核适配
    • linux 内核启动分析
    • busybox 根文件系统构建
  • 3-3.linux 驱动开发
    • 驱动开发基础
    • sysfs
    • led 驱动
    • 设备树
    • pinctrl 和 gpio 子系统
    • 并发控制
由 GitBook 提供支持
在本页
  • 递归思路
  • 二叉树递归练习
  • 二叉树最小深度(leetcode 111)
  • 路径总和 leetcode 112
  • 求根节点到叶节点数字之和 leetcode 129
  • 统计二叉树中好节点的数目 leetcode 1448
  • 二叉树的垂序遍历 987
  • 二叉树相同
  • 判断相同二叉树(leetcode 100)
  • 判断对称二叉树 leetcode 101
  • 判断平衡二叉树
  • 二叉树的右视图 leetcode 199
  • 验证二叉搜索树
  • 方法一:前序遍历
  • 方法二:中序遍历
  • 方法三
  • 最近公共祖先
  • 层序遍历
  • 二叉树遍历节点

这有帮助吗?

  1. 1-1.编程基础
  2. leetcode 刷算法题

二叉树与递归

递归思路

二叉树和递归。

  • 二叉树问题的思考方式

  • 为什么需要使用递归

  • 为什么这种写法一定能算出正确答案

  • 计算机如何执行递归

  • 另一种递归思路

举例子,计算一个二叉树的最大深度(leetcode 104),根节点到最远叶子节点的距离。一开始陷入二叉树的细节就不太好写代码了。先思考整棵树和左右子树的关系。

整棵树的最大深度 = max(左子树的最大深度, 右子树的最大深度) + 1

要计算子树的最大深度,和计算整个树的深度这个问题是一样的。

  • 原问题:计算整棵树的最大深度

  • 子问题:计算左右子树的最大深度

做循环计算,在使用同一份代码,那么这种相似问题,按道理也应该使用同一份代码计算。但这里的子问题需要把计算结果返回给上一级问题。子问题的规模比原问题小,不断向下(递),但总有尽头,即边界条件,直接返回答案(归)。

为什么这么做是对的?理论依据,数学归纳法证明。

#define MAX(a, b) (((a)>(b)) ? (a) : (b))

int maxDepth(struct TreeNode* root) {

    if (root == NULL)   return 0;
    return MAX(maxDepth(root->left), maxDepth(root->right)) + 1;
}

时间复杂度为 O(n),空间复杂度也是 O(n) 。return 怎么知道回到那个节点上去的,函数调用,栈帧。

另外一种思路,递归时除了把节点传下去,还可以把递归的层数传下去,同时维护一个全局变量,当层数大于最大值,就更新全局变量。这个思路在一些题目里也常用到。

#define MAX(a, b) (((a)>(b)) ? (a) : (b))

int result;

void dfs(struct TreeNode* root, int count)
{
    if (root == NULL)   return;

    count++;                        /* 递归层数 */
    result = MAX(count, result);    /* 记录最深递归层数 */
    dfs(root->left, count);         /* 理解返回的含义 */
    dfs(root->right, count); 
}

int maxDepth(struct TreeNode* root) 
{
    result = 0;
    dfs(root, 0);
    return result;
}

二叉树递归练习

二叉树最小深度(leetcode 111)

思路:递归遍历二叉树,同时判断是否为子叶,使用一个全局变量记录最小深度。

#define MIN(a, b) (((a)<(b)) ? (a) : (b))

int min;

void dfs(struct TreeNode* root, int count)
{
    if (root == NULL)   return;

    count++;

    if ( root->left == NULL && root->right == NULL)
        min = MIN(min, count);

    dfs(root->left, count);
    dfs(root->right, count);
}

int minDepth(struct TreeNode* root) {
    min = INT_MAX;
    dfs(root, 0);
    return (min == INT_MAX) ? 0 : min; 
}

路径总和 leetcode 112

关于递归函数参数与返回值的思考

int target;

bool dfs(struct TreeNode* root, int sum)
{
    if ( root == NULL)
        return false;
    sum += root->val;
    if ( root->left == NULL && root->right == NULL)
    {
        if ( sum == target )    return true;
        else                    return false;
    }
    return dfs(root->left, sum) || dfs(root->right, sum);
}

bool hasPathSum(struct TreeNode* root, int targetSum) {
    target = targetSum;
    return dfs(root, 0);
}

求根节点到叶节点数字之和 leetcode 129

统计二叉树中好节点的数目 leetcode 1448

思路:传入参数中有一个到当前层的最大值,然后维护一个全局变量记录符合要求的节点数。

int count;
void dfs(struct TreeNode* root, int max)
{
    if (root == NULL)       return;
    
    if (root->val >= max)
    {
        max = root->val;
        count++;
    }

    dfs(root->left, max);
    dfs(root->right, max);
}


int goodNodes(struct TreeNode* root){
    count = 0;
    dfs(root, INT_MIN);
    return count;
}

不使用全局变量?使用 return 返回数量呢?

int dfs(struct TreeNode* root, int max)
{
    if (root == NULL)   return 0;
    int ret = 0;
    if (root->val >= max)
    {
        max = root->val;
        ret = 1;
    }
    max = root->val >= max ? root->val : max;
    return ret + dfs(root->left, max) + dfs(root->right, max);
}

int goodNodes(struct TreeNode* root){
    return dfs(root, INT_MIN);
}

二叉树的垂序遍历 987

二叉树相同

判断相同二叉树(leetcode 100)

判断相同二叉树(leetcode 100)。根节点相同,然后左子树要相同,右子树也要相同。

如何判断根节点相同?节点的值相同的前提条件是两个节点都不是空节点,因此判空在先。如果两个节点都是空节点,那么返回 true 即相同,一个编程技巧,判断空节点时可以使用 p == q 来判断,只有都为 NULL 才相同,其他的地址是不可能相同的。这也是一个比较巧妙的边界条件。

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {

    if ( (p==NULL) || (q==NULL) )   return p == q;
    return (p->val == q->val) && isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}

判断对称二叉树 leetcode 101

如何思考?如果在没有上一题的的前提下,是否能想到这一步

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if ( p == NULL || q == NULL )   return p == q;
    return (p->val == q-> val) && isSameTree(p->left, q->right) && isSameTree(p->right, q->left);
}
bool isSymmetric(struct TreeNode* root) {
    if (root == NULL)   return true;
    return isSameTree(root->left, root->right);
}

判断平衡二叉树

#define MAX(a, b) (((a)>(b)) ? (a) : (b))
#define ABS(x)  (((x)>0) ? (x): (-(x)))

int max_depth(struct TreeNode* root)
{
    if (root == NULL)
        return 0;

    int l = max_depth(root->left);
    int r = max_depth(root->right);

    if ( l == -1 || r == -1)
        return -1;

    if (ABS(l-r) <= 1)
        return MAX(max_depth(root->left), max_depth(root->right)) + 1;
    else 
        return -1;
}

bool isBalanced(struct TreeNode* root) {

    if (max_depth(root) == -1)
        return false;
    return true;
}

二叉树的右视图 leetcode 199

#define MAX(a, b) (((a)>(b)) ? (a) : (b))
#define ABS(x)  (((x)>0) ? (x): (-(x)))

int max_depth;
int *result;

void find(struct TreeNode* root, int count)
{
    if (root == NULL)
        return;

    count++;
    
    if (count > max_depth)
    {
        result[max_depth] = root->val;
        max_depth = count;
    }

    find(root->right, count);
    find(root->left, count);
}


int* rightSideView(struct TreeNode* root, int* returnSize) {
    max_depth = 0;
    result = malloc(100*sizeof(int));

    find(root, 0);

    *returnSize = max_depth;
    return result;    
}

验证二叉搜索树

二叉树的遍历。前中后序遍历。

二叉树是不是二叉搜索树:对于一个节点来说,左子树节点值小于,右子树节点值大于。左子树右子树也必须是二叉搜索树。

方法一:前序遍历

区间分割。根据当前的节点值确定区间,然后递归左右子树。

先访问节点值,再递归左右子树的做法就是前序遍历(中左右)

bool dfs(struct TreeNode* root, long long int l, long long int r)
{
    if (root == NULL)
        return true;

    return  (root->val > l && root->val < r) && dfs(root->left, l, root->val) && dfs(root->right, root->val, r);
}

bool isValidBST(struct TreeNode* root) {
    return dfs(root, LLONG_MIN, LLONG_MAX);
}

在 dfs 函数中,return 后面的表达,隐含了先访问根节点,然后遍历左子树,遍历右子树。

时间复杂度为 O(n), 空间复杂度为 O(n)。

方法二:中序遍历

按照递归左子树,访问节点值,递归右子树这个顺序遍历,这就是中序遍历(左中右)。

二叉搜索树中序遍历的优秀性质:中序遍历访问节点值是严格递增的。

long long int pre = LLONG_MIN;

bool isValidBST(struct TreeNode* root) {
    if (root == NULL)
        return true;
    if (isValidBST(root->left) == false)
        return false;
    if (root->val <= pre )
        return false;
    pre = root->val;
    return isValidBST(root->right);
}

存在的问题:leetcode OJ 可能会在同一个进程里重复调用 isValidBST 函数,但是 pre 变量还没更新。同时使用 3 个测试用例,都只包含一个节点为0,但是第一个用例通过了,后面两个没过。

这个问题在一些题目里也遇到过,全局变量初始化后,运行测试用例会出问题,但是在核心代码里重新初始化就可以了。可以反向推断出 leetcode 的 OJ 是子啊一个进程里重复调用算法函数的。

可以通过 OJ 的实现

long long int pre;

bool dfs(struct TreeNode* root)
{
    if (root == NULL)
        return true;
    if (dfs(root->left) == false)
        return false;
    if (root->val <= pre )
        return false;
    pre = root->val;
    return dfs(root->right);    
}

bool isValidBST(struct TreeNode* root) {
    pre = LLONG_MIN;
    return dfs(root);
}

方法三

前序遍历的思路是把区间向下传递,还有一个思路是把节点的范围往上传。

最近公共祖先

struct TreeNode* lowestCommonAncestor(struct TreeNode* root, struct TreeNode* p, struct TreeNode* q) {

    if ( root == NULL || root == p || root == q )
        return root;
    
    struct TreeNode* left = lowestCommonAncestor(root->left, p, q);
    struct TreeNode* right = lowestCommonAncestor(root->right, p, q);

    if (left && right)
        return root;

    if (left)
        return left;

    return right;
}

层序遍历

广度优先搜索。递归从根节点出发一直往下走,层序遍历则是一行行遍历。

如何从根节点出发得到每层的值?

初始化一个 current 数组,把根节点放进去,然后开始循环。

还需要一个 next 数组,遍历 current,把左右儿子都记录到 next 中。遍历 current 的同时,记录节点值,遍历结束后添加答案。

使用两个数组实现。

#define MAX_RESULT  100000

int** result;       /* 保存结果的数组 */
int result_len;     /* 结果个数 */
int *column_size;   /* 每个结果的长度 */

struct TreeNode* current[1000];
int cur_len;
struct TreeNode* next[1000];
int nex_len;

int path[1000] = {0}; /* 单个结果 */
int path_len;       /* 单个结果的长度 */

void save_result(void)
{
    result[result_len] = malloc(path_len * sizeof(int));
    column_size[result_len] = path_len;

    for (int i=0; i<path_len; i++)
        result[result_len][i] = path[i];

    result_len++;
}

int** levelOrder(struct TreeNode* root, int* returnSize, int** returnColumnSizes) {
    result = malloc(MAX_RESULT * sizeof(int *));
    result_len = 0;
    column_size = malloc(MAX_RESULT * sizeof(int));

    cur_len = 0;

    if (root != NULL)
        current[cur_len++] = root;

    while ( cur_len != 0)
    {
        path_len = 0;
        nex_len = 0;
        for (int i=0; i<cur_len; i++)
        {
            if ( current[i]->left != NULL)
                next[nex_len++] = current[i]->left;
            if ( current[i]->right != NULL)
                next[nex_len++] = current[i]->right; 
            path[path_len++] = current[i]->val;
        }

        save_result();

        for (int i=0; i<nex_len; i++)
        {
            current[i] = next[i];
        } 

        cur_len = nex_len;
    }

    *returnSize = result_len;
    *returnColumnSizes = column_size;  /* 不是 &column_size */
    return result;
}

两个数组可以合并为一个数组,下一层的节点可以跟在当前层节点后面,这就是队列。

此外还有一题为二叉树的锯齿形层序遍历,和这个题差不多。切入的角度:在保存遍历结果时,反转数组。

类似的 leetcode 513 ,找树左下角的值,思路:返回层序遍历最后一层的第一个元素即可。

二叉树遍历节点

前序遍历的定义就是用递归定义的,二叉树本身也是用递归定义的。关于前中后,指的是中间节点的位置。

void preorder(struct TreeNode* root, int* ret, int* returnSize)
{
    if (root==NULL)
        return;

    ret[*returnSize] = root->val;
    (*returnSize)++;

    preorder(root->left, ret, returnSize);
    preorder(root->right, ret, returnSize);
}


int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    int* ret = (int*)malloc(sizeof(int) * 100);
    *returnSize = 0;
    preorder(root, ret, returnSize);
    return ret;
}

后序遍历。

void back_order(struct TreeNode* root, int *size, int *out)
{
    if (root == NULL)
        return;

    back_order(root->left, size, out);
    back_order(root->right, size, out);

    out[(*size)] = root->val;
    (*size) ++;
}

int* postorderTraversal(struct TreeNode* root, int* returnSize) {

    int *ret_array = (int *)malloc(100*sizeof(int));
    *returnSize = 0;

    back_order(root, returnSize, ret_array);
    return ret_array;
}
上一页递归与栈下一页回溯问题

最后更新于8个月前

这有帮助吗?