set及multiset使用问题总结

一、multiset 的删除操作

维护一个允许有重复元素的集合时可以使用multiset<int> ms;
假设当前ms里有三个值为5元素只删除其中一个5,保留剩下的两个5。

此时不能直接ms.erase(5); 这样会删除ms里所有值为5的元素。

可以如下操作:

auto it = ms.find(5);
if (it != ms.end()) ms.erase(it);

二、set的二分查找复杂度

s.lower_bound(x)(成员函数)的时间复杂度 O(log⁡N)。

lower_bound(s.begin(), s.end(), x)(全局函数),它的时间复杂度是 O(N)。

为什么是 O(N)?
因为全局的 lower_bound 只要求容器提供“双向迭代器”。

为了让这个函数适用于所有容器(比如vector, deque),它的底层实现是经典的先计算区间的长度len,然后每次走len/2步。

但是,set的迭代器不能随机跳跃(不能 it + 5),只能一步步 ++ 或 - - 。这导致它每次走步数时都要线性移动迭代器,最终总移动步数是O(N)级别的。

所以在set和map上进行二分查找,必须使用成员函数 s.lower_bound();,如果在set 上用全局 lower_bound,在N很大时极易TLE。

三、求set中严格小于 x 的最大元素

auto it = s.lower_bound(x);//返回的是第一个大于等于x的元素

if (it != s.begin()) {//要保证x不是最小的元素,避免越界
    --it;
    cout << *it << endl;
}

四、set的迭代器失效问题

set<int> s = {1, 2, 3, 4, 5, 6};
for (auto it = s.begin(); it != s.end(); ++it) {
    if (*it % 2 == 0) {
        s.erase(it); 
    }
}

上述代码中就出现了这个问题,在 set 中执行 s.erase(it);后,迭代器 it 会失效。

set的底层是红黑树,erase(it) 会把当前节点从树中删除并销毁。此时,it 指向了一片被释放的内存。接下来在 for 循环中执行 ++it 时,程序会试图去读取这片被销毁节点的内存来寻找下一个节点,这属于未定义行为(UB)。

正确的边遍历边删除写法如下:

for (auto it = s.begin(); it != s.end(); ) {
    if (*it % 2 == 0) {
        it = s.erase(it); // erase会返回被删元素后面的迭代器,赋值给it,不必额外++
    } 
    else {
        it++; 
    }
}

五、重载 < 运算符

把“结构体”或者“自定义类”放进 set 里时需要用到,下面是结构体重载举例。

struct Point {
    int x, y;
    // 注意:这里的 const 必须加!
    bool operator<(const Point& o) const {
        if (x != o.x) return x < o.x;       // x 从小到大排
        return y > o.y;                     //x相等时,y 从大到小排
    }
};

//之后正常使用即可
set<Point> s;

为什么只要重载 <?
set 默认使用 std::less<>,它只认 <。它判断两个元素是否“相等”的逻辑是:!(a < b) && !(b < a)。所以你只需要提供 < 的规则即可,不用重载 ==。


函数后面的 const 为什么要加?
因为 set 在内部比较时,是通过 const 迭代器访问元素的,它保证不会修改你的元素。如果你的成员函数后面不加 const,编译器会报错,提示你找不到匹配的 const 函数。

六、set 的“相等”错误

我们知道set 有个特性叫“去重”,也就是相同的元素只能存进去一个。

但是,set 怎么判断两个自定义结构体是不是“相同”呢?

因为set 底层是一棵排序树,所以它不会用 == 来判断相不相同。它用来判读的是依据你重载的 < 运算符。上面有提过,它判断两个元素是否“相等”的逻辑是:!(a < b) && !(b < a),即如果 A < B 不成立,并且 B < A 也不成立,set 就认为 A 和 B 是同一个东西。

所以在如下的情况中,set 可能会对你输入的数据进行你不想要的去重:

struct Point {
    int x, y;
    bool operator<(const Point& o) const {
        return x < o.x; // 注意这里只比较了 x
    }
};

int main() {
    std::set<Point> s;
    s.insert({1, 2});
    s.insert({1, 5});
    s.insert({1, 8});
    
    std::cout << s.size() << std::endl;
    for (auto p : s) {
        std::cout << "(" << p.x << ", " << p.y << ")" << endl;
    }
    return 0;
}

在上述代码中,因为你的重载只依照了x的值,那么所有x值相同的元素都会被判断为相等,后面进来的全都会被 erase 掉,最终set里只会存在第一个放进去的{1,2}。

所以用 set 存结构体时,重载的 < 里必须包含所有你觉得“能区分两个不同元素”的关键字。比如在上面的示例中,如果两个点只要任一坐标(x或y)不同就是不同的点,那 x 和 y 都得写进 < 的重载里进行比较。

七、经典题目

题目背景:
数轴上有若干个互不相交的区间,初始为空。
你需要支持两种操作,共 M 次(M≤10^5):

1 L R:加入一个区间 [L,R]。加入时,可能会与已有的区间发生重叠,你需要把所有与 [L,R] 相交的已有区间全部合并起来,变成一个新的大区间。(保证 L≤R)
2:查询当前总共有多少个区间。

做法:
我们可以用 std::set<std::pair<int, int>> 来存数据。
pair.first 存区间的左端点 L
pair.second 存区间的右端点 R
set 默认会按照 first 从小到大排序(如果 first 相同,按 second 排)。这样,区间在 set 里就是天然按左端点有序排列的。

1的情况,假设我们要加入区间 [L,R]。我们需要找到所有和它相交的区间并删除。
一个区间 [l,r] 和 [L,R] 相交的条件是:l <= R 且 r >= L。
因为 set 里的区间是按左端点 l 排序的,我们可以分两步走:

第一步:找右边界。
我们找第一个左端点严格大于 R 的区间。
怎么找?利用 set 的二分:auto it = s.upper_bound({R, MAX}); 
// 只要第一个元素大于R,无论第二个元素是什么,都会排在 {R, MAX} 后面。此时,it 指向了第一个绝对不相交(在 [L,R] 右边)的区间。

第二步:往左回溯删除。
从 it 的前一个迭代器 --it 开始,往左遍历。
对于当前的区间 [l, r]:

如果 r >= L,说明和 [L,R] 有重叠!我们就把它删掉,并更新 L=min(L,l), R=max(R,r),然后继续往左。
如果 r < L,说明已经到了 [L,R] 的左边,且没有重叠了,停止循环,把它加入set。

2的情况,直接输出set的size即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值