第一章:1024程序员节算法特辑导言
在每年的10月24日,我们迎来属于程序员的节日——1024程序员节。这一天不仅是对开发者辛勤付出的致敬,更是技术爱好者深入探讨核心编程技能的契机。算法,作为计算机科学的基石,贯穿于系统设计、数据处理与人工智能等各个领域,是衡量程序员逻辑思维与问题解决能力的重要标尺。为何算法如此重要
- 提升代码效率:高效的算法能显著降低时间与空间复杂度
- 应对复杂场景:在大数据、高并发系统中,算法优劣直接影响系统性能
- 技术面试核心:国内外一线科技公司普遍将算法作为筛选人才的关键环节
学习算法的实用建议
- 从基础数据结构入手,如数组、链表、栈、队列、树与图
- 掌握经典算法范式:分治、动态规划、贪心、回溯与图搜索
- 坚持每日一题,在实践中巩固理解并提升编码熟练度
示例:快速排序实现(Go语言)
// QuickSort 对整型切片进行原地排序
func QuickSort(arr []int, low, high int) {
if low < high {
// 获取分区索引
pi := partition(arr, low, high)
// 递归排序左右子数组
QuickSort(arr, low, pi-1)
QuickSort(arr, pi+1, high)
}
}
// partition 使用Lomuto分区方案
func partition(arr []int, low, high int) int {
pivot := arr[high] // 选择最后一个元素为基准
i := low - 1 // 较小元素的索引
for j := low; j < high; j++ {
if arr[j] <= pivot {
i++
arr[i], arr[j] = arr[j], arr[i]
}
}
arr[i+1], arr[high] = arr[high], arr[i+1]
return i + 1
}
常见算法时间复杂度对比
| 算法 | 最好时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 |
|---|---|---|---|
| 快速排序 | O(n log n) | O(n log n) | O(n²) |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) |
| 冒泡排序 | O(n) | O(n²) | O(n²) |
第二章:数组与字符串处理高频题解析
2.1 理论基础:双指针与滑动窗口思想
双指针技术核心
双指针通过两个变量在数组或链表上协同移动,降低时间复杂度。常见于有序数组的查找问题。滑动窗口机制
滑动窗口用于解决子数组/子串的最优化问题,通过维护一个可变窗口,动态调整左右边界。- 左指针控制窗口起始位置
- 右指针扩展窗口范围
- 条件满足时收缩左边界
for right := 0; right < n; right++ {
window += nums[right]
for window >= target {
minLen = min(minLen, right-left+1)
window -= nums[left]
left++
}
}
上述代码实现最小覆盖子数组查找。right 扩展窗口,left 在满足条件时收缩,维护最小长度。
2.2 实战应用:移除元素与原地去重技巧
在处理数组操作时,移除特定元素或实现原地去重是高频需求。这类操作要求在不分配额外空间的前提下高效完成。双指针技巧实现原地移除
使用快慢指针可在线性时间内完成元素过滤:func removeElement(nums []int, val int) int {
slow := 0
for fast := 0; fast < len(nums); fast++ {
if nums[fast] != val {
nums[slow] = nums[fast]
slow++
}
}
return slow
}
该函数中,fast 遍历所有元素,slow 指向下一个有效位置。仅当元素不等于 val 时才保留,最终返回新长度。
有序数组的原地去重
针对已排序数组,可跳过重复元素:func removeDuplicates(nums []int) int {
if len(nums) == 0 { return 0 }
write := 1
for read := 1; read < len(nums); read++ {
if nums[read] != nums[read-1] {
nums[write] = nums[read]
write++
}
}
return write
}
从索引 1 开始比较,避免越界,write 指针始终指向下一个唯一值的插入位置。
2.3 理论深化:前缀和与哈希优化策略
前缀和的基本思想
前缀和是一种预处理技术,通过构建数组前缀累加值,将区间求和查询从 O(n) 优化至 O(1)。适用于静态数组的高频区间查询场景。
vector prefix;
prefix.push_back(0); // 哨兵节点
for (int x : nums) {
prefix.push_back(prefix.back() + x);
}
// 查询 [l, r] 区间和
int sum = prefix[r+1] - prefix[l];
该代码构建了长度为 n+1 的前缀数组,prefix[i] 表示原数组前 i 个元素之和,利用差分实现快速查询。
结合哈希表的优化策略
在子数组目标和问题中,可通过哈希表记录前缀和首次出现位置,实现一次遍历求解最长子数组。- 维护当前前缀和 cur_sum
- 若 cur_sum - target 存在于哈希表,则找到合法区间
- 仅当当前前缀和未记录时写入,保证子数组最长
2.4 实战演练:最长无重复子串求解
在字符串处理中,寻找最长无重复字符的子串是一个经典问题。滑动窗口算法能高效解决此类问题。算法思路
使用双指针维护一个动态窗口,左指针控制窗口起始位置,右指针扩展窗口。通过哈希表记录字符最近出现的位置,当发现重复字符时,移动左指针跳过重复项。代码实现
func lengthOfLongestSubstring(s string) int {
lastSeen := make(map[byte]int)
left, maxLen := 0, 0
for right := 0; right < len(s); right++ {
if idx, found := lastSeen[s[right]]; found && idx >= left {
left = idx + 1
}
lastSeen[s[right]] = right
if newLen := right - left + 1; newLen > maxLen {
maxLen = newLen
}
}
return maxLen
}
上述代码中,lastSeen 记录每个字符最后一次出现的索引,left 和 right 构成滑动窗口边界。每当遇到已存在且位于当前窗口内的字符时,调整左边界。时间复杂度为 O(n),空间复杂度为 O(min(m,n)),其中 m 是字符集大小。
2.5 经典再现:接雨水问题的多角度剖析
问题本质与场景建模
接雨水问题是典型的数组区间处理问题,核心在于计算每个位置能容纳的水量。给定高度数组height,每个元素代表墙的高度,水会根据左右最高墙的短板决定积水量。
动态规划解法
通过预处理左右最大高度数组,可在 O(n) 时间内求解:
func trap(height []int) int {
n := len(height)
if n == 0 { return 0 }
leftMax, rightMax := make([]int, n), make([]int, n)
leftMax[0] = height[0]
for i := 1; i < n; i++ {
leftMax[i] = max(leftMax[i-1], height[i])
}
rightMax[n-1] = height[n-1]
for i := n-2; i >= 0; i-- {
rightMax[i] = max(rightMax[i+1], height[i])
}
water := 0
for i := 0; i < n; i++ {
water += min(leftMax[i], rightMax[i]) - height[i]
}
return water
}
该方法利用空间换时间,leftMax[i] 表示从左侧到 i 的最高墙,rightMax[i] 同理。每个位置积水为两侧最小峰值减去当前高度。
优化策略对比
- 双指针法可将空间复杂度降至 O(1)
- 单调栈法适合理解“凹槽”形成过程
- 动态规划逻辑清晰,易于扩展至二维接雨水场景
第三章:链表操作核心题目精讲
3.1 链表反转的递归与迭代实现
迭代法实现链表反转
使用三个指针逐节点翻转链接方向,时间复杂度为 O(n),空间复杂度为 O(1)。
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
next := curr.Next // 临时保存下一个节点
curr.Next = prev // 反转当前节点指针
prev = curr // 移动 prev 和 curr
curr = next
}
return prev // 新头节点
}
其中 prev 初始为空,逐步将每个节点的 Next 指向前驱,完成整体反转。
递归实现方式
递归从尾部开始构建反向连接,需确保最后一层返回的是原链表的最后一个节点。
- 递归终止条件:到达末节点或空节点
- 回溯过程中修改指针方向
- 每层返回相同的最终头节点
3.2 环形链表检测与入口定位原理
快慢指针检测环的存在
使用快慢指针(Floyd判圈算法)可高效判断链表中是否存在环。慢指针每次移动一步,快指针移动两步,若两者相遇则说明存在环。
public boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针前进一步
fast = fast.next.next; // 快指针前进两步
if (slow == fast) return true; // 相遇则有环
}
return false;
}
该算法时间复杂度为 O(n),空间复杂度 O(1)。
定位环的入口节点
当检测到环后,将一个指针重置到头节点,两指针同步逐位移动,再次相遇点即为环入口。- 设链表头到入口距离为 a
- 入口到相遇点距离为 b
- 环剩余部分为 c,则 b + c 为环周长
3.3 合并两个有序链表的高效写法
在处理链表合并问题时,关键在于利用两个链表已排序的特性,通过双指针策略降低时间复杂度。核心思路:双指针迭代法
维护两个指针分别指向两个链表的当前节点,逐个比较值大小,将较小的节点接入结果链表。func mergeTwoLists(l1, l2 *ListNode) *ListNode {
dummy := &ListNode{}
cur := dummy
for l1 != nil && l2 != nil {
if l1.Val <= l2.Val {
cur.Next = l1
l1 = l1.Next
} else {
cur.Next = l2
l2 = l2.Next
}
cur = cur.Next
}
if l1 != nil {
cur.Next = l1
} else {
cur.Next = l2
}
return dummy.Next
}
上述代码中,dummy 节点简化了头节点处理逻辑,循环结束后剩余部分直接拼接。时间复杂度为 O(m + n),空间复杂度为 O(1),是目前最优解法。
第四章:二叉树遍历与递归思维训练
4.1 深度优先搜索的三种遍历模式
深度优先搜索(DFS)在树与图结构中广泛应用,其核心可分为三种遍历模式:前序、中序和后序。这些模式决定了节点访问的时机。前序遍历:根-左-右
常用于复制树结构或路径记录。访问顺序为先处理当前节点,再递归左子树和右子树。// 前序遍历递归实现
func preorder(root *TreeNode) {
if root == nil {
return
}
fmt.Println(root.Val) // 先访问根节点
preorder(root.Left)
preorder(root.Right)
}
该代码先输出当前节点值,确保根节点优先处理,适合构建前缀结构。
中序与后序:排序与依赖解析
中序遍历(左-根-右)适用于二叉搜索树的有序输出;后序(左-右-根)常用于释放内存或计算子树结果。- 中序:输出BST时生成升序序列
- 后序:删除节点前先清理子节点
4.2 广度优先搜索与层序遍历实现
广度优先搜索(BFS)是树与图结构中常用的遍历策略,尤其适用于寻找最短路径或按层级处理节点。在二叉树中,BFS通常体现为层序遍历。基本实现思路
使用队列(Queue)结构实现BFS:从根节点开始入队,每次出队一个节点并访问其值,同时将其子节点依次入队,直到队列为空。
func levelOrder(root *TreeNode) []int {
if root == nil {
return nil
}
var result []int
queue := []*TreeNode{root}
for len(queue) > 0 {
node := queue[0] // 取出队首节点
queue = queue[1:] // 出队
result = append(result, node.Val)
if node.Left != nil {
queue = append(queue, node.Left) // 左子入队
}
if node.Right != nil {
queue = append(queue, node.Right) // 右子入队
}
}
return result
}
上述代码通过切片模拟队列,逐层访问节点。时间复杂度为 O(n),每个节点入队出队一次;空间复杂度最坏为 O(n),即完全二叉树最后一层节点数接近 n/2。
4.3 二叉搜索树的性质与验证方法
二叉搜索树的核心性质
二叉搜索树(BST)满足:对于任意节点,其左子树所有节点值均小于该节点值,右子树所有节点值均大于该节点值,且左右子树均为二叉搜索树。递归验证方法
通过维护上下界递归检查每个节点是否符合BST条件:func isValidBST(root *TreeNode) bool {
return validate(root, nil, nil)
}
func validate(node *TreeNode, min, max *int) bool {
if node == nil {
return true
}
if min != nil && node.Val <= *min {
return false
}
if max != nil && node.Val >= *max {
return false
}
left := validate(node.Left, min, &node.Val)
right := validate(node.Right, &node.Val, max)
return left && right
}
该实现通过传递最小值和最大值指针,确保当前节点值在合法区间内。每次递归更新边界,左子树上限为父节点值,右子树下限为父节点值,从而保证全局有序性。
4.4 最近公共祖先问题的递归解法
在二叉树中寻找两个节点的最近公共祖先(LCA)是典型的递归应用场景。通过后序遍历,可以自底向上回溯,判断当前节点是否为所求祖先。递归思路解析
若当前节点等于 p 或 q,则直接返回;否则递归处理左右子树。只有当两子树均返回非空时,说明当前节点为 LCA。- 递归终止条件:节点为空或等于 p/q
- 左右子树分别递归查找
- 合并结果:一空则返另一,双非空则为 LCA
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
if root == nil || root == p || root == q {
return root
}
left := lowestCommonAncestor(root.Left, p, q)
right := lowestCommonAncestor(root.Right, p, q)
if left != nil && right != nil {
return root
}
if left != nil {
return left
}
return right
}
代码中,left 和 right 分别表示在左右子树中找到的目标节点。若两者均存在,说明 root 为 LCA;否则返回非空一侧。
第五章:结语——从笔试到大厂Offer的成长路径
构建扎实的算法与数据结构基础
大厂笔试中,70%以上的题目围绕数组、链表、二叉树和动态规划展开。建议每日刷题保持手感,重点掌握以下模式:- 滑动窗口解决子数组问题
- 快慢指针检测环或找中点
- DFS/BFS在树与图中的遍历应用
系统设计能力的实战提升
面对“设计短链服务”类面试题,需具备可扩展思维。以下是核心模块拆解示例:| 模块 | 技术选型 | 关键考量 |
|---|---|---|
| ID生成 | Snowflake | 全局唯一、高并发支持 |
| 存储 | Redis + MySQL | 缓存穿透、一致性哈希 |
代码实现中的细节把控
在实现LRU缓存时,Go语言可通过组合双向链表与哈希表高效完成:
type LRUCache struct {
capacity int
cache map[int]*list.Element
list *list.List
}
type entry struct {
key, value int
}
func (c *LRUCache) Get(key int) int {
if elem, ok := c.cache[key]; ok {
c.list.MoveToFront(elem)
return elem.Value.(*entry).value
}
return -1
}
行为面试中的STAR法则应用
项目经历描述应遵循STAR结构:
Situation → Task → Action → Result
例如:“在支付系统优化中(S),响应延迟高于500ms(T),引入本地缓存+异步落库(A),QPS提升3倍,P99延迟降至80ms(R)”

被折叠的 条评论
为什么被折叠?



