拷打字节面试官之 C语言树算法-手撸10万行算法带你吃透大场面试算法 - 树结构 ,从“入门”到“走火入魔” 硬核教程

继续更新拷打面试官算法系列:树!

***新加的:2025.8.15号发现的新题目,25年例行增加的面试101热题,自己刷题发现:

(1)之字打印二叉树遍历结果

(2)和为某一值的路径(一)

(3)对称二叉树

拷打面试官系列:TreeNode树结构算法,从“入门”到“走火入魔” 硬核教程!

总纲: 恭喜你,点开了这篇足以改变你算法认知,甚至职业生涯的文章。如果你还在纠结于前序、中序、后序遍历该怎么写,还在苦恼于递归调用看不懂,那么你来对地方了。本篇,我将带你彻底打穿树结构算法的第一关:遍历。我们不止会写出漂亮、正确的代码,更会深入到CPU层面,解密递归的本质,用“人话”告诉你为什么它能实现复杂功能。

第一章:站在巨人的肩膀上——递归三兄弟的优雅舞步

总: 树结构算法的入门,就是从“三大遍历”开始。它们不仅仅是算法,更是一种思想:分而治之。递归是实现这种思想最优雅的方式,但很多人把它当成一个“黑盒”。今天,我们就要掀开这个“黑盒”的盖子,让你看到里面到底藏着什么。

1.1 前序遍历:老大哥的霸气登场

思路分析: 前序遍历的精髓,在于它的“根-左-右”访问顺序。它就像一个脾气急躁的领导,一到场就先宣示主权(访问根节点),然后才开始处理左边的事情,最后再管右边。 递归实现,就是把这个思想复制粘贴到每一个子树上:

  1. 首先,判断当前节点root是否为空。如果为空,说明到达了叶子节点的外部,直接返回,这是递归的终止条件

  2. 如果不为空,就执行第一步:访问根节点。也就是把root->val存入结果数组。

  3. 然后,执行第二步:递归处理左子树。把root->left作为新的“根”节点,调用自己。

  4. 最后,执行第三步:递归处理右子树。把root->right作为新的“根”节点,调用自己。

这种方式,把一个大问题(遍历整棵树),分解成了一个小问题(遍历一个子树),直到问题小到可以被直接解决(子树为空)。

代码实现: 我们来看你提供的代码,我帮你优化和补上了注释,并修正了一个常见的小bug。

#include <stdio.h>
#include <stdlib.h>

// TreeNode结构体定义,用于构建二叉树
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

// 辅助递归函数,用于执行前序遍历
// @param root: 当前遍历的节点
// @param res: 存放遍历结果的数组指针
// @param returnSize: 结果数组的当前大小(指针传递,便于在递归中修改)
void doFuncPreorder(struct TreeNode *root, int *res, int *returnSize) {
    // 递归终止条件:如果当前节点为空,直接返回,结束该分支的遍历
    if (root == NULL) {
        return;
    }
    
    // 1. 访问根节点:将当前节点的值存入结果数组
    // 注意:(*returnSize)++ 是一个原子操作,先解引用获取 returnSize 的值,作为数组索引,
    // 然后再对 returnSize 指向的内存进行自增,保证下一个元素能放在正确的位置。
    // 你原来写的 (*returnSize++) 是有问题的,因为++的优先级更高,会先让指针移动。
    res[(*returnSize)++] = root->val;
    
    // 2. 递归遍历左子树
    doFuncPreorder(root->left, res, returnSize);
    
    // 3. 递归遍历右子树
    doFuncPreorder(root->right, res, returnSize);
}

// 主函数:前序遍历的入口
// @param root: 二叉树的根节点
// @param returnSize: 返回数组的行数
// @return: 存储前序遍历结果的整型一维数组
int *preorderTraversal(struct TreeNode *root, int *returnSize) {
    // 预估最大节点数,分配足够大的内存。
    // 在实际面试中,需要考虑树的规模,动态调整。
    // 如果不确定,可以先用一个较小值,如果超过,再重新分配,但这会增加复杂度。
    // 稳妥起见,我们先分配一个足够大的空间。
    int *res = (int *)malloc(1000000 * sizeof(int));
    if (res == NULL) {
        // 内存分配失败处理
        *returnSize = 0;
        return NULL;
    }
    
    // 初始化结果数组的大小为0
    *returnSize = 0;
    
    // 调用辅助函数开始遍历
    doFuncPreorder(root, res, returnSize);
    
    return res;
}

1.2 中序遍历:温柔的绅士风范

思路分析: 中序遍历的访问顺序是“左-根-右”。它像一个彬彬有礼的绅士,先去拜访左边邻居(左子树),回来之后再处理自己的事情(访问根节点),最后再去拜访右边邻居。这种遍历方式有一个非常特殊的性质:**对于二叉搜索树(BST)来说,中序遍历的结果是升序排列的!**这是面试中一个非常重要的考点,你必须烂熟于心。

代码实现: 有了前序的基础,中序的代码就是小菜一碟,只需要调整一下调用顺序就行了。

#include <stdio.h>
#include <stdlib.h>

struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

// 辅助递归函数:中序遍历
void doFuncInorder(struct TreeNode *root, int *res, int *returnSize) {
    // 递归终止条件
    if (root == NULL) {
        return;
    }
    
    // 1. 递归遍历左子树
    doFuncInorder(root->left, res, returnSize);
    
    // 2. 访问根节点
    res[(*returnSize)++] = root->val;
    
    // 3. 递归遍历右子树
    doFuncInorder(root->right, res, returnSize);
}

// 主函数:中序遍历的入口
int *inorderTraversal(struct TreeNode *root, int *returnSize) {
    int *res = (int *)malloc(1000000 * sizeof(int));
    if (res == NULL) {
        *returnSize = 0;
        return NULL;
    }
    
    *returnSize = 0;
    
    doFuncInorder(root, res, returnSize);
    
    return res;
}

1.3 后序遍历:乖孩子的最后告别

思路分析: 后序遍历的访问顺序是“左-右-根”。它像一个听话的孩子,先完成左边(左子树)和右边(右子树)的任务,最后才回到自己(根节点)这里交差。这种遍历在释放树节点计算子树高度等场景中非常有用,因为它保证了在处理父节点时,它的所有子节点都已经被处理完毕。

代码实现: 同样的,我们只需要调整一下调用顺序。

#include <stdio.h>
#include <stdlib.h>

struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

// 辅助递归函数:后序遍历
void doFuncPostorder(struct TreeNode *root, int *res, int *returnSize) {
    // 递归终止条件
    if (root == NULL) {
        return;
    }
    
    // 1. 递归遍历左子树
    doFuncPostorder(root->left, res, returnSize);
    
    // 2. 递归遍历右子树
    doFuncPostorder(root->right, res, returnSize);
    
    // 3. 访问根节点
    res[(*returnSize)++] = root->val;
}

// 主函数:后序遍历的入口
int *postorderTraversal(struct TreeNode *root, int *returnSize) {
    int *res = (int *)malloc(1000000 * sizeof(int));
    if (res == NULL) {
        *returnSize = 0;
        return NULL;
    }
    
    *returnSize = 0;
    
    doFuncPostorder(root, res, returnSize);
    
    return res;
}

表格1-1:递归三兄弟的异同点

遍历方式

访问顺序

核心思想

适用场景

前序遍历

根 -> 左 -> 右

快速定位根节点,分治处理子树

复制整棵树,创建树的表达式

中序遍历

左 -> 根 -> 右

访问根节点被“夹”在左右子树之间

(最重要) 二叉搜索树的升序排列,将树拍平

后序遍历

左 -> 右 -> 根

依赖子树的结果,再处理父节点

释放树节点内存,计算子树高度、深度

第二章:告别“黑盒”——递归的本质与栈的秘密

总: 如果你只是会写上面的代码,那你还是个合格的面试者。但如果你想成为30k+的专家,你就必须理解:**递归不是魔法,它只是一个优雅的语法糖。在底层,编译器和操作系统是通过函数调用栈(Call Stack)**来模拟这个过程的。理解栈,就是理解递归的本质。

2.1 栈帧(Stack Frame)的创建与销毁

每一次函数调用,操作系统都会在内存的栈区为这个函数分配一个独立的栈帧。这个栈帧就像一个临时工作区,存放着:

  • 函数的局部变量。

  • 函数参数。

  • 调用该函数的返回地址(告诉CPU函数执行完后该回到哪里)。

当我们调用doFuncPreorder(root->left)时,一个新的栈帧被创建,并压入栈顶。当这个函数执行完毕后,它的栈帧会被弹出,局部变量被销毁,CPU跳回到保存的返回地址继续执行。递归,就是把这个“压栈-出栈”的过程,自动、重复地执行。

2.2 暴力手撕:用C语言模拟递归栈

为了让你彻底明白这个原理,我们不依赖编译器,自己用一个显式栈(一个数组)来模拟递归过程,从而实现**迭代(非递归)**版的前序遍历。

思路分析:

  1. 我们创建一个数组来作为栈,并把根节点压入栈中。

  2. 进入一个循环,只要栈不为空,就一直执行。

  3. 在循环中,我们弹出栈顶元素,访问它(存入结果数组)。

  4. 然后,我们遵循“根-左-右”的原则,但由于栈是后进先出(LIFO)的,所以我们先将右子节点压入栈,再将左子节点压入栈。这样,左子节点就会在下一次循环中先被弹出并访问到。

  5. 循环直到栈为空,遍历结束。

代码实现: 这才是真正的“硬核”时刻,面试官最爱考这种手撸栈的解法。

#include <stdio.h>
#include <stdlib.h>

// 宏定义栈的大小,需要足够大以容纳所有节点
#define STACK_SIZE 1000

// TreeNode结构体定义,同上
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

// 栈结构体
typedef struct {
    struct TreeNode* items[STACK_SIZE]; // 栈的存储数组
    int top;                            // 栈顶指针
} Stack;

// 初始化栈
void initStack(Stack* s) {
    s->top = -1;
}

// 检查栈是否为空
int isStackEmpty(Stack* s) {
    return s->top == -1;
}

// 压栈
void push(Stack* s, struct TreeNode* item) {
    if (s->top >= STACK_SIZE - 1) {
        // 栈溢出处理,在实际项目中需要动态扩容
        return;
    }
    s->items[++s->top] = item;
}

// 弹栈
struct TreeNode* pop(Stack* s) {
    if (isStackEmpty(s)) {
        return NULL;
    }
    return s->items[s->top--];
}

// 迭代(非递归)前序遍历
// @param root: 二叉树的根节点
// @param returnSize: 返回数组的行数
// @return: 存储前序遍历结果的整型一维数组
int *preorderTraversalIterative(struct TreeNode *root, int *returnSize) {
    // 预估最大节点数,分配结果数组
    int *res = (int *)malloc(1000000 * sizeof(int));
    if (res == NULL) {
        *returnSize = 0;
        return NULL;
    }
    
    *returnSize = 0;
    
    // 1. 如果树为空,直接返回
    if (root == NULL) {
        return res;
    }
    
    // 2. 初始化栈
    Stack s;
    initStack(&s);
    
    // 3. 将根节点压入栈
    push(&s, root);
    
    // 4. 进入循环,直到栈为空
    while (!isStackEmpty(&s)) {
        // 5. 弹栈,获取当前节点
        struct TreeNode* current = pop(&s);
        
        // 6. 访问节点(根)
        res[(*returnSize)++] = current->val;
        
        // 7. 先压入右子节点,再压入左子节点
        // 因为栈是 LIFO (后进先出),我们希望先处理左子树
        if (current->right != NULL) {
            push(&s, current->right);
        }
        if (current->left != NULL) {
            push(&s, current->left);
        }
    }
    
    return res;
}

对比与升华: | 特性 | 递归版 | 迭代版(显式栈) | | :--- | :--- | :--- | | 代码风格 | 简洁、优雅,符合直觉 | 复杂,需要手动管理栈 | | 内存管理 | 依赖系统栈,有栈溢出风险 | 动态分配内存,风险可控 | | 性能 | 隐藏了函数调用和返回的开销 | 避免了函数调用开销,通常更快 | | 本质 | 隐式使用系统栈 | 显式模拟系统栈 | | 面试意义 | 基础题,考察思维 | 进阶题,考察底层原理 |

结语:超越“代码”本身

老铁,通过这一篇,我们已经不仅仅是“写”了三个遍历算法,我们更重要的是理解了递归和栈的本质联系。当你下次再写递归代码时,脑子里一定要浮现出栈帧压入和弹出的动态过程。这是从“会写代码”到“理解代码”的质变,也是你拿下30k+ offer的底层内功。

在下一篇,我们将继续深入。你提供的levelOrder层序遍历,以及之字形右视图,这些都和队列这个数据结构息息相关。我们将彻底搞清楚如何用队列来做树的广度优先搜索,并用硬核的代码把它们一一实现。

期待下次,我们继续!

第三章:从栈到队列——广度优先搜索的硬核基石

总: 如果说递归遍历(DFS)是纵向地“一头扎到底”,那么层序遍历(BFS)就是横向地“逐层推进”。这种思维模式的转变,需要我们更换一个底层工具:从栈(Stack)切换到队列(Queue)。队列的**先进先出(FIFO)**特性,完美契合了我们“先处理第一层,再处理第二层”的逻辑。

3.1 队列(Queue)的抽象与实现

在C语言中,我们没有内置的队列,这给了我们一个绝佳的机会,亲手实现一个。一个队列的核心操作有三个:

  • 入队(Enqueue):在队列末尾添加一个元素。

  • 出队(Dequeue):从队列头部移除并返回一个元素。

  • 判空(isEmpty):检查队列是否为空。

为了简单高效,我们用一个循环数组来模拟队列。这比你的代码中直接使用一个固定大小的数组更优,因为它能循环利用空间,避免频繁的内存分配和移动。

图3-1:循环队列原理图

graph TD
    A[队尾指针: rear] --> B[队头指针: front]
    B --> C(出队)
    A --> D(入队)
    subgraph Q
<think>好的,我现在需要帮用户解答关于Android底层驱动开发与优化的面试建议和不足补充的问题。首先,我得仔细看看用户提供的引用内容,确保回答符合他们的需求。用户提到了BSP工程师、驱动开发、Linux驱动面试题等,所以需要结合这些方面来组织答案。首先,用户希望得到面试建议,可能包括技术准备、项目经验、常见问题等。引用3和4提到了Linux驱动面试题,比如中断、同步、设备,这些都是重点。我需要强调这些技术点,并建议用户深入理解这些概念。然后,用户提到可能的不足,需要补充哪些方面。引用1和2提到职业规划和面试经历,可能用户在实际面试中遇到知识盲区,比如对硬件原理不熟悉,调经验不足,或者对安卓特有框架了解不够。需要指出这些常见的不足,并给出学习建议,比如研究硬件册、熟悉安卓HAL层等。 另外,用户提到生产支持优化,这可能涉及稳定性、功耗优化、代码可维护性。需要结合引用中的内容,比如同步与并发、内存管理,来补充这些优化方法。 还要注意引用中的笔面试题涉及C语言、操作系统、总线协议等,所以建议用户复习这些基础知识,尤其是数据结构和指针,这些在驱动开发中很重要。最后,生成的相关问题需要围绕面试准备、技术难点、职业发展等方面,帮助用户进一步深入思考。比如如何准备技术问题,优化驱动性能的方法,生产环境中常见问题等。 现在需要确保回答结构清晰,分点列出建议和不足,每个部分都有具体的例子和解决方案。同时,引用内容要自然地融入回答中,比如在讲中断处理时提到引用3中的中断部分面试题,或者在优化建议中提到同步与并发来自引用4。还要检查是否符合格式要求:行内公式用$...$,独立公式用$$...$$,但当前问题可能不涉及数学公式,所以可能不需要。但如果有技术术语,比如设备的结构,可能需要用代码块或强调格式。总之,整理思路后,回答应包括面试准备建议、常见不足及补充、生产支持优化,最后生成相关问题。确保每个部分都参考了用户提供的引用内容,并给出实用建议。</think>### Android底层驱动开发与优化面试建议及不足补充 结合行业需求及面试经验,以下从技术准备、不足分析、优化方向三方面提供建议: --- #### **一、面试准备建议** 1. **基础技术栈强化** - **Linux内核机制**:重点掌握进程调度、中断处理(如顶半部/底半部设计[^3])、内存管理(如DMA/CMA)、同步机制(自旋锁、信号量)。 - **设备(Device Tree)**:熟悉设备语法及驱动匹配逻辑,例如: ```dts &i2c1 { camera_sensor: camera@3c { compatible = "vendor,cam-sensor"; reg = <0x3c>; }; }; ``` - **总线协议**:I2C/SPI/UART等时序及调方法(如逻辑分析仪抓波形)[^4]。 2. **Android特有框架** - **HAL层(Hardware Abstraction Layer)**:理解HIDL接口定义及Binder通信机制,例如相机HAL的`ICameraDevice`实现[^1]。 - **BSP(Board Support Package)**:掌握启动流程(Bootloader→Kernel→Init→Zygote),熟悉`init.rc`脚本配置。 3. **调与生产支持** - **稳定性分析**:掌握Kernel Panic日志解析、`ftrace`跟踪函数调用链。 - **功耗优化**:分析`wake_lock`滥用场景,使用`Battery Historian`工具定位异常唤醒。 --- #### **二、常见不足及补充方向** 1. **硬件原理不熟悉** - **问题**:仅关注驱动代码,忽略硬件寄存器配置(如Camera Sensor的I2C时序参数)。 - **建议**:阅读芯片册(如Qualcomm的SLPI协处理器文档),理解硬件行为与驱动交互。 2. **调经验不足** - **问题**:依赖`printk`调,不熟悉`JTAG`或`KGDB`在线调工具[^3]。 - **建议**:实践通过`sysfs`动态修改驱动参数(如GPIO电平)。 3. **安卓框架理解薄弱** - **问题**:仅实现内核驱动,未对接HAL层(如未实现`android.hardware.camera.provider@2.4`服务)。 - **建议**:研究AOSP中`CameraService`与HAL的交互流程[^1]。 --- #### **三、生产支持优化方向** 1. **稳定性优化** - 使用`KASAN`检测内存越界,避免野指针问题。 - 设计Watchdog机制监控驱动响应超时。 2. **性能调优** - **延迟优化**:减少中断处理时间,将耗时操作移至工作队列。 - **吞吐量优化**:采用DMA传输(如MIPI CSI-2接口的Camera数据流)。 3. **代码可维护性** - 遵循Linux编码规范(如函数命名`driver_action_device`)。 - 使用`Devicetree Overlay`动态配置硬件参数,避免硬编码。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值