递归与回溯:从汉诺塔到全排列
递归是很多人学习路上的一道坎。 它的反直觉之处在于:你没法在脑子里完整地展开整个递归过程,越想越晕。 但其实递归有套路可循,掌握了套路,就能”以不变应万变”。 递归的三要素任何递归问题,都必须明确三件事: 递归的定义:这个函数要做什么?(返回什么、有什么效果) 递归的终止条件:什么时候不再递归,直接返回? 递归的递推关系:怎么把大问题拆成子问题? 以经典的斐波那契为例: 1234int fib(int n) { if (n <= 1) return n; // 终止条件 return fib(n-1) + fib(n-2); // 递推关系} 想递归的正确姿势新手最容易犯的错,是在脑子里展开递归树——“fib(5) 调用 fib(4),fib(4) 又调用 fib(3)……”,然后彻底乱掉。 正确姿势是只关注当前这一层: 假设子问题已经解决了(相信递归) 只思考”怎么用子问题的结果,得到当前问题的结果” 然后补上终止条件 就像数学归纳法:假设 n-1 成立,证明 n 成立,再验证 n=1 成立。你不...
二分查找:边界条件为什么总是写错
二分查找有个著名的梗:“一看就会,一写就错”。 甚至有人统计过,能一次写对二分查找的工程师不到 20%。 问题几乎全出在一个地方:边界条件。今天把它彻底讲透。 二分查找的前提二分查找只能用于有序数组(或单调序列)。 它的核心思想:每次取中间值,和目标比大小,直接排除一半。 1234567891011121314int binarySearch(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { left = mid + 1; } else { ...
排序算法:快排、归并、堆排,理解比背诵重要
排序算法大概是算法入门绕不开的第一课,也是面试里被问得最多的话题之一。 但很多人学排序的方式是”背代码”,结果背了忘、忘了背。 其实只要抓住每个算法的核心思想,代码自然就能写出来。 先记住这个底线基于比较的排序算法,时间复杂度有理论下界:O(n log n)。 也就是说,任何”靠比较大小来排序”的算法,最优也就是 O(n log n),不可能更快(除非用计数排序、桶排序这种不基于比较的特殊算法)。 所以快排、归并、堆排,都是已经摸到比较排序天花板的算法。 快速排序:分治 + 分区快排的核心是分区(partition):选一个”基准值(pivot)”,把数组分成两部分——左边都比 pivot 小,右边都比 pivot 大。然后对左右两部分递归做同样的事。 123456void quickSort(int[] arr, int lo, int hi) { if (lo >= hi) return; int p = partition(arr, lo, hi); // 分区,返回 pivot 位置 quickSort(arr, lo, p - 1);...
图与图的遍历:BFS、DFS 与拓扑排序
前面讲的都是”线性”(数组、链表)和”层次”(树)结构。但现实中有大量关系是”网状”的: 社交网络里,谁关注谁 城市之间,怎么导航 项目里,任务之间的依赖 这些用树表达不了,因为树不允许”环”,而现实关系到处是环。这时候就需要图。 图的基本概念图由**顶点(Vertex)和边(Edge)**组成。 按边的方向,分为: 无向图:边没有方向(如”互为好友”) 有向图:边有方向(如”A 关注了 B”,单向) 按边是否带权重,分为: 无权图:边都一样 带权图:边有权重(如”两个城市之间的距离”) 图的两种存储方式1. 邻接矩阵用一个二维数组 matrix[i][j] 表示”从 i 到 j 是否有边”。 优点:判断”i 和 j 是否有边”是 O(1),简单直观 缺点:空间 O(n²),稀疏图(边很少)会浪费大量空间 2. 邻接表每个顶点维护一个”它指向的邻居”列表。 优点:空间 O(V + E),稀疏图很省 缺点:判断”i 和 j 是否有边”要遍历 i 的邻居列表,O(度数) 工程上邻接表更常用,因为现实中的图大多是稀疏的。 广度优先遍历(BFS)BFS 按”层”逐层向...
Docker 从入门到生产实践
Docker 从入门到生产实践 Docker 的价值不在于”会跑一个容器”,而在于把”环境一致性”从口头承诺变成工程事实。本文聚焦真正影响生产环境的几个关键点。 镜像分层:缓存用对了,构建快 10 倍Dockerfile 的每一行指令都会生成一层。构建缓存能否命中,取决于指令和上下文是否变化。 一个反面例子: 1234# 差:每次代码变动都让 COPY 之后的层全部失效FROM openjdk:17COPY . /appRUN mvn package 依赖下载是构建中最慢的环节,却每次都要重跑。正确做法是先拷贝依赖清单,再拷贝源码: 123456FROM openjdk:17WORKDIR /appCOPY pom.xml .RUN mvn dependency:go-offlineCOPY src ./srcRUN mvn package -DskipTests 依赖层只有在 pom.xml 变化时才重建,日常改动源码时构建秒级完成。 镜像瘦身:从 800MB 到 120MB大镜像的问题不只是占磁盘,更影响部署速度——每台机器都要拉一遍。 瘦身三板斧: 多阶段构建:构建...
依赖注入:为什么你的代码不该自己 new 对象
上一篇讲 SOLID 时,最后一条”依赖倒置原则(DIP)”是最难的。而依赖注入(DI)就是 DIP 的落地实现。 如果你用过 Spring,那你天天都在用 DI,只是可能没想过它背后的道理。 没有 DI 的世界先看一段”正常”的代码: 1234public class OrderService { private OrderRepository repository = new MySQLOrderRepository(); private EmailSender emailSender = new SmtpEmailSender();} 这段代码有什么问题? 强耦合:OrderService 直接依赖了具体实现 MySQLOrderRepository。要换数据库,就得改 OrderService。 难测试:想测试 OrderService,没法单独测,因为它内部硬绑定了真实的数据库和邮件服务。 重复创建:每个用到 OrderRepository 的类都自己 new 一个,浪费资源且状态难统一。 依赖注入的解法DI 的思路是:对象不再自己...
单元测试与 TDD:先写测试再写代码,真的值得吗
写代码十年,我发现一个规律:越资深的工程师,越重视测试。 而新手最常见的误区是:觉得写测试是”额外负担”,是”拖慢进度”的事。 其实恰恰相反——测试是帮你提效的,不是给你添堵的。 为什么要写单元测试1. 让你敢重构没有测试的代码,重构就是赌博。你改了 A 处,可能偷偷破坏了 B 处,而你自己完全不知道。 有了测试,重构完跑一遍测试,全绿就说明行为没变。测试是重构的安全网。 2. 让 bug 更早暴露bug 越晚发现,修复成本越高。单元测试能在开发阶段就抓住大部分逻辑错误,而不是等到线上出问题才返工。 3. 测试本身就是文档一份好的测试用例,比注释更能说明”这段代码到底该怎么用、边界条件是什么”。 测试金字塔一个常见的分层模型: 1234567 /\ / \ E2E 测试(少,慢,贵) /----\ / \ 集成测试(中) /--------\ / \ 单元测试(多,快,便宜)/------------\ 原则:底层多写,顶层少写。 单元测试快、便宜、稳定,应该是主力。E2E 测试慢、贵、脆弱,只...
函数式编程范式:纯函数、不可变数据与高阶函数
如果你长期写面向对象的代码,第一次接触函数式编程(FP)可能会觉得”别扭”。 但理解之后你会发现,它不是要取代面向对象,而是给了你另一种处理复杂问题的思维方式。 函数式编程的核心思想函数式编程有三个核心概念,理解了它们就抓住了 FP 的精髓。 1. 纯函数(Pure Function)纯函数满足两个条件: 同样的输入,永远得到同样的输出(确定性) 不产生任何副作用(不修改外部状态) 12345678910// 纯函数:输入相同,输出必然相同int add(int a, int b) { return a + b;}// 非纯函数:依赖并修改了外部状态int counter = 0;int increment() { return ++counter;} 为什么纯函数好?因为: 容易测试:不需要 mock 一堆状态 容易推理:看输入输出就能理解,不用追踪外部状态 天生线程安全:没有共享可变状态,就没有并发问题 2. 不可变数据(Immutability)函数式编程倾向于使用不可变数据:对象一旦创建,就永不改变。要”修改”...
