最新文章

20260228 143122 Cpp 入门第十六课

第十六章:二分查找——快如闪电的查找方法

你好!欢迎来到第十六章!在前面的章节,我们学习了顺序查找——从第一个元素开始一个一个找,就像在一堆书里一本一本翻。但如果书已经按编号排好序了,我们还有更快的办法:先看中间那本,如果目标编号比中间的小,就去左边找;如果大,就去右边找。这样每次都能排除一半的书,这就是二分查找(也叫折半查找)。这一章我们就来学习这种高效的查找算法!


16.1 二分查找程序范例

假设我们有一个已经按升序排好的数组,要查找某个数是否在数组中,如果在,返回它的下标。

#include <iostream>
using namespace std;

// 二分查找函数,返回下标,没找到返回-1
int binarySearch(int arr[], int n, int target) {
    int left = 0;          // 查找范围的左边界
    int right = n - 1;     // 查找范围的右边界

    while (left <= right) {
        int mid = left + (right - left) / 2;   // 中间位置,防止溢出
        if (arr[mid] == target) {
            return mid;     // 找到了,返回下标
        } else if (arr[mid] < target) {
            left = mid + 1; // 目标在右半部分
        } else {
            right = mid - 1;// 目标在左半部分
        }
    }
    return -1;              // 没找到
}

int main() {
    int arr[] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
    int n = sizeof(arr) / sizeof(arr[0]);
    int target;
    cout << "请输入要查找的数:";
    cin >> target;

    int pos = binarySearch(arr, n, target);
    if (pos != -1) {
        cout << "找到了,下标是:" << pos << endl;
    } else {
        cout << "没找到" << endl;
    }
    return 0;
}

运行示例(输入7):

请输入要查找的数:7
找到了,下标是:3

运行示例(输入6):

请输入要查找的数:6
没找到

16.2 二分查找的用法

16.2.1 二分查找的思想

二分查找就像猜数字游戏:对方想一个1-100之间的数,你每次猜中间的数,对方告诉你“大了”或“小了”,你就能排除一半的数字。这样最多猜7次就能猜到(因为2^7=128>100)。这就是二分查找的核心:每次把查找范围缩小一半

适用条件:数组必须是有序的(升序或降序)。如果数组无序,必须先排序才能用二分查找。

16.2.2 二分查找的步骤

  1. 确定查找范围的左边界 left 和右边界 right(通常初始为0和n-1)。
  2. left <= right 时:
    - 计算中间位置 mid = left + (right - left) / 2
    - 如果 arr[mid] == target,找到,返回 mid
    - 如果 arr[mid] < target,说明目标在右半部分,更新 left = mid + 1
    - 如果 arr[mid] > target,说明目标在左半部分,更新 right = mid - 1
  3. 如果循环结束还没找到,返回-1。

16.2.3 二分查找的代码实现

我们已经看到了迭代版本。也可以用递归实现:

int binarySearchRecursive(int arr[], int left, int right, int target) {
    if (left > right) return -1;   // 没找到
    int mid = left + (right - left) / 2;
    if (arr[mid] == target) {
        return mid;
    } else if (arr[mid] < target) {
        return binarySearchRecursive(arr, mid + 1, right, target);
    } else {
        return binarySearchRecursive(arr, left, mid - 1, target);
    }
}

调用时:binarySearchRecursive(arr, 0, n-1, target);

16.2.4 注意事项

  • 计算mid:用 left + (right - left) / 2 而不是 (left + right) / 2,防止 left+right 太大导致溢出。
  • 边界条件:循环条件 left <= right 要小心,如果写成 < 可能漏掉最后一个元素。
  • 数组必须有序:如果数组无序,结果不可预测。

16.2.5 二分查找的变种

有时候我们需要找的不是精确等于某个值的下标,而是满足某种条件的边界。比如:
- 找第一个等于target的位置
- 找最后一个等于target的位置
- 找第一个大于等于target的位置
- 找最后一个小于等于target的位置

这些变种在STL中对应 lower_boundupper_bound,我们也可以自己实现。


16.3 编程实例讲解

实例1:找第一个等于target的位置

当数组中有重复元素时,我们要找第一次出现的位置。

int firstOccurrence(int arr[], int n, int target) {
    int left = 0, right = n - 1;
    int result = -1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            result = mid;      // 记录找到的位置
            right = mid - 1;   // 继续在左边找
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return result;
}

实例2:找最后一个等于target的位置

int lastOccurrence(int arr[], int n, int target) {
    int left = 0, right = n - 1;
    int result = -1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            result = mid;
            left = mid + 1;    // 继续在右边找
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return result;
}

实例3:找第一个大于等于target的位置(即 lower_bound)

int lowerBound(int arr[], int n, int target) {
    int left = 0, right = n - 1;
    int result = n;   // 如果所有数都小于target,返回n(表示不存在)
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] >= target) {
            result = mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return result;
}

实例4:在一个旋转有序数组中查找(选做)

题目:一个升序数组在某个点被旋转了,比如 [4,5,6,7,0,1,2],在这个数组中查找某个数。

思路:先判断哪一半是有序的,然后根据target是否在有序部分来缩小范围。

int searchRotated(int arr[], int n, int target) {
    int left = 0, right = n - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) return mid;
        // 左半部分有序
        if (arr[left] <= arr[mid]) {
            if (target >= arr[left] && target < arr[mid]) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        } 
        // 右半部分有序
        else {
            if (target > arr[mid] && target <= arr[right]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
    }
    return -1;
}

16.4 阶段性编程练习

  1. 练习1:在有序数组 [2, 5, 8, 12, 16, 23, 38, 45, 56, 72] 中查找数字 23 和 50,分别输出结果。
  2. 练习2:编写一个函数,用二分查找求一个数的平方根(整数部分)。例如输入10,返回3(因为3²=9≤10,4²=16>10)。提示:在0到x之间二分查找。
  3. 练习3:在一个有序数组中,找第一个大于 target 的位置(即 upper_bound)。
  4. 练习4:在一个有序数组中,统计某个数出现的次数(可以用 firstOccurrence 和 lastOccurrence 计算)。
  5. 练习5:用递归实现二分查找,并在主函数中测试。

16.5 第16章编程作业

作业1:查找插入位置

给定一个有序数组和一个目标值,如果目标值在数组中,返回其下标;如果不在,返回它应该插入的位置(保持有序)。例如数组 [1,3,5,6],目标2,应返回1(插入在1和3之间)。

作业2:寻找峰值

在一个数组中,峰值是指该元素大于相邻元素(数组边界视为负无穷)。假设相邻元素不等,用二分查找找出任意一个峰值。例如 [1,2,1,3,5,6,4],峰值可能是2或6。提示:比较中间元素与右边元素,如果中间小于右边,则峰值在右边,否则在左边。

作业3:在二维矩阵中查找

有一个 m×n 的矩阵,每行从左到右递增,每列从上到下递增。编写一个高效的算法查找某个数是否存在。提示:从右上角开始,类似二分。

作业4:两个有序数组的中位数

给定两个有序数组,找出它们的中位数。要求时间复杂度 O(log(min(m,n)))。这是一个经典难题,但可以尝试。

作业5:寻找重复数

给定一个包含 n+1 个整数的数组,每个整数都在 1 到 n 之间,所以至少有一个重复。找出这个重复的数,要求不修改数组且只用O(1)额外空间。可以用二分查找思想(值域二分)。


恭喜你完成了第十六章的学习!二分查找是一种非常高效的算法,它体现了“分而治之”的思想。在实际编程中,只要遇到有序数组的查找问题,就可以考虑二分查找。加油!🚀

20260228 142501 Cpp 入门第十五课

第十五章:递归算法——函数调用自己

你好!欢迎来到第十五章!在前面的章节,我们学习了递推,从已知条件一步步推出结果。这一章我们学习另一种重要的思想:递归——函数自己调用自己。就像俄罗斯套娃,一个大娃娃里面套着一个一模一样的小娃娃。递归在解决某些问题时特别简洁,比如遍历文件夹、计算阶乘、汉诺塔等。让我们一起来探索递归的奥秘吧!


15.1 递归算法程序范例

先来看一个最简单的递归例子:计算阶乘 n!。阶乘的定义是:
- n! = 1 × 2 × 3 × … × n
- 也可以用递归定义:n! = n × (n-1)!,且 0! = 1。

#include <iostream>
using namespace std;

// 递归函数求阶乘
int factorial(int n) {
    if (n == 0) {          // 递归终止条件
        return 1;
    } else {
        return n * factorial(n - 1);   // 递归调用
    }
}

int main() {
    int n;
    cout << "请输入一个整数:";
    cin >> n;
    cout << n << "! = " << factorial(n) << endl;
    return 0;
}

运行示例(输入5):

5! = 120

这个程序里,factorial 函数在计算过程中又调用了自己,这就是递归。但递归不能无限进行下去,必须有一个终止条件(这里 n == 0 时返回1),否则就会陷入死循环(最终栈溢出)。


15.2 递归算法的用法

15.2.1 什么是递归?

递归就是一个函数直接或间接地调用自身。递归可以把一个复杂问题分解成与原问题相似但规模更小的子问题,直到子问题简单到可以直接求解(终止条件)。

生活中的递归例子:
- 俄罗斯套娃:打开一个娃娃,里面是一个更小的娃娃,直到最小的那个。
- 查字典:要查一个词,发现解释里用了另一个词,又去查那个词,直到某个词的解释简单易懂。

15.2.2 递归的要素

  1. 递归终止条件(基线条件):问题规模足够小,可以直接得到答案,不再调用自身。
  2. 递归关系(递归步骤):将原问题转化为规模更小的子问题,并调用自身解决子问题,然后组合结果。

15.2.3 递归的执行过程

factorial(3) 为例:
- 调用 factorial(3) → 3 != 0,执行 3 * factorial(2)
- 调用 factorial(2) → 2 != 0,执行 2 * factorial(1)
- 调用 factorial(1) → 1 != 0,执行 1 * factorial(0)
- 调用 factorial(0) → 0 == 0,返回 1
- 回到 factorial(1),得到 11 = 1,返回
- 回到 factorial(2),得到 2
1 = 2,返回
- 回到 factorial(3),得到 3*2 = 6,返回

这个过程像一层层深入,再一层层返回,所以递归需要系统栈来保存每一层的局部变量和返回地址。

15.2.4 递归与循环的比较

特点 递归 循环
代码简洁性 通常更简洁,符合数学定义 可能需要更多代码
可读性 对某些问题很直观 通用
效率 有函数调用开销,可能重复计算 通常更快
栈溢出风险 层次太深会栈溢出 无此问题
适用问题 树、图、分治等 线性重复

注意:能用循环解决的问题尽量用循环,但有些问题(如汉诺塔、树的遍历)用递归更自然。

15.2.5 递归的注意事项

  • 必须有终止条件,否则无限递归导致程序崩溃。
  • 递归层次不能太深,否则会栈溢出(系统栈空间有限,一般几千层)。
  • 避免重复计算:比如递归求斐波那契数会有大量重复,可以用记忆化(备忘录)优化。

15.3 编程实例讲解

实例1:递归求斐波那契数列(简单版,可能有重复计算)

#include <iostream>
using namespace std;

int fib(int n) {
    if (n == 1 || n == 2) {
        return 1;
    } else {
        return fib(n - 1) + fib(n - 2);   // 大量重复
    }
}

int main() {
    int n;
    cout << "请输入n:";
    cin >> n;
    cout << "第" << n << "项是:" << fib(n) << endl;
    return 0;
}

问题:n=40 时就会非常慢,因为重复计算太多。可以用记忆化优化。

实例2:记忆化递归(备忘录)

用一个数组保存已经算过的值,避免重复计算。

#include <iostream>
using namespace std;

const int N = 100;
long long memo[N] = {0};   // 记忆数组,初始0表示未计算

long long fib(int n) {
    if (n == 1 || n == 2) return 1;
    if (memo[n] != 0) return memo[n];   // 直接返回已计算结果
    memo[n] = fib(n - 1) + fib(n - 2);   // 计算并保存
    return memo[n];
}

int main() {
    int n;
    cout << "请输入n:";
    cin >> n;
    cout << "第" << n << "项是:" << fib(n) << endl;
    return 0;
}

实例3:汉诺塔问题

题目:有三根柱子A、B、C,A柱上有n个大小不同的圆盘,大盘在下小盘在上。要求把所有圆盘从A移到C,每次只能移动一个,且小盘不能压在大盘上。输出移动步骤。

递归思路
- 要把n个盘子从A移到C,可以先把上面n-1个盘子从A移到B(借助C),然后把最大的盘子从A移到C,最后把n-1个盘子从B移到C(借助A)。

#include <iostream>
using namespace std;

void hanoi(int n, char from, char to, char aux) {
    if (n == 1) {
        cout << "移动盘子 1 从 " << from << " 到 " << to << endl;
        return;
    }
    hanoi(n - 1, from, aux, to);          // 把上面n-1个从from移到aux
    cout << "移动盘子 " << n << " 从 " << from << " 到 " << to << endl;
    hanoi(n - 1, aux, to, from);          // 把n-1个从aux移到to
}

int main() {
    int n;
    cout << "请输入盘子数:";
    cin >> n;
    hanoi(n, 'A', 'C', 'B');
    return 0;
}

运行示例(n=3):

移动盘子 1 从 A 到 C
移动盘子 2 从 A 到 B
移动盘子 1 从 C 到 B
移动盘子 3 从 A 到 C
移动盘子 1 从 B 到 A
移动盘子 2 从 B 到 C
移动盘子 1 从 A 到 C

实例4:递归遍历文件夹(伪代码,需要用到系统函数)

虽然不能直接运行,但可以展示思想:遍历文件夹时,如果是文件就处理,如果是文件夹就递归进入。

#include <iostream>
#include <filesystem>  // C++17 需要
namespace fs = std::filesystem;

void listFiles(const fs::path &path) {
    for (const auto &entry : fs::directory_iterator(path)) {
        if (entry.is_directory()) {
            listFiles(entry.path());   // 递归子文件夹
        } else {
            cout << entry.path() << endl;
        }
    }
}

实例5:递归求最大公约数(辗转相除法)

辗转相除法的递归定义:gcd(a, b) = gcd(b, a % b),直到 b=0。

int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}

实例6:全排列生成

用递归生成一个数组的所有排列(经典回溯)。

#include <iostream>
using namespace std;

void permute(int arr[], int l, int r) {
    if (l == r) {
        for (int i = 0; i <= r; i++) cout << arr[i] << " ";
        cout << endl;
    } else {
        for (int i = l; i <= r; i++) {
            swap(arr[l], arr[i]);
            permute(arr, l + 1, r);
            swap(arr[l], arr[i]);   // 回溯
        }
    }
}

int main() {
    int arr[] = {1, 2, 3};
    int n = sizeof(arr) / sizeof(arr[0]);
    permute(arr, 0, n - 1);
    return 0;
}

15.4 阶段性编程练习

  1. 练习1:用递归求 1+2+…+n 的和。
  2. 练习2:用递归判断一个字符串是否是回文(如 “abcba”)。
  3. 练习3:用递归实现二分查找(在有序数组中找某个数)。
  4. 练习4:用递归输出杨辉三角的第n行(提示:杨辉三角可以用递归公式 C(n,k) = C(n-1,k-1) + C(n-1,k))。
  5. 练习5:用递归解决“数字反转”问题(输入一个整数,输出反转后的数,如 1234 → 4321,考虑负数?)

15.5 第15章编程作业

作业1:递归求幂

写一个递归函数 double power(double x, int n) 计算 x 的 n 次幂(n 为非负整数)。要求时间复杂度 O(n)。

作业2:猴子吃桃(递归版)

猴子第一天摘了若干桃子,当即吃了一半,还不过瘾,又多吃了一个。第二天又将剩下的桃子吃了一半,又多吃了一个。以后每天都这样。到第10天早上想再吃时,只剩一个桃子了。用递归函数求第一天摘了多少个桃子。

作业3:整数划分

将一个正整数 n 划分成若干个正整数之和,求划分的种数。例如 n=4 有 5 种划分:4, 3+1, 2+2, 2+1+1, 1+1+1+1。用递归实现。

作业4:汉诺塔步数计算

在汉诺塔问题中,移动 n 个盘子需要多少步?推导出公式并用递归验证。

作业5:八皇后问题(选做)

在 8×8 的国际象棋棋盘上放置八个皇后,使得它们互不攻击(即任意两个不在同一行、同一列、同一对角线上)。用递归回溯法求解并输出所有解(或一个解)。


恭喜你完成了第十五章的学习!递归是一种强大的编程思想,它让我们能简洁地解决许多复杂问题。虽然递归有时效率不高,但它的思维方式非常重要,也是学习更高级算法(如动态规划、回溯、分治)的基础。加油!🚀

20260228 142406 Cpp 入门第十四课

第十四章:递推算法——从已知推未知

你好!欢迎来到第十四章!你有没有玩过“多米诺骨牌”?只要推倒第一张,后面的就会一张接一张倒下。这种“由前面推出后面”的思想,就是递推算法。递推在数学和编程中非常重要,比如斐波那契数列、爬楼梯问题,都可以用递推轻松解决。这一章我们就来学习如何用递推思想解决问题!


14.1 递推算法程序范例

先来看一个经典的递推问题:斐波那契数列。数列定义:第一项和第二项都是1,从第三项开始,每一项等于前两项之和。即:
- F(1) = 1
- F(2) = 1
- F(n) = F(n-1) + F(n-2) (n≥3)

用递推算法(循环)求第 n 项:

#include <iostream>
using namespace std;

int main() {
    int n;
    cout << "请输入n:";
    cin >> n;

    if (n <= 2) {
        cout << "第" << n << "项是:1" << endl;
    } else {
        int a = 1, b = 1, c;
        for (int i = 3; i <= n; i++) {
            c = a + b;   // 递推关系
            a = b;       // 向前移动
            b = c;
        }
        cout << "第" << n << "项是:" << b << endl;
    }
    return 0;
}

运行示例(输入10):

第10项是:55

这个程序用循环不断更新前两项的值,一步步推出第 n 项。这就是递推:从已知的初始条件出发,按照递推公式逐步计算。


14.2 递推算法的用法

14.2.1 什么是递推?

递推就是利用已知的初始值,通过一定的递推关系(公式),逐步推出后面的结果。就像搭积木:先放好第一块,然后根据它放第二块,再根据前两块放第三块……

递推通常分为:
- 顺推:从已知条件向后推,比如斐波那契数列。
- 逆推:从结果向前推,但初学者先掌握顺推。

14.2.2 递推的要素

  1. 初始条件:最开始已知的值,比如 F(1)=1, F(2)=1。
  2. 递推关系:如何从前面推出后面,比如 F(n) = F(n-1) + F(n-2)。
  3. 计算顺序:通常用循环从小到大计算,避免重复。

14.2.3 递推 vs 递归

  • 递归:函数自己调用自己,代码简洁,但容易重复计算,效率低(比如斐波那契递归会算很多次)。
  • 递推:用循环从前往后算,每个值只算一次,效率高。

所以,当可以用递推时,优先用递推(循环)。


14.3 编程实例讲解

实例1:爬楼梯问题

题目:小明爬楼梯,每次可以跨1级或2级台阶。问爬到第n级台阶有多少种不同的爬法?

分析
- 到第1级:只有1种(1步)
- 到第2级:有2种(1+1 或 2)
- 到第3级:可以从第1级跨2步,或从第2级跨1步,所以方法数 = 到第1级的方法 + 到第2级的方法 = 1+2=3
- 到第4级:可以从第2级跨2步,或从第3级跨1步,所以 = 2+3=5
- 发现规律:F(n) = F(n-1) + F(n-2)(和斐波那契一样,只是初始值 F(1)=1, F(2)=2)

#include <iostream>
using namespace std;

int main() {
    int n;
    cout << "请输入台阶数n:";
    cin >> n;

    if (n == 1) {
        cout << "爬法总数:1" << endl;
    } else if (n == 2) {
        cout << "爬法总数:2" << endl;
    } else {
        int a = 1, b = 2, c;
        for (int i = 3; i <= n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        cout << "爬法总数:" << b << endl;
    }
    return 0;
}

实例2:数字三角形(简单版)

题目:输入一个正整数n,输出n行的数字三角形,第i行有i个数字,数字为1~i。

例如 n=4:

1
1 2
1 2 3
1 2 3 4

这不是递推,但可以用递推思想:第i行的数字 = 第i-1行的数字再添加一个 i。但这里我们只是输出,其实用循环即可。但我们换个真正的递推问题:

题目:数字三角形求最大路径(简单版)。有一个数字三角形,从顶部出发,每次可以向下或向右下走,求从顶部到底部的最大路径和。

例如:

   7
  3 8
 8 1 0
2 7 4 4

从顶部7开始,向下走到8,再走到1,再走到7,和为7+8+1+7=23。但最大路径是7+8+1+4=20?等等,需要计算。这个经典问题可以用递推解决:从底部向上递推。

递推思路:用二维数组 a[i][j] 存数字,dp[i][j] 表示从底部到 (i,j) 的最大路径和。从最后一行往上推:
- 最后一行 dp[n-1][j] = a[n-1][j]
- 对上面每一行:dp[i][j] = a[i][j] + max(dp[i+1][j], dp[i+1][j+1])

最后 dp[0][0] 就是答案。

#include <iostream>
#include <algorithm>
using namespace std;

int main() {
    int n;
    cout << "请输入三角形行数:";
    cin >> n;
    int a[100][100];
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= i; j++) {
            cin >> a[i][j];
        }
    }

    // 从倒数第二行开始向上递推
    for (int i = n - 2; i >= 0; i--) {
        for (int j = 0; j <= i; j++) {
            a[i][j] += max(a[i+1][j], a[i+1][j+1]);
        }
    }
    cout << "最大路径和:" << a[0][0] << endl;
    return 0;
}

这个例子展示了逆推的思想。

实例3:蜜蜂爬行

题目:蜜蜂从蜂房1出发,只能爬到编号相邻的蜂房(比如从1到2,从2到3或1,等等)。问从1爬到n有多少种不同的路径?

分析
- 到1:1种(已经在)
- 到2:可以从1来,1种
- 到3:可以从2来,也可以从1来?但1只能到2,不能直接到3?题目说相邻,所以从1只能到2,从2可以到1或3。所以到3的路径:1→2→3,只有1种。实际上这是一个斐波那契数列变种。我们仔细分析:
- 设 f[i] 表示从1到i的路径数。
- f[1] = 1
- f[2] = 1(1→2)
- f[3] = f[2] + f[1]?但1到3不能直接,所以实际上 f[3] = f[2](因为只能从2来)?不对,还可以从哪?蜜蜂可以来回爬,但一般这种问题是不允许重复的,所以通常是从左向右爬,即只能爬到比当前大的编号?这样题目才合理。通常这类题目是“蜜蜂只能向右爬”,那么 f[i] = f[i-1] + f[i-2](因为可以从i-1和i-2来)。初始 f[1]=1, f[2]=1(1→2)。这样 f[3]=f[2]+f[1]=2,即1→2→3 和 1→3(如果允许1直接到3?但题目说相邻,所以1不能直接到3)。所以矛盾。我们假设蜜蜂只能爬到编号大1的蜂房,那就是线性的,只有1种。所以需要明确规则。

通常这类问题是在一个环上?常见的递推题:蜜蜂从蜂房a爬到蜂房b,只能爬向相邻的蜂房,且不能回头,求路径数。这实际上就是斐波那契数列:f(n) = f(n-1) + f(n-2),初始 f(1)=1, f(2)=2?我们来看标准版:在一个直线上,蜜蜂只能向右爬,那么到第i个蜂房的路径数 = 到i-1的路径数 + 到i-2的路径数(因为可以从i-1跨一步,或从i-2跨两步)。这样 f(1)=1, f(2)=2(1→2 或 直接到2?但1不能直接到2?实际上,如果蜂房编号连续,那么从1到2只有一种走法:1→2。但如果我们允许跨两步,那从1可以到2?不,跨两步就到3了。所以常见的是“每次可以走1步或2步”,那就是爬楼梯问题,初始 f(1)=1, f(2)=2。我们以爬楼梯为例即可。

所以我们可以直接用爬楼梯的代码。

实例4:平面分割问题

题目:n条直线最多能把平面分成多少个区域?

分析
- 0条直线:1个区域
- 1条直线:2个区域
- 2条直线:最多4个区域(相交)
- 3条直线:最多7个区域
- 递推关系:第k条直线最多与前面k-1条直线相交于k-1个点,从而被分成k段,每段将原来的一个区域一分为二,所以增加k个区域。
- 设 f(n) 表示n条直线最多区域数,则 f(0)=1, f(n)=f(n-1)+n。

#include <iostream>
using namespace std;

int main() {
    int n;
    cout << "请输入直线条数:";
    cin >> n;
    int f = 1;  // 0条直线
    for (int i = 1; i <= n; i++) {
        f = f + i;   // 递推公式
    }
    cout << "最多能分成" << f << "个区域" << endl;
    return 0;
}

14.4 阶段性编程练习

  1. 练习1:求斐波那契数列的第20项(用递推循环)。
  2. 练习2:一个人上台阶,每次可以跨1级、2级或3级,求上10级台阶有多少种不同的走法。
  3. 练习3:用递推求 n!(阶乘),n! = n * (n-1)!,初始 1! = 1。
  4. 练习4:一对兔子,从出生后第3个月起每个月都生一对兔子。假设兔子不死,问第n个月有多少对兔子?(这就是斐波那契数列的由来,但初始值不同:第1个月1对,第2个月1对,第3个月2对,第4个月3对,第5个月5对… 即 f(1)=1, f(2)=1, f(3)=2, f(4)=3… 其实 f(n)=f(n-1)+f(n-2) 对 n≥3,f(1)=1, f(2)=1。就是斐波那契。)
  5. 练习5:用递推求杨辉三角的第n行(用一维数组递推)。

14.5 第14章编程作业

作业1:母牛的故事

有一头母牛,它每年年初生一头小母牛。每头小母牛从第4个年头开始,每年年初也生一头小母牛。假设没有死亡,问第n年时共有多少头牛?(经典递推题:f(n) = f(n-1) + f(n-3),初始 f(1)=1, f(2)=2, f(3)=3)

作业2:铺砖问题

用 1×2 的砖铺满 2×n 的地面,有多少种铺法?(递推关系:f(n) = f(n-1) + f(n-2),初始 f(1)=1, f(2)=2)

作业3:骨牌铺满

用 2×1 和 2×2 的骨牌铺满 2×n 的地面,有多少种铺法?(递推:f(n) = f(n-1) + 2*f(n-2),初始 f(1)=1, f(2)=3)

作业4:猴子吃桃

猴子第一天摘了若干桃子,当即吃了一半,还不过瘾,又多吃了一个。第二天又将剩下的桃子吃了一半,又多吃了一个。以后每天都这样。到第10天早上想再吃时,只剩一个桃子了。问第一天共摘了多少桃子?(用逆推:从第10天剩1个,往前推第9天的桃子数等。)

作业5:数字三角形最大路径(文件版)

从文件读入一个数字三角形(第一行是行数,后面是三角形数字),用递推求最大路径和,并输出路径(可选)。


恭喜你完成了第十四章的学习!递推是一种非常实用的算法思想,它把复杂问题分解成简单步骤,一步一步推进。加油!🚀

20260228 142314 Cpp 入门第十三课

第十三章:枚举算法——一一列举,找出答案

你好!欢迎来到第十三章!你有没有遇到过这样的问题:一个密码锁有三位数字,忘了密码,只能一个一个试,直到打开。这种“把所有可能都试一遍”的方法,就是枚举算法(也叫穷举法)。虽然听起来有点笨,但计算机最擅长的就是重复劳动,而且速度飞快!这一章我们就来学习如何用枚举思想解决各种有趣的问题。


13.1 枚举算法程序范例

先来看一个最简单的枚举问题:找出1到100之间所有能被3整除的数。

#include <iostream>
using namespace std;

int main() {
    cout << "1到100之间能被3整除的数有:" << endl;
    for (int i = 1; i <= 100; i++) {
        if (i % 3 == 0) {
            cout << i << " ";
        }
    }
    cout << endl;
    return 0;
}

运行结果

1到100之间能被3整除的数有
3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 99

这就是枚举:把所有可能的数(1到100)都拿出来,逐个判断是否满足条件(能被3整除)。虽然简单,但枚举是很多复杂算法的基础。


13.2 枚举算法的用法

13.2.1 什么是枚举算法?

枚举算法就是把所有可能的情况都列举出来,然后逐个检查哪些是符合要求的。就像警察破案时,把所有嫌疑人一个个调查,找出真正的罪犯。

枚举算法通常包含三个步骤:
1. 确定枚举范围:要试哪些可能性?比如1到100、所有的两位数等。
2. 确定判断条件:什么样的结果是我们要的?比如能被3整除、等于某个值等。
3. 用循环遍历所有可能:用 forwhile 循环,对每个可能进行判断。

13.2.2 枚举的适用场景

  • 搜索空间有限,能全部枚举完(比如最多几百万次)。
  • 没有更高效的数学方法,或者问题很简单。
  • 常用于密码破解、组合问题、方程求解等。

13.2.3 枚举的优化——剪枝

有些时候枚举的范围很大,但很多情况明显不可能,我们可以提前排除,减少循环次数。这叫做剪枝

例如,找三位数中满足 a² + b² = c² 的勾股数,如果 a 和 b 都从1枚举到100,c 也要从1到100,三重循环就是100万次。但我们可以利用 c = sqrt(a²+b²) 直接计算,并检查是否为整数,这样大大减少循环。


13.3 编程实例讲解

实例1:百钱买百鸡

题目:公鸡5元一只,母鸡3元一只,小鸡1元三只。用100元买100只鸡,问公鸡、母鸡、小鸡各多少只?

分析
- 设公鸡 x 只,母鸡 y 只,小鸡 z 只。
- 满足两个方程:
- x + y + z = 100
- 5x + 3y + z/3 = 100(注意小鸡价格是1元3只,所以 z 必须是3的倍数)
- 枚举范围:x 最多 20(因为5×20=100),y 最多 33(因为3×33=99),z = 100 - x - y 且必须是3的倍数且非负。

#include <iostream>
using namespace std;

int main() {
    cout << "所有可能的买法:" << endl;
    for (int x = 0; x <= 20; x++) {
        for (int y = 0; y <= 33; y++) {
            int z = 100 - x - y;
            if (z >= 0 && z % 3 == 0 && 5*x + 3*y + z/3 == 100) {
                cout << "公鸡:" << x << "只,母鸡:" << y << "只,小鸡:" << z << "只" << endl;
            }
        }
    }
    return 0;
}

运行结果

所有可能的买法:
公鸡:0只,母鸡:25只,小鸡:75只
公鸡:4只,母鸡:18只,小鸡:78只
公鸡:8只,母鸡:11只,小鸡:81只
公鸡:12只,母鸡:4只,小鸡:84只

实例2:水仙花数

题目:水仙花数是指一个三位数,其各位数字的立方和等于该数本身。例如 153 = 1³ + 5³ + 3³。找出所有的水仙花数。

分析
- 枚举范围:100 到 999。
- 对每个数,分离出百位、十位、个位,计算立方和,判断是否相等。

#include <iostream>
using namespace std;

int main() {
    cout << "水仙花数有:" << endl;
    for (int num = 100; num <= 999; num++) {
        int a = num / 100;            // 百位
        int b = (num / 10) % 10;       // 十位
        int c = num % 10;              // 个位
        if (a*a*a + b*b*b + c*c*c == num) {
            cout << num << " ";
        }
    }
    cout << endl;
    return 0;
}

运行结果

水仙花数有:
153 370 371 407

实例3:完美数

题目:完美数是指一个数恰好等于它的因子之和(不包括自身)。例如 6 = 1+2+3。找出1000以内的所有完美数。

分析
- 枚举范围:2 到 1000(因为1不算)。
- 对每个数,找出所有小于它的因子,求和,判断是否相等。

#include <iostream>
using namespace std;

int main() {
    cout << "1000以内的完美数有:" << endl;
    for (int num = 2; num <= 1000; num++) {
        int sum = 0;
        for (int i = 1; i <= num / 2; i++) {   // 因子最大不超过 num/2
            if (num % i == 0) {
                sum += i;
            }
        }
        if (sum == num) {
            cout << num << " ";
        }
    }
    cout << endl;
    return 0;
}

运行结果

1000以内的完美数有
6 28 496

实例4:找零问题(枚举+优化)

题目:用1元、2元、5元纸币凑成10元,有多少种凑法?

分析
- 设1元 x 张,2元 y 张,5元 z 张。
- 方程:x + 2y + 5z = 10,且 x,y,z ≥ 0。
- 枚举范围:z 从0到2(因为5*2=10),y 从0到5,x = 10 - 2y - 5z,检查是否非负。

#include <iostream>
using namespace std;

int main() {
    int count = 0;
    for (int z = 0; z <= 2; z++) {
        for (int y = 0; y <= 5; y++) {
            int x = 10 - 2*y - 5*z;
            if (x >= 0) {
                cout << "1元:" << x << "张, 2元:" << y << "张, 5元:" << z << "张" << endl;
                count++;
            }
        }
    }
    cout << "共有" << count << "种凑法" << endl;
    return 0;
}

实例5:质数判断(枚举因子)

虽然质数判断通常用数学优化,但也可以用枚举思想。

#include <iostream>
#include <cmath>
using namespace std;

bool isPrime(int n) {
    if (n <= 1) return false;
    for (int i = 2; i <= sqrt(n); i++) {   // 枚举可能的因子
        if (n % i == 0) return false;
    }
    return true;
}

int main() {
    int num;
    cout << "请输入一个整数:";
    cin >> num;
    if (isPrime(num)) {
        cout << num << " 是质数" << endl;
    } else {
        cout << num << " 不是质数" << endl;
    }
    return 0;
}

13.4 枚举算法的优化技巧

  1. 缩小枚举范围:分析问题,去掉明显不可能的情况。例如百钱买百鸡中,公鸡最多20只,而不是100。
  2. 减少循环层数:能用公式计算的就不循环。比如勾股数中,c 可以用 a、b 计算。
  3. 利用对称性:如果 a 和 b 互换结果相同,可以只枚举 a ≤ b,避免重复。
  4. 剪枝:在循环中提前判断,如果某个条件已经不满足,就跳过后续循环。

13.5 阶段性编程练习

  1. 练习1:找出100到200之间所有的素数。
  2. 练习2:一个两位数,十位与个位之和是9,十位与个位之积等于这个两位数的一半,求这个两位数。
  3. 练习3:用1元、2元、5元、10元纸币凑成20元,有多少种凑法?
  4. 练习4:找出所有满足 a³ + b³ = c³ + d³ 的1到30之间的整数组合(a,b,c,d互不相等,且 a≤b, c≤d,避免重复)。
  5. 练习5:一个四位数的密码,已知千位是百位的2倍,十位是个位的3倍,且这个四位数各位数字之和是20,求这个密码。

13.6 第13章编程作业

作业1:搬砖问题

36块砖,36人搬,男人一次搬4块,女人一次搬3块,小孩两人抬1块。一次搬完,问男人、女人、小孩各多少人?

作业2:三色球问题

有红、黄、蓝三种颜色的球,红球3个,黄球3个,蓝球6个。从中任意摸出8个球,计算摸出的球的各种颜色搭配。

作业3:ABCD × 4 = DCBA

有一个四位数ABCD(A≠0),乘以4后等于DCBA,求这个四位数。(提示:A只能是1或2,D只能是4或8等)

作业4:完美立方

找出所有满足 a³ = b³ + c³ + d³ 的1到30之间的整数,其中 a,b,c,d 互不相等,且 b≤c≤d。

作业5:熄灯问题(选做)

有一个5×6的灯阵,按下一个灯会改变它自身和上下左右四个灯的状态(亮变灭,灭变亮)。给定初始状态,求一种按法使得所有灯都熄灭。这是一个经典的枚举问题,可以用枚举第一行所有可能(2^6=64种),然后递推后面。


恭喜你完成了第十三章的学习!枚举算法虽然简单,但它是很多复杂算法的基础。当你遇到一个问题不知道怎么做时,不妨想一想:能不能把所有可能都试一遍?加油!🚀

20260228 142212 Cpp 入门第十二课

第十二章:排序算法——让数据井然有序

你好!欢迎来到第十二章!在日常生活中,我们经常需要把东西排好序,比如按身高排队、按分数排名、按时间顺序排列照片。在计算机科学中,排序是最基础也最重要的算法之一。虽然C++的STL提供了强大的 sort 函数,但学习排序算法能帮助我们理解计算机如何工作,锻炼逻辑思维。这一章我们就来学习几种经典的排序算法,并亲手实现它们!


12.1 什么是排序算法?

排序就是将一组数据按照某种规则重新排列,比如从小到大(升序)或从大到小(降序)。

想象你有一堆乱序的扑克牌,你要把它们按数字从小到大整理好。你会怎么做?不同的方法对应不同的排序算法。

本章我们将学习:
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序(进阶)
- 归并排序(进阶)


12.2 冒泡排序

12.2.1 算法思想

冒泡排序就像水中的气泡,轻的气泡会慢慢浮到上面。每次比较相邻的两个元素,如果顺序不对(比如前大后小),就交换它们。这样每一轮都会把最大的数“冒泡”到最后。

过程演示(升序):
初始数组:[5, 3, 8, 1, 2]

第一轮:
- 比较5和3,5>3,交换 → [3, 5, 8, 1, 2]
- 比较5和8,5<8,不交换 → [3, 5, 8, 1, 2]
- 比较8和1,8>1,交换 → [3, 5, 1, 8, 2]
- 比较8和2,8>2,交换 → [3, 5, 1, 2, 8](最大数8已就位)

第二轮:
- 比较3和5,3<5,不交换 → [3, 5, 1, 2, 8]
- 比较5和1,5>1,交换 → [3, 1, 5, 2, 8]
- 比较5和2,5>2,交换 → [3, 1, 2, 5, 8](次大数5就位)

第三轮:
- 比较3和1,3>1,交换 → [1, 3, 2, 5, 8]
- 比较3和2,3>2,交换 → [1, 2, 3, 5, 8](第三大3就位)

第四轮:
- 比较1和2,1<2,不交换 → 已完成

12.2.2 代码实现

#include <iostream>
using namespace std;

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {          // 需要 n-1 轮
        for (int j = 0; j < n - 1 - i; j++) {  // 每轮比较次数减少
            if (arr[j] > arr[j + 1]) {          // 如果前大后小,交换
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int arr[] = {5, 3, 8, 1, 2};
    int n = sizeof(arr) / sizeof(arr[0]);

    bubbleSort(arr, n);

    cout << "排序后:";
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;

    return 0;
}

12.2.3 优化

如果某一轮没有发生任何交换,说明数组已经有序,可以提前结束。

void bubbleSortOptimized(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        bool swapped = false;               // 标记是否交换
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);    // 可以用swap函数
                swapped = true;
            }
        }
        if (!swapped) break;                 // 没有交换,退出
    }
}

12.2.4 阶段性练习

  1. 练习1:用冒泡排序对数组 [9, 2, 5, 1, 7] 进行升序排序,写出每轮结果。
  2. 练习2:实现降序冒泡排序(从大到小)。
  3. 练习3:统计冒泡排序中交换的次数。

12.3 选择排序

12.3.1 算法思想

选择排序的思路很简单:每一轮从未排序的部分中找出最小的元素,放到已排序部分的末尾。

过程演示(升序):
初始:[5, 3, 8, 1, 2]
- 第一轮:找到最小元素1,与第一个元素5交换 → [1, 3, 8, 5, 2]
- 第二轮:从剩余[3,8,5,2]中找到最小2,与第二个元素3交换 → [1, 2, 8, 5, 3]
- 第三轮:从剩余[8,5,3]中找到最小3,与第三个元素8交换 → [1, 2, 3, 5, 8]
- 第四轮:从剩余[5,8]中找到最小5,与第四个元素5(自身)不交换 → 完成

12.3.2 代码实现

void selectionSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;                     // 假设当前是最小
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;                  // 更新最小下标
            }
        }
        if (minIndex != i) {                   // 交换
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

12.3.3 阶段性练习

  1. 练习1:用选择排序对数组 [8, 3, 6, 1, 4] 排序,写出每轮结果。
  2. 练习2:实现降序选择排序。
  3. 练习3:选择排序的交换次数最多是多少?

12.4 插入排序

12.4.1 算法思想

插入排序就像整理扑克牌:每次将一张牌插入到已经有序的手牌中的正确位置。

过程演示(升序):
初始:[5, 3, 8, 1, 2]
- 第一张牌5,有序部分:[5]
- 取3,插入到5前面 → [3, 5, 8, 1, 2]
- 取8,比5大,位置不变 → [3, 5, 8, 1, 2]
- 取1,依次比较8、5、3,插入最前 → [1, 3, 5, 8, 2]
- 取2,依次比较8、5、3,插入3和1之间 → [1, 2, 3, 5, 8]

12.4.2 代码实现

void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];           // 当前要插入的元素
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];    // 向后移动
            j--;
        }
        arr[j + 1] = key;           // 插入正确位置
    }
}

12.4.3 阶段性练习

  1. 练习1:用插入排序对 [7, 2, 5, 1, 9] 排序,写出每一步后数组的变化。
  2. 练习2:实现降序插入排序。
  3. 练习3:插入排序在什么情况下最快?什么情况下最慢?

12.5 快速排序(进阶)

12.5.1 算法思想

快速排序采用分治策略:选择一个基准值(pivot),将数组分成两部分,左边都比基准小,右边都比基准大,然后递归地对左右两部分排序。

过程演示(以第一个元素为基准):
数组:[5, 3, 8, 1, 2]
- 基准5,从右找小于5的2,从左找大于5的8,交换2和8 → [5, 3, 2, 1, 8]
- 继续,从右找小于5的1,从左找大于5的没有,交换1和基准位置?实际上标准做法是用两个指针,最终将基准放到正确位置。
最终得到 [2, 3, 1, 5, 8],左边都小于5,右边大于5。
- 递归排序左边 [2,3,1] 和右边 [8]。

12.5.2 代码实现

int partition(int arr[], int low, int high) {
    int pivot = arr[low];          // 选第一个为基准
    int i = low, j = high;
    while (i < j) {
        while (i < j && arr[j] >= pivot) j--; // 从右找小于pivot的
        if (i < j) arr[i++] = arr[j];
        while (i < j && arr[i] <= pivot) i++; // 从左找大于pivot的
        if (i < j) arr[j--] = arr[i];
    }
    arr[i] = pivot;                // 基准归位
    return i;                      // 返回基准位置
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

调用时:quickSort(arr, 0, n-1);

12.5.3 阶段性练习

  1. 练习1:模拟快速排序对 [4, 7, 2, 1, 5, 3] 的过程。
  2. 练习2:尝试以中间元素为基准实现快速排序。

12.6 排序算法的比较

算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性
冒泡排序 O(n²) O(n²) O(1) 稳定
选择排序 O(n²) O(n²) O(1) 不稳定
插入排序 O(n²) O(n²) O(1) 稳定
快速排序 O(n log n) O(n²) O(log n) 不稳定
归并排序 O(n log n) O(n log n) O(n) 稳定

稳定性:如果两个相等的元素在排序前后的相对位置不变,则排序算法是稳定的。


12.7 编程实例讲解

实例1:成绩排序

题目:输入n个学生的姓名和成绩,按成绩从高到低排序,如果成绩相同则按姓名字典序排序。

#include <iostream>
#include <string>
#include <algorithm>  // 用sort,但我们可以手写排序
using namespace std;

struct Student {
    string name;
    int score;
};

// 冒泡排序(按成绩降序,成绩相同按姓名升序)
void sortStudents(Student stu[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (stu[j].score < stu[j + 1].score || 
                (stu[j].score == stu[j + 1].score && stu[j].name > stu[j + 1].name)) {
                swap(stu[j], stu[j + 1]);
            }
        }
    }
}

int main() {
    int n;
    cout << "请输入学生人数:";
    cin >> n;
    Student stu[100];
    for (int i = 0; i < n; i++) {
        cin >> stu[i].name >> stu[i].score;
    }
    sortStudents(stu, n);
    cout << "\n排序结果:" << endl;
    for (int i = 0; i < n; i++) {
        cout << stu[i].name << " " << stu[i].score << endl;
    }
    return 0;
}

实例2:排序算法效率对比

用随机数生成大量数据,测试不同排序算法的时间。

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

// 冒泡排序、选择排序、插入排序、快速排序函数声明...

int main() {
    const int N = 10000;
    int arr1[N], arr2[N], arr3[N], arr4[N];
    srand(time(0));
    for (int i = 0; i < N; i++) {
        int val = rand() % 10000;
        arr1[i] = arr2[i] = arr3[i] = arr4[i] = val;
    }

    clock_t start, end;

    start = clock();
    bubbleSort(arr1, N);
    end = clock();
    cout << "冒泡排序时间:" << double(end - start) / CLOCKS_PER_SEC << "秒" << endl;

    // 测试其他排序...
    return 0;
}

12.8 第12章编程作业

作业1:三种简单排序的对比

编写程序,生成1000个随机整数,分别用冒泡、选择、插入排序进行排序,并输出每种排序的交换次数和比较次数(在代码中加入计数器)。

作业2:排序字符串数组

输入若干单词(字符串),用插入排序按字典序排序输出。

作业3:快速排序的实现

实现快速排序,并测试其效率。

作业4:排序稳定性验证

创建一个包含姓名和分数的结构体数组,其中有两个同分不同名的学生。分别用稳定和不稳定的排序算法排序,观察他们的相对顺序是否改变。

作业5:自定义排序规则

编写一个程序,按以下规则对整数排序:奇数在前,偶数在后;奇数按升序,偶数按降序。例如 [3,2,5,1,8,4] 排序后应为 [1,3,5,8,4,2]。


恭喜你完成了第十二章的学习!现在你已经掌握了多种排序算法,能够处理各种排序问题了。加油!🚀

20260228 142049 Cpp 入门第十一课

第十一章:算法入门——模拟与高精度

你好!欢迎来到第十一章!从这一章开始,我们将正式进入算法学习。算法是解决问题的步骤和方法,就像菜谱一样,告诉计算机一步一步怎么做。本章我们学习两类基础算法:模拟高精度

  • 模拟:按照题目描述的过程,一步一步用程序实现,就像“照葫芦画瓢”。
  • 高精度:当数字太大,连 long long 都存不下时,我们需要用数组或字符串来存储和计算,比如求 100 的阶乘。

让我们一起探索吧!


11.1 模拟算法

11.1.1 什么是模拟?

模拟就是让计算机模仿现实世界的过程。比如,你要写一个程序模拟电梯的运行:有人按按钮,电梯就移动到那一层。你只需要按照电梯的规则一步一步写代码。

11.1.2 模拟程序范例

我们来看一个简单的模拟题:猜拳游戏。两个人玩石头剪刀布,规则是石头赢剪刀、剪刀赢布、布赢石头。输入两人的出拳(0代表石头,1代表剪刀,2代表布),输出谁赢。

#include <iostream>
using namespace std;

int main() {
    int a, b;
    cout << "请输入A和B的出拳(0石头 1剪刀 2布):";
    cin >> a >> b;

    if (a == b) {
        cout << "平局" << endl;
    } else if ((a == 0 && b == 1) || (a == 1 && b == 2) || (a == 2 && b == 0)) {
        cout << "A赢了" << endl;
    } else {
        cout << "B赢了" << endl;
    }

    return 0;
}

这就是一个简单的模拟:根据游戏规则判断胜负。

11.1.3 模拟的步骤

  1. 读懂题目:明确题目描述的规则和过程。
  2. 找出变量:哪些东西会变化,需要用变量表示。
  3. 模拟过程:用循环和判断一步步实现。
  4. 输出结果:根据模拟得到的结果输出。

11.1.4 模拟实例讲解

实例1:报数游戏(约瑟夫环简化)

题目:n个人围成一圈,从第1个人开始报数,数到m的人出列,然后从下一个人继续报数。输入n和m,输出出列顺序。

#include <iostream>
using namespace std;

int main() {
    int n, m;
    cout << "请输入总人数n和报数m:";
    cin >> n >> m;

    bool inGame[100] = {false}; // 标记是否还在游戏中,假设最多100人
    for (int i = 0; i < n; i++) inGame[i] = true;

    int count = 0;      // 当前报的数
    int outCount = 0;   // 已经出列的人数
    int i = 0;          // 当前人的下标

    while (outCount < n) {
        if (inGame[i]) {
            count++;
            if (count == m) {
                cout << i + 1 << " "; // 输出编号(从1开始)
                inGame[i] = false;
                outCount++;
                count = 0;
            }
        }
        i = (i + 1) % n; // 下一个,形成环
    }
    cout << endl;

    return 0;
}

运行示例

请输入总人数n和报数m:5 3
3 1 5 2 4
实例2:电梯模拟

题目:有一部电梯,初始在第1层。输入一系列请求,每个请求包含目标楼层和方向(上或下)。电梯每次移动一层,每移动一层花费1秒,停靠开门关门花费5秒。按请求顺序处理(即先来先服务),计算总时间。

#include <iostream>
using namespace std;

int main() {
    int n;
    cout << "请输入请求个数:";
    cin >> n;

    int currentFloor = 1;
    int totalTime = 0;

    for (int i = 0; i < n; i++) {
        int target;
        cout << "请输入目标楼层:";
        cin >> target;

        // 移动时间 = 楼层差绝对值
        totalTime += abs(target - currentFloor);
        currentFloor = target;
        // 停靠时间
        totalTime += 5;
    }

    cout << "总时间:" << totalTime << "秒" << endl;

    return 0;
}

11.1.5 阶段性编程练习(模拟)

  1. 练习1:模拟一个简单的计算器。输入两个数和运算符(+、-、*、/),输出结果,注意除数为0的情况。
  2. 练习2:模拟一个自动售货机。商品价格5元,投入硬币(1元、5元、10元),计算总金额,如果足够支付,输出找零,否则提示金额不足。
  3. 练习3:模拟一个猜数字游戏(电脑随机生成1-100的数,用户猜,提示大小,直到猜中,统计次数)。
  4. 练习4:模拟一个排队系统。有n个人排队,每分钟处理一个人,新来的人排到队尾。输入初始队列(用数组表示),然后模拟3分钟,每分钟输出当前队首被服务,并加入一个新来的人(编号为n+1、n+2…)。

11.2 高精度算法

11.2.1 为什么需要高精度?

C++中,int 大约能存到21亿(10位),long long 大约能存到9×10^18(19位)。如果要计算 100!(约100位),或者两个100位的整数相加,普通变量就不够用了。我们需要用数组字符串来存储每一位数字,然后模拟手工计算的过程。

11.2.2 高精度存储

通常用字符串读入,然后倒序存入整型数组(因为手工计算是从低位到高位)。

例如:数字 12345 存在数组里:a[0]=5, a[1]=4, a[2]=3, a[3]=2, a[4]=1

11.2.3 高精度加法

两个大数相加,从低位到高位逐位相加,处理进位。

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

// 高精度加法,返回结果字符串
string add(string s1, string s2) {
    // 将字符串反转,便于从低位开始计算
    reverse(s1.begin(), s1.end());
    reverse(s2.begin(), s2.end());

    // 确保s1较长
    if (s1.length() < s2.length()) swap(s1, s2);

    int carry = 0; // 进位
    for (size_t i = 0; i < s1.length(); i++) {
        int a = s1[i] - '0';
        int b = i < s2.length() ? s2[i] - '0' : 0;
        int sum = a + b + carry;
        s1[i] = sum % 10 + '0';
        carry = sum / 10;
    }
    if (carry) {
        s1 += carry + '0'; // 最高位有进位
    }
    reverse(s1.begin(), s1.end());
    return s1;
}

int main() {
    string a, b;
    cout << "请输入两个大整数:";
    cin >> a >> b;
    cout << "和:" << add(a, b) << endl;
    return 0;
}

11.2.4 高精度减法

减法需要注意借位,并且要保证结果是非负的(先比较大小)。

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

// 比较两个字符串表示的数,若a>=b返回true
bool greaterOrEqual(string a, string b) {
    if (a.length() != b.length()) return a.length() > b.length();
    return a >= b;
}

// 高精度减法,要求a>=b
string subtract(string a, string b) {
    reverse(a.begin(), a.end());
    reverse(b.begin(), b.end());

    string result;
    int borrow = 0;
    for (size_t i = 0; i < a.length(); i++) {
        int digitA = a[i] - '0' - borrow;
        int digitB = i < b.length() ? b[i] - '0' : 0;
        if (digitA < digitB) {
            digitA += 10;
            borrow = 1;
        } else {
            borrow = 0;
        }
        result += (digitA - digitB) + '0';
    }
    // 去除前导零
    while (result.length() > 1 && result.back() == '0') {
        result.pop_back();
    }
    reverse(result.begin(), result.end());
    return result;
}

int main() {
    string a, b;
    cout << "请输入两个大整数(a >= b):";
    cin >> a >> b;
    if (!greaterOrEqual(a, b)) {
        cout << "a必须大于等于b" << endl;
    } else {
        cout << "差:" << subtract(a, b) << endl;
    }
    return 0;
}

11.2.5 高精度乘法(高精度×低精度)

一个大数乘以一个普通整数(如 long long 范围内)。

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

string multiply(string a, int b) {
    reverse(a.begin(), a.end());
    int carry = 0;
    for (size_t i = 0; i < a.length(); i++) {
        int cur = (a[i] - '0') * b + carry;
        a[i] = cur % 10 + '0';
        carry = cur / 10;
    }
    while (carry) {
        a += carry % 10 + '0';
        carry /= 10;
    }
    reverse(a.begin(), a.end());
    return a;
}

int main() {
    string a;
    int b;
    cout << "请输入大整数a和普通整数b:";
    cin >> a >> b;
    cout << "积:" << multiply(a, b) << endl;
    return 0;
}

11.2.6 高精度乘法(高精度×高精度)

两个大数相乘,模拟竖式乘法。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

string multiply(string a, string b) {
    int lenA = a.length(), lenB = b.length();
    vector<int> res(lenA + lenB, 0); // 结果最多 lenA+lenB 位

    // 将字符串反转存入整型数组
    reverse(a.begin(), a.end());
    reverse(b.begin(), b.end());
    for (int i = 0; i < lenA; i++) {
        for (int j = 0; j < lenB; j++) {
            res[i + j] += (a[i] - '0') * (b[j] - '0');
            res[i + j + 1] += res[i + j] / 10; // 进位
            res[i + j] %= 10;
        }
    }

    // 去除前导0
    while (res.size() > 1 && res.back() == 0) res.pop_back();
    // 转成字符串
    string result;
    for (int i = res.size() - 1; i >= 0; i--) {
        result += res[i] + '0';
    }
    return result;
}

int main() {
    string a, b;
    cout << "请输入两个大整数:";
    cin >> a >> b;
    cout << "积:" << multiply(a, b) << endl;
    return 0;
}

11.2.7 高精度阶乘

计算 n!,n 较大时结果很长,用高精度乘法。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

string multiply(string a, int b) {
    // 复用之前的高精度乘低精度
    reverse(a.begin(), a.end());
    int carry = 0;
    for (size_t i = 0; i < a.length(); i++) {
        int cur = (a[i] - '0') * b + carry;
        a[i] = cur % 10 + '0';
        carry = cur / 10;
    }
    while (carry) {
        a += carry % 10 + '0';
        carry /= 10;
    }
    reverse(a.begin(), a.end());
    return a;
}

string factorial(int n) {
    string result = "1";
    for (int i = 2; i <= n; i++) {
        result = multiply(result, i);
    }
    return result;
}

int main() {
    int n;
    cout << "请输入n:";
    cin >> n;
    cout << n << "! = " << factorial(n) << endl;
    return 0;
}

11.2.8 阶段性编程练习(高精度)

  1. 练习1:输入两个大整数,输出它们的和、差(保证非负)。
  2. 练习2:输入一个大整数和一个普通整数,输出它们的积。
  3. 练习3:计算斐波那契数列的第100项(用高精度)。
  4. 练习4:输入两个大整数,输出它们的乘积。

11.3 编程实例讲解

实例3:高精度加法应用——大数求和

题目:输入若干个大整数(以0结束),求它们的总和。

#include <iostream>
#include <string>
using namespace std;

string add(string a, string b) {
    // 复用之前的加法代码
    // ...
}

int main() {
    string sum = "0", num;
    while (true) {
        cin >> num;
        if (num == "0") break;
        sum = add(sum, num);
    }
    cout << "总和:" << sum << endl;
    return 0;
}

实例4:模拟+高精度——大数阶乘之和

题目:求 1! + 2! + … + n!,其中 n 较大(例如 n=50)。

#include <iostream>
#include <string>
using namespace std;

string multiply(string a, int b) { /* ... */ }
string add(string a, string b) { /* ... */ }

string factorial(int n) { /* ... */ }

int main() {
    int n;
    cout << "请输入n:";
    cin >> n;
    string sum = "0";
    for (int i = 1; i <= n; i++) {
        sum = add(sum, factorial(i));
    }
    cout << "1!+2!+...+" << n << "! = " << sum << endl;
    return 0;
}

11.4 第11章编程作业

作业1:模拟银行排队

银行有2个窗口,客户到达时间和服务时间已知,模拟每个客户在哪个窗口办理,以及等待时间。输入客户数n,以及每个客户的到达时间和服务时间(整数),输出每个客户的开始服务时间和结束时间。

作业2:高精度加法器

编写一个程序,可以不断输入两个大整数,输出它们的和,直到用户输入”0 0”结束。

作业3:高精度乘法器

类似作业2,但做乘法。

作业4:大数比较

输入两个大整数,比较它们的大小(大于、小于、等于)。

作业5:回文数判断(高精度版)

输入一个很大的整数(可能100位),判断它是否是回文数(正读反读一样)。如果是,输出YES;否则输出NO。

作业6:模拟+高精度——大数进制转换

输入一个十进制大数(用字符串),将其转换为二进制(也用字符串输出)。


恭喜你完成了第十一章的学习!模拟是算法中最直接的思想,高精度则让我们突破了数据范围的限制。加油!🚀

20260227 152127 Cpp 入门第十课

第十章:C++的百宝箱——STL(标准模板库)

你好!欢迎来到第十章!在前面的章节,我们学习了数组、链表、栈、队列等数据结构,但每次都要自己写代码实现,很麻烦。其实C++已经给我们准备了一个强大的百宝箱,里面有很多常用的数据结构和算法,可以直接拿来用,这就是STL(Standard Template Library,标准模板库)。学完本章,你就能轻松地使用各种容器和算法,让编程变得事半功倍!


10.1 STL程序范例

先来看一个简单的STL程序:使用 vector(动态数组)存储一些整数,然后用 sort 排序,最后用迭代器输出。

#include <iostream>
#include <vector>   // vector容器
#include <algorithm> // 算法,比如sort
using namespace std;

int main() {
    // 创建一个vector,存储整数
    vector<int> numbers;

    // 向尾部添加元素
    numbers.push_back(5);
    numbers.push_back(2);
    numbers.push_back(8);
    numbers.push_back(1);
    numbers.push_back(9);

    cout << "排序前:";
    for (int i = 0; i < numbers.size(); i++) {
        cout << numbers[i] << " ";
    }
    cout << endl;

    // 使用STL的排序算法
    sort(numbers.begin(), numbers.end());

    cout << "排序后:";
    // 使用迭代器遍历
    for (vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    return 0;
}

运行结果

排序前:5 2 8 1 9 
排序后:1 2 5 8 9 

这个程序展示了STL的核心三部分:容器(vector)、算法(sort)、迭代器(iterator)。下面我们一一学习。


10.2 STL概述

STL是C++标准库的一部分,它提供了三大组件:

  • 容器:存储数据的结构,比如动态数组、链表、栈、队列、集合、映射等。
  • 算法:对容器中的数据进行操作的函数,比如排序、查找、替换等。
  • 迭代器:连接容器和算法的桥梁,像指针一样访问容器中的元素。

使用STL的好处:代码简洁、高效、可靠,不用自己重复造轮子。


10.3 容器

容器是用来存放数据的对象。根据组织方式不同,分为序列式容器关联式容器

10.3.1 vector(动态数组)

vector 是一个可以动态增长的数组,支持随机访问(用下标),在尾部插入删除效率高。

基本操作
#include <iostream>
#include <vector>
using namespace std;

int main() {
    // 定义
    vector<int> v1;               // 空vector
    vector<int> v2(5, 10);        // 5个元素,每个都是10
    vector<int> v3 = {1, 2, 3, 4}; // 初始化列表(C++11)

    // 添加元素
    v1.push_back(20);   // 尾部添加
    v1.push_back(30);

    // 访问元素
    cout << v1[0] << endl;         // 下标访问,不检查越界
    cout << v1.at(1) << endl;      // at会检查越界,抛出异常

    // 获取大小
    cout << "v1大小:" << v1.size() << endl;

    // 遍历
    for (int i = 0; i < v1.size(); i++) {
        cout << v1[i] << " ";
    }
    cout << endl;

    // 使用迭代器遍历
    for (vector<int>::iterator it = v1.begin(); it != v1.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    // 使用范围for循环(C++11)
    for (int x : v1) {
        cout << x << " ";
    }
    cout << endl;

    // 删除最后一个元素
    v1.pop_back();

    // 清空
    v1.clear();

    return 0;
}

常用成员函数
- push_back():尾部添加
- pop_back():删除尾部
- size():元素个数
- empty():是否为空
- clear():清空
- begin()end():返回首尾迭代器
- insert()erase():插入和删除(较复杂,需要迭代器)

10.3.2 list(双向链表)

list 是双向链表,不支持随机访问,但插入和删除非常快(尤其在中间)。

#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> lst = {5, 2, 8, 1, 9};

    // 尾部添加
    lst.push_back(10);
    // 头部添加
    lst.push_front(0);

    // 遍历(只能用迭代器,不能用下标)
    for (list<int>::iterator it = lst.begin(); it != lst.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    // 排序(list有自己的sort成员函数,比通用算法快)
    lst.sort();

    for (int x : lst) {
        cout << x << " ";
    }
    cout << endl;

    return 0;
}

10.3.3 deque(双端队列)

deque 是双端队列,支持在头尾快速插入删除,也支持随机访问(但比vector稍慢)。

#include <iostream>
#include <deque>
using namespace std;

int main() {
    deque<int> dq = {1, 2, 3};
    dq.push_back(4);    // 尾部加
    dq.push_front(0);   // 头部加

    for (int i = 0; i < dq.size(); i++) {
        cout << dq[i] << " ";   // 支持下标
    }
    cout << endl;

    dq.pop_back();      // 删除尾部
    dq.pop_front();     // 删除头部
    return 0;
}

10.3.4 stack(栈)

stack 是适配器容器,它基于其他容器(默认deque)实现,提供后进先出(LIFO)接口。

#include <iostream>
#include <stack>
using namespace std;

int main() {
    stack<int> st;
    st.push(10);   // 入栈
    st.push(20);
    st.push(30);

    cout << "栈顶元素:" << st.top() << endl; // 30
    st.pop();      // 出栈(无返回值)
    cout << "栈顶元素:" << st.top() << endl; // 20
    cout << "栈大小:" << st.size() << endl;  // 2

    while (!st.empty()) {
        cout << st.top() << " ";
        st.pop();
    }
    return 0;
}

10.3.5 queue(队列)

queue 也是适配器容器,提供先进先出(FIFO)接口。

#include <iostream>
#include <queue>
using namespace std;

int main() {
    queue<int> q;
    q.push(10);   // 入队
    q.push(20);
    q.push(30);

    cout << "队头:" << q.front() << endl; // 10
    cout << "队尾:" << q.back() << endl;  // 30
    q.pop();      // 出队(队头)
    cout << "队头:" << q.front() << endl; // 20
    return 0;
}

10.3.6 set(集合)

set 是一个有序的集合,元素唯一,自动排序(默认升序)。查找效率高(红黑树)。

#include <iostream>
#include <set>
using namespace std;

int main() {
    set<int> s;
    s.insert(5);
    s.insert(2);
    s.insert(8);
    s.insert(2);   // 重复插入无效

    // 遍历(有序)
    for (set<int>::iterator it = s.begin(); it != s.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    // 查找
    set<int>::iterator it = s.find(5);
    if (it != s.end()) {
        cout << "找到了:" << *it << endl;
    } else {
        cout << "没找到" << endl;
    }

    // 删除
    s.erase(2);
    return 0;
}

10.3.7 map(映射)

map 存储键值对(key-value),键唯一,自动排序。

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> scores;
    scores["小明"] = 98;        // 通过键赋值
    scores["小红"] = 95;
    scores["小刚"] = 88;

    // 遍历
    for (map<string, int>::iterator it = scores.begin(); it != scores.end(); ++it) {
        cout << it->first << " : " << it->second << endl;   // first是键,second是值
    }

    // 查找
    string name = "小红";
    map<string, int>::iterator it = scores.find(name);
    if (it != scores.end()) {
        cout << name << "的成绩是:" << it->second << endl;
    } else {
        cout << "没找到" << endl;
    }

    return 0;
}

常用成员函数
- insert(make_pair(key, value)) 或直接用 [key] = value(但 [] 如果键不存在会创建)
- find(key):查找,返回迭代器,找不到返回 end()
- erase(key):删除


10.4 迭代器

迭代器是一种类似于指针的对象,用来遍历容器中的元素。所有容器都提供 begin()end() 成员函数,返回指向第一个元素和最后一个元素之后位置的迭代器。

迭代器分类(按功能):
- 输入迭代器:只读,一次遍历
- 输出迭代器:只写,一次遍历
- 前向迭代器:可读写,只能向前(如 forward_list
- 双向迭代器:可读写,能前后移动(如 listsetmap
- 随机访问迭代器:可读写,支持 +-[] 等(如 vectordeque

使用示例

vector<int> v = {1, 2, 3, 4, 5};

// 正向迭代器
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
    *it = *it * 2;   // 修改元素
}

// 常量迭代器(只读)
for (vector<int>::const_iterator it = v.cbegin(); it != v.cend(); ++it) {
    cout << *it << " ";
}

// 反向迭代器
for (vector<int>::reverse_iterator rit = v.rbegin(); rit != v.rend(); ++rit) {
    cout << *rit << " ";
}

C++11 可以用 auto 简化:

for (auto it = v.begin(); it != v.end(); ++it) { ... }

10.5 算法

STL提供了大量通用算法,都在 <algorithm> 头文件中。它们通过迭代器操作容器,不依赖于具体容器类型。

10.5.1 sort 排序

#include <algorithm>
vector<int> v = {5, 2, 8, 1, 9};
sort(v.begin(), v.end());                // 升序
sort(v.begin(), v.end(), greater<int>()); // 降序

也可以自定义排序规则(用函数或函数对象):

bool cmp(int a, int b) {
    return a > b;   // 降序
}
sort(v.begin(), v.end(), cmp);

10.5.2 find 查找

vector<int> v = {5, 2, 8, 1, 9};
auto it = find(v.begin(), v.end(), 8);
if (it != v.end()) {
    cout << "找到了,位置:" << it - v.begin() << endl;
} else {
    cout << "没找到" << endl;
}

10.5.3 其他常用算法

  • reverse:反转
    cpp reverse(v.begin(), v.end());
  • count:计数
    cpp int cnt = count(v.begin(), v.end(), 5);
  • max_elementmin_element:找最大最小值
    cpp auto maxIt = max_element(v.begin(), v.end()); cout << "最大值:" << *maxIt << endl;
  • binary_search:二分查找(要求容器有序)
    cpp if (binary_search(v.begin(), v.end(), 8)) { ... }
  • copy:复制
    cpp vector<int> v2(v.size()); copy(v.begin(), v.end(), v2.begin());

10.5.4 算法与迭代器的配合

算法通过迭代器操作数据,所以同一个算法可以用于不同的容器。例如 find 可以用于 vector、list、deque 等。


10.6 编程实例讲解

实例1:统计学生成绩(使用vector和map)

题目:输入若干学生姓名和成绩,以“end”结束。然后输出所有学生的成绩,并按成绩从高到低排序输出。

#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <string>
using namespace std;

struct Student {
    string name;
    int score;
};

bool cmp(const Student &a, const Student &b) {
    return a.score > b.score;   // 降序
}

int main() {
    vector<Student> students;
    string name;
    int score;

    cout << "请输入学生姓名和成绩(输入end结束):" << endl;
    while (true) {
        cin >> name;
        if (name == "end") break;
        cin >> score;
        students.push_back({name, score});
    }

    // 排序
    sort(students.begin(), students.end(), cmp);

    // 输出
    cout << "\n成绩单(按分数降序):" << endl;
    for (const auto &s : students) {
        cout << s.name << " : " << s.score << endl;
    }

    return 0;
}

实例2:单词计数器(使用map)

题目:输入一行英文句子,统计每个单词出现的次数(忽略大小写)。

#include <iostream>
#include <map>
#include <string>
#include <cctype>
#include <sstream>   // 字符串流
using namespace std;

string toLower(string s) {
    for (char &c : s) {
        c = tolower(c);
    }
    return s;
}

int main() {
    string line;
    cout << "请输入一行英文:";
    getline(cin, line);

    map<string, int> wordCount;
    stringstream ss(line);
    string word;

    while (ss >> word) {
        word = toLower(word);
        // 去掉标点符号(简单处理:只保留字母数字)
        string clean;
        for (char c : word) {
            if (isalnum(c)) {
                clean += c;
            }
        }
        if (!clean.empty()) {
            wordCount[clean]++;
        }
    }

    cout << "单词统计:" << endl;
    for (const auto &pair : wordCount) {
        cout << pair.first << " : " << pair.second << endl;
    }

    return 0;
}

实例3:集合运算(使用set)

题目:输入两个集合(整数),输出它们的并集、交集和差集。

#include <iostream>
#include <set>
#include <algorithm>
#include <iterator>
using namespace std;

int main() {
    set<int> set1, set2;
    int n, x;

    cout << "请输入第一个集合的元素个数:";
    cin >> n;
    cout << "请输入" << n << "个整数:";
    for (int i = 0; i < n; i++) {
        cin >> x;
        set1.insert(x);
    }

    cout << "请输入第二个集合的元素个数:";
    cin >> n;
    cout << "请输入" << n << "个整数:";
    for (int i = 0; i < n; i++) {
        cin >> x;
        set2.insert(x);
    }

    // 并集
    set<int> unionSet;
    set_union(set1.begin(), set1.end(), set2.begin(), set2.end(),
              inserter(unionSet, unionSet.begin()));
    cout << "并集:";
    for (int v : unionSet) cout << v << " ";
    cout << endl;

    // 交集
    set<int> interSet;
    set_intersection(set1.begin(), set1.end(), set2.begin(), set2.end(),
                     inserter(interSet, interSet.begin()));
    cout << "交集:";
    for (int v : interSet) cout << v << " ";
    cout << endl;

    // 差集(set1 - set2)
    set<int> diffSet;
    set_difference(set1.begin(), set1.end(), set2.begin(), set2.end(),
                   inserter(diffSet, diffSet.begin()));
    cout << "差集(set1 - set2):";
    for (int v : diffSet) cout << v << " ";
    cout << endl;

    return 0;
}

实例4:使用栈判断括号匹配

题目:输入一个字符串,包含 ()[]{},判断括号是否匹配。

#include <iostream>
#include <stack>
#include <string>
using namespace std;

bool isMatching(char open, char close) {
    return (open == '(' && close == ')') ||
           (open == '[' && close == ']') ||
           (open == '{' && close == '}');
}

bool checkBrackets(const string &s) {
    stack<char> st;
    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') {
            st.push(c);
        } else if (c == ')' || c == ']' || c == '}') {
            if (st.empty() || !isMatching(st.top(), c)) {
                return false;
            }
            st.pop();
        }
    }
    return st.empty();
}

int main() {
    string expr;
    cout << "请输入表达式:";
    cin >> expr;
    if (checkBrackets(expr)) {
        cout << "括号匹配" << endl;
    } else {
        cout << "括号不匹配" << endl;
    }
    return 0;
}

10.7 阶段性编程练习

练习1:vector练习

输入n个整数,存入vector,然后删除所有偶数,输出剩下的奇数。

练习2:list练习

创建一个list,存放10个随机整数(1-100)。使用list的sort排序,然后反转,输出结果。

练习3:map电话簿

实现一个简单的电话簿,用map存储姓名和电话号码。支持添加、删除、查找、显示所有联系人。

练习4:set去重

输入一串整数,可能有重复,用set去除重复后按升序输出。

练习5:队列模拟

用queue模拟一个打印任务队列。每个任务有名称和页数,按先进先出处理,并输出处理顺序。

练习6:算法综合

生成一个vector包含20个随机整数(1-100),然后:
- 排序
- 查找是否存在50
- 统计大于50的个数
- 删除所有小于30的元素(提示:用erase配合remove_if)


10.8 第10章编程作业

恭喜你学完了STL!现在来挑战几个综合题目,用STL容器和算法解决问题。

作业1:学生成绩管理系统(STL版)

用vector存储学生信息(结构体包含姓名、学号、各科成绩)。实现:
- 添加学生
- 删除学生(按学号)
- 修改成绩
- 按总成绩排序并输出
- 按学号查找
- 统计各科平均分、最高分、最低分

作业2:文章词频统计

读入一篇文章(可以从文件读或手动输入),统计每个单词出现的次数,忽略大小写和标点,最后按词频降序输出前10个单词。

作业3:迷宫最短路径(选做)

用queue实现广度优先搜索(BFS)求解迷宫最短路径。迷宫用二维vector表示,0可走,1障碍。

作业4:多项式计算器

用map存储多项式(指数为键,系数为值)。实现两个多项式的加法、减法、乘法。

作业5:停车场模拟

用stack模拟停车场,queue模拟等待车道。车辆到达时,如果有空位则停入stack,否则进入queue;车辆离开时,需要将上面的车暂时移出(用另一个stack),然后让目标车离开,再移回。输出每次操作后的状态。

20260227 151910 Cpp 入门第九课

第九章:深入理解内存——指针、引用和动态内存

你好!欢迎来到第九章!在前面的章节,我们学习了变量、数组、函数和结构体。你有没有想过,变量在内存中到底存放在哪里?我们如何直接操作内存地址?这一章我们将学习C++中最强大也最需要小心的特性——指针。同时还会学习它的好兄弟引用,以及如何动态地分配内存。学完本章,你就能更深入地理解计算机内存的运作方式!


9.1 指针程序范例

先来看一个最简单的指针程序:定义一个整数变量,然后用指针指向它,通过指针修改它的值。

#include <iostream>
using namespace std;

int main() {
    int a = 10;          // 定义一个普通变量
    int *p;              // 定义一个指针变量,它可以存放整数的地址
    p = &a;              // 把a的地址赋值给p,现在p指向a

    cout << "a的值 = " << a << endl;
    cout << "a的地址 = " << &a << endl;   // &是取地址运算符
    cout << "p的值 = " << p << endl;       // p存的就是a的地址
    cout << "p指向的值 = " << *p << endl;  // *是间接访问运算符,得到p指向的内容

    *p = 20;             // 通过指针修改a的值
    cout << "修改后a的值 = " << a << endl;

    return 0;
}

运行结果(地址会因运行环境不同而变化):

a的值 = 10
a的地址 = 0x61ff08
p的值 = 0x61ff08
p指向的值 = 10
修改后a的值 = 20

这个程序展示了指针的基本操作:&取地址,*解引用。就像你可以通过门牌号找到房子,指针就是内存地址,通过它可以访问变量。


9.2 指针的用法

9.2.1 指针的概念

指针是一个变量,它存储的是另一个变量的内存地址。简单说,指针就是地址的“容器”。每个变量在内存中都有一个地址,就像每间教室都有一个门牌号。

  • &:取地址运算符,获得变量的地址。
  • *:间接访问运算符(解引用),通过地址访问该地址存储的值。

9.2.2 指针的定义和赋值

定义指针的格式:类型 *指针名;
- 例如:int *p; 表示p是一个指向整数的指针。
- double *dp; 表示指向double的指针。
- char *cp; 表示指向字符的指针。

指针必须初始化,否则会变成野指针(指向随机地址),非常危险。

int a = 5;
int *p = &a;   // 定义时初始化
int *q;        // 未初始化,危险!
q = &a;        // 之后赋值也可以

9.2.3 指针的运算

指针可以像整数一样进行加减运算,但它的加减是以指向的类型大小为单位的。

int a[5] = {10, 20, 30, 40, 50};
int *p = &a[0];   // 指向第一个元素

cout << "*p = " << *p << endl;        // 输出10
cout << "*(p+1) = " << *(p+1) << endl; // 输出20,p+1指向下一个整数

指针还可以做减法,得到两个指针之间的元素个数。

9.2.4 指针与数组

数组名在大多数情况下可以看作指向第一个元素的指针。

int a[5] = {1, 2, 3, 4, 5};
int *p = a;          // 等价于 p = &a[0]

for (int i = 0; i < 5; i++) {
    cout << *(p + i) << " ";   // 用指针访问数组
}

但数组名不是真正的指针,它是常量,不能修改。

9.2.5 指针与函数

指针作为函数参数

通过传递指针,函数可以修改调用者的变量(因为知道了地址)。

#include <iostream>
using namespace std;

void swap(int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}

int main() {
    int a = 3, b = 5;
    swap(&a, &b);   // 传递地址
    cout << "a = " << a << ", b = " << b << endl;   // a=5, b=3
    return 0;
}
指针作为函数返回值

函数可以返回指针,但要小心不能返回局部变量的地址(因为函数结束局部变量就销毁了)。

int* findMax(int *arr, int size) {
    int *max = arr;
    for (int i = 1; i < size; i++) {
        if (arr[i] > *max) {
            max = &arr[i];
        }
    }
    return max;   // 返回指向最大元素的指针(安全,因为数组生命周期还在)
}

9.2.6 空指针和野指针

  • 空指针:指向空地址的指针,用 nullptr(C++11)或 NULL 表示。解引用空指针会导致程序崩溃。
    cpp int *p = nullptr; // 推荐使用nullptr if (p != nullptr) { *p = 10; // 安全 }

  • 野指针:未初始化或指向已释放内存的指针,使用它非常危险,可能导致程序崩溃或数据损坏。一定要避免!初始化指针是最好的预防。


9.3 引用

9.3.1 引用的概念

引用是给变量起的一个别名,它和原变量共享同一块内存。定义引用时,必须初始化,而且不能改变指向。

int a = 10;
int &b = a;   // b是a的引用,b就是a的别名
b = 20;       // 修改b相当于修改a
cout << a;    // 输出20

9.3.2 引用的用法

引用常用于函数参数,避免拷贝,并且可以直接修改实参。

#include <iostream>
using namespace std;

void swap(int &x, int &y) {   // 引用参数
    int temp = x;
    x = y;
    y = temp;
}

int main() {
    int a = 3, b = 5;
    swap(a, b);   // 直接传变量,不需要取地址
    cout << "a = " << a << ", b = " << b << endl;   // a=5, b=3
    return 0;
}

引用也可以作为函数返回值,但同样不能返回局部变量的引用。

9.3.3 引用与指针的区别

特性 指针 引用
初始化 可以不初始化(但危险) 必须初始化
可修改 可以改变指向其他变量 一旦绑定,不能改变
访问方式 * 解引用 直接像普通变量一样用
空值 可以为 nullptr 不能为空
用途 动态内存、数组操作等 函数参数、操作符重载等

简单说:引用是更安全、更方便的指针,但功能稍弱。


9.4 动态内存分配

有时候我们不知道程序运行时会需要多少内存(比如需要根据用户输入决定数组大小),这时就需要动态内存分配。C++中用 newdelete 来申请和释放内存。

9.4.1 new和delete

  • new 在堆上分配内存,返回指向该内存的指针。
  • delete 释放由 new 分配的内存。
int *p = new int;   // 分配一个整数大小的内存
*p = 10;
cout << *p << endl;
delete p;           // 释放内存,避免内存泄漏
p = nullptr;        // 将指针置空,避免野指针

也可以初始化:

int *p = new int(20);   // 分配并初始化为20

9.4.2 动态数组

new[] 分配数组,用 delete[] 释放。

int n;
cout << "请输入数组大小:";
cin >> n;
int *arr = new int[n];   // 动态分配数组

for (int i = 0; i < n; i++) {
    arr[i] = i * 10;
}

for (int i = 0; i < n; i++) {
    cout << arr[i] << " ";
}
cout << endl;

delete[] arr;            // 释放数组内存
arr = nullptr;

9.4.3 内存泄漏

如果 new 分配了内存,但忘记 delete,这块内存在程序结束前都无法再使用,造成内存泄漏。程序运行时间越长,占用的内存越多,最终可能崩溃。所以一定要成对使用 new/deletenew[]/delete[]

重要规则
- new 对应 delete
- new[] 对应 delete[]
- 不要重复 delete
- 释放后将指针置空。


9.5 编程实例讲解

实例1:用指针实现字符串复制(C风格)

#include <iostream>
using namespace std;

void stringCopy(char *dest, const char *src) {
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';   // 添加字符串结束符
}

int main() {
    char s1[100] = {0};
    char s2[] = "Hello, C++!";
    stringCopy(s1, s2);
    cout << "复制后的字符串:" << s1 << endl;
    return 0;
}

实例2:动态创建结构体

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int age;
    double score;
};

int main() {
    Student *p = new Student;   // 动态分配一个学生结构体
    p->name = "小明";           // 用->访问成员,等价于 (*p).name
    p->age = 12;
    p->score = 98.5;

    cout << "姓名:" << p->name << ",年龄:" << p->age << ",成绩:" << p->score << endl;

    delete p;   // 释放内存
    p = nullptr;
    return 0;
}

实例3:动态数组求平均值

#include <iostream>
using namespace std;

int main() {
    int n;
    cout << "请输入学生人数:";
    cin >> n;

    double *scores = new double[n];
    double sum = 0;

    for (int i = 0; i < n; i++) {
        cout << "请输入第" << i+1 << "个学生的成绩:";
        cin >> scores[i];
        sum += scores[i];
    }

    double avg = sum / n;
    cout << "平均分:" << avg << endl;

    delete[] scores;
    scores = nullptr;
    return 0;
}

实例4:用引用交换两个数(对比指针)

#include <iostream>
using namespace std;

void swapByPointer(int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}

void swapByReference(int &x, int &y) {
    int temp = x;
    x = y;
    y = temp;
}

int main() {
    int a = 3, b = 5;
    swapByPointer(&a, &b);
    cout << "a=" << a << ", b=" << b << endl;   // 5,3

    int c = 10, d = 20;
    swapByReference(c, d);
    cout << "c=" << c << ", d=" << d << endl;   // 20,10
    return 0;
}

9.6 第9章编程作业

恭喜你学完了指针、引用和动态内存!现在来挑战几个综合题目。

作业1:动态数组排序

编写程序,让用户输入数组大小n,然后动态分配数组,输入n个整数,用冒泡排序(用指针操作数组)排序后输出,最后释放内存。

作业2:字符串统计(用指针)

编写函数 int countWords(const char *str),统计字符串中单词的个数(单词之间用空格分隔)。在 main 中测试。

作业3:动态学生信息管理

用结构体 Student 和动态内存实现:
- 输入学生人数n
- 动态创建学生数组
- 输入每个学生的姓名、年龄、成绩
- 按成绩从高到低排序(用指针或引用实现交换)
- 输出排序后的结果
- 释放内存

作业4:引用与指针对比

写三个函数,分别用传值、传指针、传引用的方式实现一个函数 increment,将参数增加1。在 main 中测试,并观察原变量的变化。

作业5:动态二维数组(选做)

动态创建一个 m×n 的二维数组(用指针的指针),输入矩阵并计算转置。注意释放内存。


好了,第九章的内容就到这里!指针是C++中最灵活也最复杂的部分,需要多加练习才能熟练掌握。记住:指针一定要初始化,new和delete要成对出现。下一章我们将学习更高级的内容——类和对象,进入面向对象编程的世界。加油!🚀

20260227 151824 Cpp 入门第八课

第八章:自定义数据类型——结构体和联合体

你好!欢迎来到第八章!在前面的章节,我们学习了基本数据类型(int、double、char)和数组(一组相同类型的数据)。但生活中很多事物是由不同类型的数据组成的,比如一个学生有姓名(字符串)、年龄(整数)、身高(小数)。如果能把它们组合成一个整体,就方便多了!这就是结构体。另外还有一种特殊的类型叫联合体,可以节省内存。这一章我们就来学习如何自定义这些数据类型。


8.1 结构体程序范例

先来看一个例子:定义一个学生结构体,包含姓名、年龄、成绩,然后创建学生变量并输出信息。

#include <iostream>
#include <string>
using namespace std;

// 定义结构体类型 Student
struct Student {
    string name;   // 姓名
    int age;       // 年龄
    double score;  // 成绩
};

int main() {
    // 创建结构体变量并初始化
    Student stu1 = {"小明", 12, 98.5};
    Student stu2;

    // 给stu2的成员赋值
    stu2.name = "小红";
    stu2.age = 11;
    stu2.score = 95.0;

    // 输出学生信息
    cout << "学生1:" << stu1.name << ",年龄" << stu1.age << ",成绩" << stu1.score << endl;
    cout << "学生2:" << stu2.name << ",年龄" << stu2.age << ",成绩" << stu2.score << endl;

    return 0;
}

运行结果

学生1:小明,年龄12,成绩98.5
学生2:小红,年龄11,成绩95

8.2 结构体的用法

8.2.1 结构体的概念

结构体是一种可以包含多个不同数据类型的成员的数据类型。它就像一张表格,每一列都有不同的类型。你可以把结构体看作是自己创造的一种新类型,然后像使用int一样用它定义变量。

8.2.2 定义结构体类型

格式:

struct 结构体名 {
    数据类型 成员1;
    数据类型 成员2;
    ...
};   // 注意最后有分号

例如:

struct Point {
    int x;
    int y;
};

struct Book {
    string title;
    string author;
    double price;
};

8.2.3 创建结构体变量

有几种方式:

// 方式1:先定义类型,再定义变量
struct Point p1;   // C++中可以省略struct关键字,直接写 Point p1;

// 方式2:定义类型的同时定义变量
struct Point {
    int x;
    int y;
} p2, p3;   // 定义了p2和p3两个变量

// 方式3:直接定义匿名结构体变量
struct {
    int x;
    int y;
} p4;   // 但这种无法再用这个类型定义其他变量

8.2.4 初始化结构体变量

可以在定义时用大括号初始化:

Point p1 = {10, 20};           // x=10, y=20
Point p2 = {0};                // x=0, y=0(未指定的成员自动初始化为0)

如果使用C++11,也可以这样:

Point p3 {30, 40};   // 等号可选

8.2.5 访问结构体成员

使用点号 . 访问成员变量:

p1.x = 100;
p1.y = 200;
cout << "(" << p1.x << ", " << p1.y << ")" << endl;

8.2.6 结构体数组

可以把结构体放进数组,就像普通类型一样:

Student class3[3] = {
    {"小明", 12, 98.5},
    {"小红", 11, 95.0},
    {"小刚", 12, 88.0}
};

// 遍历输出
for (int i = 0; i < 3; i++) {
    cout << class3[i].name << " " << class3[i].age << " " << class3[i].score << endl;
}

8.2.7 结构体作为函数参数

结构体可以像普通变量一样传递给函数:

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int age;
    double score;
};

// 输出学生信息(传值,会复制一份)
void printStudent(Student stu) {
    cout << stu.name << " " << stu.age << " " << stu.score << endl;
}

// 修改学生年龄(传引用,可以直接修改原变量)
void setAge(Student &stu, int newAge) {
    stu.age = newAge;
}

int main() {
    Student s = {"小明", 12, 98.5};
    printStudent(s);
    setAge(s, 13);
    printStudent(s);   // 年龄变为13
    return 0;
}

8.2.8 结构体作为返回值

函数可以返回结构体:

Student createStudent(string name, int age, double score) {
    Student s;
    s.name = name;
    s.age = age;
    s.score = score;
    return s;
}

int main() {
    Student s = createStudent("小红", 11, 95.0);
    printStudent(s);
    return 0;
}

8.2.9 结构体嵌套

结构体的成员可以是另一个结构体:

struct Date {
    int year;
    int month;
    int day;
};

struct Student {
    string name;
    int age;
    double score;
    Date birthday;   // 嵌套结构体
};

int main() {
    Student s = {"小明", 12, 98.5, {2012, 5, 20}};
    cout << "出生日期:" << s.birthday.year << "年" << s.birthday.month << "月" << s.birthday.day << "日" << endl;
    return 0;
}

8.2.10 阶段性编程练习(结构体基础)

  1. 练习1:定义一个结构体 Rectangle,包含长和宽(整数)。写一个函数计算面积并返回。
  2. 练习2:定义一个结构体 Time,包含时、分、秒。写一个函数输入一个时间,输出它过了多少秒(从0:0:0开始)。
  3. 练习3:定义结构体 Student,包含姓名、学号、3门课成绩。输入5个学生,计算每个学生的平均分,并按平均分从高到低排序输出。
  4. 练习4:用结构体嵌套表示一个学生的家庭地址(省、市、街道),并输入输出。

8.3 联合体

8.3.1 联合体程序范例

联合体(union)和结构体类似,但它的所有成员共用同一块内存,也就是说,同时只能存储一个成员的值。它主要用于节省内存或处理不同类型的数据。

#include <iostream>
using namespace std;

union Data {
    int i;
    double d;
    char c;
};

int main() {
    Data data;
    data.i = 10;          // 现在存储整数
    cout << "整数:" << data.i << endl;

    data.d = 3.14;        // 现在存储浮点数,覆盖了之前的整数
    cout << "浮点数:" << data.d << endl;

    data.c = 'A';         // 现在存储字符
    cout << "字符:" << data.c << endl;

    // 注意:此时 data.i 的值已经无效,因为内存被覆盖
    cout << "整数(已无效):" << data.i << endl;   // 可能输出乱码

    return 0;
}

8.3.2 联合体的用法

  • 联合体的定义和结构体类似,只是关键字是 union
  • 所有成员共享同一块内存,所以联合体的大小等于最大成员的大小。
  • 在同一时刻只能使用一个成员,否则会导致数据混乱。
  • 常用于需要多种类型但只存一种的场景,比如协议解析、节省内存等。
定义和使用
union Value {
    int i;
    float f;
    char str[20];
};

int main() {
    Value v;
    v.i = 100;
    cout << v.i << endl;
    v.f = 3.14;   // 覆盖
    cout << v.f << endl;
    // 注意:不能再访问 v.i,因为值已被覆盖
    return 0;
}

8.3.3 联合体和结构体的区别

  • 结构体:每个成员都有自己的内存空间,可以同时存储所有成员的值。
  • 联合体:所有成员共用同一块内存,只能存储一个成员的值。

8.3.4 阶段性编程练习(联合体)

  1. 练习1:定义一个联合体,包含 int、double、char,分别输入并输出,观察内存覆盖现象。
  2. 练习2:用联合体实现一个可以存储整数或浮点数的变量,并编写一个函数,根据类型打印(需要额外用一个变量标记当前类型,这通常和联合体一起使用,构成“带标记的联合体”)。

8.4 枚举类型(补充知识)

虽然大纲没有明确要求,但枚举类型也是自定义数据类型的一种,常和结构体一起使用,这里简单介绍一下。

枚举用于定义一组命名的整数常量,使代码更易读。

#include <iostream>
using namespace std;

enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN };
// 默认 MON=0, TUE=1, ...

int main() {
    Weekday today = WED;
    if (today == WED) {
        cout << "今天是星期三" << endl;
    }

    // 枚举值可以转成整数
    cout << "MON = " << MON << endl;   // 输出0

    return 0;
}

可以指定枚举的值:

enum Color { RED = 1, GREEN = 2, BLUE = 4 };

C++11引入了强类型枚举(enum class),但初学者了解基本枚举即可。


8.5 编程实例讲解

实例1:学生成绩管理系统(结构体数组)

题目:有N个学生,每个学生有姓名、学号、语文、数学、英语成绩。要求:
- 输入学生信息
- 计算每个学生的总分和平均分
- 输出所有学生信息(包括总分平均分)
- 按总分从高到低排序输出

#include <iostream>
#include <string>
#include <algorithm>   // 用sort需要,但我们自己实现排序函数
using namespace std;

struct Student {
    string name;
    int id;
    int chinese;
    int math;
    int english;
    int total;      // 总分,可以计算后存储
    double average; // 平均分
};

// 输入学生信息
void inputStudent(Student &s) {
    cout << "请输入姓名、学号、语文、数学、英语:";
    cin >> s.name >> s.id >> s.chinese >> s.math >> s.english;
    s.total = s.chinese + s.math + s.english;
    s.average = s.total / 3.0;
}

// 输出学生信息
void printStudent(const Student &s) {
    cout << s.name << "\t" << s.id << "\t" << s.chinese << "\t" << s.math << "\t" << s.english
         << "\t总分:" << s.total << "\t平均:" << s.average << endl;
}

// 按总分冒泡排序(降序)
void sortStudents(Student stu[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (stu[j].total < stu[j + 1].total) {   // 降序
                Student temp = stu[j];
                stu[j] = stu[j + 1];
                stu[j + 1] = temp;
            }
        }
    }
}

int main() {
    const int N = 3;   // 假设3个学生
    Student students[N];

    // 输入
    for (int i = 0; i < N; i++) {
        cout << "请输入第" << i+1 << "个学生信息:" << endl;
        inputStudent(students[i]);
    }

    // 排序
    sortStudents(students, N);

    // 输出
    cout << "\n排序后的学生信息(按总分降序):" << endl;
    cout << "姓名\t学号\t语文\t数学\t英语\t总分\t平均" << endl;
    for (int i = 0; i < N; i++) {
        printStudent(students[i]);
    }

    return 0;
}

实例2:点与矩形(结构体嵌套)

定义一个点结构体,一个矩形结构体(由左上点和右下点确定)。写函数判断一个点是否在矩形内(包括边界)。

#include <iostream>
using namespace std;

struct Point {
    int x;
    int y;
};

struct Rectangle {
    Point topLeft;     // 左上角
    Point bottomRight; // 右下角
};

// 判断点p是否在矩形r内
bool isPointInRect(const Point &p, const Rectangle &r) {
    return (p.x >= r.topLeft.x && p.x <= r.bottomRight.x &&
            p.y <= r.topLeft.y && p.y >= r.bottomRight.y);   // 注意y轴方向:左上角y大,右下角y小
}

int main() {
    Rectangle rect = { {0, 10}, {10, 0} };   // 左上(0,10),右下(10,0)
    Point p;
    cout << "输入点的坐标(x y):";
    cin >> p.x >> p.y;

    if (isPointInRect(p, rect)) {
        cout << "点在矩形内" << endl;
    } else {
        cout << "点不在矩形内" << endl;
    }

    return 0;
}

实例3:带标记的联合体(简单模拟)

有时我们需要一个变量能存储不同类型的数据,并记住当前是什么类型。可以用结构体包含一个枚举类型和一个联合体。

#include <iostream>
#include <string>
using namespace std;

enum DataType { TYPE_INT, TYPE_DOUBLE, TYPE_CHAR };

struct Variant {
    DataType type;
    union {
        int i;
        double d;
        char c;
    } data;
};

void printVariant(const Variant &v) {
    switch (v.type) {
        case TYPE_INT:
            cout << "整数:" << v.data.i << endl;
            break;
        case TYPE_DOUBLE:
            cout << "浮点数:" << v.data.d << endl;
            break;
        case TYPE_CHAR:
            cout << "字符:" << v.data.c << endl;
            break;
    }
}

int main() {
    Variant v1, v2, v3;
    v1.type = TYPE_INT;
    v1.data.i = 100;

    v2.type = TYPE_DOUBLE;
    v2.data.d = 3.14159;

    v3.type = TYPE_CHAR;
    v3.data.c = 'A';

    printVariant(v1);
    printVariant(v2);
    printVariant(v3);

    return 0;
}

8.6 第8章编程作业

恭喜你学完了结构体和联合体!现在来挑战几个综合题目。

作业1:图书管理系统

定义一个结构体 Book,包含书名、作者、出版社、价格、库存数量。实现以下功能:
- 输入一批图书信息(假设最多100本)
- 按书名查找图书,输出信息
- 按价格区间查找(如输入最低价和最高价)
- 统计库存总量和总价值

作业2:日期计算器

定义结构体 Date(年、月、日)。写函数:
- 判断某年是否是闰年
- 计算两个日期之间相差多少天(假设在同一年,或不同年)
- 输入一个日期,输出它是该年的第几天

作业3:学生成绩统计(文件版扩展)

用结构体数组存储学生信息,包括姓名、学号、多门课成绩。要求:
- 从文件读取(暂时可以先从键盘输入)
- 计算每门课的平均分、最高分、最低分
- 按总成绩排名
- 输出不及格学生名单

作业4:联合体应用——解析IP地址

IP地址通常用点分十进制表示,但也可以看作一个32位整数。用联合体实现:可以分别以整数形式和4个字节形式访问同一个IP。写程序输入一个整数形式的IP,输出点分十进制;或者反过来。(提示:可以用 unsigned intunsigned char 数组共用内存)

作业5:简单通讯录

定义结构体 Contact,包含姓名、电话、邮箱。实现一个简单的通讯录管理系统,支持添加、删除、修改、查找、显示所有联系人。


好了,第八章的内容就到这里!你已经学会了如何用结构体组织不同类型的数据,用联合体节省内存,还了解了枚举类型。这些自定义类型让程序能更好地描述现实世界中的事物。加油!🚀

20260227 120331 Cpp 入门第七课

第七章:让程序更聪明——函数

你好!欢迎来到第七章!在前面的章节,我们写的程序都是顺序执行的,代码都挤在 main 函数里。如果有一段代码需要重复使用,难道要复制粘贴很多遍吗?那太麻烦了!这时候就需要函数——把一段代码打包成一个“积木块”,需要的时候随时调用。这一章我们就来学习如何自己创造函数,让程序更聪明、更简洁!


7.1 函数程序范例

先来看一个简单的例子:我们写一个函数,用来计算两个整数的和,然后在 main 里调用它。

#include <iostream>
using namespace std;

// 定义一个函数,名字叫 add,它的功能是返回两个整数的和
int add(int x, int y) {
    int result = x + y;
    return result;      // 把结果返回给调用者
}

int main() {
    int a = 10, b = 20;
    int sum = add(a, b);   // 调用 add 函数,把 a 和 b 传进去,得到返回值
    cout << "a + b = " << sum << endl;

    int c = 30, d = 40;
    cout << "c + d = " << add(c, d) << endl;   // 也可以直接输出返回值

    return 0;
}

运行结果

a + b = 30
c + d = 70

这个程序里,我们定义了一个 add 函数,它接受两个整数参数,返回它们的和。在 main 中,我们可以多次调用它,不用重复写加法代码。


7.2 函数的用法

7.2.1 函数的概念

函数就是一段可以重复使用的代码块,它有自己的名字,你可以通过这个名字来执行它。就像你有一个“做作业”的流程:拿出作业本、写作业、检查、合上本子。你可以把这个流程打包成一个函数叫 doHomework(),每次想写作业时就调用它。

函数的好处:
- 避免重复代码:相同的逻辑只写一次。
- 模块化:把大问题拆成小问题,每个函数解决一个小问题。
- 便于修改和维护:如果需要修改某个功能,只需改函数内部,不用到处找。

7.2.2 语句块与作用域

语句块就是用大括号 {} 括起来的一组语句。比如 if 语句的后面、循环的后面,还有函数体都是语句块。

作用域指的是变量在程序中的有效范围。在一个语句块内定义的变量,只能在这个块内部使用,块外面是访问不到的。这叫做局部变量

#include <iostream>
using namespace std;

int main() {
    int a = 10;   // 这是 main 函数内的局部变量

    if (a > 5) {
        int b = 20;   // b 只在这个 if 块内有效
        cout << a << " " << b << endl;   // 可以访问 a 和 b
    }

    // cout << b;   // 错误!b 在这里已经不存在了
    return 0;
}

函数也是语句块,函数内部定义的变量只属于这个函数,其他函数不能直接访问。

7.2.3 自定义函数介绍

定义一个函数的基本格式:

返回值类型 函数名(参数列表) {
    // 函数体:要执行的代码
    return 返回值;   // 如果返回值类型不是 void,必须返回对应类型的值
}
  • 返回值类型:函数执行完后要返回什么类型的数据,比如 intdoublechar,如果没有返回值就用 void
  • 函数名:自己起名字,要符合变量命名规则,最好能说明功能。
  • 参数列表:函数需要的输入数据,可以有多个,用逗号分隔,每个参数要写明类型和名字。也可以没有参数,写成 ()(void)
  • 函数体:具体执行的代码。
  • return 语句:把结果返回给调用者。如果返回值类型是 void,可以没有 return,或者只写 return; 表示结束函数。

例子:一个没有参数、没有返回值的函数

#include <iostream>
using namespace std;

// 输出欢迎信息
void sayHello() {
    cout << "你好,欢迎学习C++!" << endl;
}

int main() {
    sayHello();   // 调用函数
    sayHello();   // 可以多次调用
    return 0;
}

例子:有参数但没有返回值

#include <iostream>
using namespace std;

// 输出两个数的和,但不返回结果
void printSum(int x, int y) {
    cout << x << " + " << y << " = " << x + y << endl;
}

int main() {
    printSum(3, 5);
    printSum(10, 20);
    return 0;
}

7.2.4 函数的返回值

return 语句有两个作用:
1. 结束函数的执行,返回到调用它的地方。
2. 把后面的值返回给调用者。

例子:返回较大值的函数

#include <iostream>
using namespace std;

int max(int x, int y) {
    if (x > y) {
        return x;
    } else {
        return y;
    }
}

int main() {
    int a = 15, b = 20;
    int m = max(a, b);
    cout << "较大的数是:" << m << endl;
    return 0;
}

注意:如果函数声明了返回值类型(非 void),那么所有分支都必须有 return,否则编译错误。

7.2.5 函数的形参与实参

  • 形参(形式参数):定义函数时写的参数,就像占位符,告诉调用者需要传什么类型的数据。比如 int add(int x, int y) 中的 xy
  • 实参(实际参数):调用函数时实际传递的值,比如 add(3, 5) 中的 35

调用时,实参会复制给形参,然后在函数内部使用形参。函数内部对形参的修改不会影响实参(除非传的是指针或引用,但初学者先不管)。

#include <iostream>
using namespace std;

void change(int x) {
    x = 100;   // 修改形参
    cout << "函数内部 x = " << x << endl;
}

int main() {
    int a = 10;
    change(a);
    cout << "main 中 a = " << a << endl;   // a 还是 10,没变
    return 0;
}

7.2.6 函数的声明

在C++中,函数必须先声明或定义,然后才能调用。如果函数的定义写在调用之后,就需要提前声明(也叫函数原型)。

#include <iostream>
using namespace std;

// 函数声明,告诉编译器有这个函数,后面再定义
int max(int x, int y);

int main() {
    int a = 5, b = 8;
    cout << max(a, b) << endl;
    return 0;
}

// 函数定义
int max(int x, int y) {
    return (x > y) ? x : y;
}

函数声明只需要写返回值类型、函数名和参数类型,可以省略参数名(但建议保留,便于阅读)。

7.2.7 函数的调用与递归

调用很简单,写函数名加括号和实参即可。

递归:函数自己调用自己。就像俄罗斯套娃,一层套一层。递归必须有一个结束条件,否则会无限循环。

例子:用递归计算阶乘(n! = 1×2×…×n)

#include <iostream>
using namespace std;

int factorial(int n) {
    if (n == 0 || n == 1) {   // 递归结束条件
        return 1;
    } else {
        return n * factorial(n - 1);   // 自己调用自己
    }
}

int main() {
    int n;
    cout << "请输入一个整数:";
    cin >> n;
    cout << n << "! = " << factorial(n) << endl;
    return 0;
}

执行过程(比如 n=3):
- factorial(3) 返回 3 * factorial(2)
- factorial(2) 返回 2 * factorial(1)
- factorial(1) 返回 1
- 然后一步步返回:factorial(2) = 21=2, factorial(3)=32=6。

递归虽然有趣,但初学者容易绕晕。刚开始只要理解概念就好。

7.2.8 数字查找之顺序和二分

这一节结合函数来实现查找算法。

顺序查找

在一个数组中找一个数,从第一个开始一个一个比较,直到找到或找完。

#include <iostream>
using namespace std;

// 顺序查找函数:在数组a中找key,返回下标,找不到返回-1
int sequentialSearch(int a[], int n, int key) {
    for (int i = 0; i < n; i++) {
        if (a[i] == key) {
            return i;   // 找到了,返回下标
        }
    }
    return -1;   // 没找到
}

int main() {
    int arr[] = {34, 67, 12, 89, 45, 23, 56};
    int n = sizeof(arr) / sizeof(arr[0]);
    int key;
    cout << "请输入要查找的数:";
    cin >> key;
    int pos = sequentialSearch(arr, n, key);
    if (pos != -1) {
        cout << "找到了,位置是:" << pos << endl;
    } else {
        cout << "没找到" << endl;
    }
    return 0;
}
二分查找

二分查找要求数组必须是有序的(升序)。它每次都和中间元素比较,如果等于就找到;如果小于中间,就在左半部分找;如果大于,就在右半部分找。这样每次都能排除一半。

#include <iostream>
using namespace std;

// 二分查找函数:在升序数组a中找key,返回下标,找不到返回-1
int binarySearch(int a[], int n, int key) {
    int left = 0, right = n - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;   // 防止溢出
        if (a[mid] == key) {
            return mid;
        } else if (a[mid] < key) {
            left = mid + 1;   // 去右半部分
        } else {
            right = mid - 1;  // 去左半部分
        }
    }
    return -1;
}

int main() {
    int arr[] = {12, 23, 34, 45, 56, 67, 89};   // 必须有序
    int n = sizeof(arr) / sizeof(arr[0]);
    int key;
    cout << "请输入要查找的数:";
    cin >> key;
    int pos = binarySearch(arr, n, key);
    if (pos != -1) {
        cout << "找到了,位置是:" << pos << endl;
    } else {
        cout << "没找到" << endl;
    }
    return 0;
}

对比:顺序查找适用于任何数组,但慢;二分查找快,但要求数组有序。


7.3 编程实例讲解

实例1:判断素数函数

写一个函数,判断一个整数是否是素数(只能被1和自身整除的数)。

#include <iostream>
#include <cmath>   // 用sqrt函数
using namespace std;

bool isPrime(int n) {
    if (n <= 1) return false;
    for (int i = 2; i <= sqrt(n); i++) {
        if (n % i == 0) {
            return false;
        }
    }
    return true;
}

int main() {
    int num;
    cout << "请输入一个整数:";
    cin >> num;
    if (isPrime(num)) {
        cout << num << " 是素数" << endl;
    } else {
        cout << num << " 不是素数" << endl;
    }
    return 0;
}

实例2:求最大公约数函数(辗转相除法)

#include <iostream>
using namespace std;

int gcd(int a, int b) {
    while (b != 0) {
        int temp = a % b;
        a = b;
        b = temp;
    }
    return a;
}

int main() {
    int x, y;
    cout << "请输入两个整数:";
    cin >> x >> y;
    cout << "最大公约数是:" << gcd(x, y) << endl;
    return 0;
}

实例3:递归求斐波那契数列第n项

斐波那契数列:1, 1, 2, 3, 5, 8, 13, … 从第三项起,每一项等于前两项之和。

#include <iostream>
using namespace std;

int fib(int n) {
    if (n == 1 || n == 2) {
        return 1;
    } else {
        return fib(n - 1) + fib(n - 2);
    }
}

int main() {
    int n;
    cout << "请输入项数:";
    cin >> n;
    cout << "第" << n << "项是:" << fib(n) << endl;
    return 0;
}

注意:递归虽然简单,但效率低,因为重复计算很多。可以用循环改进。

实例4:数组排序函数

把冒泡排序封装成函数,可以对任意整数数组排序。

#include <iostream>
using namespace std;

// 冒泡排序函数,升序
void bubbleSort(int a[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (a[j] > a[j + 1]) {
                int temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
            }
        }
    }
}

// 输出数组函数
void printArray(int a[], int n) {
    for (int i = 0; i < n; i++) {
        cout << a[i] << " ";
    }
    cout << endl;
}

int main() {
    int arr[] = {34, 67, 12, 89, 45};
    int n = sizeof(arr) / sizeof(arr[0]);

    cout << "排序前:";
    printArray(arr, n);

    bubbleSort(arr, n);

    cout << "排序后:";
    printArray(arr, n);

    return 0;
}

7.4 第7章编程作业

恭喜你学完了函数!现在来挑战几个综合题目,检验一下学习成果。

作业1:计算器函数

写四个函数:addsubmuldiv(除法要考虑除数为0的情况)。然后在 main 中让用户输入两个数和运算符,调用对应的函数计算结果。

作业2:数组统计函数

写一个函数,接受一个整数数组和数组长度,返回数组中的最大值、最小值、平均值(可以返回多个值吗?初学者可以用引用参数或返回结构体,但这里可以定义多个函数分别求,或者用全局变量。建议先定义多个函数:int maxArray(int a[], int n)int minArray(...)double avgArray(...))。

作业3:进制转换函数

写一个函数,将一个十进制整数转换为二进制字符串(用string返回)。提示:不断除以2取余,最后反转字符串。

作业4:递归求幂

用递归实现求 x 的 n 次幂(n为非负整数)。x^n = x * x^(n-1),当n=0时返回1。

作业5:猜数字游戏(函数版)

把猜数字游戏的主要逻辑写成函数:比如 void playGame(int maxNumber, int maxTries),在 main 中调用开始游戏。


好了,你已经学会了如何自己创造函数,把程序拆分成一个个小模块,这样代码更清晰、更容易维护。加油!🚀