课程定位:面向计算机编程三级(GESP Level
3)学员的系统化自学教材与实战讲义。
适用对象:已掌握 C++
基本语法(顺序、分支、基础循环),希望系统攻克计算机底层数据编码、进位制转换、位运算微操、一维数组进阶、字符串处理、枚举算法与模拟算法的编程自学者。
教学特色:坚持“第一性原理”,拒绝死记硬背。从概念起源、数学直觉与内存图景出发,辅以步进手推表(Dry
Run)、规范代码范式与经典例题精解,建立完整严密的算法与编程思维体系。
欢迎进入 GESP 三级系统的学习殿堂!
在一级和二级阶段,你学习了变量、算术表达式、分支条件(if-else、switch)和循环语句(for、while)。那个阶段的核心任务是**“熟悉语法,把日常逻辑翻译成代码”**。
而从三级开始,你的编程学习将发生质的跃迁:
┌─────────────────────────────────────────────────┐
│ GESP 三级知识大厦架构 │
└───────────────────────┬─────────────────────────┘
│
┌──────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 底层原理篇 │ │ 核心结构篇 │ │ 算法思维篇 │
├──────────────┤ ├──────────────┤ ├──────────────┤
│· 原码反码补码│ │· 一维数组应用│ │· 算法与流程图│
│· 进位制转换 │ │· 字符串与文本│ │· 枚举算法优化│
│· 比特级位运算│ │· 桶与频数统计│ │· 规则时序模拟│
└──────────────┘ └──────────────┘ └──────────────┘
自学建议:
本讲核心目标:理解计算机底层为什么只有加法器;掌握正负数在机器中的表示形式;吃透原码、反码、补码的推导法则;理解 8 位二进制数的范围为什么是 。
我们在纸上计算减法很简单:。 但是在真实的计算机 CPU 内部,硬件设计极其追求极简与高速。制造一个既能做加法又能做减法的“加减法器”,电路极其复杂且成本高昂。工程师们提出了一个天才的想法:能不能把减法变成加法?
如果能用一套统一的二进制编码,让加法电路把负数加进去之后,自然得出正确的结果,硬件电路就能省下一半的成本!这就是机器数编码诞生的起源。
想象你手腕上的机械钟表,现在指针指向 6 点,你想让它指向 3 点,你有两种办法:
在表盘这个系统里,减 3 和加 9 的效果是完全等价的! 这里的 就叫做**“模(Modulus)”, 就是 针对模 的“补数(Complement)”**。
在有限位数的计算机存储单元里(例如 8 位 byte,满 就会像表盘一样自然溢出归零),减去一个数,等价于加上它的补码!
在定长二进制数中,最高位(最左边的一位)被规定为符号位:
0 表示正数(Positive);1 表示负数(Negative)。 其余各位称为“数值位”。定义:符号位加上数值本身的绝对值二进制。
以 8 位二进制为例:
0000 01011000 0101如果直接用原码做加法,计算 :
0000 0001 (+1 的原码)
+ 1000 0001 (-1 的原码)
───────────
1000 0010 (结果是 -2 的原码!计算完全错误!)
不仅如此,原码还存在两个 0:0000 0000 是
,1000 0000
是
。两个零在逻辑判断中会造成巨大的混乱。因此,原码不能直接参与计算机内部运算。
为了尝试解决减法问题,先驱们设计了反码:
以 8 位二进制为例:
0000 01011000 0101
符号位不变,后面 7 位翻转
1111 1010反码可以计算某些加法,但仍然存在
(0000 0000)和
(1111 1111)并存的尴尬,且末位需要额外循环进位。
在补码体系中,彻底消灭了“负零”,并且加减法完美统一!
规则 1:正数的原码、反码、补码完全相同!(三码合一)
规则 2:负数的补码 = 其反码 + 1。
数值位取反 最后一位加 1
负数原码 ───────────────────────> 负数反码 ───────────────────> 负数补码
(符号位为 1) (符号位不变) (逢二进一)
000 0101;1000 0101;1111 1010;1111 1011。 0000 0101 (+5 的补码)
+ 1111 1011 (-5 的补码)
───────────
1 0000 0000 (最高位溢出的 1 超出 8 位,被硬件自然丢弃)
= 0000 0000 (结果正好是 0!毫无偏差!)
口诀:从右往左数,找到第一个 1,这个
1 及其右边的所有 0 保持不变,该 1
左边的所有位(符号位除外)全部取反!
1 0 0 0 0 1 0 1
最右边的 1 在最后一位;1 1 1 1 1 0 1 1,一步出结果!8 位二进制一共有 种不同的 0/1 组合:
0000 0000 到 0111 1111:对应十进制的
(共
128 个正数和零);1111 1111 到 1000 0001:对应十进制的
(共
127 个负数);1000 0000。它到底代表什么?在原码和反码里,1000 0000 曾被浪费在
上。但在补码数学体系中:
因此,计算机将 1000 0000 赋予了
!它没有对应的
8 位原码和反码。
📌 核心结论:有符号整数的表示范围
- 8 位有符号数(C++ 的
char):,即 ;- 16 位有符号数(C++ 的
short):,即 ;- 32 位有符号数(C++ 的
int):,即 。
请拿出一张草稿纸,独立手推以下三题,做完后再点击查看解析:
1111 0010,求它对应的十进制真值;0000 1100
()
与 1111 0101
(),写出运算过程与最终结果。000 1101;1000 1101;1111 0010;1111 0011。1111 0010 求真值:
1000 1101;1000 1110; 0000 1100 (+12)
+ 1111 0101 (-11)
───────────
1 0000 0001 (进位 1 溢出丢弃)
= 0000 0001 (对应十进制 +1,计算完全正确!)本讲核心目标:理解基数与位权的概念;熟练掌握二进制、八进制、十进制、十六进制之间的数学转换原理(按权展开法、短除法、分组映射法);能用 C++ 代码实现任意进制转换。
我们从小学一年级学数学,最习惯十进制。为什么十进制里 代表三百二十五? 因为每一个位置上的数字,其重要程度是不一样的:
这里包含两个核心概念:
进位制的通则:“逢基数进一,借一当基数”。
| 进位制 | 允许使用的数字字符 | C++ 字面量前缀 | 缩写标记 | 典型应用场景 |
|---|---|---|---|---|
| 二进制 (Binary) | 0, 1 |
0b (如 0b1011) |
B | 底层晶体管高低电平 |
| 八进制 (Octal) | 0, 1, 2, 3, 4, 5, 6, 7 |
0 (如 075) |
O / Q | Linux 文件权限管理(如 777) |
| 十进制 (Decimal) | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 |
无前缀 | D | 人类日常生活认知 |
| 十六进制 (Hexadecimal) | 0~9, A, B, C, D, E, F |
0x (如 0x2F) |
H | 内存地址、网页颜色编码 (#FF0000) |
📌 十六进制字母的数字对应(必背):
A = 10, B = 11, C = 12, D = 13, E = 14, F = 15。
通用公式:对于任意 进制数 ,其等价的十进制值为:
十进制转 进制采用短除法:
将十进制 53 转换为二进制:
2 | 53
+────
2 | 26 ...... 余数 1 (最后一位 / 低位) ↑
+──── │
2 | 13 ...... 余数 0 │ 倒
+──── │ 序
2 | 6 ...... 余数 1 │ 抄
+──── │ 录
2 | 3 ...... 余数 0 │
+──── │
2 | 1 ...... 余数 1 │
+──── │
0 ...... 余数 1 (第一位 / 高位) │
结论:(53)₁₀ = (110101)₂
因为 ,,它们之间不需要经过十进制折腾,直接“按位打包”即可!
001 101 011
1 5 3
。0001 1010 1101
1 A(10) D(13)
。二进制: 0 0 0 1 1 0 1 0 1 1 0 1
位权: 8 4 2 1 8 4 2 1 8 4 2 1
分值: 1 8+2=10(A) 8+4+1=13(D)
十六进制: 1 A D
在编程题中,常常要求我们把一个十进制数转换成 进制()。请研读并敲写下面的核心代码模板:
#include <bits/stdc++.h>
using namespace std;
// 字符映射表:下标 0~15 对应字符 '0'~'F'
const string DIGITS = "0123456789ABCDEF";
// 函数功能:将十进制数 n 转换成 k 进制字符串
string toBase(long long n, int k) {
if (n == 0) return "0"; // 边界特判:0 本身返回 "0"
string result = "";
while (n > 0) {
int remainder = n % k; // 除基取余
result += DIGITS[remainder]; // 记录余数对应的字符
n /= k; // 商继续参与下一轮除法
}
// 因为是倒序取余,所以要把字符串反转回来
reverse(result.begin(), result.end());
return result;
}
int main() {
long long n = 675;
cout << "十进制 675 转二进制: " << toBase(n, 2) << endl;
cout << "十进制 675 转八进制: " << toBase(n, 8) << endl;
cout << "十进制 675 转十六进制: " << toBase(n, 16) << endl;
return 0;
}本讲核心目标:理解位运算的直接硬件执行属性;吃透 6 种位运算符的运算规则与真值表;深刻认识位运算优先级的陷阱;掌握奇偶判断、乘除倍增、异或魔力与状态掩码等经典实战应用。
日常算术运算(如加减乘除)是面向“数值”的。而位运算(Bitwise Operators)是面向“内存中的每一个二进制比特位(Bit)”的。 因为 CPU 内部的逻辑门电路(AND门、OR门、XOR门)天然就能一步完成位操作,位运算不需要进位判断与除法器介入,运算耗时仅需一个 CPU 时钟周期(纳秒级),速度极其惊人!
我们用一个比特位(只有 0 和 1)来说明逻辑:
&,AND)|,OR)^,XOR)~,NOT)x,在补码体系下永远有数学恒等式:~x = -(x + 1)。比如
~5 = -6。<<,Left
Shift)x << k
等价于
!3 << 2
。>>,Right
Shift)x >> k 等价于
(向下整除)!25 >> 2
。六大位运算速查对比表(设 a = 5 即 0101B, b = 3 即 0011B):
a: 0 1 0 1 (5)
b: 0 0 1 1 (3)
─────────────────────
a & b: 0 0 0 1 (1)
a | b: 0 1 1 1 (7)
a ^ b: 0 1 1 0 (6)
~a: ...11111010 (-6)
a << 1: 1 0 1 0 (10)
a >> 1: 0 0 1 0 (2)
请在脑海中建立极度深刻的警惕:位运算符的优先级低于大多数算术和关系运算符!
// 惨剧现场:
if (n & 1 == 0) // 你的本意是判断 n 是偶数因为比较运算符 == 的优先级高于
&,编译器会先执行 1 == 0(结果是
false 即 0),整个表达式变成了:
if (n & 0) // 无论 n 是几,n & 0 永远是 0 (假)!条件永远不成立!🛡️ 黄金保命法则:写位运算,必须无条件加圆括号!
正确写法:if ((n & 1) == 0)
if ((n & 1) == 1) {
// 奇数(二进制最末位是 1)
} else {
// 偶数(二进制最末位是 0)
}a ^= b;
b ^= a;
a ^= b; // 此时 a 和 b 的值已完成对调!💡 经典面试与认证真题:寻找唯一落单的数
题目:给出 个整数,其中有 对数字两两成对出现,只有一个数字单独出现一次。找出该数字。
解法:根本不需要开数组统计频数!把所有输入的数字全部^(异或)在一起:成对出现的数字 全部抵消归零,剩下的就是那个落单的值!时空复杂度 时间、 空间!
n & (n - 1)这是一个精妙绝伦的数学技巧: 任何一个正整数
,其二进制形式形如
...100...0。
会将最右边的那个 1 变成 0,而后面的
0 全变成 1。两者一做 &
操作,正好精准抹除最右边的那个 1!
n: 1 0 1 1 0 0
n - 1: 1 0 1 0 1 1
────────────────────
n&(n-1): 1 0 1 0 0 0 (原来第三位的 1 消失了,其余保持不变!)
应用:统计二进制中有几个 1(汉明重量)
int countOnes(int n) {
int count = 0;
while (n > 0) {
n = n & (n - 1); // 每次抹掉一个 1
count++;
}
return count;
}本讲核心目标:理解算法的严格定义与五大特征;熟练掌握流程图的绘制标准与图形规范;学会阅读伪代码并使用草稿纸变量跟踪法(Dry Run)推演算法执行逻辑。
如果说编程语言是“乐器”,那么算法就是“乐谱”。
算法(Algorithm)是解题方案的准确而完整的描述,是一系列解决问题的清晰指令。
并不是所有的过程都能被称为算法。一个合格的计算机算法必须同时满足以下 五大基本特征(选择题极高频考点):
流程图用规范的几何图形来表达算法的控制流,是程序设计的重要蓝图。CCF GESP 官方考纲严格要求掌握以下几何图形的语义:
| 图形符号 | 几何名称 | 标准功能含义 | 范例 |
|---|---|---|---|
| ╭──────╮ │ │ ╰──────╯ |
圆角矩形 / 椭圆形 | 起止框:表示算法的开始(Start)或结束(End) | 开始 / 结束 |
| /──────/ / / /──────/ |
平行四边形 | 输入/输出框:表示读取外部数据或打印输出 | 输入 / 输出 |
| ┌──────┐ │ │ └──────┘ |
普通矩形 | 处理框:执行数据赋值、数学计算等操作 | |
| /\ / \ \ / / |
菱形 | 判断/分支框:包含一个条件表达式,根据成立与否分出
True 和 False 两条流向 |
? / ? |
| ───────► | 带箭头的流线 | 流程线:指出控制流下一步的执行方向 | 指向下一个处理步骤 |
伪代码是一种介于自然语言和计算机语言之间的文字,它舍弃了严格的语法符号(如头文件、分号),专注于表达算法的逻辑控制。
遇到伪代码阅读题,绝不要光靠大脑空想!最佳习惯是:在纸上列一个表格,表头写上所有变量名,跟着循环一步一步写下每个变量在每一时刻的数值变迁。
输入 N
a <- 1
b <- 1
i <- 3
WHILE i <= N DO
c <- a + b
a <- b
b <- c
i <- i + 1
END WHILE
输出 b
草稿纸 Dry Run 跟踪表(假设输入 ):
| 步数 / 事件 | 循环条件 | |||||
|---|---|---|---|---|---|---|
| 初始化 | 3 | 5 | 1 | 1 | - | 满足 () |
| 第 1 轮循环 | 3 | 5 | 1 | 2 | 2 | 变为 4 |
| 第 2 轮循环 | 4 | 5 | 2 | 3 | 3 | 变为 5 |
| 第 3 轮循环 | 5 | 5 | 3 | 5 | 5 | 变为 6 |
| 终止判断 | 6 | 5 | - | - | - | 不满足 (),退出 |
输出结果:b = 5。
算法本质识别:这正是著名的**斐波那契数列(Fibonacci
Sequence)**递推!
本讲核心目标:理解一维数组在内存中的连续存储模型与寻址原理;掌握 0-index 下标规范与越界防范;精通一维数组的六大核心操作范式(流式遍历、累加均值、打擂极值、频数统计、就地逆序、桶数组思想)。
假设班上有 50 名学生,我们要计算平均分,并找出所有高于平均分的学生。
如果没有数组,你需要定义 50
个变量:int s1, s2, s3, ..., s50;。 读入需要写 50 行
cin,求和需要写 50
个加号。更绝望的是,算完平均分后,你无法用一个简单的循环把这 50
个变量逐一比较!
数组(Array)应运而生:它是一种将相同类型的多个数据,在内存中按顺序连续存放的数据结构。
数组名: a
物理内存: [ 85 ] [ 92 ] [ 78 ] [ 64 ] [ 99 ]
元素下标: 0 1 2 3 4 (长度为 5)
在 C++ 中,当你写下 int a[5]; 时:
a
本质上就是这段内存的首地址(起始地址);⚠️ 一生的警钟:严防数组下标越界(Out-of-Bounds)!
int a[100];的合法下标永远是0到99!
绝对没有a[100]!访问a[100]会去读写不属于你的未知内存,可能导致程序崩溃(RE)、得到乱码垃圾值,或者在正式评测时直接被判 0 分!
| 对比维度 | 全局数组(定义在 main 函数外) |
局部数组(定义在函数内部) |
|---|---|---|
| 存放区域 | 静态数据区(数据段/BSS) | 系统栈(Stack) |
| 可用空间 | 极大(轻松开到几千万字节,如 级) | 极小(通常仅几 MB,开到 必闪退崩溃) |
| 默认初始值 | 所有格子自动清零为 0 | 未定义垃圾随机数! |
💡 B 班实战守则:只要数组元素个数超过 1000,统一写在
main函数外面(全局)!
#include <bits/stdc++.h>
using namespace std;
// 建议开在全局,多开 10 个格子以绝越界隐患
const int MAXN = 1005;
int a[MAXN];
int main() {
int n;
if (!(cin >> n)) return 0;
// 范式 1:顺序读取
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// 范式 2:累加求和与防截断平均分
long long sum = 0;
for (int i = 0; i < n; i++) {
sum += a[i];
}
// 必须用 (double) 强转,否则整数除法会丢掉小数
double avg = (double)sum / n;
// 范式 3:打擂台法找最大值与其位置
int max_val = a[0]; // 先让第 0 号当擂主
int max_idx = 0;
for (int i = 1; i < n; i++) {
if (a[i] > max_val) {
max_val = a[i];
max_idx = i;
}
}
// 范式 4:条件统计(统计超过平均分的人数)
int above_cnt = 0;
for (int i = 0; i < n; i++) {
if (a[i] > avg) {
above_cnt++;
}
}
// 范式 5:首尾双指针就地逆序翻转
int left = 0, right = n - 1;
while (left < right) {
swap(a[left], a[right]);
left++;
right--;
}
// 打印结果
cout << "平均分: " << fixed << setprecision(2) << avg << endl;
cout << "最高分: " << max_val << " 出现在下标 " << max_idx << endl;
cout << "超均分人数: " << above_cnt << endl;
cout << "逆序输出: ";
for (int i = 0; i < n; i++) cout << a[i] << " ";
cout << endl;
return 0;
}假设考试成绩在
分之间,有 10 万名考生的分数,如何统计每个分数出现的次数?
传统方法是对每个分数去搜索比对,效率极低。
桶思想(Bucket / Frequency
Array):用数组的“下标”直接代表“数值本身”!
int cnt[105] = {0}; // cnt[s] 存储分数为 s 的学生总人数
for (int i = 0; i < n; i++) {
int score;
cin >> score;
cnt[score]++; // 听到这个分数,对应的桶里加 1 票
}
// 统计完毕,如果想知道得 100 分的有几人:直接看 cnt[100]!本讲核心目标:理解字符
char与 ASCII 的底层映射;掌握 C++ 标准库string的对象模型;彻底破解cin与getline的换行符缓冲区死锁;熟练运用子串提取、检索匹配、替换反转与字符库工具。
char 与 ASCII
码的本质计算机内部没有文字,每一个字符在内存中本质上都是一个 8 位整数(即 ASCII 编码):
'0' 的码值是 48;'A' 的码值是 65;'a' 的码值是 97;' ' 的码值是 32。📌 字符运算两大黄金公式:
- 数字字符转实际数值:
c - '0'(例:'7' - '0' = 55 - 48 = 7);- 大小写字母快速转化:
- 大写转小写:
c + 32或tolower(c);- 小写转大写:
c - 32或toupper(c)。
std::string
对象模型C 语言中使用以 \0 结尾的字符数组
char s[100],容易产生越界溢出。C++ 提供了强大的现代字符串类
std::string:
+ 拼接,使用 ==,
<, > 比较字典序!cin >> s;"Hello World" 只能读进 "Hello")。getline(cin, s);\n 才停止,空格会被完整读入。先看一段错误示范:
int n;
string s;
cin >> n; // 键盘输入: 3 然后按了回车
getline(cin, s); // ❌ 灾难:s 读到的是回车符后的空行!什么都没拿到!原因解析:cin >> n 只取走了数字
3,而把紧随其后的回车符 \n 留在了输入缓冲区。接下来的
getline
一看缓冲区开头就是回车,立刻认为这一行结束了,直接返回空串!
🛡️ 解惑良药:
cin.ignore()清除缓存cin >> n; cin.ignore(); // 关键:吃掉留在缓冲区里的换行符! getline(cin, s); // 现在可以顺畅读入整行文本了!
string
核心函数全景图#include <bits/stdc++.h>
using namespace std;
int main() {
string s = "ABCDEFGHIJ";
// 1. 获取长度
int len = s.size(); // len = 10,也可以用 s.length()
// 2. 截取子串:s.substr(起始位置, 截取长度)
// ⚠️ 注意:第二个参数是【截取字符个数】,绝不是结束下标!
string sub = s.substr(2, 4); // 从下标 2 开始切 4 个字符
cout << "sub: " << sub << endl; // 输出: CDEF
// 3. 查找子串:s.find(目标串, 从哪开始找=0)
string text = "I love C++ and C++ is fast";
size_t pos = text.find("C++");
if (pos != string::npos) {
cout << "找到了!第一次出现在下标: " << pos << endl; // 7
}
// 4. 字符串替换:s.replace(起始位置, 替换长度, 替换的新串)
s.replace(2, 4, "999");
cout << "replace 后: " << s << endl; // AB999GHIJ
// 5. 字符判断库 <cctype>
char ch = '8';
if (isdigit(ch)) cout << ch << " 是数字!" << endl;
char ch2 = 'a';
if (isalpha(ch2)) cout << ch2 << " 是字母!" << endl;
cout << "大写形式: " << (char)toupper(ch2) << endl; // 'A'
return 0;
}本讲核心目标:理解枚举算法的底层哲学(不重不漏地穷尽解空间);掌握确定状态变量、划定搜索边界、构建判定条件的三步法;深入学习代数降维与剪枝技巧(以百钱买百鸡、水仙花数为例)。
在很多同学眼中,“枚举(Enumeration)”听起来很土,甚至觉得只有不会高级算法的人才会用穷举。 这是极大的认知误区! 计算机的核心优势就是:每秒能做上亿次运算,不知疲倦,分秒不差。如果一个问题的可能解只有 10 万个,计算机花 0.001 秒就能把所有可能检查一遍。在有限的解空间内,枚举是最稳妥、最不可能出错的满分策略!
无论多复杂的枚举题,都可以严格拆解为三个问答:
1. 枚举谁? ───> 选定状态变量(循环变量是谁?)
2. 空间有多大? ───> 确定上界与下界(循环从几到几,保证不重不漏)
3. 如何验证? ───> 编写 if 条件准则(符合条件的保留,否则丢弃)
题目描述:打印所有三位“水仙花数”。水仙花数是指一个三位数,其各位数字的立方和恰好等于该数本身。例如:。
三步推演:
#include <bits/stdc++.h>
using namespace std;
int main() {
for (int n = 100; n <= 999; n++) {
// 核心基本功:三位数的数位拆解
int ge = n % 10; // 取模 10 得到个位数
int shi = (n / 10) % 10; // 先整除 10 砍掉个位,再模 10 得到十位
int bai = n / 100; // 整除 100 得到百位
if (bai * bai * bai + shi * shi * shi + ge * ge * ge == n) {
cout << n << " 是水仙花数!" << endl;
}
}
return 0;
}问题:公鸡 5 元一只,母鸡 3 元一只,小鸡 1 元三只。用 100 元钱买 100 只鸡,公鸡、母鸡、小鸡各多少只?
for (int x = 0; x <= 100; x++) { // 公鸡
for (int y = 0; y <= 100; y++) { // 母鸡
for (int z = 0; z <= 100; z++) { // 小鸡
if (x + y + z == 100 && 5 * x + 3 * y + z / 3.0 == 100) { ... }
}
}
}
// 循环执行了 100 * 100 * 100 = 1,000,000 次!一共买 100 只鸡。如果公鸡买了 只,母鸡买了 只,那么小鸡的数量必然是 !根本不需要第三重循环! 此外,100 元全买公鸡最多买 20 只,全买母鸡最多买 33 只。我们可以大大压缩枚举范围:
#include <bits/stdc++.h>
using namespace std;
int main() {
// 公鸡最多 20 只,母鸡最多 33 只
for (int x = 0; x <= 20; x++) {
for (int y = 0; y <= 33; y++) {
int z = 100 - x - y; // 降维:直接计算得出
if (z < 0) continue;
// 小鸡三只一元,数量必须是 3 的倍数
if (z % 3 == 0) {
// 用全整数算式,避免 double 小数精度误差
if (5 * x + 3 * y + z / 3 == 100) {
cout << "公鸡: " << x << " 只, 母鸡: " << y
<< " 只, 小鸡: " << z << " 只" << endl;
}
}
}
}
return 0;
}运算次数从 100 万次骤降到 次!这就是算法优化的魅力。
本讲核心目标:掌握模拟算法(Simulation)的本质特征;精通状态变量定义、规则时序推演与终态控制的设计范式;攻克角谷猜想、约瑟夫环基础模型、时钟推进等经典题型;掌握草稿纸单步调试秘诀。
模拟题是各类编程考试中最常见的题型。这类题目的特征是:
模拟题看似简单,却是最考验基本功的题型,稍微疏忽一个细小边界,就会满盘皆输。
题目描述:日本数学家角谷静夫提出:任何一个正整数 ,如果它是奇数,则把它乘以 3 加 1;如果它是偶数,则把它除以 2。重复这个过程,最终必然会停在 1。输入正整数 ,打印其变迁过程并统计总步数。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long n; // 过程中数值可能剧烈膨胀,防溢出开 long long
cin >> n;
int steps = 0;
while (n != 1) { // 终止条件:n 到达 1
if (n % 2 == 1) {
cout << n << "*3+1=" << n * 3 + 1 << endl;
n = n * 3 + 1; // 奇数转移规则
} else {
cout << n << "/2=" << n / 2 << endl;
n = n / 2; // 偶数转移规则
}
steps++;
}
cout << "总共经过了 " << steps << " 步变迁到 1。" << endl;
return 0;
}题目描述: 个人围成一圈,编号从 到 。从第 1 个人开始报数,报到第 的人出列淘汰,下一个人重新从 1 开始报数,直到所有人出列。输出所有人的出列顺序。
设计思维推演:
bool out[105] 表示,true 代表已经出局;alive:当前圈内还剩几个人(初值为
);cur:当前小人走到几号了;count:当前报数报到几了。#include <bits/stdc++.h>
using namespace std;
bool eliminated[105]; // 默认为 false,都在圈内
int main() {
int n, m;
cin >> n >> m;
int remaining = n; // 剩下的人数
int cur = 1; // 从 1 号开始
int count = 0; // 当前报的数
while (remaining > 0) {
if (!eliminated[cur]) { // 只有没被淘汰的人才报数
count++;
if (count == m) {
// 报到 m 的人出局
cout << cur << " ";
eliminated[cur] = true;
remaining--;
count = 0; // 重置报数
}
}
// 环形推进:如果到了 n 号,下一个回到 1 号
cur++;
if (cur > n) cur = 1;
}
cout << endl;
return 0;
}当你认真推演、动手敲完本讲义的每一道例题时,你已经掌握了计算机底层的数据运转密码,跨过了一维数组、字符串与经典算法这道最重要的分水岭!
下一步的学习目标(GESP 四级先瞻):
代码没有捷径,每一行亲手敲出的有效代码,都在塑造你未来的计算思维。加油!