算法笔记常见问题及记录

  1. 接受了考试后:购买了专栏真题,邮件里有写是哪个卷子
  2. cout输出浮点数后几位:保留小数点可以使用printf("%.2f",a),也可以使用iomanip库的fixed函数设定固定小数点格式,使用setprecision(设置保留小数点位数)
        cout<<(a/b)<<endl<<fixed<<setprecision(2)<<(double(a)/double(b));
        fixed<<setprecision(0)<<d
  3. 对于基本数据类型和字符数组,printf函数通常直接传递变量的值或数组名,无需使用&符号
  4. 两个整型变量的四则运算结果仍然是整型,因此它的除法是整除的,也即只取了除法的整数部分(即),于是a/b就是一个整数。如果希望使用浮点除法的话,需要先将ab强制转换成浮点型,然后再进行除法操作,因此(double)a / ba / (double)b(double)a / (double b)1.0 * a / b等操作都是可行的,但(double)(a / b)是不行的,因为这样做是把a / b的结果转成浮点型,也即将整除后的结果(整数)转成浮点型,就达不到我们想要的效果了
  5. cin不支持格式化输入比如输入yyyy-mm-dd,需要使用scanf格式化输入
    int num;
    scanf("%d", &num);
    
    float value;
    scanf("%f", &value);
    
    char ch;
    scanf("%c", &ch);
    
    char str[100];
    scanf("%s", str);
    
    ‌限制读取的长度‌:
    char str[10];
    scanf("%5s", str); // 最多读取4个字符(留一个位置给字符串结束符'\0')
    ‌匹配特定的字符‌:
    char str[10];
    scanf("%[abcd]", str); // 只读取字符a、b、c、d,遇到其他字符停止
    ‌不匹配某些字符‌:
    char str[10];
    scanf("%[^0-9]", str); // 读取除数字之外的所有字符
    ‌丢弃某些字符‌:
    char str[10];
    scanf("%*[a-z]"); // 丢弃所有小写字母,不往变量中存放
    scanf("%s", str); // 接着读取字符串
  6. string拼接字符串:“a”+“b”   转换字符串类型 to_string()
  7. c++写的函数必须在主函数之前,或者在主函数之前声明
    #include <iostream>
    
    // 函数原型声明
    void myFunction();
    
    int main() {
        myFunction(); // 调用函数
        return 0;
    }
    
    // 函数定义
    void myFunction() {
        std::cout << "Hello, World!" << std::endl;
    }
  8. getchar()返回输入流的asc2码值,因此可以直接和字符比较,也可以和asc2码比较

    char a = ' '; // 空格字符
    if (a == 32) {
        // 这是正确的,因为' '的ASCII码值是32
        // 但是通常我们更习惯直接与字符比较
    }
    
    // 或者更直接地与字符比较
    if (a == ' ') {
        // 这是更常见和直观的方式
    }

     

  9. 遇到换行输入的数据时,需要使用getchar()来读取换行符,否则会将换行符读取到第二个输入的字符变量中
    使用cin进行输入时,并不会因为输入整数而多一个换行符。实际上,cin会忽略输入中的空格、制表符和换行符,只提取有效的数据部分。
    在使用scanf进行输入时,需要注意输入数据的格式和字符之间的分隔符,以避免出现意外的输入行为。特别是在处理多组数据时,可能需要使用额外的字符变量来吸收换行符或空格符,以正确分隔各组数据。
  10. cin除了可以用回车分割输入,使用空格也能分开多个变量如图:


  11. 数学函数都在cmath头文件下,以下是本题涉及的常见数学函数,它们都接收浮点型,返回的也是浮点型

    1. fabs(a)a的绝对值
    2. floor(a)a的向下取整,会保留原有的小数点后位数
    3. ceil(a)a的向上取整
    4. round(a)a的四舍五入(需要注意 %.0f 使用的是四舍六入五成双规则(为5时根据前一位数字的奇数偶数来判断进位),与 round 函数不同)
    5. pow(a, b)ab次方,其中b也可以是浮点型,数学中的“幂运算”(次方)英文是 power(如 23 读作 “2 to the power of 3”)。
    6. sqrt(a)a的算术平方根(即开根号)
    7. log(a)a的以自然对数e为底的对数
  12. float单精度是浮点数,double是双精度浮点数,单精度float的高次幂可能会损失精度,需要使用double进行
  13. %取余:

    除数不能为0:如果尝试用0作为除数进行取余操作,程序将会抛出运行时错误(例如,在C++中通常是抛出std::overflow_error异常)。因此,在使用 % 运算符之前,应该确保除数不为0。

    正负数的取余:取余操作的结果的正负性与被除数的正负性相同。例如,-7 % 4 的结果是 -3,因为 -7 除以 4 的余数是 -3。如果需要得到非负的余数,可以通过调整表达式来实现,例如使用 (a % b + b) % b 来确保结果总是非负的。

  14. switch语句相当于是特殊情况下的连续if-else,即根据给定的值是不同的情况下应该分别执行哪些代码。

    在本题中,需要根据n012345、大于5这些不同情况,去输出不同的结果。注意每个case结束后需要使用break语句结束整个switch,不然会一直往下执行其他的case部分。

  15. vscode 编译带中文的文件或者路径会乱码,导致debug错误,修改为字母
    • 在使用continue语句时,要确保它不会导致程序陷入死循环或产生其他不期望的行为。
    • continue语句通常与条件语句(如if)一起使用,以根据特定条件跳过某些循环迭代。在循环语句中,当遇到continue语句时,程序会跳过当前循环体中continue语句之后的所有语句,直接进行下一次循环的条件判断。
  16. 数据类型 数组名[数组长度];
    int arr[5];
    其中,数据类型表示数组中元素的类型,数组名是你给数组起的名字,数组长度是一个常量表达式,表示数组中元素的数量。
  17. 指针是一个变量,但它存储的不是普通数据,而是内存地址(类似快递单号记录包裹的位置)。通过指针,可以直接操作内存中的数据。
  18. 全局数组

    当我们试图在函数中开一个大数组时,很容易使程序报错退出,这是因为在函数(包括主函数)中定义变量时,变量会存放在“栈”区,栈区可以分配的内存较小,所以在开大数组的时候会因为申请不到内存而报错退出。而如果我们把数组开在函数外面,那么将会存放在“堆”区,堆区可以分配的内存较大,适合开大数组。

  19. memset是一个用于批量设置内存内容的函数。cstring库
    你可以把它想象成一个“批量填充工具”,比如:

    • 把一块内存全部填成 0(清空)。

    • 把字符数组的每个字节都设为某个字符(比如 *)。

    • 快速初始化结构体或数组。
      函数原型

      #include <cstring>  // 必须包含头文件

      void* memset(void* ptr, int value, size_t num);

  20.  const修饰的全局数组必须显式初始化并且内容不可更改
    const int a[maxln]; // ❌ 错误
  21. cin.getline:仅C++可用,C语言不可用。需要添加#include <iostream>头文件,并且在头文件后增加一行using namespace std;。然后我们可以像下面这样使用,其中第一个参数是字符数组,第二个参数是最大允许读入的字符个数,设置为字符数组的长度即可。
  22. 特性 getline getchar
    输入单位 整行(含空格) 单个字符
    终止符处理 自动清除 保留在缓冲区
    返回值类型 输入流对象 ASCII码值(int类型)
    内存管理 自动(string)或手动(字符数组) 需手动分配
    典型用途 读取句子、多行文本 逐字符处理、清理缓冲区

    // getline读取整行(含空格)
    string line;
    getline(cin, line);  // 输入"Hello World",line="Hello World"
    
    // getchar逐字符读取
    char ch;
    while ((ch = getchar()) != '\n') {  // 输入"Hello World"
        cout << ch;  // 逐个输出字符,直到遇到回车
    }
  23. cin.getline 的第一个参数必须是 字符数组(char[],而 a 是 std::string 数组,类型不匹配。

    改用 std::getline 读取 std::string 对象:

    getline(cin, a[i]); // ✅ 正确(无需指定长度,自动处理内存)
  24. 当使用 cout << str 输出字符数组时:(也可以使用puts(str))

    1. 从数组首地址开始逐个输出字符。

    2. 遇到 \0 停止输出(不会继续输出后面的内存内容)。

    3. 最终效果是输出完整的字符串,无需手动循环。

    4. 字符数组以\0结尾,cin.getline 会自动在输入的字符串末尾添加 \0
  25.  strlen函数的实现原理是,从头开始扫描字符数组,直到碰到结束符\0时结束扫描。由于这个遍历的过程,使得strlen函数的时间复杂度是O(len),其中len表示字符数组的长度。因此,当我们需要遍历字符串时,尽可能提前使用strlen函数计算好长度,而不要在for循环中写类似for (int i = 0; i < strlen(str); i++)的语句,因为这会导致每次循环都要重新执行strlen函数来计算一次长度,使整体时间复杂度上升一个级别。<string.h>
  26. 假设我们使用字符数组来存储字符串,那么可以使用 strcmp 函数来比较两个字符串的字典序大小:

    • 如果 strcmp(s1, s2) == 0,那么两个字符串相同
    • 如果 strcmp(s1, s2) < 0,那么 s1 的字典序小于 s2
    • 如果 strcmp(s1, s2) > 0,那么 s1 的字典序大于 s2

    需要注意,strcmp 函数返回的结果并不一定是 1 或者 −1,所以不能使用等于 1 或 −1 的方式来进行判断。

  27. 字典序(Lexicographical Order) 是指按照字符的ASCII值逐个比较字符串的顺序,类似于字典中单词的排列方式。
  28. 假定我们使用字符数组来存储字符串,那么可以使用 strcat 函数来拼接字符串,其中 strcat(s1, s2) 表示把 s2 拼接到 s1 后面。

    注意,strcat 函数会将原字符串修改,而不是产生一个新的字符串,因此直接输出拼接后的 s1 即可。
    或者string类型可以直接用+好连接两个字符串,string类型也可以使用><=进行比较

  29. sscanf 可以将字符数组按某个格式匹配提取出其中的某些部分,并将这些部分赋值到变量中。使用的方式和 scanf 类似,我们可以把 scanf(格式串, &变量) 视为从程序输入中按格式字符串的格式提取出变量,那么 sscanf(字符数组, 格式串, &变量) 就是从字符数组中按格式字符串的格式提取出变量。并且 sscanf 和 scanf 的返回内容都是成功读取的变量个数。
    String Scan formatted字符扫描格式sscanf

    其函数原型通常如下:

     
    int sscanf(const char *str, const char *format, ...);
    • str 是要解析的字符串。
    • format 是格式控制字符串,指定了要提取的数据的类型和方式。
    • 后面的省略号表示一系列的指针参数,用来接收解析后的数据。

    sscanf 函数返回成功匹配和赋值的个数,如果发生错误或到达字符串末尾,则返回 EOF

    在C 语言标准库中
  30. c++,else if有空格不是elseif
  31. sprintf 是 “string print formatted” 的缩写,意思是 “格式化地打印到字符串中”。从名字上可以直观地理解其功能,即像 printf 函数一样进行格式化输出,但输出的目标是一个字符串而不是标准输出设备。

  32. sprintf和sscanf和scanf和printf类似,scanf都是通过&对变量赋值,print直接读取变量值不用&
    在 C++ 中,当函数按值传递参数、进行简单变量赋值与操作时不用 `&` 直接传变量;而函数使用引用或指针参数、进行动态内存分配、调用要求传地址的标准库函数(如 `scanf`)时,需要用 `&` 传递变量地址。 
  33. 格式控制
    %02 用于格式化输出,指定宽度为 2 且不足时用 0 填充;常见格式控制如 %d 输出十进制有符号整数、%u 输出无符号整数、%x/%X 输出十六进制、%o 输出八进制、%f 输出浮点数、%c 输出字符、%s 输出字符串、%% 输出 % 字符,还可通过 .n 指定小数位数等。
    示例代码如下:
    #include <cstdio>
    int main() {
        int num = 5;
        double f = 3.14;
        const char* str = "abc";
        char result[50];
        std::sprintf(result, "Int:%02d, Float:%.2f, String:%s", num, f, str);
        std::printf("%s\n", result);
        return 0;
    }
  34. 函数的局部变量作用域

    在函数中改变局部变量的值并不会影响到函数外面,因此函数中加 1 后原先的变量还是原值。

  35. 指针变量是一种特殊的变量,它存储的不是普通的数据值,而是内存地址。这个地址指向计算机内存中的某个位置,该位置可能存放着其他变量、数组、对象或者函数等。借助指针,程序能够直接对内存中的数据进行访问和操作,这极大地提升了编程的灵活性与效率,但同时也增加了编程的复杂度和出错的风险。
    C++ 中指针变量存储内存地址,声明时用 * 指定类型,可通过 & 取址初始化,支持加减、比较运算,数组名常隐式转换为指针,可作函数参数修改实参值,函数也能返回指针,但用 new 分配内存后需用 delete 释放防泄漏。
    int* ptr=&a;将变量a的地址存入ptr
    在 C++ 中,void add(int *a, int *b) 和 void add(int* a, int* b) 这两种写法是等价的,它们都正确地声明了一个名为 add 的函数,该函数接受两个指向 int 类型的指针作为参数。
  36. 引用

    如果我们希望将函数内变量的修改影响到函数外,那么可以使用指针变量。除此之外,我们还可以使用引用(&)。具体来说,可以在函数的参数中将可能修改值的参数名的前面加上 &,这样在函数内关于该变量的修改就会影响到函数外。使用引用作为函数参数时,并没有修改内存地址,而是利用引用和原变量共享内存的特性,直接在原变量的内存地址上进行数据修改,使得函数内的修改能够影响到函数外的变量。引用提供了一种更简洁、更安全的方式来实现函数内外数据的同步修改,避免了指针使用时可能出现的空指针、野指针等问题。

  37. 结构体可以将多个相同或不同类型的变量组合在一起,语法如下:

    struct Point {
        int x, y;
    };

     

  38. 构造函数是在结构体内部定义的一种特殊函数,可以用来构造结构体变量,它的 函数名和结构体名相同,并且 没有返回类型
    构造函数的核心作用之一就是对结构体(或类)的成员变量进行初始化。在创建结构体(或类)对象时,构造函数会自动被调用,确保对象的成员变量拥有合适的初始值,避免使用未初始化的变量引发的未定义行为。
  39. strcpy(name,_name)复制字符串,前者复制给后者

  40. 在结构体内部声明指向自身类型的指针是合法且实用的
    struct Node {
        int id;
        Node *left;
        Node *right;
    
        // 默认构造函数
        Node() : id(0), left(nullptr), right(nullptr) {}
    
        // 带参数的构造函数
        Node(int _id, Node *_left = nullptr, Node *_right = nullptr)
            : id(_id), left(_left), right(_right) {}
    };
    解引用操作符 ->:当结构体变量 p 是普通类型时,我们用 . 来访问它的内部变量;而当结构体变量 p 是指针类型时,我们除了可以用 (*p).id 的方式以外,还可以使用 p -> id 的方式。-> 是用于指针类型的对象访问其成员变量或调用其成员函数的操作符。
    取指针所指向对象的成员变量的值:
    node3.left->id  当你使用 node3.left->id 时,实际上是先让程序找到 node3.left 所指向的内存地址处的对象,然后再访问该对象里名为 id 的成员变量。
  41.  nullptr,对指针赋空值
  42. 由于浮点数的计算容易产生精度误差,因此对浮点数之间的比较需要特殊处理。

    • 当 a−b 大于一个正的很小的数(例如 10−8)时,可以认为 a>b
    • 当 a−b 小于一个负的绝对值很小的数(例如 −10−8)时,可以认为a<b   
  43. 可以借助 std::cin 的状态来实现类似 while(scanf(...) != EOF) 的效果。std::cin 在读取数据失败或者遇到文件结束符时,会进入错误状态,你可以利用这个特性来构建循环。
    假设你要持续读取整数,直到输入结束,以下是示例代码:
     
     
    #include <iostream>
    
    int main() {
        int num;
        while (std::cin >> num) {
            // 这里可以对读取到的 num 进行处理
            std::cout << "读取到的整数是: " << num << std::endl;
        }
        return 0;
    }

    代码解释

    • std::cin >> num 这个表达式会尝试从标准输入读取一个整数到 num 中。当读取成功时,std::cin 会返回一个 std::istream 对象,这个对象在布尔上下文中会被转换为 true;当读取失败(例如遇到文件结束符或者输入格式错误)时,会返回 false,从而退出循环。
  44. do-while 循环的特点是先执行循环体,再判断循环条件。这就意味着,即使还没有从输入读取到有效的 a 和 b 值,循环体也会执行一次,从而导致第一次输出可能是错误的结果。
    41中使用while合适,使用dowhile会先执行以此计算
  45. break 语句本身只能跳出它所在的那一层循环,无法直接跳出两层嵌套循环。
    可以借助一个标志变量赋值并跳出
  46. 使用 goto 语句

    goto 语句可以无条件地跳转到程序中指定的标签处,借助这个特性能够直接跳出两重循环。不过,goto 语句会破坏程序的结构化,降低代码的可读性和可维护性,所以在实际编程中要谨慎使用。

     
     
    #include <iostream>
    
    int main() {
        for (int i = 0; i < 5; ++i) {
            for (int j = 0; j < 5; ++j) {
                if (i == 2 && j == 3) {
                    goto endLoop;  // 跳转到标签 endLoop 处
                }
                std::cout << "i: " << i << ", j: " << j << std::endl;
            }
        }
    endLoop:
        std::cout << "已跳出两层循环" << std::endl;
        return 0;
    }

    代码解释:当在内层循环中满足 i == 2 && j == 3 条件时,使用 goto endLoop; 语句跳转到标签 endLoop 所在的位置,从而实现跳出两层循环。
  47. swap()

    • 头文件:包含在 <algorithm> 头文件中。<algorithm>库:stl算法库,源自拉丁语,算法的意思,swap()就是<algorithm>中的函数,

    • 功能:用于交换两个对象的值,适用于各种基本数据类型(如 intdoublechar 等)以及支持赋值操作的自定义类型。
    • 示例代码
    •  
       
      #include <iostream>
      #include <algorithm>
      
      int main() {
          int x = 10;
          int y = 20;
          std::swap(x, y);
          std::cout << "x: " << x << ", y: " << y << std::endl;
          return 0;
      }
  48. 选择排序的思路是将序列分为已排序和未排序两个部分,每次从未排序部分中选择一个最小的元素,将它加入到已排序部分的最后,这样已排序部分最终就会形成一个从小到大的顺序。
    定义两重循环,先选出数组第一个值准备在二重循环中开始比较,并且使用k标记未分类与分类的边界
    在二重循环根据k,之前循环标记的起始值,开始挑出最小值,具体过程就是,如果遇到更小的,就将分类的起始点下标移到它身上,继续向后遍历,最后取到的k就是最小的数组下标
    使用最小的数组下标,和一重循环标记的a[i]交换位置,并将未分类边界向后移动一位
    void swaparr()
    {
        for (int i = 0; i < n; i++)
        {
            // a[i]
            // 从i开始都是没分类的,将j赋给k,记录未分类的下标
            k = i;
            for (int j = k; j < n; j++)
            {
                //在本轮循环中选出最小的值a[k]
                if (a[k]>a[j])
                {
                    //如果有最小的就去拿最小的向后继续遍历比较
                    k = j;
                }
            }
            swap(a[i], a[k]);
        }
    }
  49. 插入排序的思路是将序列分为已排序和未排序两个部分,每次将未排序部分的第一个元素在已排序部分中找到一个位置,使得将该元素插入到该位置时已排序部分仍然有序。

    将第一个元素标记为已排序

    对于每一个未排序的元素 X

      “提取” 元素 X

      i = 最后排序过元素的索引 到 0 的遍历

        如果当前元素 j > X

          将排序过的元素向右移一格

        跳出循环并在此插入 X


    void insertarr()
    {
        for (int i = 0; i < n; i++)
        {//外层循环是挑选比较元素的过程
            //选择比较的元素拿出来
            maxmin=a[i];
            j=i;
            //j-1>0保证前面还有数据推
            //同时j是准备分类的元素的下标,取出来那个位置就空了,j-1就是空的前一位已分类的最后一位,在循环结束时要确保未分类的最后一位是最大的
           while (j-1>=0&&a[j-1]>maxmin)
           {//内层循环是将外层挑选元素取出来比较的过程,从i比到已排序的数组头
            //如果被比较的元素比拿出来的元素值小,将被比较的元素右移,
            //j--逐一一直比到b[j-1]不存在即j-1<0
            a[j]=a[j-1];
            j--;
           }
           //将的拿出来的元素放到计算出的位置j
            a[j]=maxmin;        
        }
    }
  50. sort 函数(#include <algorithm>)的主要功能是对指定范围内的元素进行排序,使其按照特定的顺序排列。默认情况下,它会按照元素的升序进行排序,但通过提供自定义的比较函数,也可以实现降序排序或者根据其他规则进行排序。
    默认顺序排序,如需逆序则定义了一个自定义的比较函数 compare,该函数接受两个整数参数 ab,并返回 a > b 的结果。在调用 std::sort 函数时,我们将这个自定义比较函数作为第三个参数传递给 sort 函数,从而实现了降序排序。
    #include <iostream>
    #include <algorithm>
    #include <vector>
    
    // 自定义比较函数,实现降序排序
    bool compare(int a, int b) {
        return a > b;
    }
    
    int main() {
        std::vector<int> vec = {5, 2, 8, 1, 9};
        // 使用自定义比较函数进行排序
        std::sort(vec.begin(), vec.end(), compare);
        // 输出排序后的结果
        for (int num : vec) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
        return 0;
    }

    sort函数主要是快排,选择混合排序,不稳定,可以使用stable_sort排序
    sort(arr, arr + n) 中,arr 是数组的起始地址,arr + n 是数组结束位置的下一个地址

    这个函数可以对数组或容器中的元素进行排序,字符按照字典序,默认情况下是按字典序(lexicographical order)从小到大排序。

  51.  定义sort中较为复杂的比较规则
    int comparesc1(Student x, Student y)
    {
        if (x.chscore == y.chscore)
        {
            //按姓名升序排列
            return x.name < y.name;
        }
        else
        {
            //按分数降序排列
            return x.chscore > y.chscore;
        }
    }
  52. 构造函数的名字必须和结构体名一致,且没有返回类型。还要记得定义一个默认构造函数

     

    struct Student
    {
        string name;
        int chscore;
        int mascore;
        int totalscore;
        Student(string str, int sc1, int sc2)
        {
            name = str;
            chscore = sc1;
            mascore = sc2;
            totalscore = sc1 + sc2;
        }
    };
  53.  调用构造函数的时机导致初始化错误,导致输出的总分异常

    struct Student
    {
        string name;
        int chscore;
        int mascore;
        int totalscore;
        //默认构造函数
        Student(){
            totalscore=0;
        }
        Student(string str, int sc1, int sc2)
        {
            name = str;
            chscore = sc1;
            mascore = sc2;
            totalscore = sc1 + sc2;
        }
    };
    
    
        Student stu[n];
        for (int i = 0; i < n; i++)
        {
            //在循环里给默认构造函数赋值时,不会调用构造函数,所以导致总分一直没有赋值为空
            cin >> stu[i].name >> stu[i].chscore >> stu[i].mascore;
        }
       
    初始化stu数组时,如果在构造函数中处理总分,是不会相加的,因为调用的是默认的构造函数,此时调用的是默认构造函数,为了避免这种情况,在循环输入无法调用构造函数时应该重新给stu数组赋值或使用构造函数给数组赋值
    类似如下:
    Student stu[n];
        for (int i = 0; i < n; i++)
        {
            //在循环里给默认构造函数赋值时,不会调用构造函数,所以导致总分一直没有赋值为空
            cin >> stu[i].name >> stu[i].chscore >> stu[i].mascore;
            stu[i].totalscore=stu[i].chscore+stu[i].mascore;
        }

     

  54. 逻辑与运算符 && 有短路特性,也就是当 i > 0 这个条件为 false 时,就不会再计算后面的 a[i - 1] == a[i]。所以当 i0 时,i > 0false,就不会去访问 a[-1],从而避免了数组越界的问题。
     
    if (a[i - 1]==a[i]&&i - 1>0)
    在 C++ 里,逻辑与运算符 && 是从左到右进行计算的。当 i0 时,a[i - 1] 也就是 a[-1],这会访问数组越界的位置,进而引发未定义行为。即便后面的 i - 1 > 0 能判断出不应该进行比较,但由于前面已经发生了越界访问,错误已经产生。
  55. 逻辑与运算符 && 有短路特性,也就是当 i > 0 这个条件为 false 时,就不会再计算后面的 a[i - 1] == a[i]。所以当 i0 时,i > 0false,就不会去访问 a[-1],从而避免了数组越界的问题。
    if (a[i - 1]==a[i]&&i - 1>0)
    在 C++ 里,逻辑与运算符 && 是从左到右进行计算的。当 i0 时,a[i - 1] 也就是 a[-1],这会访问数组越界的位置,进而引发未定义行为。即便后面的 i - 1 > 0 能判断出不应该进行比较,但由于前面已经发生了越界访问,错误已经产生。
  56. 哈希映射(Hash Map),也称为散列映射,是一种重要的数据结构,它提供了一种高效的方式来存储和检索键值对。
  57. 哈希表(Hash Table)是一种重要的数据结构,它通过哈希函数来实现快速的数据插入、查找和删除操作。
    哈希表利用哈希函数将键(Key)映射到一个固定大小的数组索引上,这个数组被称为哈希表。当需要存储或查找一个键值对时,先通过哈希函数计算键对应的索引,然后在该索引位置进行操作。理想情况下,每个键都能被唯一地映射到一个索引,这样可以实现 \(O(1)\) 时间复杂度的插入、查找和删除操作。但在实际应用中,可能会出现多个键映射到同一个索引的情况,这被称为哈希冲突,需要通过一些方法(如链地址法、开放寻址法)来解决。
    C++ 标准库在 <unordered_map><unordered_set> 头文件中

    插入元素

    对于 std::unordered_map,可以使用 [] 运算符或 insert() 函数插入键值对;对于 std::unordered_set,可以使用 insert() 函数插入元素。

    插入元素

    对于 std::unordered_map,可以使用 [] 运算符或 insert() 函数插入键值对;对于 std::unordered_set,可以使用 insert() 函数插入元素。

    查找元素

    对于 std::unordered_map,可以使用 [] 运算符或 find() 函数查找键对应的值;对于 std::unordered_set,可以使用 find()count() 函数判断元素是否存在。
     

    删除元素

    可以使用 erase() 函数删除 std::unordered_mapstd::unordered_set 中的元素。




  58. return c - 'a'; 的作用是将小写字母字符 c 转换为一个 0 到 25 的整数,对应字母表中的位置(a→0,b→1,...,z→25)。其核心逻辑基于 ASCII 码的数值计算。
    核心思想是利用 ASCII 码的连续性,将字符的偏移量转换为整数索引。
  59. 将数组中的所有元素都设置为0:
    int hashTable[maxln] = {0};
    或者使用memset(hashtable,0,maxln)
  60. 从a-z遍历字母
    for (char c = 'a'; c <= 'z'; c++)
        {
            //如果该字符在hashtable中的值大于0,输出
            if (hashTable[getHashKey(c)])
            {
                cout << i  <<" "<<hashTable[getHashKey(c)]<<endl;
    
                /* code */
            }


  61. cin 在读取字符串时,会在遇到空白字符(如空格、制表符、换行符)时停止读取。例如,如果你输入 Hello Worldcin 只会将 Hello 存储到字符数组中,World 部分会留在输入缓冲区等待下一次读取。可以直接写入字符数组,cin>>str;
    在使用cin写入字符数组时,
    cin.get(ch) 逐个字符地读取输入并写入字符数组,直到读取到换行符 '\n' 或者数组已满(i < MAX_LENGTH - 1)。
     cin>>str;
        int n = strlen(str);
        for (int i = 0; i < n; i++)
        {
            hashTable[getHashKey(str[n])]++;
        }


  62. string 适合处理动态长度、需要进行复杂操作的字符串,而 char 数组在与 C 语言交互、对内存和性能有特殊要求或处理固定长度字符串时更有优势。
  63. 在 C++ 里,char 本质上是一种整数类型(有 signed charunsigned char 之分),它能够像整数一样参与算术运算作为数组索引使用。字符在计算机中是以 ASCII 码(或者其他字符编码)形式存储的,每个字符对应一个整数值。例如,字符 'a' 的 ASCII 码值是 97,'b' 的 ASCII 码值是 98 。
  64. 字符串到整数的映射:将每个字符串映射到一个唯一的整数。由于字符串由三个大写字母组成,我们可以将其视为一个26进制的数,并转换为十进制数。
    对于字符串s2s1s0,我们将其转换为整数的方式是:a2×26^2+a1×26+a0,其中ai是字符si减去'A'后的值。
    int getHashkey(char s[])
    {
        return (s[0] - 'A') * 26 * 26 + (s[1] - 'A') * 26 + (s[2] - 'A');
    }


  65. 函数接受一个字符数组作为参数。在 C 和 C++ 中,当把数组作为参数传递给函数时,实际上传递的是数组首元素的地址,也就是一个指针。所以 char s[] 这种写法等同于 char* s,它们是完全等价的。
    int getHashKey(char s[]) {
        return (s[0] - 'A') * 26 * 26 + (s[1] - 'A') * 26 + (s[2] - 'A');
    }
  66. 分治算法的核心思想是将一个规模较大、难以直接解决的问题,分解成若干个规模较小、相互独立且与原问题形式相同的子问题,然后逐个解决这些子问题,最后将子问题的解合并起来,得到原问题的解。这种思想体现了 “分而治之” 的策略,通过将大问题化简为小问题,降低问题的复杂度,使得问题更容易被解决。
    分治算法通常包含以下三个基本步骤:
    1. 分解(Divide):将原问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题。
    2. 解决(Conquer):若子问题规模较小而容易被解决则直接求解,否则递归地解各个子问题。
    3. 合并(Combine):将各个子问题的解合并为原问题的解。
  67. 递归函数通常包含两个部分:
    • 基准情况(Base Case):也称为终止条件,是递归函数停止调用自身的条件。如果没有基准情况,递归函数会无限调用自身,最终导致栈溢出错误。
    • 递归情况(Recursive Case):在不满足基准情况时,函数会调用自身来解决一个规模更小的子问题。
      先写这个问题的最小规模的解,就是基准情况
      然后再从最大规模n开始看待,就是递归情况,不断调用
      如下代码:

  68. string类型的计算字符长度需要使用str.length()
    size() 和 length():返回 std::string 对象中字符的数量。
    substr(size_t pos = 0, size_t len = npos):返回 std::string 对象中从位置 pos 开始长度为 len 的子字符串。
    compare(const std::string& str):将 std::string 对象与另一个 std::string 对象进行字典序比较,根据结果返回负数、零或正数。
    在 C++ 中,std::string 类型是可以相加的。std::string 类对 + 和 += 运算符进行了重载,通过这些重载运算符,能够很方便地实现字符串的拼接操作。下面为你详细介绍相关内容。
  69. 动态规划(DP)是一种通过将复杂问题分解为更小的子问题来解决的技术。
    比如数塔问题:
    int getMax(int i, int j)
    {
        //i代表当前塔顶在哪一行,n表示塔的当前行数
        if (i == n)//如果是最下面一行,数塔只有一个元素
        {
            return a[n][j];//返回它本身
        }
        else//如果不是最后一行
        {
            //分别计算左下角节点和右下角节点作为塔顶时的结果,挑选最大的那个返回
            return max(getMax(i + 1, j), getMax(i + 1, j + 1))+a[i][j];
        }
    }
  70. string 类型可以使用 str[i] 这种形式来访问字符串中的字符
    bool isPalindrome(int i, int j) {
        if (i >= j) { // 边界条件
            return true;
        } else {
            return (s[i] == s[j]) && isPalindrome(i + 1, j - 1); // 递归调用
        }
    }
  71.  贪心算法(Greedy Algorithm)是一种在每一步选择中采取当前状态下最优决策,以期望最终得到全局最优解的算法策略。其核心思想是“局部最优,全局最优”,但并非所有问题都适用。
    活动选择问题
    • 问题描述:选择最多数量的互不重叠活动。

    • 贪心策略:每次选结束时间最早的活动。

    • 代码示例

       
      #include <algorithm>
      #include <vector>
      using namespace std;
      
      struct Activity {
          int start, end;
      };
      
      bool compare(Activity a1, Activity a2) {
          return a1.end < a2.end;
      }
      
      int maxActivities(vector<Activity> activities) {
          sort(activities.begin(), activities.end(), compare);
          int count = 1;
          int lastEnd = activities[0].end;
          
          for (int i = 1; i < activities.size(); i++) {
              if (activities[i].start >= lastEnd) {
                  count++;
                  lastEnd = activities[i].end;
              }
          }
          return count;
      }
     霍夫曼编码(Huffman Coding)
    • 问题描述:构造最优前缀编码以最小化总编码长度。

    • 贪心策略:合并频率最小的两个节点。

    • 代码示例

       
      // 优先队列(最小堆)存储节点
      priority_queue<Node*, vector<Node*>, Compare> minHeap;
      
      while (heap.size() > 1) {
          Node* left = heap.top(); heap.pop();
          Node* right = heap.top(); heap.pop();
          Node* merged = new Node('$', left->freq + right->freq);
          merged->left = left;
          merged->right = right;
          heap.push(merged);
      }

    最小生成树(Prim算法)
    • 问题描述:在连通图中找到边权和最小的生成树。

    • 贪心策略:每次选择与当前树连接的权重最小的边。

    • 代码示例

       
      void primMST(vector<vector<int>> graph) {
          int V = graph.size();
          vector<int> parent(V);  // 存储父节点
          vector<int> key(V, INT_MAX);  // 存储最小权重
          vector<bool> inMST(V, false);  // 标记是否加入MST
      
          key[0] = 0;
          parent[0] = -1;
      
          for (int count = 0; count < V - 1; count++) {
              int u = minKey(key, inMST);  // 找到未加入MST的最小key节点
              inMST[u] = true;
      
              for (int v = 0; v < V; v++) {
                  if (graph[u][v] && !inMST[v] && graph[u][v] < key[v]) {
                      parent[v] = u;
                      key[v] = graph[u][v];
                  }
              }
          }
          printMST(parent, graph);
      }

    贪心算法的优缺点

    优点 缺点
    实现简单,代码清晰 可能无法得到全局最优解
    时间复杂度通常较低(如O(n)) 需要严格证明正确性
    适用于实时或大规模问题 对问题特性要求严格

    贪心 vs 动态规划

    特性 贪心算法 动态规划
    决策性质 每步选择不可逆 保存子问题解,可能回溯
    时间复杂度 通常更低 通常较高(如O(n²))
    适用问题 具有贪心选择性质的问题 具有重叠子问题和最优子结构的问题
    典型问题 活动选择、霍夫曼编码 背包问题、最长公共子序列

    应用场景

    1. 调度问题:任务调度、会议安排。

    2. 图论问题:最短路径(Dijkstra)、最小生成树(Prim/Kruskal)。

    3. 数据压缩:霍夫曼编码。

    4. 组合优化:部分背包问题、找零钱问题(硬币无限)。


    贪心算法正确性证明方法

    1. 数学归纳法:证明每个步骤的贪心选择最终导向全局最优。

    2. 交换论证:假设存在更优解,通过交换步骤证明矛盾。

    3. 权重分析:如霍夫曼编码中,证明合并频率最低的节点最优。

  72. 在功能上,classstruct 几乎没有区别,都可以定义成员变量、成员函数、构造函数、析构函数、静态成员等,也都支持继承、多态等面向对象的特性。
    虽然 classstruct 存在上述区别,但在实际使用中,你可以根据具体的需求和设计理念来选择使用 class 还是 struct。如果需要严格的数据封装和隐藏,使用 class 更合适;如果只是简单的数据聚合,使用 struct 会更方便。
  73. vector<int> v(n, k); 是 ​构造函数语法,只能在定义vector时初始化。如果先声明空vector(vector<int> v;),后续再试图通过 v(n, k); 初始化,会被编译器误认为是调用名为 v 的成员函数,而vector类没有这样的成员函数。
  74. 迭代器的类型需严格匹配容器类型元素类型,但并非必须为 vector<int>。灵活使用 auto 或 const_iterator 可简化代码并增强安全性。
    vector<string> v_str;
    for (auto it = v_str.begin(); it != v_str.end(); ++it) {
        cout << *it << endl;  // 正确:it 类型为 vector<string>::iterator
    }
  75. vector不能直接通过下标运算符 v[i] 进行赋值操作 ​除非已经提前分配了足够的空间。需要先初始化v(n,x);
  76. C++中的vector类重载了比较运算符,可以直接使用<>等运算符进行比较。。在C++中,向量的比较是基于字典序的,即从第一个元素开始逐个比较,直到找到不相等的元素为止。如果所有元素都相等,但v1的长度小于v2,则v1 < v2也为真。
    1. 二维vector本质上是“容器的容器”,即外层vector的每个元素本身是一个vector<int>
      当初始化外层vector时,需要指定每个元素的初始值。在二维vector初始化中,内层的vector<int>()vector<int>(cols)是匿名对象,用于定义外层vector每个元素的初始化规则。这种写法符合C++语法,且能明确传递构造逻辑
      vector<vector<int>>vs(n,vector<int>());
      1. 当声明vector<vector<int>> vec(5, vector<int>(3))时:

        • 外层vector的元素类型是vector<int>(即每个元素本身是一个一维vector
        • vector<int>(3)匿名对象,用于定义内层vector的默认构造方式(长度为3,元素默认初始化为0)
        • 外层vector会复制这个匿名对象5次,形成5行3列的二维结构
           
        2.匿名对象vector<int>()在初始化时仅作为模板传递给外层容器,外层容器会根据模板批量生成内层vector实例。这些实例在内存中通过外层vector的索引(如vec[0]vec[1])访问,无需单独命名
       
  77.  [] 的优先级高于 *,因此等价于 *(iw[i]) = temp;
  78. set是一个有序集合,它内部实现了红黑树(一种平衡二叉搜索树),因此插入元素时会自动排序,并且每个元素都是唯一的,不会重复。
  79. set 的迭代器是 ​双向迭代器,不支持 + 或 - 操作(只有随机访问迭代器如 vector 允许此操作)。set基于平衡二叉搜索树(如红黑树),内存地址并未连续,无法+操作下一个地址,但是支持++代表访问下一个节点
  80. 所有标准容器的遍历均以 it != s.end() 为终止条件,确保安全访问所有有效元素。s.end()​ 指向容器最后一个元素的下一个位置**,而不是最后一个元素本身。
  81. s.begin() 确实指向容器的第一个元素,而 s.end() 指向最后一个元素的下一个位置。这种设计遵循 C++ 标准库的半开区间(Half-Open Range)​规范
    元素布局: [1] → [2] → [3] → [哨兵]
    迭代器:   ↑            ↑        ↑
             begin()      end()-1   end()
  82. find 方法的作用
    ​功能:在 set 中查找指定键值(key)的元素。
    ​返回值:
    如果找到该键值,返回一个迭代器,指向找到的元素。
    如果未找到,返回 set::end()(即指向容器末尾之后的位置的迭代器)。
  83. std::set 没有 push_back
    因为其元素位置由排序规则自动管理。
    正确方法:使用 insert 或 emplace 插入元素。
    适用场景:当需要维护有序且唯一的集合时,优先选择 std::set;若需要手动控制插入位置,应使用序列容器(如 std::vectorstd::list)。
  84. set 的 erase 方法不仅支持通过值删除元素,还支持通过迭代器删除。这是 std::set 的灵活性设计之一。以下是详细说明:
    用法 功能 时间复杂度
    erase(iterator pos) 删除迭代器指向的元素 O(1)
    erase(const key_type& key) 删除指定值的元素(若存在) O(log n)
    erase(iterator first, iterator last) 删除迭代器范围 [first, last) 内的所有元素 O(k)(k为删除元素的数量)




  85. 使用s.clear()函数清空s
  86. 使用s.size()函数输出s中当前的元素个数。
  87. map是一个关联容器,它存储键值对,并根据键自动排序。
    容器 数据结构 有序性 键的要求
    std::map 红黑树 键按升序排列 必须支持 < 运算符(或自定义比较器)
    std::unordered_map 哈希表 无序 必须支持哈希函数(std::hash)和 == 运算符
  88. 使用迭代器遍历map。迭代器itmp.begin()开始,到mp.end()结束。在每次循环中,使用it -> firstit -> second分别访问键和值,并输出它们。
  89. std::mapfind()函数用于查找容器中是否存在指定的键,并返回指向该键值对的迭代器。以下是详细介绍:

    1. 函数原型

    iterator find(const Key& key);          // 查找键为key的元素
    const_iterator find(const Key& key) const;  // const版本

    2. 参数

    • key:要查找的键,类型必须与map的键类型匹配。

    3. 返回值

    • 找到键:返回指向该键值对的迭代器(类型为std::map<Key, Value>::iterator)。
    • 未找到键:返回map.end()
  90. queue 容器简介

    std::queue​ 是 C++ 标准库中的一种 ​容器适配器​(Container Adapter),遵循 ​先进先出(FIFO)​​ 原则。它基于其他底层容器(如 deque 或 list)实现,提供受限的接口以强制 FIFO 操作。
    它允许我们在一端(队尾)添加元素,在另一端(队首)移除元素。

    核心操作

    • ​**push()**:在队尾插入元素。
    • ​**pop()**:移除队首元素(不返回元素值)。
    • ​**front()**:访问队首元素。
    • ​**back()**:访问队尾元素。
    • ​**empty()**:判断队列是否为空。
    • ​**size()**:返回队列元素数量。
  91. 优先队列(priority_queue)。优先队列是一种特殊的队列,其中每个元素都有一个优先级,优先级最高的元素总是位于队列的前端。
    priority_queue默认使用vector作为底层存储

    优先队列(如 C++ 中的 priority_queue)是 ​单端队列,其操作特性如下:

    1. 单端操作

    • 插入(push)​:元素从队列的“尾部”插入(底层容器的尾部,如 vector::push_back)。
    • 删除(pop)​只能从队列的头部删除元素​(即移除优先级最高的元素,如堆顶)。
    • 访问(top)​只能访问队列头部的元素​(即堆顶元素)。
  92. 类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。
    成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义。
    double Box::getVolume(void)
    {
        return length * breadth * height;
    }
  93. 优先队列是单端的:仅支持从头部删除元素(优先级最高),尾部插入元素。
    双端队列是线性的:支持两端操作,但无法按优先级动态排序。
    双端优先队列需自定义实现:若需同时操作最大/最小值,需结合堆或平衡树。
  94. 如何定义重载函数

    f1.price > f2.price 时认为 f1 < f2 成立,具体实现如下:

    struct Fruit {
        int price;
        // 其他成员变量...
    
        // 重载 < 操作符
        bool operator<(const Fruit &x) const {
            return this->price > x.price; // 当当前对象的 price > x 的 price 时返回 true
        }
    };

    关键点解释

    1. 函数签名

      • operator<:表示重载的是 < 操作符。
      • const Fruit &x:传入的另一个对象,类型为 const 引用。
      • 末尾的 const:保证该函数不会修改调用对象(*this)的成员变量
         
    2. 比较逻辑

      • 当 this->price > x.price 时返回 true,即 f1 < f2 成立的条件是 f1.price > f2.price

  95. priority_queue 默认是最大堆,内部通过 std::less<T> 比较元素(即默认使用 operator< 判断元素的优先级,)。如果需要自定义类型的排序方式,重载该类的<运算符即可
    bool operator<(const Fruit &other) const
        {
            return this->price > other.price;
        }
  96. C++标准库中的pair对象支持直接比较操作。比较的规则是先比较第一个元素,如果第一个元素相等,则比较第二个元素。根据比较结果,输出相应的字符串。
  97. 全排列是指将n个不同的元素按照所有可能的顺序进行排列。为了实现这一目标,我们可以利用C++标准库中的next_permutation函数,该函数能够生成给定序列的下一个排列。

    序列需可修改:操作范围必须是可变的双向迭代器(如数组或 vector)。
    next_permutation 按字典序重新排列给定范围内的元素,生成当前序列的下一个更大的排列。若当前排列已是最大(降序),则返回 false 并将序列调整为最小排列(升序)

     
    int a[] = {1, 2, 3};
    do {
        // 输出所有排列
    } while (next_permutation(a, a+3));
    输出结果为所有6种排列组合,如 1 2 3 → 1 3 2 → ... → 3 2 1
     

    反向函数 prev_permutation
    与 next_permutation 对应,prev_permutation 生成上一个更小的排列,适用于降序字典序操作

     

     

  98. 使用fill(a, a + n, k)函数,将数组a从开始位置到第n个位置的元素全部设置为k。这里的a是数组的起始地址,a + n是数组的第n个元素的地址。

  99. 一维数组的字符名代表第一个元素的指针,二维数组的字符名代表第一行整行的指针,

    若需获取首元素地址,需通过以下方式:

    • a[0]&a[0][0](类型为int*)。
    • *a解引用后得到首行首元素的地址.
       
  100. sort()函数也可以排序字符穿,需要起始地址与结束地址
  101. lower_bound 是 C++ 标准库中用于在有序序列中高效查找元素的算法函数,其核心功能是返回第一个不小于​(即大于或等于)目标值的元素位置。
  102. 定义指针变量时必须加*,*在定义指针变量时是类型标识符,用于表明这是一个指针类型。

    操作地址时无需加*,对指针进行地址运算或赋值时,直接使用指针变量名(不带*),因为此时操作的是地址本身

    解引用时必须加*,*在非定义场景下是解引用运算符,用于访问指针指向的数据,解引用的本质
    *p等价于访问指针变量p保存的地址对应的内存单元

  103. getline(cin, s) 的返回值是 cin,相当于再写一个 cin >> c
    在需要处理完整行输入、包含空格或混合输入场景下,getline是更安全、健壮的选择,而cin仅适用于简单、无空格的输入。
  104. 在C++程序中,键盘输入内容进入缓冲区的条件与流的工作机制密切相关。以下是具体场景的总结:


    一、用户按下回车键时触发行缓冲

     

    键盘输入属于标准输入流(std::cin),默认采用行缓冲模式。这意味着:

    1. 用户在键盘输入数据时,字符会暂时存储在输入缓冲区中,但不会立即提交给程序
    2. 只有当用户按下回车键(换行符 \n)​时,输入缓冲区中的当前行数据才会被刷新,并传递给程序处理。
      例如:
     
    int num;
    std::cin >> num;  // 输入 "123\n",回车后缓冲区内容被读取

    二、缓冲区未满时的暂存

     

    输入缓冲区有固定容量(通常为4096或8192字节),在以下情况下数据会暂存缓冲区:

    1. 未达到缓冲区容量上限:输入的数据量不足以填满缓冲区,且用户未按回车键。
    2. 程序未主动读取:即使缓冲区中有数据,如果程序未调用std::cin或相关函数读取,数据仍保留在缓冲区中。

    三、全缓冲模式下的数据累积

     

    若通过std::cin.rdbuf()自定义缓冲区为全缓冲模式:

    1. 数据会持续累积到缓冲区中,​直到缓冲区填满才会触发刷新。
    2. 例如:
     
    std::string buffer(1024, '\0');
    std::cin.rdbuf(buffer.data());  // 设置自定义全缓冲区

    四、程序主动刷新缓冲区

     

    以下操作会强制刷新输入缓冲区,使数据进入程序:

    1. 调用std::cin.get()std::getline():这些函数会直接读取缓冲区内容(包括未按回车的情况)。
    2. 使用std::cin.ignore()std::cin.clear():手动清空或重置缓冲区状态。
    3. 程序终止或流关闭:缓冲区内容自动刷新。

    五、特殊场景:无缓冲模式

     

    若将输入流设置为无缓冲模式(如std::cin.sync_with_stdio(false)):

    1. 键盘输入的字符会立即传递给程序,无需等待回车或缓冲区填满。
    2. 此模式通常用于实时交互场景,但会牺牲性能。

    总结

    场景 触发条件 典型示例
    行缓冲(默认) 用户按下回车键 std::cin读取整行输入
    全缓冲(自定义) 缓冲区填满 文件输入流或自定义缓冲区
    程序主动刷新 调用读取函数或清空操作 std::cin.ignore(1000, '\n')
    无缓冲模式 手动设置同步禁用 std::cin.sync_with_stdio(false)

    键盘输入通常采用行缓冲,理解缓冲区机制可避免常见问题(如残留换行符干扰后续输入)。实际开发中,建议优先使用std::getline逐行读取并手动处理解析逻辑

     
  105. size_t是C++中处理内存相关操作的核心类型,其无符号特性和平台自适应性使其成为表示大小、索引的首选。在涉及内存分配、容器操作或跨平台开发时,优先使用size_t而非int可显著减少潜在错误。size_t是一种无符号整数类型,专门用于表示对象大小、内存容量、数组索引等与内存空间相关的非负整数值
  106. v1.end() 指向容器最后一个元素的下一个位置​(尾后迭代器),直接解引用 *it 会导致未定义行为​(如程序崩溃或输出乱码)。
    1. 正向迭代器的定义与功能
      迭代器是一种泛化的指针,用于顺序访问容器中的元素,隐藏了不同容器的底层实现差异(如数组、链表等)

       

      。通过统一的接口(如 ++* 操作符),迭代器实现了对容器的遍历、修改和算法操作。

    2. ​**begin() 和 end() 的指向**

      • ​**begin():指向容器的第一个有效元素**。例如,对于 vector<int> vec = {1,2,3}vec.begin() 指向元素 1
         
      • ​**end():指向容器最后一个元素的下一个位置​(称为“尾后迭代器”),​不可解引用**。例如,vec.end() 指向元素 3 之后的位置
         
      • 特殊场景:若容器为空,begin() == end(),均指向尾后位置
         
  107. 反向迭代器的定义与功能
    反向迭代器(reverse_iterator)用于逆序遍历容器,其操作方向与正向迭代器相反。例如,++ 操作符实际使迭代器向前移动(指向更早的元素)

     

    rbegin() 和 rend() 的指向

    • ​**rbegin():指向容器的最后一个有效元素。例如,vec.rbegin() 指向元素 3
       
    • ​**rend():指向容器第一个元素的前一个位置​(称为“首前迭代器”),​不可解引用**。例如,vec.rend() 指向元素 1 之前的位置
       
  108. //倒叙遍历字符串,string的end()是换行,所以-1
       for (auto it = s16.end()-1; it!=s16.begin()-1; it--)
  109. 随机访问迭代器,它允许以常数时间复杂度直接跳转到容器中的任意位置,并支持类似指针的算术运算。

    迭代器的随机访问是C++ STL中迭代器的最高级功能类别,它允许以常数时间复杂度直接跳转到容器中的任意位置,并支持类似指针的算术运算。以下是其核心特性与实现原理的详细说明:

    一、随机访问的定义与核心功能

    1. 直接定位任意元素
      随机访问迭代器支持通过算术运算符(如 it + nit - n)直接跳转到目标位置,无需逐一遍历。例如:

      std::vector<int> vec = {10, 20, 30, 40};
      auto it = vec.begin() + 2;  // 直接定位到第三个元素(30)

      这种能力依赖于容器底层连续内存的特性(如 vectordeque

       
    2. 算术运算与关系比较
      支持加减整数(+=-=)、比较运算符(<><=>=)和下标访问(it[n])。例如:

      if (it1 < it2) { /* 比较位置关系 */ }
      int val = it[3];  // 直接访问第4个元素

      这使得算法(如 std::sort)能高效操作容器元素

       
    3. 时间复杂度与性能优势
      所有随机访问操作的时间复杂度均为 ​O(1),适用于大数据量场景(如排序、二分查找)。例如,std::sort 算法依赖随机访问迭代器实现快速元素交换

       

    二、与其他迭代器的区别

    迭代器类型 支持操作 典型容器
    随机访问迭代器 所有双向迭代器操作 + 算术运算、比较、下标访问 vectordeque、数组
    双向迭代器 支持前后移动(++--),但不支持跳跃访问 listmapset
    前向迭代器 仅支持单向移动(++),不可后退 forward_list
    输入/输出迭代器 仅支持读取或写入操作,且只能单向遍历一次 流迭代器(如 istream_iterator

    示例对比

    • 双向迭代器需通过循环访问第n个元素:
       
      std::list<int> lst = {1, 2, 3, 4};
      auto it = lst.begin();
      std::advance(it, 2);  // 需遍历两次,时间复杂度O(n)
    • 随机访问迭代器直接跳转:
      std::vector<int> vec = {1, 2, 3, 4};
      auto it = vec.begin() + 2;  // 直接定位,时间复杂度O(1)

    三、适用场景与容器支持

    1. 高频随机访问操作
      如快速排序、二分查找、矩阵运算等算法,依赖随机访问迭代器实现高效元素定位

       
    2. 支持的容器类型

      • 连续内存容器vectordequestring、原生数组
         
      • 不支持的容器list(双向迭代器)、map/set(双向迭代器)、forward_list(前向迭代器)
         
    3. 扩展应用
      通过 std::nextstd::prev 等适配器函数,可为非随机访问容器(如 map)模拟随机访问行为,但性能仍为O(n)

       
      std::map<int, int> m;
      auto it = std::next(m.begin(), 3);  // 遍历三次,时间复杂度O(n)

    四、注意事项

    1. 迭代器失效
      在 vector 中插入/删除元素可能导致所有迭代器失效(因内存重分配)

       
    2. 性能权衡
      随机访问迭代器虽高效,但要求容器内存连续;非连续结构(如链表)需权衡功能与存储灵活性

       
  110. 删除链表节点

    在数组模拟链表的场景中,通过 nodes4[last].next = nodes4[i].next; 实现节点删除需要精准定位前驱节点正确调整指针。以下是具体实现逻辑与注意事项:

    一、删除节点的核心逻辑

    1. ​前驱节点的定位

    • 关键变量last 表示当前节点 i 的前驱节点,nodes4[i].next 是当前节点的后继节点。
    • 操作步骤
      // 遍历链表时维护前驱节点 last
      int last = -1; // 初始化为无效值
      for (int i = first; i != -1; last = i, i = nodes4[i].next) {
          if (nodes4[i].data == k) { // 找到要删除的节点
              if (last == -1) { // 删除头节点
                  first = nodes4[i].next; // 头指针指向下一个节点
              } else { // 删除中间或尾节点
                  nodes4[last].next = nodes4[i].next; // 前驱节点跳过当前节点[2,5](@ref)
              }
          }
      }
    • 作用:通过 last 记录前驱节点,直接修改其 next 指针以跳过当前节点 i
  111. 对链表的操作,头插法,删除元素,最好使用一个while循环使用两个指针进行修改,current当前执政与last上一个指针
    链表节点的删除本质上是 ​​“逻辑断链”​,即通过调整指针的指向,将被删除节点从链表的逻辑连接中移除。该操作不直接销毁物理存储的节点数据,但后续遍历时无法再访问到该节点
  112. 在C++中,std::reverse是一个常用的算法函数,用于反转序列容器或数组中元素的顺序。以下是其核心用法和特性的综合介绍:

    1. ​基本用法
    ​头文件:需包含 <algorithm>。
    ​函数原型:
    template <class BidirectionalIterator>
    void reverse(BidirectionalIterator first, BidirectionalIterator last);
    参数 first 和 last 表示反转范围的起始和结束迭代器,范围为 ​左闭右开区间 [first, last)。
    ​作用:将指定区间内的元素顺序完全颠倒,如 [1,2,3,4] 反转为 [4,3,2,1]。

  113. 滑窗算法


     

  114. 正则表达式<regex>
    regex_search
    https://zhuanlan.zhihu.com/p/562283567
    普通字符
    直接匹配字符本身,如 a 匹配字母 "a",123 匹配字符串 "123"。

    元字符(特殊符号)
    具有特殊含义的符号,需转义(如 \* 匹配字符 "*"):

    .:匹配任意单个字符(除换行符)。
    ^ 和 $:分别匹配字符串的开头和结尾。
    |:逻辑“或”,如 a|b 匹配 "a" 或 "b"。

    ​预定义字符类
    简化常见字符集的匹配:

    \d:匹配数字(等价于 [0-9])。
    \w:匹配字母、数字或下划线(等价于 [a-zA-Z0-9_])。
    \s:匹配空白符(如空格、换行符)。

    ​量词(限定符)

    控制字符重复次数:

    *:匹配前一个字符 0 次或多次(贪婪匹配)。
    +:匹配前一个字符 1 次或多次。
    ?:匹配前一个字符 0 次或 1 次。
    {n,m}:匹配前一个字符至少 n 次,至多 m 次。
    ​分组与捕获

    ():将部分模式分组,如 (ab)+ 匹配 "abab" 等。
    (?:):非捕获分组,仅分组不保存匹配结果。

  115. 在代码中,字符串内的反斜杠 \ 需要根据语言规则转义,否则会被解释为普通字符。
    std::regex pattern("\\d+");
  116. 在C++中,函数参数传递有两种主要方式:传值(by value)和传引用(by reference)。传值会创建参数的副本,而传引用则直接使用原始变量,不会复制。
    int minTotalDifference(int n, int d, vector& skills)这里,vector& skills中的&符号表示skills是一个引用参数。这意味着函数内部对skills的任何修改都会影响到传入的原始vector,而不是操作它的副本。如果没有&符号,参数会通过传值方式传递,函数内部处理的是原vector的一个副本,修改不会影响原数据。引用参数可以直接使用原始数据的引用,​不拷贝数组,适合传递大型对象(如 vector、string 等)。如果参数写成 vector<int> skills(不带 &),调用函数时会拷贝整个 skills 数组。如果数组很大,会浪费时间和内存。

  117. 在代码 vector<vector<int>> dp(n+1, vector<int>(n+1, INT_MAX)) 中,INT_MAX 的作用是为二维动态规划数组 dp 的所有元素设置初始值为最大可能整数

posted @ 2025-01-25 20:05  HY10-SALTEST-FISH  阅读(25)  评论(0)    收藏  举报