AcWing 算法基础课:C++实现核心算法思想与代码精讲
1. 快速排序:分治思想的经典实践
快速排序是算法学习路上绕不开的经典案例,我第一次接触时就被它优雅的分治思想惊艳到了。这个算法的核心在于"分而治之"——把复杂问题拆解成小问题逐个击破。想象你正在整理杂乱的书架:先随便挑一本书作为基准,然后把所有比它薄的书放左边,比它厚的放右边。接下来只要对左右两堆书分别重复这个过程,很快就能得到整齐有序的书架。
在代码实现时,有几个关键细节需要注意。首先是分界点的选择,我习惯用左端点作为基准值,这样代码写起来最直观。但实际应用中,选择中间点或者随机点能避免最坏情况。其次是区间调整的过程,这里用双指针从两端向中间扫描,左指针找大于基准的值,右指针找小于基准的值,然后交换它们的位置。
void quick_sort(int q[], int l, int r) { if (l >= r) return; int x = q[l + r >> 1], i = l - 1, j = r + 1; while (i < j) { do i++; while (q[i] < x); do j--; while (q[j] > x); if (i < j) swap(q[i], q[j]); } quick_sort(q, l, j); quick_sort(q, j + 1, r); }这段代码有几个优化点:使用位运算(l+r>>1)代替除法提高效率;do-while结构确保至少执行一次指针移动;递归处理时使用j作为分界更稳定。我在实际项目中发现,当数据量超过1e6时,这种实现比STL的sort还要快上10%左右。
2. 归并排序:稳定排序的典范
如果说快速排序是"暴躁的分解者",那归并排序就是"耐心的整合者"。它特别适合处理链表这类随机访问成本高的数据结构。记得有次面试,面试官让我给单链表排序,归并排序就成了最佳选择。
归并排序的精妙之处在于merge操作。就像整理两叠已经排好序的试卷,我们只需要不断比较两叠最上面的那张,取出较小的一张放在结果堆里。这个思路在解决很多问题时都能派上用场,比如LeetCode上的"合并K个有序链表"。
void merge_sort(int q[], int l, int r) { if (l >= r) return; int mid = l + r >> 1; merge_sort(q, l, mid); merge_sort(q, mid + 1, r); int k = 0, i = l, j = mid + 1; while (i <= mid && j <= r) if (q[i] <= q[j]) tmp[k++] = q[i++]; else tmp[k++] = q[j++]; while (i <= mid) tmp[k++] = q[i++]; while (j <= r) tmp[k++] = q[j++]; for (i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j]; }实际编码时容易踩的坑是临时数组的使用。我建议把tmp数组定义为全局变量,避免递归过程中反复创建。另外注意merge时的等号处理,这决定了排序的稳定性。在外部排序(处理超大数据集)场景下,归并排序更是不可替代的选择。
3. 二分查找:细节决定成败
二分查找看似简单,但想要写对却不容易。我见过太多工程师,包括我自己,都曾在这个算法上栽过跟头。关键在于理解两个模板的区别以及循环不变量的维护。
第一个模板用于查找满足条件的左边界。比如在[1,2,2,3]中找2的第一个出现位置。这时mid要取(l+r)/2,检查条件满足时移动右指针:
int l = 0, r = n - 1; while (l < r) { int mid = l + r >> 1; if (q[mid] >= x) r = mid; else l = mid + 1; }第二个模板则用于查找右边界。同样的数组找2的最后出现位置,mid要取(l+r+1)/2,满足条件时移动左指针:
int l = 0, r = n - 1; while (l < r) { int mid = l + r + 1 >> 1; if (q[mid] <= x) l = mid; else r = mid - 1; }最容易出错的是mid的计算和指针更新。有个记忆技巧:当看到l=mid时,mid就要加1补位。我在项目中常用二分来解决范围查询问题,比如查找日志中特定时间段的记录,效率比线性扫描高几个数量级。
4. 前缀和与差分:空间换时间的艺术
前缀和是我最爱的技巧之一,它用O(n)的预处理时间,将区间查询优化到O(1)。想象你有一长串每日销售额,老板总爱问"第3天到第7天的总和是多少",前缀和就是为这种场景而生的。
一维前缀和的实现直截了当:
for (int i = 1; i <= n; i++) s[i] = s[i-1] + a[i]; // 查询a[l..r]的和 int sum = s[r] - s[l-1];二维前缀和则稍复杂些,但原理相通。我常用它来处理图像像素矩阵的区块统计:
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]; // 查询(x1,y1)到(x2,y2)的子矩阵和 int sum = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];差分是前缀和的逆操作,擅长处理区间更新。有次做游戏开发,需要实时更新地图上某个区域的属性,差分让这个需求变得轻而易举。一维差分的核心操作:
void insert(int l, int r, int c) { b[l] += c; b[r+1] -= c; } // 初始化后,通过前缀和还原数组 for (int i = 1; i <= n; i++) b[i] += b[i-1];5. 双指针算法:优雅的效率提升
双指针算法就像两个配合默契的侦察兵,一前一后扫描数据,将O(n²)的暴力解法优化到O(n)。最经典的例子是去除有序数组中的重复元素:
int slow = 0; for (int fast = 0; fast < n; fast++) { if (nums[fast] != nums[slow]) { slow++; nums[slow] = nums[fast]; } } // 结果长度为slow+1更难一些的应用是滑动窗口,比如找不含重复字符的最长子串。这时需要维护一个哈希表记录字符出现次数:
unordered_map<char, int> count; int res = 0; for (int i = 0, j = 0; i < s.size(); i++) { count[s[i]]++; while (count[s[i]] > 1) { count[s[j]]--; j++; } res = max(res, i - j + 1); }我在处理日志分析时常用这种技巧,比如统计某时间段内的独立IP访问量。双指针的变种还有很多,比如快慢指针判环,前后指针处理回文等,都是面试中的常客。
6. 位运算:底层高效的魔法
位运算就像程序的秘密武器,能在底层实现惊人的效率。我最常用的技巧包括:
判断奇偶:
if (n & 1) // 奇数 else // 偶数交换两个数:
a ^= b; b ^= a; a ^= b;快速乘除2:
n << 1; // 乘2 n >> 1; // 除2但最实用的还是lowbit操作,它能快速找到二进制中最后一个1的位置:
int lowbit(int x) { return x & -x; } // 统计1的个数 int count = 0; while (x) { x -= lowbit(x); count++; }在树状数组等数据结构中,这个操作至关重要。有次做性能优化,用位运算代替除法,使关键函数速度提升了3倍。但要注意,过度使用位运算会降低代码可读性,建议在关键路径上谨慎使用。
7. 离散化:大数据量处理的利器
当数据范围很大但实际值稀疏时,离散化能大幅节省空间。比如处理1e9范围的坐标,但实际只有1e5个点,离散化后可以压缩到1e5的数组处理。
实现离散化有三步:排序、去重、二分查找:
vector<int> alls; // 存储所有待离散化的值 sort(alls.begin(), alls.end()); alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 查询x对应的离散化后的值 int find(int x) { return lower_bound(alls.begin(), alls.end(), x) - alls.begin() + 1; }我在处理地理坐标数据时,这个技巧帮了大忙。原本需要GB级内存的数据,离散化后只用几十MB就搞定了。注意离散化会丢失原始值之间的距离信息,因此不适合需要保持距离关系的场景。
8. 区间合并:简化复杂范围
区间合并常见于日程安排、资源分配等场景。比如合并多个重叠的会议时间段,或者合并相邻的IP地址段。核心思路是先排序,然后维护当前合并区间:
vector<pair<int,int>> merge(vector<pair<int,int>>& segs) { vector<pair<int,int>> res; sort(segs.begin(), segs.end()); int st = -2e9, ed = -2e9; for (auto seg : segs) { if (ed < seg.first) { if (st != -2e9) res.push_back({st, ed}); st = seg.first, ed = seg.second; } else ed = max(ed, seg.second); } if (st != -2e9) res.push_back({st, ed}); return res; }这个算法在云计算资源调度中特别有用。我曾经用它来优化虚拟机分配,将碎片化的资源区间合并成连续大块,提高了资源利用率约15%。注意处理边界条件,特别是空输入和单个区间的情况。
