学习笔记:C语言程序设计

信息在计算机中的表示

用0和1表示各种信息

  • 计算机的电路由逻辑门电路组成,一个逻辑门电路可以表示0或1。
  • 一个二进制位,取值只能是0或1,称为一个比特(bit),简写:b
  • 八个二进制位,称为一个字节(byte),简写:B
  • 1024 B=1 KB,1024 KB=1 MB,1024 MB=1 GB,1024 GB=1 TB
  • 由8个0或1组成的串,一共256种不同的组合,足以表示阿拉伯数字以及英语中用到的所有字母和标点符号。这就是ASCII编码方案。

十进制到二进制的互相转换

  • K进制数到十进制数的转换:按权相加法
int AnyToTen(string num, int K)  // num是要转换的字符串,K是进制(2<=K<=36)
{
    int ans = 0;  // ans是转换的结果
    for(int i = 0; i < num.size(); i++)  // 从高位开始循环遍历
	{
		char ch = num[i];  // ch是num各位的字符
        int base;  // base是该位本来的数
		if(ch >= '0' && ch <= '9')  // 如果ch是数字
            base = ch - '0';
		else if(ch >= 'a' && ch <= 'z')  // 如果ch是小写字母
            base = ch - 'a' + 10;
        else if(ch >= 'A' && ch <= 'Z')  // 如果ch是大写字母
            base = ch - 'A' + 10;
        else  // 如果ch是非法字符
            break;
        ans = ans * K + base;
	}
    return ans;
}
  • 十进制数到K进制数的转换:除留余数法
string TenToAny(int num, int K)  // num是要转换的数,K是转换结果的进制(2<=K<=36)
{
    string ans = "";  // ans是转换的结果
    int remain;  // remain是余数
    do {
        remain = num % K;  // 取余
        num /= K;
        if(remain >= 10)  // 如果超出十进制,需要用大写字母表示
            ans += remain - 10 + 'A';
        else  // 如果没超出十进制,用数字表示
            ans += remain + '0';
    } while(num);  // 循环遍历取余数
    reverse(ans.begin(), ans.end());  // 字符串逆序
    return ans;
}

为什么学习C++而不是C语言

  • C语言功能弱,C++优势在于大规模程序。
  • 课程学习的内容是C++的一部分,即C语言+STL(Standard Template Library,标准模板库)。

第一个C++程序

  • 输出:Hello, world!
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    printf("Hello, world!\n");
    return 0;
}

变量

什么是变量

  • 变量就是一个代号,程序运行时系统会自动为变量分配内存空间,对变量的访问就是对其代表的内存空间的访问。
  • 变量有名字和类型两种属性,不同变量的名字对应了内存中的不同地址,变量的类型决定了一个变量占用多少个字节。
  • 在C++语言中,变量必须先定义才能使用,一个变量不能定义两次。

变量的命名规则

  • 变量由大小写字母、数字和下划线构成,中间不能有空格,长度不限,不能以数字开头。
  • 变量名是大小写相关的,name和Name是不同的两个变量。
  • 变量名不能和C++的保留字重复。

数据类型

C++的数据类型

  • C++的基本数据类型
    • (有符号)整型:int(4), long(4), short(2), long long(8)
    • 无符号整型:unsigned int(4), unsigned long(4), unsigned short(2), unsigned long long(8)
    • 字符型:char(1), unsigned char(1)
    • 浮点型:float(4), double(8)
    • 布尔型:bool(一般是1)
  • 除基本数据类型外,C++还允许程序员自定义数据类型。

有符号整数的表示方式

  • 将最左位(最高位)看作符号位。
  • 非负数:符号位为0,其余位为绝对值。
  • 负数:符号位为1,其余位为绝对值取反再加1
整数二进制形式十六进制形式
00000 0000 0000 00000000
10000 0000 0000 00010001
2570000 0001 0000 00010101
327670111 1111 1111 11117FFF
-327681000 0000 0000 00008000
-11111 1111 1111 1111FFFF
-21111 1111 1111 1110FFFE

常量

什么是常量

  • 常量就是在程序运行过程中值不会发生改变,而且一眼就能看出其值的量。
  • 常量也可以分成多种:整型,浮点型,字符型,字符串,符号常量。

整型常量

  • 十六进制整型常量:以0x开头,如0xFFA,-0x1a。
  • 八进制整型常量:以0开头,如01,-0456。

字符型常量

  • 字符型常量:用单引号('')括起来,如’a’,’:’。
  • 字母和数字的ASCII编码:
    • ‘0’-‘9’:48-57
    • ‘A’-‘Z’:65-90
    • ‘a’-‘z’:97-122
  • 转义字符:以反斜杠(\)开头,如’\n’, ‘\r’。
转义字符含义ASCII值
\0空字符(NULL)000
\a响铃(BEL)007
\b退格(BS) ,输出位置回退一个字符008
\t水平制表(HT) ,输出位置跳到下一个Tab位置009
\n换行(LF) ,输出位置移到下一行开头010
\r回车(CR) ,输出位置移到本行开头013

字符串常量

  • 字符串常量:用双引号("")括起来,如"a",“abc”。
  • ""表示空串,即不包含任何字符的字符串。

输入输出

输入输出控制符

  • 在printf和scanf中可以使用以百分号(%)开头的控制符,指明要输入或输出的数据的类型以及格式。
  • 测试:用scanf读入不同类型的变量
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int n; char c; float m;
    scanf("%d%c%f", &n, &c, &m);  // 输入:34k 234.45
    printf("%d %c %f", n, c, m);  // 输出:34 k 234.449997
    return 0;
}
常用格式控制符作用
%c读入或输出char变量
%d读入或输出int或short变量
%lld读入或输出long long变量(64位整数)
%nd输出整数,宽度不足n字符时用空格填充
%0nd输出整数,宽度不足n字符时用0填充
%f读入或输出float变量,输出时保留小数点后6位
%lf读入或输出double变量,输出时保留小数点后6位
%.nf输出浮点数,精确到小数点后n位
%o以八进制形式读入或输出整型变量
%x以十六进制形式读入或输出整型变量
%u以无符号形式读入或输出整型变量

赋值运算符、算术运算符和算术表达式

赋值运算符

  • 赋值运算符用于给变量赋值:=, +=, -=, *=, /=, %=
  • a += b 等效于 a = a + b,但是执行速度更快,其他类似。

算术运算符

  • 算术运算符用于数值运算:+, -, *, /, %, ++, –
  • 运算符和操作数构成表达式。表达式的值的类型,以操作数中精度高的类型为准。
    • 精度:double > long long > int > short > char

加、减、乘运算的溢出

  • 整数进行加、减、乘运算可能导致计算结果超出结果的类型所能表示的范围,这种情况称为溢出。整数类型的溢出部分直接被丢弃。
  • 实数(浮点数)也可能溢出,结果不易预测。
  • 测试:整数相加导致溢出
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    unsigned int n1 = 0xffffffff;
    cout << n1 << endl;  // 输出:4294967295
    unsigned int n2 = n1 + 3;  // 0xffffffff + 3 = 0x100000002,导致溢出
    cout << n2 << endl;  // 输出:2
    return 0;
}

关系运算符、逻辑运算符和逻辑表达式

关系运算符

  • 关系运算符用于数值的比较:==, !=, >, <, >=, <=
  • 比较的结果是bool类型。bool类型变量只有两种取值,true或false,false等价于0,true等价于非0整型值。

逻辑运算符

  • 逻辑运算符用于表达式的逻辑操作:&&, ||, !

强制类型转换运算符以及运算符优先级

强制类型转换运算符

  • 强制类型转换运算符用于将操作数转换为指定类型:类型名

运算符优先级(从高到低)

  • 自增(++),自减(–),非(!)
  • 其他算术运算符(+, -, *, /, %)
  • 关系运算符(==, !=, >, <, >=, <=)
  • 其他逻辑运算符(&&, ||)
  • 赋值运算符(=, +=, -=, *=, /=, %=)

if语句和switch语句

if语句

  • 例题:输入年份,判断该年份是否是建国整十周年、建党整十周年以及是否是闰年。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int year;  // 年份
    scanf("%d", &year);  // 输入年份
    
    if(year < 0) {  // 如果year是负数
        printf("Illegal year.\n");  // 年份不合法
    }
    else {
        printf("Legal year.\n");  // 年份合法
        
        if((year > 1949) && ((year - 1949) % 10 == 0))  // 如果year是建国整十周年
            printf("Lucy year.\n");
        else if((year > 1921) && ((year - 1921) % 10 == 0))  // 如果year是建党整十周年
            printf("Good yaer.\n");
        else if((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0))  // 如果year是闰年
            printf("Leap year.\n");
        else  // 如果year是普通年份
            printf("Common year.\n");
    }
    return 0;
}

switch语句

  • 例题:输入整数,输出对应的星期的英文单词。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int n;  // 整数n
    scanf("%d", &n);  // 输入整数n

    switch(n) {  // 表达式的值必须是整型
        case 1:  // 常量表达式必须是整型的常量
            printf("Monday"); break;  // 进入分支后会一直执行语句,直到碰到break
        case 2:
            printf("Tuesday"); break;
        case 3:
            printf("Wednesday"); break;
        case 4:
            printf("Thursday"); break;
        case 5:
            printf("Friday"); break;
        case 6:
            printf("Saturday"); break;
        case 7:
            printf("Sunday"); break;
        default:
            printf("Illegal");
        
    }
    return 0;
}

for循环

for循环

  • 例题:连续输出26个字母。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int i;  // 循环控制变量
    for(i = 0; i < 26; i++) {
        cout << char('a' + i);  // 强制转换成char类型
        // printf("%c", 'a' + i);
    }
    return 0;
}
  • 例题:输入正整数n和m,在1~n的数中取两个不同的数,使其和是m的因子,计算共有多少种不同的取法。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int n, m;  // 正整数n和m
    cin >> n >> m;  // 输入正整数n和m

    int total = 0;  // 不同的取法总数
    for(int i = 1; i < n; i++) {  // 取第一个数,共有n-1种取法
        for(int j = i + 1; j <= n; j++) {  // 取第二个数,第二个数要比第一个数大,避免重复
            if(m % (i + j) == 0){  // 如果i和j的和是m的因子
                total++;  // 添加该取法
            }
        }
    }
    cout << total;  // 输出不同的取法总数
    return 0;
}

while循环和do while循环

while循环

  • 例题:输入若干不超过100的正整数,输入0标志输入结束,输出其中的最大值、最小值以及所有数的和。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int num;  // 正整数num
    int max = 0, min = 100, sum = 0;  // 最大值、最小值、所有数的和
    cin >> num;  // 输入正整数num,num不大于100
    while(num) {  // 如果num为0则结束循环
        if(num > max) {  // 如果num大于max,更新max
            max = num;
        }
        if(num < min) {  // 如果num小于min,更新min
            min = num;
        }
        sum += num;  // 更新sum
        cin >> num;  // 继续输入正整数num
    }
    cout << max << " " << min << " " << sum;  // 输出最大值、最小值、所有数的和
    return 0;
}
  • 例题:用牛顿迭代法求输入的数的平方根。
#include <iostream>
#include <cstdio>
using namespace std;

double EPS = 0.001;  // 控制计算精度

int main()
{
    double a;
    cin >> a;  // 输入a,求a的平方根

    if(a >= 0) {
        double X = a / 2;  // 猜测一个值X
        double lastX = X + EPS + 1;  // 确保能够进行至少一次迭代
        while((X - lastX > EPS) || (lastX - X > EPS)) {  // 如果精度未达到要求,则继续迭代
            lastX = X;  // 保存当前近似值
            X = (X + a / X) / 2;  // 根据迭代公式求下一个近似值
        }
        cout << X;  // 输出满足精度的近似值
    }
    else {
        cout << "It can't be nagitive.";  // 输入的数不能是负数
    }
    return 0;
}

do while循环

  • 例题:输出1~10000以内所有2的整数次幂。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int n = 1;  // 2的0次幂
    do {  // 至少执行一次循环
        cout << n << " ";  // 输出2的整数次幂
        n *= 2;  // 计算下一个2的整数次幂
    } while(n < 10000);  // 如果n超出范围,则结束循环
    return 0;
}

break语句和continue语句

break语句

  • 可以出现在循环体中,其作用是跳出直接包含break语句的那一重循环。
  • 例题:输入正整数n和m,在n~m的数中找出一对和最小的兄弟数,如果有多对,就找出弟数最小的那一对。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int n, m;  // 正整数n和m
    cin >> n >> m;  // 输入正整数n和m

    int num1 = m + 1, num2 = m + 1;  // 正整数num1和num2为当前已经找到的最佳兄弟数
    for(int i = n; i < m; i++) {  // 取弟数,共有m-n种取法
        if(i > ((num1 + num2) / 2 + 1)) {  // 如果弟数大于最佳兄弟数的和的一半,则之后的循环无意义
            break;  // 跳出该循环,减少消耗
        }

        for(int j = i + 1; j <= m; j++) {  // 取兄数,兄数要比弟数大,避免重复
            if((i + j) > (num1 + num2)){  // 如果该兄弟数的和大于最佳兄弟数的和,则之后的循环无意义
                break;  // 跳出该循环,减少消耗
            }

            if((i * j) % (i + j) == 0) {  // 如果i和j为兄弟数
                if((i + j) < (num1 + num2)) {  // 如果该兄弟数的和小于最佳兄弟数的和
                    num1 = i; num2 = j;  // 更新最佳兄弟数
                }
                else if(((i + j) == (num1 + num2)) && (i < num1)) {  // 如果弟数小于最佳兄弟数的弟数
                    num1 = i; num2 = j;  // 更新最佳兄弟数
                }
            }
        }
    }

    if(num1 == (m + 1)) {  // 如果没找到兄弟数
        cout << "No solution.";  // 输出:No solution.
    }
    else {  // 如果找到兄弟数
        cout << num1 << ", " << num2;  // 输出兄弟数
    }
    return 0;
}

continue语句

  • 可以出现在循环体中,其作用是立即结束本次循环,并回到循环开头判断是否要进行下一次循环。

OJ编程题输入数据的处理

输入数据的处理

  • scanf表达式的值为int,表示成功读入的变量个数。
  • scanf表达式的值为EOF(-1),说明输入数据已经结束。
  • cin表达式的值为bool类型,表示是否成功读入所有变量。
  • 测试:输入数据的处理
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int num1, num2;
    // scanf表达式的值为int类型,表示成功读入的变量个数。
    int count = scanf("%d%d", &num1, &num2);  // 输入两个整数或其他
    printf("%d", count);  // 输出成功读入的变量个数

    int num3, num4;
    // scanf表达式的值为EOF(-1),说明输入数据已经结束。
    while(scanf("%d%d", &num3, &num4) != EOF) {  // 不停输入两个整数,直到输入【Ctrl+Z】,然后输入【Enter】
    // 或:while(scanf("%d%d", &num3, &num4) == 2) {
        printf("%d", num3 + num4);  // 输出它们的和
    }

    int num5, num6;
    // cin表达式的值为bool类型,表示是否成功读入所有变量。
    while(cin >> num5 >> num6) {  // 不停输入两个整数,直到输入【Ctrl+Z】,然后输入【Enter】
        printf("%d", num5 + num6);  // 输出它们的和
    }
    
    return 0;
}

处理无结束标记的OJ编程题输入

  • 例题:输入若干正整数,输出其中的最大值。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int num, max = 0;
    while(scanf("%d", &num) != EOF) {  // 不停输入正整数num,直到输入【Ctrl+Z】,然后输入【Enter】
    // 或:while(scanf("d", &num) == 1) {
    // 或:while(cin >> num) {
        if(num > max) {  // 如果num大于当前最大值
            max = num;  // 更新当前最大值
        }
    }
    printf("%d", max);  // 输出最大值
    return 0;
}

用freopen重定向输入

  • 测试:将输入由键盘重定向为文件
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    freopen("D:\\WorkSpace\\VSCode\\CodeDemo\\input.txt", "r", stdin);
    // 之后所有输入都来自文件:D:\\WorkSpace\\VSCode\\CodeDemo\\input.txt
    
    int num, max = 0;
    while(cin >> num) {  // 不停输入正整数num,直到输入【Ctrl+Z】,然后输入【Enter】
        if(num > max) {  // 如果num大于当前最大值
            max = num;  // 更新当前最大值
        }
    }
    printf("%d", max);  // 输出最大值
    return 0;
}

循环例题选讲

循环例题选讲

  • 例题:输入整数a和正整数n,输出乘方a的n次。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int a, n;
    cin >> a >> n;  // 输入整数a和正整数n

    int result = a;  // result为乘方计算的结果
    for(int i = 1; i < n; i++) {  // 循环n-1次
        result *= a;  // 不断与a相乘
    }
    
    cout << result;  // 输出结果
    return 0;
}
  • 例题:输入正整数k,输出斐波那契数列中的第k个数。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int k;
    cin >> k;  // 输入正整数k

    int a1 = 1, a2 = 1;  // a1和a2是斐波那契数列中的前两个数
    if((k == 1) || (k == 2)) {  // 如果k是1或2
        cout << 1 << endl;  // 直接输出结果1
    }
    else {  // 如果k大于2
        int sum;  // sum为斐波那契数列中的第k个数
        for(int i = 3; i <= k; i++) {  // 循环k-2次,直到i==k
            sum = a1 + a2;  // 计算斐波那契数列中的第i个数
            a1 = a2; a2 = sum;  // 更新a1和a2,即往后挪一个数
        }
        cout << sum << endl;  // 输出结果sum
    }
    return 0;
}
  • 例题:输入正整数n,输出不大于n的正整数的阶乘的和。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int n;
    cin >> n;  // 输入正整数n
    
    int sum = 0;  // sum为阶乘的和
    int factorial = 1;  // factorial为阶乘
    for(int i = 1; i <= n; i++) {  // 循环n次
        factorial *= i;  // 计算i的阶乘
        sum += factorial;  // 计算1!+2!+3!+...+n!
    }

    cout << sum;  // 输出阶乘的和sum
    return 0;
}
  • 例题:输入正整数n(n>=2),输出不大于n的全部质数。
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int n;
    cin >> n;  // 输入正整数n
    
    cout << 2 << endl;  // 输出第一个质数2
    for(int i = 3; i <= n; i += 2) {  // 循环遍历3~n,跳过所有偶数,每次判断i是否是质数
        int factor;
        for(factor = 3; factor < i; factor += 2) {  // 循环遍历3~i,跳过所有偶数,每次判断factor是否是i的因子
            if(i % factor == 0) {  // 如果factor是i的因子
                break;  // i不是质数,跳出循环
            }
            if(factor * factor > i) {  // 如果factor大于i的平方根,则之后的循环无意义
                break;  // 不需要重复考虑之后的因子,跳出循环
            }
        }
        if(factor * factor > i) {  // 如果没有执行过第一个if语句的break语句,表示i没有因子,即i是质数
            cout << i << endl;  // 输出i
        }
    }

    return 0;
}

数组

数组

  • 可以用来表达类型相同的元素的集合。元素的编号称为数组下标,元素的个数称为数组长度,数组名表示数组的地址。
  • 例题:输入100个正整数,按逆序输出。
#include <iostream>
#include <cstdio>
using namespace std;

// 使用符号常量,便于修改
#define NUM 100  // 数组长度
// 数组一般不定义在main函数中,尤其是大数组
int a[NUM];  // 定义数组a,有NUM个元素,每个元素都是int类型

int main()
{
    for(int i = 0; i < NUM; i++) {  // 循环输入正整数,储存到数组a[0]~a[99]
        cin >> a[i];
    }
    for(int i = NUM - 1; i >= 0; i--) {  // 循环输出正整数,按逆序输出a[99]~a[0]
        cout << a[i] << " ";
    }
    return 0;
}
  • 例题:输入正整数n(n>=2),输出不大于n的全部质数,用筛法(挨拉托色尼筛法)求解。
#include <iostream>
#include <cstdio>
using namespace std;

#define MAX_NUM 100  // 最大数组长度
int isPrime[MAX_NUM];  // 定义数组isPrime,isPrime[i]的值为1,则说明i是质数

int main()
{
    for(int i = 2; i <= MAX_NUM; i++) {  // 开始假设所有数都是质数
        isPrime[i] = 1;  // 初始值设为1
    }
    
    for(int i = 2; i <= MAX_NUM; i++) {  // 循环遍历所有数,逐步划掉其中的合数
        if(isPrime[i]) {  // 只需考虑质数作为因子的情况,合数作为因子的情况已经包含在内
            for(int j = 2*i; j <= MAX_NUM; j += i) {  // 循环遍历所有i的倍数
                isPrime[j] = 0;  // 将i的倍数设置为0,即划掉该合数
            }
        }
    }

    for(int i = 2; i <= MAX_NUM; i++) {  // 循环遍历数组isPrime
        if(isPrime[i]) {  // 如果值为1
            cout << i << endl;  // 输出该质数
        }
    }
    
    return 0;
}

数组初始化

数组初始化

  • 在定义数组时,可以给数组中的元素赋初值,没有赋值的元素自动赋0值。
    • 如:int a[10] = {0, 1, 2, 3, 4};
  • 在定义数组时,如果给所有元素赋值,则可以不给出元素的个数。
    • 如:int a[] = {1, 2, 3};

用数组取代复杂分支结构

  • 例题:输入整数1~7,输出对应星期的英文单词。
#include <iostream>
#include <cstdio>
#include <string>  // 使用string必须包含该头文件
using namespace std;

string weekdays[] = {  // 定义数组weekdays,存放字符串常量
    "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};

int main()
{
    int n;
    cin >> n;  // 输入整数n
    if((n >= 1) && (n <= 7)) {  // 如果输入合法
        cout << weekdays[n-1];  // 输出对应的字符串
    }
    else {  // 如果输入非法
        cout << "Illegal.";  // 输出错误提示信息
    }
    return 0;
}
  • 例题:已知2012年1月25日是星期三,输入包含年、月、日的日期,输出该日期对应的星期。
#include <iostream>
#include <cstdio>
using namespace std;

int monthdays[13] = {  // 定义数组monthdays,存放各月份的天数
    -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

int main()
{
    int year, month, date;
    cin >> year >> month >> date;  // 输入包含年、月、日的日期

    int days = 0;  // days为输入的日期从该天起过的天数
    for(int y = 2012; y < year; y++) {  // 循环遍历经过的年份
        if((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) {  // 如果y是闰年
            days += 1;  // 额外加1天
        }
        days += 365;  // 计算相差年份的天数
    }

    if((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)) {  // 如果year是闰年
        monthdays[2] = 29;  // 修改数组中的2月天数
    }
    for(int m = 1; m < month; m++) {  // 循环遍历经过的月份
        days += monthdays[m];  // 计算相差月份的天数
    }

    days += date - 22;  // 计算相差日期的天数
    cout << days % 7 << endl;  // 输出该日期对应的星期
    return 0;
}

数组越界

数组越界

  • 数组下标可以是任何整数,可以是负数,也可以大于数组长度,不会导致编译错误。
    • 如:int a[10]; a[-2] = 5; a[20] = 10;
  • 当数组下标不在数组长度范围内,可能写入别的变量或指令的内存空间,会导致程序运行错误或程序崩溃。
  • 用变量作为数组下标时,变量为负数或太大,会导致数组越界

二维数组

二维数组

  • 定义N行M列的二维数组:T a[N][M];
    • 数组占用了一片连续的存储空间,大小为sizeof(a)或N×M×sizeof(T)
    • 二维数组的每一行,可以直接当作一维数组使用,如a[0],a[1]。
  • 在定义二维数组时,如果对每行初始化,则可以不给出行数。
    • 如:int a[][3] = {{80, 75, 92}, {61, 65}};
  • 例题:输入正整数m和n,再输入m×n的矩阵,输入正整数p和q,在输入p×q的矩阵,输出矩阵相乘的结果矩阵。
#include <iostream>
#include <cstdio>
using namespace std;

#define ROWS 8  // 最大行数
#define COLS 8  // 最大列数
int a[ROWS][COLS];  // 定义m行n列的矩阵
int b[ROWS][COLS];  // 定义p行q列的矩阵
int c[ROWS][COLS];  // 定义m行q列的结果矩阵

int main()
{
    int m, n;
    cin >> m >> n;  // 输入正整数m和n
    for(int i = 0; i < m; i++) {  // 循环遍历矩阵a每行
        for(int j = 0; j < n; j++) {  // 循环遍历矩阵a每列
            cin >> a[i][j];  // 输入矩阵a的元素
        }
    }

    int p, q;
    cin >> p >> q;  // 输入正整数p和q
    for(int i = 0; i < p; i++) {  // 循环遍历矩阵b每行
        for(int j = 0; j < q; j++) {  // 循环遍历矩阵b每列
            cin >> b[i][j];  // 输入矩阵b的元素
        }
    }

    for(int i = 0; i < m; i++) {  // 循环遍历矩阵c每行
        for(int j = 0; j < q; j++) {  // 循环遍历矩阵c每列
            c[i][j] = 0;  // 对矩阵c初始化
            for(int k = 0; k < n; k ++) {  // 循环遍历矩阵a的每列或矩阵b的每行
                c[i][j] += a[i][k] * b[k][j];  // 计算矩阵c的元素
            }
        }
    }

    for(int i = 0; i < m; i++) {  // 循环输出矩阵c每行
        for(int j = 0; j < q; j++) {  // 循环输出矩阵c每列
            cout << c[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}

函数

函数

  • 函数可以将实现某一功能,并需要反复使用的代码包装起来形成一个功能模块。
  • 测试:Max函数
// 求两个整型变量中的较大值
int Max(int x, int y)  // x和y为形参
{
    if(x > y)
        return x;
    else
        return y;
}
  • 测试:isPrime函数
// 判断n是否是质数
bool IsPrime(unsigned int n)
{
    if(n <= 1) {  // 特殊情况说明
        return false;
    }
    for(int i = 2; i < n; i++) {  // 循环遍历2~n-1
        if(n % i == 0) {  // 如果i为n的因子
            return false;  // n不是质数
        }
    }
    return true;  // 如果n没有因子,n是质数
}
  • 例题:输入三角形的三个顶点坐标,输出三条边的长度。
#include <iostream>
#include <cstdio>
using namespace std;

#define EPS 0.001  // 控制计算精度

// 求a的平方根,参考:1-C语言程序设计\week 3\6-NewtonSquare.cpp
double Sqrt(double a)
{
    double X = a / 2;  // 猜测一个值X
    double lastX = X + EPS + 1;  // 确保能够进行至少一次迭代
    while((X - lastX > EPS) || (lastX - X > EPS)) {  // 如果精度未达到要求,则继续迭代
        lastX = X;  // 保存当前近似值
        X = (X + a / X) / 2;  // 根据迭代公式求下一个近似值
    }
    return X;  // 输出满足精度的近似值
}

// 求点(x1, y1)和点(x2, y2)的距离
double Distance(double x1, double y1, double x2, double y2)
{
    return Sqrt((x1-x2) * (x1-x2) + (y1-y2) * (y1-y2));
}

int main()
{
    int x1, y1, x2, y2, x3, y3;  // 三个顶点坐标:(x1, y1), (x2, y2), (x3, y3)
    cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3;  // 输入三角形的三个顶点坐标
    double length1 = Distance(x1, y1, x2, y2);
    double length2 = Distance(x2, y2, x3, y3);
    double length3 = Distance(x1, y1, x3, y3);
    cout << length1 << ", " << length2 << ", " << length3 << endl;  // 输出三条边的长度
    return 0;
}

函数的声明

  • 一般来说,函数的定义出现在函数调用语句之前,否则会导致编译错误。
  • 如果函数调用语句之前有函数声明,则不一定要有定义。
  • 函数声明也称为函数的原型,参数名称可以省略。
    • 如:double Distance(double, double, double, double);

main函数

  • C/C++程序从main函数开始执行,到main函数中的return语句结束。

函数参数的传递

  • 函数的形参是实参的拷贝,且形参的改变不会影响到实参(除非形参类型是数组、引用或对象)。
  • 测试:Swap函数
// 交换形参a和b
void Swap(int a, int b)
{
    int tmp;  // tmp是暂时保存数据的中间值
    tmp = a; a = b; b = tmp;  // 交换a和b的数据
    cout << "Swap: a = " << a << ", b = " << b << endl;  // 输出形参
    return;
}

数组作为函数参数

  • 传递引用,形参数组改变后,实参数组也会改变。形参数组的地址就是实参数组的地址。
  • 测试:ArrayMax函数
// 求数组a中的最大值
int ArrayMax(int a[], int length)  // length为数组长度
{
    int max = a[0];
    for(int i = 0; i < length; i++) {  // 循环遍历数组下标
        if(max < a[i]) {  // 如果max小于a[i]
            max = a[i];  // 更新max
        }
    }
    return max;
}
  • 一维数组作为形参时,不用写出数组的元素个数。
    • 如:void PrintArray(int a[]) { }
  • 二维数组作为形参时,必须写出数组的列数,不用写出行数,编译器必须知道列数才能根据下标算出元素的地址。
    • 如:void PrintArray(int a[][5]) { }

递归

递归

  • 一个函数,自己调用自己,就称为递归
  • 递归函数必须有终止条件,否则就会无穷递归导致程序无法终止甚至崩溃。
  • 测试:Factorial函数
// 求n的阶乘
int Factorial(int n)
{
    if(n < 2)  // 终止条件
        return 1;
    else
        return n * Factorial(n-1);  // 递归
}
  • 测试:Fibonacci函数
// 求斐波那契数列中的第n项
int Fibonacci(int n)
{
    if((n == 1) || (n == 2))  // 终止条件
        return 1;
    else
        return Fibonacci(n-1) + Fibonacci(n-2);
}

库函数和头文件

库函数和头文件

  • 库函数:C/C++标准规定的、编译器自带的函数,如cin,cout。
  • 头文件:C++编译器提供许多头文件,如iostream,cmath。
  • 头文件内部包含许多库函数的声明以及其他信息。

位运算

位运算

  • 位运算用于对整数类型(int、char、long等)变量中的某一位(bit)或若干位进行操作。

位运算符

  • 按位与(&):可以将变量中的某些位清0并保留其他位不变,也可以获取变量中的某一位。
    • 例如,将int型变量n的低8位清0,其余位不变:n &= 0xffffff00;
    • 例如,判断int型变量n的第7位(从右往左,从0开始数)是否为1:n & 0x80 == 0x80
  • 按位或(|):可以将变量中的某些位置1并保留其他位不变。
    • 例如,将int型变量n的低8位置1,其余位不变:n |= 0xff;
  • 按位异或(^):可以将变量中的某些位取反并保留其他位不变,也可以交换两个变量的值。
    • 例如,将int型变量n的低8位取反,其余位不变:n ^= 0xff;
    • 例如,交换a和b的值:a ^= b; b ^= a; a ^= b;
  • 按位非(~):可以将操作数中的二进制位0变成1,1变成0。
  • 左移(<<):左移时,高位丢弃,低位补0。左移1位,相当于乘以2。
    • 例如,将操作数a左移n位:a << n
  • 右移(>>):右移时,低位丢弃,高位补与符号位相同的数。右移1位,相当于除以2,并且结果相小取整。
    • 例如,将操作数a右移n位:a >> n
  • 测试:位操作符
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int n1 = 15;  // n1:0000 0000 0000 0000 0000 0000 0000 1111
    n1 >>= 2;  // n1右移2位:0000 0000 0000 0000 0000 0000 0000 0011
    printf("n1 = %d\n", n1);  // 输出:3

    short n2 = -15;  // n2:1111 1111 1111 0001
    n2 >>= 3;  // n2右移3位:1111 1111 1111 1110
    printf("n2 = %d\n", n2);  // 输出:-2

    unsigned short n3 = 0xffe0;  // n3:1111 1111 1110 0000
    n3 >>= 4;  // n3右移4位:1111 1111 1111 1110
    printf("n3 = %d\n", n3);  // 输出:0xffe

    char n4 = 15;  // n4:0000 1111
    n4 >>= 3;  // n4右移3位:0000 0001
    printf("n4 = %d\n", n4);  // 输出:1

    return 0;
}

字符串

字符串的形式

  • 字符串常量
    • 用双引号("")括起来,如"CHINA",“C++ program”。
    • 占据内存的字节数为字符串长度加1,多出来的是结尾字符’\0’。
    • 空串("")也是合法的字符串常量,占据1字节的内存。
  • 用char数组存放字符串
    • 数组存放的字符串为’\0’前面的字符组成。
    • 数组的内容可以在初始化时设定,也可以用C++库函数修改,也可以用对元素赋值的方式修改。
  • string对象
    • string是C++标准模板库里的一个类,专门用于处理字符串。

用scanf读入字符串

  • 用scanf可以将字符串读入字符数组,直到读入空格为止,并自动添加结尾字符’\0’。
    • 如:scanf("%s", word);
  • 在数组长度不足的情况下,scanf可能导致数组越界。

读入一行到字符数组

  • 读入一行(行长度不超过bufSize-1)或bufSize-1个字符到字符数组,并自动添加结尾字符’\0’。
    • 如:cin.getline(buf, bufSize);
  • 读入一行(行长度没有限制)到字符数组,并自动添加结尾字符’\0’。
    • 如:gets(buf);
  • 回车换行符不会写入buf,但是会从输入流中去掉。

字符串库函数

  • 字符串复制:strcpy
    • strcpy(dest, src):把src指向的字符串复制到dest,返回dest。
  • 字符串拼接:strcat
    • strcat(dest, src):把src指向的字符串追加到dest指向的字符串的结尾,返回dest。
  • 字符串比较:strcmp
    • strcmp(str1, str2):比较str1和str2,若相等则返回0,若str1大则返回正数,若str1大则返回负数。
  • 求字符串长度:strlen
    • strlen(str):计算str的长度,不包括’\0’在内,返回str的长度。
  • 字符串转换为大写:strupr
    • strupr(str):将str中的字符转换为大写,返回str。
  • 字符串转换为小写:strlwr
    • strlwr(str):将str中的字符转换为小写,返回str。
  • 测试:字符串库函数的使用
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

// 输出词典序小的字符串
void PrintSmall(char str1[], char str2[])
{
    if(strcmp(str1, str2) <= 0)  // 如果str1小于等于str2
        cout << str1;
    else  // 如果str1大于str2
        cout << str2;
}

int main()
{
    char str1[30], str2[40];

    strcpy(str1, "Hello");  // 字符串复制:str1 = "Hello"
    strcpy(str2, str1);  // 字符串复制:str2 = str1 = "Hello"
    cout << "(1) " << str2 << endl;  // 输出:(1) Hello

    strcat(str1, ", world");  // 字符串拼接:str1 = "Hello, world"
    cout << "(2) " << str1 << endl;  // 输出:(2) Hello, world

    cout << "(3) "; PrintSmall("abc", str2); cout << endl;  // 输出:(3) Hello
    cout << "(4) "; PrintSmall("abc", "aaa"); cout << endl;  // 输出:(4) aaa

    int len = strlen(str1);  // 求str1的长度len
    cout << "(5) " << len << endl;  // 输出:(5) 12

    strupr(str1);  // 字符串转换为大写:str1 = "HELLO, WORLD"
    cout << "(6) " << str1 << endl;  // 输出:(6) HELLO, WORLD

    char s[100] = "test";
    for(int i = 0; i < strlen(s); i++) {  // 每次循环都调用strlen函数,造成浪费
        s[i] = s[i] + 1;  // 此句并不重要,只是为了说明要访问s[i]
    }
    // 修改1
    int length = strlen(s);  // 调用一次strlen函数
    for(int i = 0; i < length; i++) {
        s[i] = s[i] + 1;
    }
    // 修改2
    for(int i = 0; s[i]; i++) {  // s[i]='\0'时结束循环
        s[i] = s[i] + 1;
    }

    return 0;
}

字符串

  • 例题:输入两个字符串,判断后者是否为前者的子串,输出子串第一次出现的位置。
// 判断str2是否是str1的子串,求子串第一次出现的位置
int Strstr(char str1[], char str2[])
{
    if(str2[0] == 0) {  // 空串是任何串的子串,且出现位置为0
        return 0;
    }
    else {  // 如果str2不是空串
        for(int i = 0; str1[i]; i++) {  // 循环遍历str1的每个字符
            int j = 0, k = i;  // j为str2的下标,k为str1的假设子串的下标
            for( ; str2[j]; j++, k++) {  // 循环遍历str2的每个字符
                if(str2[j] != str1[k]) {  // 如果str2不等于str1的假设子串
                    break;  // 跳出循环,继续尝试下一个
                }
            }
            if(str2[j] == 0) {  // 如果str2[j]='\0',即str2是str1的子串
                return i;  // 返回子串第一次出现的位置
            }
        }
        return -1;  // 如果str2不是str1的子串,返回-1
    }
}

指针的基本概念和用法

指针的基本概念

  • 指针(指针变量)是大小为4个字节(或8个字节)的变量,其内容代表一个内存地址。
  • 通过指针,能够对该指针指向的内存区域进行读写。

指针的用法

  • *:间接引用运算符
    • p:T类型的指针,存放变量(*p)的地址
    • *p:T类型的变量,存放从地址p开始的sizeof(T)个字节的内容
  • &:取地址运算符
    • x:T类型的变量
    • &x:T类型的指针,存放变量(x)的地址
  • 测试:指针的用法
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    char ch1 = 'A';  // 此时ch1的值为'A'
    char *pc = &ch1;  // pc指向变量ch1
    *pc = 'B';  // *pc相当于ch1,此时ch1的值为'B'
    char ch2 = *pc;  // *pc相当于ch1,此时ch2的值为'B'
    pc = &ch2;  // pc指向变量ch2
    *pc = 'D';  // *pc相当于ch2,此时ch2的值为'D'
    return 0;
}

指针的意义和互相赋值

指针的作用

  • 通过指针,程序能访问的内存区域不仅限于变量所占据的数据区域。
  • 在C++中,指针p指向变量a的地址,对p进行加减操作,就能访问a前后的内存区域。

指针的互相赋值

  • 不同类型的指针,如果不经过强制类型转换,不能直接互相赋值。

指针的运算

指针的运算

  • 两个同类型的指针可以比较存放地址的大小。
  • 两个同类型的指针可以相减。
    • p1-p2:p1与p2之间可以存放多少T类型的变量
  • 指针加减一个整数的结果是指针。
    • p+n,p-n:p指向*p前后的内存区域
  • 指针可以自增或自减。
    • ++p,p++:p指向*p+sizeof(T)
    • –p,p–:p指向*p-sizeof(T)
  • 指针可以用下标运算符([])进行运算。
    • p[n]:相当于*(p+n)
  • 测试:指针的运算
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int *p1, *p2;  // 定义int类型指针p1和p2
    char *pc1, *pc2;  // 定义char类型指针pc1和pc2

    p1 = (int *)100;  // p1的值为100,类型为int *
    p2 = (int *)200;  // p2的值为200,类型为int *
    cout << "(1) " << p2 - p1 << endl;  // 输出:(1) 25
    // p2 - p1 = (200-100)/sizeof(int) = 100/4 = 25

    pc1 = (char *)p1;  // pc1的值为100,类型为char *
    pc2 = (char *)p2;  // pc1的值为200,类型为char *
    cout << "(2) " << pc1 - pc2 << endl;  // 输出:(2) -100
    // pc2 - pc1 = (100-200)/sizeof(char) = -100/1 = -100

    int n = 4;
    int *p3 = p2 + n;  // p3的值为216,类型为int *
    cout << "(3) " << p3 - p1 << endl;  // 输出:(3) 29
    // p3 - p1 = (216-100)/sizeof(int) = 116/4 29
    
    cout << "(4) " << (pc2 - 10) - pc1 << endl;  // 输出:(4) 90
    // (pc2 - 10) - pc1 = ((200-10)-100)/1 = 90

    return 0;
}

通过指针实现自由访问内存

  • 例题:如何访问int型变量a前面的一个字节?
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int a;  // 定义int型变量a
    // int *p = &a;  // &a是int类型的指针,int类型占4个字节
    char *p = (char *)&a;  // 需要强制类型转换,char类型占1个字节
    p--;  // p指向变量a前面的1个字节
    printf("%c", *p);  // 可能导致运行错误
    *p = 'A';  // 可能导致运行错误
    return 0;
}

指针作为函数参数

空指针

  • 地址0不能访问,指向地址0的指针就是空指针,空指针的值为0或NULL。
  • 指针可以作为条件表达式使用,如果指针的值为NULL,则表达式的值为false。

指针作为函数参数

  • 测试:Swap函数
#include <iostream>
#include <cstdio>
using namespace std;

// 交换形参*p1和*p2
void Swap(int *p1, int *p2)
{
    int tmp;  // tmp是暂时保存数据的中间值
    tmp = *p1; *p1 = *p2; *p2 = tmp;  // 交换*p1和*p2的数据
    return;
}

int main()
{
    int a = 4, b = 5;
    Swap(&a, &b);  // 调用Swap函数,传递地址,修改指针指向的数据
    cout << "a = " << a << ", b = " << b << endl;  // 输出实参
    return 0;
}

指针和数组

指针和数组

  • 数组的名字是一个指针常量,指向数组的起始地址。
  • 数组a在编译时其值已经确定,不能对a进行赋值,但可以用a对相同类型的指针赋值。
  • 测试:指针和数组
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int a[200];  // 定义数组a
    int *p;  // 定义指针p
    p = a;  // p指向数组a的起始地址,即p指向a[0]

    *p = 10;  // 相当于:a[0] = 10
    *(p+1) = 20;  // 相当于:a[1] = 20
    p[0] = 30;  // 相当于:a[0] = 30
    p[4] = 40;  //相当于:a[4] = 40

    for(int i = 0; i < 10; i++) {  // 对数组a的前10个元素赋值
        *(p+i) = i;  // 相当于:a[i] = i(p[i] = i)
    }

    p++;  // p指向a[1]
    cout << p[0] << endl;  // p[0]的值为a[1],输出:1
    p = a + 6;  // p指向a[6]
    cout << *p << endl;  // *p的值为a[6](*p等价于p[0]),输出:6
    return 0;
}
  • 例题:颠倒一个数组。
#include <iostream>
#include <cstdio>
using namespace std;

// 求数组p的逆序数组
void Reverse(int *p, int size)  // 形参中数组可以表示为指针形式
{
    for(int i = 0; i < size/2; i++) {
        int tmp = p[i];  // tmp是暂时保存数据的中间值
        p[i] = p[size-1-i];  // p[i]为顺序取值
        p[size-1-i] = tmp;  // p[size-1-i]为逆序取值
    }
    return;
}

int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    Reverse(a, sizeof(a)/sizeof(int));
    for(int i = 0; i < 5; i++) {
        cout << *(a+i) << ",";  // 输出:5,4,3,2,1,
    }
    return 0;
}

指针和二维数组、指向指针的指针

指针和二维数组

  • 定义N行M列的二维数组:T a[N][M];
    • a[i](i是整数)是一维数组,可以看作是T类型的指针。
    • a[i]的地址:数组a的起始地址+i×M×sizeof(T)

指向指针的指针

  • 定义指向指针的指针:T **p;
    • p是指向指针的指针,*p是指向变量的指针,**p是存放T类型的变量。
  • 测试:指向指针的指针
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    int *p;  // p是指向int类型的指针
    int **pp;  // pp是指向int类型的指针的指针
    int n = 1234;  // n是int类型的变量

    p = &n;  // p指向n,即p存放的内容是n的地址
    pp = &p;  // pp指向p,即pp存放的内容是p的地址
    
    cout << *(*pp) << endl;  // *pp就是p,所以*(*pp)就是n,输出:1234
    return 0;
}

指针和字符串

指针和字符串

  • 测试:指针和字符串
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
    char *p = "Please input your name: \n";  // p是字符串常量
    cout << p;
    
    char name[20];  // name是字符串数组名
    char *pName = name;  // pName是char类型的指针,指向数组name的起始地址
    cin >> pName;  // pName也可以看作字符串数组名,等效于name

    cout << "Your name is " << pName;
    return 0;
}

字符串库函数

字符串库函数功能
strcat将一个字符串连接到另一个字符串后面
strncat将一个字符串的前n个字符连接到另一个字符串后面
strchr查找某字符在字符串中最先出现的位置
strrchr查找某字符在字符串中最后出现的位置
strstr求子串的位置
strcmp比较两个字符串的大小(大小写相关)
stricmp比较两个字符串的大小(大小写无关)
strncmp比较两个字符串的前n个字符
strcpy复制字符串
strncpy复制字符串的前n个字符
strlen求字符串长度
strlwr将字符串变成小写
strupr将字符串变成大写
strtok抽取被指定字符分隔的字符串
atoi将字符串转换为整数
atof将字符串转换为实数
itoa将整数转换为字符串

字符串库函数

  • 测试:字符串库函数的使用
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

int main()
{
    char str1[100] = "12345";
    char str2[100] = "abcdefg";
    char str3[100] = "ABCDE";
    
    strncat(str1, str2, 3);  // 字符串拼接n个字符:str1 = "12345abc"
    cout << "(1) " << str1 << endl;  // 输出:(1) 12345abc
    
    strncpy(str1, str3, 3);  // 字符串复制n个字符:str1 = "ABC45abc"
    cout << "(2) " << str1 << endl;  // 输出:(2) ABC45abc

    strncpy(str2, str3, 6);  // 字符串复制n个字符:str2 = "ABCDE"
    cout << "(3) " << str2 << endl;  // 输出:(3) ABCDE

    int cmpResult = strncmp(str1, str3, 3);  // 字符串比较n个字符:str1和str3的前3个字符相等
    cout << "(4) " << cmpResult << endl;  // 输出:(4) 0

    char *p = strchr(str1, 'B');  // 查找字符最先出现的位置:p指向str1中的字符'B'
    if(p)  // 如果p的值不为NULL,即str1中存在字符'B'
        cout << "(5) " << p-str1 << ", " << *p << endl;  // 输出:(5) 1, B
    else  // 如果str1中不存在字符'B'
        cout << "(5) No Found" << endl;

    p = strstr(str1, "45a");  // 查找子串出现的位置:p指向str1中的子串"45a"
    if(p)  // 如果p的值不为NULL,即str1中存在子串"45a"
        cout << "(6) " << p-str1 << ", " << p << endl;  // 输出:(6) 3, 45abc
    else  // 如果str1中不存在子串"45a"
        cout << "(6) No Found" << endl;
    
    cout << "strtok usage demo: " << endl;  // 演示strtok的用法
    char str[] = "- This, a sample string, OK.";
    p = strtok(str, " ,.-");  // 从str中逐个抽取被指定字符( ,.-)分割的字符串
    while(p != NULL) {  // 如果p的值不为NULL,即找到一个子串
        cout << p << endl;
        p = strtok(NULL, " ,.-");  // 后续调用,第1个参数必须是NULL
    }

    return 0;
}

void指针和内存操作函数

void指针

  • 可以用任何类型的指针对void指针进行赋值或初始化。
  • 因为sizeof(void)没有定义,所以对于void指针p:*p,++p,–p,p+n,p-n等均没有定义。

内存操作库函数

  • 内存初始化:memset
    • memset(dest, ch, n):将从dest开始的n个字节都设置为ch(ch的最低字节),返回dest。
  • 内存复制:memcpy
    • memcpy(dest, src, n):将从src开始的n个字节复制到从dest开始的n个字节,返回dest。
  • 测试:内存操作库函数的使用
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

// 如何编写自定义内存操作函数MyMemcpy
void *MyMemcpy(void *dest, const void *src, int n)
{
    char *pDest = (char *)dest;
    char *pSrc = (char *)src;
    for(int i = 0; i < n; i++) {  // 将源块的内容(*pSrc)复制到目的块(*pDest)
        *(pDest+i) = *(pSrc+i);  // 逐个字节复制
    }
    return dest;
    // 如果源区域和目标区域有重叠,则会出错
    // 目标区域的开始部分(源区域的结束部分)存放的内容会被改写
}

int main()
{
    int a[100];
    memset(a, 0, sizeof(a));  // 将数组a的元素设置为0,即初始化数组a

    char szName[200] = " ";  // 数组szName的元素都为'\0'
    memset(szName, 'a', 10);  // 将szName的前10个字节设置为'a'
    cout << szName << endl;  // 输出:aaaaaaaaaa

    int a1[5] = {0, 1, 2, 3, 4};
    int a2[5] = {5, 6, 7, 8, 9};
    memcpy(a2, a1, 3*sizeof(int));  // 将数组a1的前3个元素的内容复制到数组a2
    for(int i = 0; i < 5; i++) {
        cout << a2[i] << ",";  // 输出:0,1,2,8,9,
    }
    cout << endl;

    MyMemcpy(a2, a1, 5*sizeof(int));  // 将数组a1的所有内容复制到数组a2
    for(int i = 0; i < 5; i++) {
        cout << a2[i] << ",";  // 输出:0,1,2,3,4,
    }

    return 0;
}

函数指针

基本概念

  • 程序运行期间,函数会占用一段连续的内存空间,函数名就是函数所占内存区域的起始地址。
  • 使指针指向函数的起始地址,通过该函数指针就可以调用该函数。

函数指针

  • 定义函数指针:int (*pf)(int, char);
    • pf是函数指针,所指向的函数返回int类型,参数为int类型、char类型等。
  • 可以用一个原型匹配的函数名给一个函数指针赋值。
  • 测试:函数指针的使用
#include <iostream>
#include <cstdio>
using namespace std;

// 输出较小的数
void PrintMin(int a, int b)
{
    if(a < b)
        printf("%d", a);
    else
        printf("%d", b);
}

int main()
{
    void (*pf)(int, int);  // pf是函数指针
    int x = 4, y = 6;
    pf = PrintMin;  // pf指向函数PrintMin
    pf(x, y);  // 通过pf调用指向的函数PrintMin
    return 0;
}

函数指针和qsort库函数

  • 快速排序:qsort
    • qsort(base, nitems, size, pfCompare):可以对任意类型的数组进行排序。
    • base:数组名(数组起始地址)。
    • nitems:数组元素的个数。
    • size:数组元素的大小(以字节为单位),由此可以计算每个元素的地址。
    • pfCompare:函数指针,指向比较函数,由此可以确定元素谁在前谁在后的规则。
  • 比较函数编写规则:pfCompare(elem1, elem2)
    • 如果elem1应该排在elem2前面,则返回负整数。
    • 如果elem2应该排在elem1前面,则返回正整数。
    • 如果都可以排在前面,则返回0。
  • 测试:qsort函数的使用
#include <iostream>
#include <cstdio>
using namespace std;

#define NUM 5

// 自定义比较函数
int MyCompare(const void *elem1, const void * elem2)
{
    unsigned int *p1 = (unsigned int *)elem1;  // 直接用*elem1是非法的,编译器不知道*elem1有多少字节
    unsigned int *p2 = (unsigned int *)elem2;
    return (*p1 % 10) - (*p2 % 10);  // 比较*p1和*p2的个位数的大小,按从小到大排序
}

int main()
{
    unsigned int an[NUM] = {8, 123, 11, 10, 4};
    qsort(an, NUM, sizeof(unsigned int), MyCompare);  // 参数:数组名,元素个数,元素大小,比较函数
    // 函数原型:void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
    for(int i = 0; i < NUM; i++) {
        printf("%d,", an[i]);  // 输出:10,11,123,4,8,
    }
    return 0;
}

结构(struct)

结构(struct)

  • C++允许程序员定义新的数据类型,该数据类型(结构)可以用来定义变量(结构变量)。
    • 如:struct Student { int ID; … }; Student Stu;
  • 结构变量中的成员变量通常是连续存放在内存空间,因此结构变量大小就是所有成员变量大小之和。
  • 两个同类型的结构变量,可以互相赋值,但不能进行比较运算。
  • 结构的成员变量可以是任何类型,也可以是另一个结构类型,也可以是指向本结构类型的指针。

访问结构变量的成员变量

  • 结构变量的成员变量可以和普通变量一样使用,也可以取得其地址。
    • 如:Stu.ID = 12345; int *p = &Stu.ID;

结构数组

  • 测试:结构数组的使用
#include <iostream>
#include <cstdio>
using namespace std;

struct Date {
    int year;
    int month;
    int day;
};
struct StudentEx {
    unsigned ID;
    char szName[20];
    float fGPA;
    Date birthday;
};

int main()
{
    // StudentEx MyClass[50];
    StudentEx MyClass[50] = {
        {1234, "Tom", 3.78, {1984, 12, 28}},
        {1235, "Jack", 3.25, {1984, 12, 23}},
        {1236, "Mary", 4.00, {1984, 12, 21}},
        {1237, "Jone", 2.78, {1984, 2, 28}}
    };

    MyClass[1].ID = 1267;
    MyClass[2].birthday.year = 1986;
    int n = MyClass[2].birthday.month;
    cin >> MyClass[0].szName;

    return 0;
}

指向结构变量的指针

  • 定义指向结构变量的指针,通过该指针可以访问其指向的结构变量的成员变量。
    • 如:Student *pStu = &stu; pStu->ID = 12345;

全局变量、局部变量、静态变量

全局变量和局部变量

  • 定义在所有函数外面的变量称为全局变量
  • 定义在函数内部的变量称为局部变量(函数的形参也是局部变量)。
  • 全局变量在所有函数中均可以使用,局部变量只能在定义它的函数内部使用。

静态变量

  • 全局变量都是静态变量。
  • 局部变量定义为静态变量时,需要在前面加关键字static。

静态变量和非静态变量

  • 静态变量的地址,在整个程序运行期间,都是固定不变的。
  • 非静态变量的地址,每次函数调用时都可能不同,在函数的一次执行期间不变。
  • 如果没有初始化,静态变量自动赋初值为0(每个bit都是0),非静态变量的值为随机。
  • 测试:局部变量作为静态变量
#include <iostream>
#include <cstdio>
using namespace std;

void Func()
{
    static int n = 4;  // 静态变量只执行一次初始化语句
    // int n = 4;  // 非静态变量每次都执行初始化语句
    cout << n << endl;
    n++;
}

int main()
{
    Func(); Func(); Func();
    return 0;
}
  • 例题:从字符串中逐个抽取被指定字符分割的子串。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

// 从字符串p中逐个抽取被指定字符sep分割的子串
char *Strtok(char *p, char *sep)
{
    static char *start;  // start是静态变量,只执行一次初始化语句
    if(p)  // 如果p的值不为NULL,即第1次调用函数
        start = p;  // 设置本次查找子串的起点
    
    for( ; *start && strchr(sep, *start); start++)  // 如果字符*start出现在字符串sep中,继续循环
        ;  // 跳过分隔符号,直到遇到非分隔符号
    if(*start == 0) {  // 如果字符串start已经循环遍历到结尾字符'\0'
        return NULL;  // 返回空值
    }

    char *q = start;  // q为下一个子串
    for( ; *start && !strchr(sep, *start); start++)  // 如果字符*start没有出现在字符串sep中,继续循环
        ;  // 跳过非分隔符号,直到遇到分隔符号
    if(*start) {  // 如果*start不为NULL,即字符串没有到结尾
        *start = 0;  // 添加结尾字符'\0',独立分开子串
        start++;  // 设置后续调用的起点,延续指针start
    }
    return q;  // 返回子串
}

int main()
{
    char str[] = "- This, a sample string, OK.";
    char *p = Strtok(str, " ,.-");  // 从str中逐个抽取被指定字符( ,.-)分割的字符串
    while(p != NULL) {  // 如果p的值不为NULL,即找到一个子串
        cout << p << endl;
        p = Strtok(NULL, " ,.-");  // 后续调用,第1个参数必须是NULL
    }
    return 0;
}

变量的作用域和生存期

标识符的作用域

  • 变量名、函数名、类型名统称为标识符
  • 标识符能够起作用的范围,称为标识符的作用域
    • 在单文件的程序中,结构、函数和全局变量的作用域是其定义所在的整个文件。
    • 局部变量的作用域是从定义语句开始到右大括号(})结束。
    • 函数形参的作用域是整个函数。
    • 在for循环中,循环控制变量的作用域是整个循环。
  • 同名的标识符的作用域,在小的作用域中,作用域大的标识符会被屏蔽。

变量的生存期

  • 变量的生存期指的是,在此期间变量占有的内存空间只能归它所有,不能存放别的东西。
    • 全局变量的生存期,从程序被装入内存开始,到整个程序结束。
    • 静态局部变量的生存期,从定义语句第一次执行开始,到整个程序结束。
    • 非静态局部变量的生存期,从定义语句开始,到作用域之外结束。
    • 函数形参的生存期,从函数执行开始,到函数返回结束。

选择排序、插入排序、冒泡排序

十大排序算法

  • 冒泡排序、选择排序、插入排序、归并排序、快速排序、希尔排序、堆排序、计数排序、桶排序、基数排序
  • 菜鸟教程:https://www.runoob.com/w3cnote/ten-sorting-algorithm.html

选择排序

  • 选择排序的算法描述
    • 在未排序序列中找到最小/大元素,存放到排序序列的起始位置(最小/大元素与起始位置交换)。
    • 从剩余未排序元素中继续寻找最小/大元素,然后放到已排序序列的末尾。
    • 重复第二步,直到所有元素均排序完毕。
  • 例题:使用选择排序对数组从小到大排序。
// 选择排序函数
void SelectionSort(int a[], int size)
{
    for(int i = 0; i < size-1; i++) {  // 循环遍历数组a
        int tmpMin = i;  // tmpMin是剩下未排序的元素中的最小元素的下标
        for(int j = i+1; j < size; j++) {  // 循环遍历剩下未排序的元素
            if(a[j] < a[tmpMin]) {
                tmpMin = j;  // 更新tmpMin
            }
        }
        int tmp = a[i];  // 交换最小元素和排列序列末尾元素
        a[i] = a[tmpMin];
        a[tmpMin] = tmp;
    }
}

插入排序

  • 插入排序的算法描述
    • 将序列的第1个元素看作是有序序列,将第2个元素到最后1个元素看作是未排序序列。
    • 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。
    • 如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
  • 例题:使用插入排序对数组从小到大排序。
// 插入排序函数
void InsertionSort(int a[], int size)
{
    for(int i = 1; i < size; i++) {  // 循环遍历未排序序列(右边)
        for(int j = 0; j < i; j++) {  // 循环遍历有序序列(左边)
            if(a[i] < a[j]) {  // a[i]正好小于a[j],需要插入到a[j]前面
                int tmp = a[i];  // 暂时保存a[i]
                for(int k = i; k > j; k--) {  // 循环遍历j-1~i
                    a[k] = a[k-1];  // 从a[j]~a[i-1]逐个元素向后移1个元素
                }
                a[j] = tmp;  // 把a[i]插入适当位置,即插入a[j]原来的位置
                break;
            }
        }
    }
}

冒泡排序

  • 冒泡排序的算法描述
    • 依次比较相邻的两个元素,如果前者比后者大,就交换它们的位置,移动结束后最大的元素会在最后。
    • 针对所有的元素重复以上的步骤,除了最后的已排序序列。
    • 持续每次对越来越少的元素重复以上的步骤,直到没有任何一对数字需要比较。
  • 例题:使用冒泡排序对数组从小到大排序。
// 冒泡排序函数
void BubbleSort(int a[], int size)
{
    for(int i = size-1; i > 0; i--) {  // 循环遍历未排序序列(左边)
        for(int j = 0; j < i; j++) {  // 依次比较相邻的两个元素,移动结束后最大的元素会在最后
            if(a[j] > a[j+1]) {  // 如果前者比后者大
                int tmp = a[j];  // 交换两个元素的位置
                a[j] = a[j+1];
                a[j+1] = tmp;
            }
        }
    }
}

程序或算法的时间复杂度

程序或算法的时间复杂度

  • 一个程序或算法的时间效率,称为时间复杂度,简称为复杂度。
  • 复杂度常用字母O和字母n表示,如O(n),O(n2)。
    • n代表问题的规模
  • 平均复杂度和最坏复杂度可能一致,也可能不一致。
  • 如果复杂度是多个n的函数之和,则只关心随n增长最快的函数。
    • O(n3+n2)→O(n3)
    • O(2n+n3)→O(2n)
    • O(n!+n2)→O(n!)
  • 常见的时间复杂度
    • 常数复杂度:O(1)
    • 对数复杂度:O(log(n))
    • 线性复杂度:O(n)
    • 多项式复杂度:O(nk)
    • 指数复杂度:O(an)
    • 阶乘复杂度:O(n!)
  • 常见算法的时间复杂度
    • 在无序数列中查找某个数(顺序查找):O(n)
    • 在有序数列中查找某个数(二分查找):O(log(n))
    • 平面上有n个点,求任意两点之间的距离:O(n2)
    • 插入排序、选择排序、冒泡排序:O(n2)
    • 快速排序:O(n*log(n))

STL排序算法

STL概述

  • STL:标准模板库(Standard Template Library)
  • 包含常用的算法,如排序查找;还包含常用的数据结构,如可变长数组、链表、字典等。

用sort进行排序

  • 对基本类型的数组从小到大排序:sort(a+n1, a+n2);
    • 排序范围是数组a的下标[n1, n2),如果n1是0则可以省略。
  • 对基本类型的数组从大到小排序:sort(a+n1, a+n2, greater());
    • functional提供了一堆基于模板的比较函数对象。
  • 自定义比较函数:sort(begin, end, cmp);
    • begin是要排序的数组的起始地址,end是结束地址,cmp是排序的方法。
    • cmp(elem1, elem2):如果elem1应该排在elem2前面,则返回true。
  • 重载结构体或类的比较运算符:sort(begin, end, Student());
    • struct Student{ bool operator()(const T &elem1, const T &elem2) { … } };
  • 测试:sort函数的使用
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

struct Rule1 {  // 按从大到小排序
    bool operator() (const int &elem1, const int &elem2) {
        return elem1 > elem2;
    }
};
struct Rule2 {  // 按个位数从小到大排序
    bool operator() (const int &elem1, const int &elem2) {
        return (elem1 % 10) < (elem2 % 10);
    }
};

struct Student {
    char name[20];
    int id;
    double gpa;
};
Student students[] = {
    {"Jack", 112, 3.4},
    {"Mary", 102, 3.8},
    {"Mary", 117, 3.9},
    {"Ala", 333, 3.5},
    {"Zero", 101, 4.0}
};

struct StudentRule1 {  // 按姓名从小到大排序
    bool operator() (const Student &s1, const Student &s2) {
        if(stricmp(s1.name, s2.name) < 0)
            return true;
        return false;
    }
};
struct StudentRule2 {  // 按ID从小到大排序
    bool operator() (const Student &s1, const Student &s2) {
        return s1.id < s2.id;
    }
};
struct StudentRule3 {  // 按GPA从高到低排序
    bool operator() (const Student &s1, const Student &s2) {
        return s1.gpa > s2.gpa;
    }
};

void Print(int a[], int size)
{
    for(int i = 0; i < size; i++) {
        cout << a[i] << ",";
    }
    cout << endl;
}

void PrintStudents(Student stu[], int n)
{
    for(int i = 0; i < n; i++) {
        cout << "(" << stu[i].name << "," << stu[i].id << "," << stu[i].gpa << "),";
    }
    cout << endl;
}

int main()
{
    int a[] = {12, 45, 3, 98, 21, 7};
    int size = sizeof(a) / sizeof(int);

    sort(a, a+size);  // 按从小到大排序
    cout << "(1) "; Print(a, size);

    sort(a, a+size, Rule1());  // 按从大到小排序
    cout << "(2) "; Print(a, size);

    sort(a, a+size, Rule2());  // 按个位数从小到大排序
    cout << "(3) "; Print(a, size);

    int n = sizeof(students) / sizeof(Student);

    sort(students, students+n, StudentRule1());  // 按姓名从小到大排序
    cout << "(4) "; PrintStudents(students, n);

    sort(students, students+n, StudentRule2());  // 按ID从小到大排序
    cout << "(5) "; PrintStudents(students, n);

    sort(students, students+n, StudentRule3());  // 按GPA从高到低排序
    cout << "(6) "; PrintStudents(students, n);

    return 0;
}

STL二分查找算法

用binary_search进行二分查找

  • 在从小到大排序的基本类型的数组中进行二分查找:binary_search(a+n1, a+n2, value);
    • 在下标范围是[n1, n2)的查找区间内查找等于value的元素,返回值是true或false。
  • 重载结构体或类的比较运算符:binary_search(begin, end, value, Student());
    • 查找时的排序规则,必须和排序时的排序规则一致。
    • a与b相等:a必须在b前面、b必须在a前面,这两种情况都不成立。
  • 测试:binary_search函数的使用
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

struct Rule {  // 按个位数从小到大排序
    bool operator() (const int &elem1, const int &elem2) {
        return (elem1 % 10) < (elem2 % 10);
    }
};

void Print(int a[], int size)
{
    for(int i = 0; i < size; i++) {
        cout << a[i] << ",";
    }
    cout << endl;
}

int main()
{
    int a[] = {12, 45, 3, 98, 21, 7};

    sort(a, a+6);
    Print(a, 6);
    cout << "result: " << binary_search(a, a+6, 12) << endl;
    cout << "result: " << binary_search(a, a+6, 77) << endl;
    
    sort(a, a+6, Rule());  // 按个位数从小到大排序
    Print(a, 6);
    cout << "result: " << binary_search(a, a+6, 7) << endl;
    cout << "result: " << binary_search(a, a+6, 8, Rule()) << endl;

    return 0;
}

用lower_bound进行二分查找下界

  • 在从小到大排序的基本类型的数组中进行二分查找:lower_bound(a+n1, a+n2, value);
    • 返回T类型的指针p,*p是查找区间里下标最小的、大于等于value的元素。
    • 如果找不到,p指向下标是n2的元素。
  • 重载结构体或类的比较运算符:lower_bound(begin, end, value, Student());
    • *p是查找区间里下标最小的、按自定义排序规则可以排在value后面的元素。

用upper_bound进行二分查找上界

  • 在从小到大排序的基本类型的数组中进行二分查找:upper_bound(a+n1, a+n2, value);
    • *p是查找区间里下标最小的、大于value的元素。
  • 重载结构体或类的比较运算符:upper_bound(begin, end, value, Student());
    • *p是查找区间里下标最小的、按自定义排序规则必须排在value后面的元素。
  • 测试:lower_bound和upper_bound函数的使用
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

#define NUM 7

struct Rule {  // 按个位数从小到大排序
    bool operator() (const int &elem1, const int &elem2) {
        return (elem1 % 10) < (elem2 % 10);
    }
};

void Print(int a[], int size)
{
    for(int i = 0; i < size; i++) {
        cout << a[i] << ",";
    }
    cout << endl;
}

int main()
{
    int a[NUM] = {12, 5, 3, 5, 98, 21, 7};
    int *p;

    sort(a, a+NUM);  // 按从小到大排序
    Print(a, NUM);  // 输出:3,5,5,7,12,21,98,
    
    p = lower_bound(a, a+NUM, 5);  // 找到下标最小的、大于等于5的元素
    cout << "value: " << *p << ", position: " << p-a << endl;  // 输出:value: 5, position: 1
    p = upper_bound(a, a+NUM, 5);  // 找到下标最小的、大于5的元素
    cout << "value: " << *p << ", position: " << p-a << endl;  // 输出:value: 7, position: 3
    p = upper_bound(a, a+NUM, 13);  // 找到下标最小的、大于13的元素
    cout << "value: " << *p << ", position: " << p-a << endl;  // 输出:value: 21, position: 5

    sort(a, a+NUM, Rule());  // 按个位数从小到大排序
    Print(a, NUM);  // 输出:21,12,3,5,5,7,98

    p = lower_bound(a, a+NUM, 16, Rule());  // 找到下标最小的、按自定义排序规则可以排在16后面的元素
    cout << "value: " << *p << ", position: " << p-a << endl;  // 输出:value: 7, position: 5
    p = lower_bound(a, a+NUM, 25, Rule());  // 找到下标最小的、按自定义排序规则可以排在25后面的元素
    cout << "value: " << *p << ", position: " << p-a << endl;  // 输出:value: 5, position: 3
    p = upper_bound(a, a+NUM, 5, Rule());  // 找到下标最小的、按自定义排序规则必须排在5后面的元素
    cout << "value: " << *p << ", position: " << p-a << endl;  // 输出:value: 7, position: 5

    p = upper_bound(a, a+NUM, 18, Rule());  // 找到下标最小的、按自定义排序规则必须排在18后面的元素
    if(p == a+NUM)  // 如果找不到,p指向a[NUM],即空
        cout << "Not Found." << endl;
    cout << "value: " << *p << ", position: " << p-a << endl;  // 输出:value: 0, position: 7
    
    return 0;
}

multiset,set,multimap,map

平衡二叉树数据结构

  • 在大量增加数据、删除数据的同时进行查找数据,不能用排序+二分查找,每次增删数据后要重新排序。
  • 可以用平衡二叉树数据结构存放数据,在STL中有四种排序容器:multiset,set,multimap,map。

multiset

  • 定义multiset变量:multiset st;
    • st可以存放T类型的数据,数据可以重复,并且能时刻维持数据有序。
    • st.insert插入数据,st.erase删除数据,st.find查找数据,复杂度都是O(log(n))。
  • 测试:multiset的使用
#include <iostream>
#include <cstdio>
#include <cstring>
#include <set>  // 使用multiset和set需要包含此头文件
using namespace std;

int main()
{
    multiset<int> st;
    int a[10] = {1, 14, 12, 13, 7, 13, 21, 19, 8, 8};
    for(int i = 0; i < 10; i++) {
        st.insert(a[i]);  // 插入数据,容器st的数据是a[i]的复制数据
    }

    multiset<int>::iterator i;  // 迭代器,相当于指针
    for(i = st.begin(); i != st.end(); i++) {  // 循环遍历容器st的数据
        cout << *i << ",";  // 输出:1,7,8,8,12,13,13,14,19,21,
    }
    cout << endl;

    i = st.find(19);  // 查找数据,返回值是迭代器
    if(i == st.end()) {  // 如果找不到,则返回end()
        cout << "Not found." << endl;
    }
    else {  // 如果找到,则返回指向找到的元素的迭代器
        cout << "Found number: " << *i << endl;
    }

    i = st.lower_bound(13);  // 返回13的第一个可插入位置,即大于等于13的最小元素
    cout << "Found lower_bound: " << *i << endl;  // 输出:Found lower_bound: 13
    i = st.upper_bound(8);  // 返回8的最后一个可插入位置,即大于8的最小元素
    cout << "Found upper_bound: " << *i << endl;  // 输出:Found upper_bound: 12

    st.erase(i);  // 删除迭代器i指向的元素,即12
    for(i = st.begin(); i != st.end(); i++) {
        cout << *i << ",";  // 输出:1,7,8,8,13,13,14,19,21,
    }

    return 0;
}

multiset的迭代器

  • 定义multiset的迭代器:multiset::iterator p;
    • p是迭代器,相当于指针,指向multiset中的元素,访问multiset中的元素要通过迭代器。
    • 迭代器可以++和–,用!=和==比较,不可以比较大小,加减整数。
    • st.begin()是指向st第一个元素的迭代器,st.end()是指向st最后一个元素的迭代器。

自定义排序规则的multiset

  • 测试:自定义排序规则的multiset的使用
#include <iostream>
#include <cstdio>
#include <cstring>
#include <set>
using namespace std;

struct Rule {  // 按个位数从小到大排序
    bool operator() (const int &elem1, const int &elem2) {
        return (elem1 % 10) < (elem2 % 10);
    }
};

int main()
{
    int a[10] = {1, 14, 12, 13, 7, 13, 21, 19, 8, 8};

    multiset<int, greater<int>> st1;  // 按从大到小排序
    for(int i = 0; i < 10; i++)
        st1.insert(a[i]);  // 插入数据
    multiset<int, greater<int>>::iterator i;  // st1的迭代器
    for(i = st1.begin(); i != st1.end(); i++)
        cout << *i << ",";  // 输出:21,19,14,13,13,12,8,8,7,1,
    cout << endl;

    multiset<int, Rule> st2;  // 按个位数从小到大排序
    for(int i = 0; i < 10; i++)
        st2.insert(a[i]);  // 插入数据
    multiset<int, Rule>::iterator j;  // st2的迭代器
    for(j = st2.begin(); j != st2.end(); j++)
        cout << *j << ",";  // 输出:1,21,12,13,13,14,7,8,8,19,
    cout << endl;

    j = st2.find(133);  // 查找数据,即与133相等的元素,根据自定义排序规则,133等同于13
    // find(x):在排序容器中找元素y,使得x必须排在y前面、y必须排在x前面,这两种情况都不成立。
    cout << "Found number: " << *j << endl;  // 输出:Found number: 13

    return 0;
}
  • 测试:自定义排序规则的multiset的使用
#include <iostream>
#include <cstdio>
#include <cstring>
#include <set>
using namespace std;

struct Student {
    char name[20];
    int id;
    double score;
};
Student students[] = {
    {"Jack", 112, 78},
    {"Mary", 102, 85},
    {"Ala", 333, 92},
    {"Zero", 101, 70},
    {"Cindy", 102, 78}
};

struct Rule {  // 按分数从高到低排序,分数相同则按姓名从小到大排序
    bool operator() (const Student &s1, const Student &s2) {
        if(s1.score != s2.score)
            return s1.score > s2.score;
        else
            return (strcmp(s1.name, s2.name) < 0);
    }
};

int main()
{
    multiset<Student, Rule> st;
    for(int i = 0; i < 5; i++)
        st.insert(students[i]);  // 插入数据
    
    multiset<Student, Rule>::iterator p;
    for(p = st.begin(); p != st.end(); p++)
        cout << p->score << " " << p->name << " " << p->id << endl;
    
    Student s = {"Mary", 1000, 85};
    p = st.find(s);  // 查找数据
    // 因为排序规则不考虑ID,两个Mary谁排在前面都可以,所以看作相等的数据
    if(p != st.end())  // 如果找到等于s的数据
        cout << p->score << " " << p->name << " " << p->id << endl;  // 输出:85 Mary 102

    return 0;
}

set

  • set和multiset的区别是容器里不能有重复数据(重复数据是指两个元素排序无所谓前后)。
  • set插入数据可能不成功。
  • 测试:set的使用
#include <iostream>
#include <cstdio>
#include <cstring>
#include <set>
using namespace std;

int main()
{
    set<int> st;
    int a[10] = {1, 14, 12, 13, 7, 13, 21, 19, 8, 8};
    for(int i = 0; i < 10; i++)
        st.insert(a[i]);
    cout << st.size() << endl;  // 输出:8

    set<int>::iterator i;
    for(i = st.begin(); i != st.end(); i++)
        cout << *i << ",";  // 输出:1,7,8,12,13,14,19,21
    cout << endl;

    pair<set<int>::iterator, bool> result = st.insert(12);
    // pair<set<int>::iterator, bool>
    // 等价于:
    // struct {
    //     set<int>::iterator first;
    //     bool second;
    // };
    if(!result.second)  // 如果插入不成功
        cout << *result.first << " already exists." << endl;
    else
        cout << *result.first << " inserted." << endl;
    
    return 0;
}

pair

  • pair<T1, T2>类型等价于:struct { T1 first; T2 second; };

multimap

  • 定义multimap变量:multimap<T1, T2> mp;
    • mp里的元素都是pair形式,都等同于:struct { T1 first; T2 second; };
    • multimap中的元素按照first排序和查找,默认排序规则是按从小到大排序。
  • 例题:学生成绩录入和查询系统,可以录入(Add name id score)或查询(Query score),两种输入交替出现。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>  // 使用multimap和map需要包含此头文件
using namespace std;

struct StudentInfo {
    int id;
    char name[20];
};
struct Student {
    int score;  // 按分数从小到大排序
    StudentInfo info;
};

int main()
{
    multimap<int, StudentInfo> mp;  // int对应score,StudentInfo对应info
    
    char cmd[20];
    while(cin >> cmd) {
        if(cmd[0] == 'A') {  // 如果输入Add,表示录入
            Student st;
            cin >> st.info.name >> st.info.id >> st.score;  // 继续输入name、id、score
            // 向mp插入数据,其first是st.score,second是st.info
            mp.insert(make_pair(st.score, st.info));  // make_pair生成一个pair<int, StudentInfo>变量
        }

        else if(cmd[0] == 'Q') {  // 如果输入Query,表示查询
            int score;
            cin >> score;  // 继续输入score
            // 从mp查询数据,查询下标最大的、小于score的数据
            multimap<int, StudentInfo>::iterator p;
            p = mp.lower_bound(score);  // 此时[mp.begin(), p)都是小于score的数据

            if(p == mp.begin()) {  // 如果mp中没有比score低的数据
                cout << "Nobody" << endl;
            }
            else {  // 如果mp中有比score低的数据
                p--;  // 此时p指向mp中分数比score低的最高分获得者
                score = p->first;

                // 从mp查询数据,查询分数等于score、学号最大的数据
                multimap<int, StudentInfo>::iterator maxp = p;
                int maxid = p->second.id;
                for(; p->first == score; p--) {  // 循环遍历所有分数等于score的数据
                    if(p->second.id > maxid) {  // 找出这些数据中学号最大的数据
                        maxp = p;
                        maxid = p->second.id;
                    }
                    if(p == mp.begin()) {  // 如果循环到起点,mp中没有数据
                        break;  // 结束循环
                    }
                }
                
                cout << maxp->second.name << " " << maxp->second.id << " " << maxp->first << endl;
            }
        }
    }
    return 0;
}

map

  • map和multimap的区别是容器里不能有关键字重复的数据
  • 可以使用中括号([]),下标为关键字,返回first和关键字相同的second。
  • map插入数据可能不成功。
  • 测试:map的使用
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
using namespace std;

struct Student {
    string name;
    int score;
};
Student students[5] = {
    {"Jack", 89},
    {"Tom", 74},
    {"Cindy", 87},
    {"Alysa", 87},
    {"Micheal", 98}
};

int main()
{
    map<string, int> mp;
    for(int i = 0; i < 5; i++) {
        mp.insert(make_pair(students[i].name, students[i].score));  // 插入数据
    }

    cout << mp["Jack"] << endl;  // 输出:89
    mp["Jack"] = 60;  // 修改关键字为"Jack"的元素的second
    cout << mp["Jack"] << endl;  // 输出:60

    for(map<string, int>::iterator i = mp.begin(); i != mp.end(); i++) {
        cout << "(" << i->first << ", " << i->second << ") ";
    }  // 输出:(Alysa, 87) (Cindy, 87) (Jack, 60) (Micheal, 98) (Tom, 74)
    cout << endl;

    Student st;
    st.name = "Jack"; st.score = 99;
    pair<map<string, int>::iterator, bool> p = mp.insert(make_pair(st.name, st.score));
    if(p.second)  // 如果插入数据成功
        cout << "(" << p.first->first << ", " << p.first->second << ")" << endl;
    else
        cout << "Insertion failed." << endl;
    
    mp["Harry"] = 78;  // 插入新的元素,其first是"Harry",second是78
    map<string, int>::iterator q = mp.find("Harry");
    cout << "(" << q->first << ", " << q->second << ")" << endl;  // 输出:(Harry, 78)

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值