C++ 学习笔记(1):STL、Vector 与 Set

背景

最近在尝试入坑蓝桥杯,于是先从 C++ 开始学起,这里记个笔记。这里我的笔记是跟着这个教程来的。沙比学校天天整些屁事都没什么空折腾。

前言

笔者是 JS / TS 写的比较多,以前写过 C 但是有点忘了,所以文章里都是和 JS 进行对比着方便快速理解。

同时其实我还有几个小问题,嘻嘻。没有解答,就是写文章的时候提醒一下自己要全面一点,复习哈。

  • 指针的 *& 是什么用?指针的本质是?
  • 和 JS 比有没有类似的,异同点是?
  • C++ 和 C 有什么区别?
  • 本质上迭代器和数组索引遍历是类似的,但是为什么要单独抽象一个迭代器出来?

复习

指针

  • 声明指针:int *p; 表示一个指向 int 类型的指针。
  • 取地址:&x 获取变量 x 的内存地址。
  • 解引用:*p 访问指针 p 指向的内存地址上的数据。
#include <iostream>
using namespace std;

int main() {
    int x = 10;
    int *p = &x; // p 存储的是 x 的地址
    cout << "x 的值是: " << x << endl;  // 输出 10
    cout << "p 指向的值是: " << *p << endl; // 解引用,输出 10
    return 0;
}

其实就是内存和值互相指来指去的,JS 里面没有可以类比的,要说比较像的就是对象赋值,举个例子:

let obj = { value: 10 };
let ref = obj; // ref 是 obj 的引用
ref.value = 20;
console.log(obj.value); // 输出 20

JS 里面如果要避免这种情况只能深拷贝了~ 因为默认的赋值方式(如 let ref = obj)只是将对象的引用赋值给新的变量,并没有创建新的独立对象。

STL

概念

STL 是一个 C++ 的工具库,分为三大组件,分别是容器、算法、迭代器。

容器

常见容器类型有三种,第一种是序列式容器(即元素按顺序存储,支持高效的插入和删除)。常见的有这几个:vector,动态数组,支持随机访问;deque,双端队列,支持高效的两端操作;list,双向链表,支持高效的任意位置插入和删除。

第二种是关联式容器(基于键值对存储,元素通常自动排序)。常见的有这几个:set,集合,不允许重复元素;map,键值对映射,每个键唯一;multisetmultimap,允许重复键。

最后一种是无序容器(基于哈希表存储,元素无序)。常见的有unordered_set,无序集合;unordered_map,无序映射。

其实换句话说 JS 基本都有类似功能的方法/库,比如说 Map 和 Set 都是自带的,JS 里面直接实例化后即可使用。虽然实现方式还是有差异,STL 的 Map 和 Set 底层通常用平衡二叉树实现,默认是有序的;而 JS 是基于 HashMap 实现的。

另外 C++ 还存在 auto 这个类型,即编译器自动推断变量类型,被叫做“类型推导关键字”,意思是让编译器推导变量的具体类型。如果你是 JS 或者 Python 写的比较多的应该马上觉得很熟悉了,这不是动态语言么?

虽然差距很大,C++ 的 auto 只是编译器编译时推到,执行后其实已经是静态了(auto x = 4编译完会变成int x = 4,某种程度上和#define接近);而 JS / Python 这类动态语言在 Runtime 的类型就是动态的。

算法

算法是用来操作容器的工具集,包括排序、搜索、变换等常见操作。STL 的算法库与任何容器兼容。

常见算法有很多,如下。

  • 排序类:sort()stable_sort()
  • 查找类:find()binary_search()
  • 变换类:transform()replace()
  • 集合操作:set_union()set_intersection()

算法使用迭代器(见下文)作为操作对象,不直接与容器绑定,因此非常灵活。

其实看到这也非常眼熟,就比如 sort() 这个在 JS 的 Array 对象里最常见的方法,原来这个在 C++ 这种老登(bushi)语言里被叫统称为算法啊!不过 STL 和 JS 的 sort() 还是有些许不同的。STL 的 sort() 是一个函数,可以对任意范围内的元素排序,不仅限于容器,比如数组、列表都可以用。

而 JS 的 sort() 仅仅只是数组的一个方法,只针对数组排序;STL 的 sort() 的实现底层使用了快速排序 + 插入排序,比 JS 的 sort() 更快。

迭代器

迭代器是一种指针类似的对象,用于在容器中遍历元素。迭代器分类如下:

  • 输入迭代器:只读访问,例如 std::istream_iterator
  • 输出迭代器:只写访问,例如 std::ostream_iterator
  • 前向迭代器:支持单向遍历,例如 std::forward_list 的迭代器。
  • 双向迭代器:支持前后遍历,例如 std::list 的迭代器。
  • 随机访问迭代器:支持随机访问,例如 std::vector 的迭代器。

一般常见的就两种,一种是拿 begin() 和 end() 用来返回容器的起点和终点迭代器,另外一种是用 *it 访问迭代器指向的值,++it 进行移动。

迭代器可以理解为一种“指针”,它用来在容器中遍历元素,其实这不就是遍历数组里面的值啊!但为什么弄这么复杂?因为 STL 容器不能像数组索引那样直接遍历,STL 容器都是各种各样的数据结构,不像数组那样连续,所以只能使用指针来遍历里面的值。

深入一点来说,数组在内存中的数据是连续的,你通过数组索引访问的时候,其实就是arr[i] = *(arr + i),arr 是这个数组的基准地址,i 就是往后推几位,数组索引本质上就是最开始的地址 + 索引数;而 STL 本身就是高层抽象的东西,STL 容器包含了很多种数据结构,每种数据结构的访问方式都是不一样的,所以 STL 库把迭代器给抽象了出来,最终作为一种库给开发人员调用。

迭代器其实就是一种抽象。它的实现完全依赖于容器的底层数据结构。如果容器是连续的(如 vector),迭代器可以直接封装指针操作;如果容器是非连续的(如 list、set),迭代器内部会包含指向当前元素的指针,同时封装了访问下一个/上一个元素的逻辑。对于复杂的数据结构(如红黑树),迭代器会依赖树的中序遍历算法来实现移动。

另外,STL 中的迭代器不仅仅是用来遍历的工具,还能通过它修改容器内容,甚至对任意范围的元素进行操作。

所以迭代器其实就是面向容器的“智能索引”,它不是简单的指针,而是高度抽象和适配不同数据结构的工具。

Vector

Vector 是 STL 容器的一种,全称叫可变数组,但如果你熟悉 JS 你就会发现这不就是里面的数组吗。STL 里面的 vector 的特性和 JS 里面很像哦,支持动态扩容(数组大小可以自动调整)、连续内存存储(可以高效地进行随机访问)、高效尾部操作(尾部插入和删除操作非常快)。感觉这里没啥好说的就纯看语法写写就好了,放一段 Example。

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

int main() {
    vector<int> v = {3, 1, 4, 1, 5};
    v.push_back(9); // 添加元素 9
    sort(v.begin(), v.end()); // 排序

    cout << "排序后的元素: ";
    for (auto num : v) {
        cout << num << " ";
    }
    cout << endl;

    v.pop_back(); // 删除最后一个元素
    cout << "大小: " << v.size() << endl;

    return 0;
}

问了下 AI,vector 一般在算法里有啥地方用得到?他说这三个:

  • 在需要动态增长的数组时使用,例如存储所有输入数据。
  • 排序和去重,常用 sort + unique 去重。
vector<int> v = {1, 2, 2, 3, 3, 4};
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
  • 二分查找,用 lower_bound 和 upper_bound 高效查找元素位置。
vector<int> v = {1, 2, 4, 4, 5};
int pos = lower_bound(v.begin(), v.end(), 4) - v.begin(); // 找到第一个 >= 4 的位置

然后这个是语法:

操作 说明 示例代码
vector<T> v; 定义一个存储类型为 T 的动态数组 vector<int> v;
v.push_back(x); 在尾部添加元素 x v.push_back(10);
v.pop_back(); 删除尾部元素 v.pop_back();
v.size(); 获取数组大小 int n = v.size();
v[i] / v.at(i) 访问第 i 个元素(at 带边界检查) cout << v[0];
v.clear(); 清空所有元素 v.clear();
v.begin() / v.end() 返回首迭代器和尾迭代器,用于遍历 for (auto it = v.begin(); it != v.end(); ++it)
v.insert(pos, x); 在迭代器位置 pos 插入元素 x v.insert(v.begin(), 5);
v.erase(pos); 删除迭代器位置 pos 的元素 v.erase(v.begin());
sort(v.begin(), v.end()) v 进行升序排序 sort(v.begin(), v.end());

Set

set 是 STL 中的 集合容器,JS 里也有,特性一样:存储唯一值(不允许重复元素)、自动排序(默认以升序存储)、基于红黑树实现(插入、删除、查找的时间复杂度为 O(log n))。还是丢一段 Example。

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

int main() {
    set<int> s = {10, 20, 10, 30}; // 自动去重和排序
    s.insert(40); // 插入元素 40
    s.erase(20);  // 删除元素 20

    cout << "集合中的元素: ";
    for (auto num : s) {
        cout << num << " ";
    }
    cout << endl;

    if (s.count(30)) {
        cout << "30 存在于集合中" << endl;
    }

    return 0;
}

Set 常用方法有这几个:

  • 高效查找和去重,集合自动去重,插入后元素总是唯一。
  • 范围查找,用迭代器找到范围内的值。
auto it = s.lower_bound(15); // 找到第一个 >= 15 的元素
  • 动态集合,用于实时存储和查询,如滑动窗口。

这个是语法:

操作 说明 示例代码
set<T> s; 定义一个存储类型为 T 的集合 set<int> s;
s.insert(x); 插入元素 x,如果已存在则插入失败 s.insert(10);
s.erase(x); 删除值为 x 的元素 s.erase(10);
s.count(x); 检查是否存在值为 x 的元素(返回 1 或 0) if (s.count(5))
s.find(x); 返回指向值为 x 的迭代器(找不到返回 s.end() auto it = s.find(10);
s.size(); 获取集合大小 int n = s.size();
s.begin() / s.end() 返回首迭代器和尾迭代器,用于遍历 for (auto it = s.begin(); it != s.end(); ++it)
s.clear(); 清空集合中的所有元素 s.clear();
s.lower_bound(x); 返回指向第一个 >= x 的迭代器 auto it = s.lower_bound(15);
s.upper_bound(x); 返回指向第一个 > x 的迭代器 auto it = s.upper_bound(20);
posted @ 2024-11-19 15:16  AurLemon  阅读(2)  评论(0编辑  收藏  举报