C++容器之std::set和std::unordered_set

下面代码展示了向std::set(std::unordered_set)插入数据的方法(代码示例1):

// 示例代码1
#include <array>
#include <iostream>
#include <iterator>
#include <memory>
#include <set>
#include <unordered_set>
#include <utility>

// 下面用了选择使用 std::unordered_set 或 std::set
// 这里使用了 std::set
// using set_type = std::unordered_set<int>;
using set_type = std::set<int>;

// set_type使用 std::set<int> 或 std::unordered_set<int>
// 唯一的差别就是这个函数
// 这是因为它们的存储方式不同导致的 ---- 这个下次会解释
std::ostream& operator<<(std::ostream& os, const set_type& sets) {
    if (sets.empty()) {
        os << "{}";
    } else {
        os << "{" << *sets.begin();
        for (auto iter = std::next(sets.begin()); iter != sets.end(); ++iter) {
            os << ", " << *iter;
        }
        os << "}";
    }
    os  << std::endl;
    return os;
}

int main()
{
    set_type sets;

    std::cout << "元素数据为" << std::endl;
    std::cout << sets;

    {
        // 下面这句同 auto result = sets.insert(10);
        std::pair<set_type::iterator, bool> result = sets.insert(10);
        // result.first 是 值为 10 的迭代器
        // result.second 表示是否真正发生了insert操作
        // -- 这是因为 std::set 不允许同一个值反复插入,
        //    当已经存在 10 时,这时就不会发生真实的插入
        //    但result.first还是有效的 指向的是已经存在的
        std::cout << "插入10后为" << std::endl;
        std::cout << sets;
    }

    {
        // 下面这句同 auto result = sets.insert(std::move(15));
        std::pair<set_type::iterator, bool> result = sets.insert(std::move(15));
        std::cout << "插入15后为" << std::endl;
        std::cout << sets;
    }

    {
        // 这个insert方法的返回值和上面的result.first一致
        // 另外,第一个参数的推荐的插入位置 -- 重点是推荐,这个看附录二有更多解释
        set_type::iterator iter = sets.insert(sets.begin(), 5);
        std::cout << "推荐在开头插入5后为" << std::endl;
        std::cout << sets;
    } 

    {
        set_type::iterator iter = sets.insert(sets.end(), std::move(8));
        std::cout << "推荐在结尾插入8后为" << std::endl;
        std::cout << sets;
    }

    {
        std::array<int, 4> a = {51, 52, 53, 54};
        // 使用迭代器批量插入 --- 没有返回值(即void类型)
        sets.insert(a.begin(), a.end());
        std::cout << "插入迭代器数据后为" << std::endl;
        std::cout << sets;
    }

    {
        // 使用std::initializer_list批量插入
        sets.insert({31, 35, 41});
        std::cout << "插入td::initializer_list数据后为" << std::endl;
        std::cout << sets;
    }

    {
        // 开始已经插入10, 这里插入重复数据
        auto result = sets.insert(10);
        std::cout << "再次插入10后为" << std::endl;
        std::cout << sets;
        std::cout << "返回迭代器为 " << *result.first << std::endl;
        std::cout << "插入是否成功 " << std::boolalpha << result.second << std::endl;
    }
}

运行结果(使用std::set)如下:

元素数据为
{}
插入10后为
{10}
插入15后为
{10, 15}
推荐在开头插入5后为
{5, 10, 15}
推荐在结尾插入8后为
{5, 8, 10, 15}
插入迭代器数据后为
{5, 8, 10, 15, 51, 52, 53, 54}
插入td::initializer_list数据后为
{5, 8, 10, 15, 31, 35, 41, 51, 52, 53, 54}
再次插入10后为
{5, 8, 10, 15, 31, 35, 41, 51, 52, 53, 54}
返回迭代器为 10
插入是否成功 false

除了insert方法,还有emplace方法。但emplace方法的返回值为std::pair类型,这个比insert简单就不举例了。

需要注意的一点,std::set和std::unordered_set是不能(通过迭代器等)修改元素的。这是因为,修改元素就可能会导致内部的顺序变化,这就不是简单的修改元素属性了。当然,可以通过删除后添加的方式进行修改,看下面代码(代码示例2):

// 代码示例2
#include <iostream>
#include <set>

std::ostream& operator<<(std::ostream& os, conststd::set<int>& sets) {
    if (sets.empty()) {
        os << "{}";
    } else {
        os << "{" << *sets.begin();
        for (auto iter = std::next(sets.begin()); iter != sets.end(); ++iter) {
            os << ", " << *iter;
        }
        os << "}";
    }
    os  << std::endl;
    return os;
}

int main()
{
    std::set<int> a = {10, 20, 30, 40};
    std::cout << a;

    // 查找 10
    auto iter = a.find(10);
    // 10 是否找到
    if (iter != a.end()) {
        // 删除 10
        a.erase(iter);
        // 添加 35
        a.insert(35);
    }
    std::cout << a;
}

运行结果如下:

{10, 20, 30, 40}
{20, 30, 35, 40}

std::set和std::unordered_set等可以使用find方法判断指定元素是否存在。std::set这类容器主要使用的就是erase、find和insert(包括emplace)方法。

附录:

附录一

关于std::set的必要性。在实际使用中,std::map比std::set的存在感高很多,但是,我还是计划使用很大的篇幅说明std::set的用法,这能在很大程度上帮助理解std::map。

本质上可以认为std::set内的元素不可以修改,但std::map可以修改元素,这就是std::map更能够广泛使用的原因。这也在一定程度上说明了使用std::set的场景(不需要修改元素的属性)。

另,std::set也可以修改元素的属性,要删除旧的后,添加新的。

附录二

std::set(和std::unordered_set)的insert方法在插入一个元素时,可以提供推荐的插入位置(附近),这个实际上是不能影响实际的插入位置的,但影响插入的效率,看下面代码(代码示例3):

// 代码示例3
#include <cstdlib>
#include <chrono>
#include <iostream>
#include <set>

// 该代码使用 std::set效果才明显

class timing
{
public:
    timing() : start_(std::chrono::high_resolution_clock::now()) {}
    ~timing() {
        auto end = std::chrono::high_resolution_clock::now();
        auto d = std::chrono::duration_cast<std::chrono::milliseconds>(end - start_);
        std::cout << "耗时: " << d.count() << "秒" << std::endl;
    }
private:
    std::chrono::time_point<std::chrono::high_resolution_clock> start_;
};

int main()
{
    // 根据机器性能,自行调整loop_count的值
    constexprstd::size_t loop_count = 100000;

    {
        std::set<int> sets;

        std::cout << "前插入" << loop_count << "项";
        timing t;

        for (std::size_t index = 0; index < loop_count; ++index) {
            sets.insert(sets.begin(), index);
        }
    }

    {
        std::set<int> sets;

        std::cout << "后插入" << loop_count << "项";
        timing t;

        for (std::size_t index = 0; index < loop_count; ++index) {
            sets.insert(sets.end(), index);
        }
    }
}

该代码因为计算耗时,有一定的随机性,大致结果如下:

前插入100000项耗时: 21秒
后插入100000项耗时: 18秒

非常高的概率是后插入耗时更小,这和std::set的内部实现方式有关,下次,就看看这个吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宋康

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值