当前位置: 首页 > news >正文

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%。注意处理边界条件,特别是空输入和单个区间的情况。

http://www.jsqmd.com/news/691923/

相关文章:

  • 中欧跨境电商车队推荐:可靠运输服务选择 - 品牌排行榜
  • 特征工程第一步:5分钟搞定sklearn方差过滤,让你的模型跑得更快更准
  • 国康私人医生:高端居家养老服务首选 - 资讯焦点
  • 对话式AI提示词工程:核心原则与实战技巧
  • SAM数据引擎:从人工标注到全自动掩码生成的演进之路
  • 从CPU指纹到安全检测:如何利用CPUID与LBR/BTS揪出隐藏的系统后门?
  • 2026年全国口碑好的ISO14064温室气体认证公司推荐,专业认证企业全解析 - myqiye
  • 微信时光机:用WeChatExporter永久珍藏你的对话回忆
  • 深入剖析 Docker 容器 D-Bus 连接报错:从原理到实战解决
  • 机器学习问答系统优化:应对概念漂移与性能挑战
  • Godot 4 实战:基于JSON数据与预制体动态构建可切换阵型的战斗场景
  • 2026年3月优质的商业计划书机构推荐,产业园区建设规划/节能评估报告,商业计划书咨询公司找哪家 - 品牌推荐师
  • 2026年3月激光淬火厂商推荐,十字轴激光熔覆/齿圈激光淬火/球铁行星架激光淬火/钛合金激光熔覆,激光淬火公司选哪家 - 品牌推荐师
  • 3步实现隐私安全的本地语音识别:TMSpeech终极实战指南
  • 思源黑体TTF构建深度解析:从源码到高质量字体的一键转换实战
  • 2026年贵州手提袋定制无起订量采购指南:本地现货快速交付方案 - 优质企业观察收录
  • 逆向实战:用Frida Hook搞定某小说App的AES加密数据(附完整脚本)
  • 3分钟学会Jable视频下载工具:Chrome插件+本地程序完整指南
  • Voxtral-4B-TTS-2603惊艳效果展示:印地语电影台词+德语古典音乐解说语音
  • 2026年本地GRS认证公司哪家好,实力强售后完善的品牌解读 - 工业品牌热点
  • 京东 e 卡提现至微信步骤专业解析 - 购物卡回收找京尔回收
  • 【2026最新版|收藏必备】Youtu-RAG开源框架详解:从入门到实战,小白也能玩转Agentic RAG大模型
  • 告别IDEA付费插件!用Eclipse+WindowBuilder免费搞定Java GUI界面设计(附IDEA项目迁移指南)
  • ZYNQ7035 PS读写PL端DDR3:从MIG IP核配置到C代码实战的保姆级避坑指南
  • 聊聊2026年商丘能提供可靠互联网营销方案的公司,怎么选择 - 工业品牌热点
  • GD32硬件I2C外设实战:从协议解析到驱动开发
  • 如何判断京东e卡98折回收平台的真假呢? - 购物卡回收找京尔回收
  • 漫谈2026年专业的本地有哪些GRS认证公司服务商,靠谱吗 - 工业推荐榜
  • Netty保姆级全解析|技术背景+核心知识点+生产实战教程
  • Ray Tune 超参数调优(上)