数据结构与算法—树状数组
1. 树状数组是什么?为什么需要它?想象一个场景:你有一个数组 a,需要频繁地做两件事: 修改数组中某个元素的值。 查询数组中任意一个区间的和(例如,从索引 l 到 r 的所有元素之和)。 我们可以用一些朴素的方法来解决: 普通数组: 修改元素:O(1),非常快。 查询区间和:O(n),需要遍历区间,如果查询次数很多,会非常慢。 前缀和数组: 我们预处理一个 preSum 数组,preSum[i] = a[1] + ... + a[i]。 查询区间和 [l, r]:O(1),只需计算 preSum[r] - preSum[l-1],非常快。 修改元素 a[i]:O(n),因为 a[i] 的改变会影响到 preSum[i] 及之后的所有元素,你需要更新 preSum[i], preSum[i+1], ..., preSum[n],非常慢。 可以看到,这两种方法在“修改”和“查询”上都有一个操作是 O(n) 的,无法同时做到高效。 树状数组 (Fenwick Tree) 就是为了解决这个问题而生的。它是一种巧妙的数据结构,可以在 O(log n) 的时间复杂度内完成...
CPP学习笔记—智能指针
1. 为什么需要智能指针?在 C/C++ 中,内存管理是一个核心但又极易出错的话题。传统的内存管理依赖于程序员手动使用 new 和 delete (或 malloc 和 free) 来申请和释放内存。这导致了几个经典的问题: 内存泄漏 (Memory Leaks):忘记调用 delete。当一个动态分配的对象不再被使用,但其内存没有被释放时,这块内存就无法被再次使用,造成了内存泄漏。长时间运行的程序中,内存泄漏会耗尽系统资源,导致程序崩溃。 悬挂指针 (Dangling Pointers):一个指针指向的内存已经被释放,但指针本身没有被置为 nullptr。如果之后不小心通过这个悬挂指针访问或修改内存,会导致未定义行为(Undefined Behavior),通常表现为程序崩溃或数据损坏。 重复释放 (Double Free):对同一块内存调用两次或多次 delete。这也是一种严重的未定义行为,通常会导致程序崩溃。 异常安全问题:在 new 和 delete 之间如果发生异常,delete 语句可能永远不会被执行,从而导致内存泄漏。 12345678void...
CPP学习笔记—特殊成员函数
1. 引言:对象生命周期与 RAIIC++ 中,对象有明确的生命周期:它被创建,然后在使用结束后被销毁。构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数这六个函数(通常被称为特殊成员函数)就是用来管理这个生命周期的。它们控制着对象的: 创建 (Construction) 销毁 (Destruction) 拷贝 (Copying) 移动 (Moving) 它们是实现 C++ 核心设计哲学 RAII (Resource Acquisition Is Initialization) 的基石。RAII 意味着在对象的构造函数中获取资源(如内存、文件句柄、锁),并在其析构函数中释放资源,从而将资源的生命周期与对象的生命周期绑定在一起,避免资源泄漏。 2. 核心示例:一个简单的 Buffer 类我们将使用一个简单的 Buffer 类来贯穿整个讲解。这个类在堆上分配了一块整数数组,因此它需要手动管理内存资源。 1234567891011121314151617181920#include <iostream>#include...
CPP学习笔记—深拷贝与浅拷贝
C++ 中的深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是一个 C++ 实际开发中至关重要的核心概念,因为它直接关系到程序的正确性和内存安全。 1. 问题的根源:指针和动态内存要理解深拷贝和浅拷贝,首先必须明白问题的根源在哪里。如果一个类只包含基本数据类型(如 int, double)或者不持有动态资源的对象(如 std::string,它自己内部处理好了深拷贝),那么我们通常不需要关心这个问题。 问题出现在当一个类的成员变量是指针,并且这个指针指向了在堆(Heap)上动态分配的内存时。 来看一个简单的例子,一个自定义的字符串包装类: 123456789101112131415161718class MyString {private: char* _data; size_t _len;public: MyString(const char* s = "") { _len = strlen(s); _data = new char[_len + 1]; //...
CPP学习笔记—多态
什么是多态?多态,从字面意思上看,是“多种形态”的意思。在 C++ 中,多态是面向对象编程(OOP)的三大核心特性之一(另外两个是封装和继承)。它允许你使用一个统一的接口来处理不同类型的对象,使得程序具有更好的可扩展性和灵活性。 一个通俗的例子是:你有一个通用的“遥控器”(接口),这个遥控器上有个“开/关”按钮。当你用这个遥控器对着电视机按“开/关”,电视机就会打开或关闭;当你用同一个遥控器对着空调按“开/关”,空调就会打开或关闭。遥控器本身并不知道它控制的是电视机还是空调,它只知道发出一个“开/关”的指令。具体执行这个指令的是哪个设备,以及这个设备如何执行(电视机是点亮屏幕,空调是启动压缩机),是在运行时才决定的。 在 C++ 中,这个“遥控器”就是基类指针或引用,而“电视机”和“空调”就是派生类对象。 C++ 中的多态分类C++ 中的多态主要分为两类: 静态多态(编译时多态):在程序编译期间就已经确定了函数调用的地址。它的执行速度快,但灵活性稍差。 动态多态(运行时多态):在程序运行期间才能确定调用哪个函数。这是我们通常所说的...
CPP学习笔记—四种强制类型转换
C++中的四种强制类型转换:static_cast、dynamic_cast、const_cast 和 reinterpret_cast。 为什么需要新的类型转换?在C++之前,C语言使用一种通用的强制类型转换语法,例如 (new_type)expression 或 new_type(expression)。这种C风格的转换方式存在几个问题: 过于粗暴:它可以在任何类型之间进行转换,无论是相关的还是不相关的,这使得它非常不安全。 意图不明:从语法上无法清晰地看出程序员想要做什么样的转换(例如,是移除const、进行继承体系中的转换,还是进行底层的位模式重新解释)。 难以搜索:在代码库中搜索 ( 符号来定位所有的类型转换是非常困难的,不利于代码审查和维护。 为了解决这些问题,C++引入了四个功能更明确、更安全的命名转换操作符。它们让代码的意图更加清晰,并允许编译器进行更严格的检查。 1. static_caststatic_cast...
基本排序算法
一、冒泡排序1. 什么是冒泡排序?冒泡排序(Bubble Sort)是一种简单直观的排序算法。它得名于其工作方式,即较小(或较大)的元素会像水中的气泡一样,通过不断交换,慢慢“浮”到数列的顶端。 a. 核心思想它重复地遍历待排序的数列,一次比较两个相邻的元素,如果它们的顺序(如从大到小或从小到大)错误就把它们交换过来。遍历数列的工作会重复地进行,直到没有再需要交换的元素为止,这意味着整个数列已经排序完成。 b. 工作原理 比较相邻元素:从列表的第一个元素开始,比较它和下一个元素。 交换:如果第一个元素比第二个元素大(以升序为例),就交换它们的位置。 向后移动:继续比较下一对相邻的元素(即新的第二个和第三个元素),重复步骤2。 完成一轮:持续这个过程,直到列表的末尾。经过第一轮遍历后,最大的元素会被放置在列表的最后一个位置。 重复:对除了最后一个元素之外的所有元素,重复以上步骤。每一轮遍历都会将当前未排序部分的最大元素放到其最终位置。 终止:当某一轮遍历中没有发生任何交换时,说明列表已经完全排序,可以提前终止算法。 2. 图解示例让我们用一个简单的例子 [5, 1, 4, 2,...
通信方式对比—UART/SPI/IIC
一、UARTUART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是一种非常基础且广泛使用的串行通信协议。它不是像SPI或I²C那样的“总线”,而通常是一种点对点的通信方式。几乎所有的微控制器和计算机都包含UART硬件,它也是我们最常接触到的调试接口(例如通过USB转串口模块在电脑上查看单片机的打印信息)。 1. UART的核心概念 通用(Universal): 意味着它的通信参数(如速度、数据位、校验位等)是可配置的,可以与各种不同的设备进行通信。 异步(Asynchronous): 这是UART最核心的特点。与SPI和I²C不同,UART通信的双方没有共享的时钟线。发送方和接收方必须事先约定好相同的通信速率(波特率),接收方依靠数据信号本身的变化(起始位)来开始同步并按约定的速率接收数据。 收发器(Receiver/Transmitter): UART硬件模块通常包含一个独立的接收器和一个独立的发送器,因此可以同时进行数据的接收和发送,实现 全双工(Full-Duplex) 通信。 2....
时间复杂度与空间复杂度分析
在算法竞赛和面试中,根据题目给出的数据量大小来推断所需算法的时间复杂度和空间复杂度,是一项至关重要的核心技能。它能迅速排除掉不切实际的暴力解法,将思考方向聚焦在可行的算法上。 一、核心原理:计算机的运算速度这个推断方法的基石是对现代计算机运算速度的一个估算。在算法竞赛平台(如 LeetCode、Codeforces、牛客网等)上,评测机通常能在 1秒内执行大约 10^7 到 10^8 次计算操作。 为了保险起见,我们通常以 10^8 作为上限参考,但更稳妥的估算是 10^7。这意味着,如果你的算法总计算量超过了这个数量级,就很可能会“超时”(Time Limit Exceeded, TLE)。 基本公式:数据规模 N 带入 时间复杂度表达式 ≈ 总计算量 ≤ 10^8 二、时间复杂度 “速查表” (Cheat Sheet)这张表是解决算法问题的“第一直觉”,强烈建议记在心里。假设题目给出的主要数据规模是 N。 数据规模 N 的范围 可接受的时间复杂度 常见算法示例 N ≤ 10~12 O(N!)、O(N *...
Python中的装饰器
1. 开篇:为什么需要装饰器?装饰器的核心思想是在不修改被装饰对象(通常是函数或方法)的源代码和调用方式的前提下,为其增加额外的功能。 一个简单的问题场景假设我们有一个核心业务函数: 12def business_logic(): print("执行核心业务逻辑...") 现在,我们有一个新的需求:在每次执行这个函数前后,打印一条日志,记录函数的开始和结束。 不使用装饰器的解决方案方案一:直接修改函数代码(最差的方式) 1234def business_logic(): print("开始执行函数...") print("执行核心业务逻辑...") print("函数执行结束。") 弊端: 违反了开放/封闭原则:我们修改了函数的内部实现。 代码冗余:如果有很多函数都需要这个日志功能,我们就得在每个函数里重复添加这些代码,违反了 DRY (Don’t Repeat Yourself)...












