std::set中索引位置的元素是什么?

55

我遇到了这个问题:在普通的std::set中,我似乎无法选择索引位置上的项。这是STD的错误吗?

以下是一个简单的示例:

#include <iostream>
#include <set>

int main()
{
    std::set<int> my_set;
    my_set.insert(0x4A);
    my_set.insert(0x4F);
    my_set.insert(0x4B);
    my_set.insert(0x45);

    for (std::set<int>::iterator it=my_set.begin(); it!=my_set.end(); ++it)
        std::cout << ' ' << char(*it);  // ups the ordering

    //int x = my_set[0]; // this causes a crash!
}

有什么我可以做来解决这个问题吗?


9
my_set[0] 不应该被编译。 - chris
3
你提出了错误的问题,因为你正在使用错误的容器。每个标准容器都是根据特定数量的用途而设计的,因此不允许其他操作(直接执行)。所以,首先你需要确定你需要进行哪些操作,然后选择正确的容器 - Matthieu M.
2
可能是从集合中获取任意索引的元素的重复问题。 - Ciro Santilli OurBigBook.com
最初这是一个玩笑问题,但回答证明它们非常有用。如果认真对待,它确实是一个重复的问题。 - hauron
9个回答

117

它不会导致崩溃,只是无法编译。 set 不能通过索引访问。

您可以像这样获取第n个元素:

std::set<int>::iterator it = my_set.begin();
std::advance(it, n);
int x = *it;

假设 my_set.size() > n,那当然。你应该知道这个操作的时间大约与 n 成正比。在 C++11 中有更好的写法:

int x = *std::next(my_set.begin(), n);

再次强调,首先必须了解n是否在范围内。


当然,如果OP期望my_set[0]返回0x4A,这仍然不能达到预期的效果。 - Useless
4
@Useless: 如果他们期望它返回循环中先前的第一个值,那么它就是正确的。一般来说,如果提问者不知道什么是“set”,那么他们会看到一系列令人惊讶的结果,直到最终放弃并查阅文档。;-p - Steve Jessop
2
抱歉,但这个问题本来是个笑话(这些值是“JOKE”的ASCII十六进制代码,但集合是无序的,因此迭代不会产生相同的结果)。然而,我不知道std::advance或std::next,所以感谢您分享! - hauron
1
@hauron 我并不太关心你是如何提出这个问题的。但我相信我和其他13个人认为你应该接受这个答案。 - Jonathan Mee
@Parabolord:没错。因为我们在讨论索引,我是从第0个元素开始计数的,但这并不一定是每个人都期望的 :-) - Steve Jessop
显示剩余2条评论

10

尝试一下,你将能够以另一种方式使用 set,即 ordered_set

这在竞赛编程中非常常见

希望这对所有人都有所帮助!

#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
using namespace __gnu_pbds;
#define ordered_set tree<int,null_type,less<int>,rb_tree_tag,tree_order_statistics_node_update>

现在您可以使用

order_of_key (k) : Number of items strictly smaller than k .
find_by_order(k) : K-th element in a set (counting from zero). //This is what you need 


[https://www.geeksforgeeks.org/ordered-set-gnu-c-pbds/][1]

1
你救了我。非常感谢 <3。 - Moaz Fathy

8

你无法以常数时间访问它。

但是你可以在O(n)时间内访问任何元素。 例如:

std::set<int>::iterator it;
it=my_set.begin();
advance(it,n);
cout<<*it;

非常聪明的答案!std::advance(可通过头文件<iterator>获得)可能作为C++17的新功能引入。 - undefined

8

2

我认为std::set没有比O(n)更好的方法来实现这个功能,但是最近我使用了一个集合和一棵二进制索引树来创建这个数据结构。它可以完成std::set能做的大多数事情,而且可以在O(log n)时间内获取元素的索引,以及在O((log n) * (log n))时间内获取特定索引处的元素:

#include <iostream>
#include <iomanip>
#include <algorithm>
#include <math.h>
#include <vector>
#include <queue>
#include <bitset>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
using namespace std;
typedef pair<int, int> pii;
typedef pair<pii, int> piii;
typedef long long ll;
typedef pair<ll, ll> pll;
#define max(n, m) ((n>m)?n:m)
#define min(n, m) ((n<m)?n:m)
#define f first
#define s second

struct ss
{
    // binary index tree (to mark elements)
    int bit[1000010]; // set this number to the max you will use
    // set (to store the numbers in order)
    set<int> nums;
    // the maximum element in the set (NOTE: this data structure works with marking in the BIT array, but you can make this better by using an unordered set to store all values that could appear inside of the set, but this will increase runtime by a high constant factor)
    int mx;
    // constructor
    ss(int maxEl)
    {
        mx = maxEl + 5;
    }
    int sum(int arr[], int idx)
    {
        int ans = 0;
        idx ++;
        if(idx > mx + 5) return -1;
        while(idx > 0)
        {
            ans += arr[idx];
            idx -= idx & (-idx);
        }
        return ans;
    }
    void update(int arr[], int idx, int val, int size)
    {
        idx ++;
        while(idx <= size)
        {
            arr[idx] += val;
            idx += idx & (-idx);
        }
    }
    int bs(int l, int r, int idx)
    {
        int mid = (l + r) / 2;
        if(l == r) return mid + 1;
        if(l == r - 1)
        {
            if(sum(bit, r) == idx) return r + 1;
            return r;
        }
        if(sum(bit, mid) <= idx) return bs(mid, r, idx);
        return bs(l, mid - 1, idx);
    }
    // regular set functions
    set<int>::iterator find(int num) { return nums.find(num); }
    set<int>::iterator lower_bound(int num) { return nums.lower_bound(num); }
    set<int>::iterator upper_bound(int num) { return nums.upper_bound(num); }
    int size() { return (int)nums.size(); }
    set<int>::iterator begin() { return nums.begin(); }
    set<int>::iterator end() { return nums.end(); }
    bool empty() { return nums.empty(); }
    // slightly modified insert and erase functions to also mark stuff in BIT (still O(log n) though)
    void insert(int num)
    {
        if(nums.find(num) == nums.end())
            update(bit, num, 1, mx); // marks the element in the BIT if it doesn't already exist
        nums.insert(num);
    }
    void erase(int num)
    {
        if(nums.find(num) != nums.end())
            update(bit, num, -1, mx); // unmarks the element in the BIT if it exists in the set
        nums.erase(num);
    }
    // gets index (0-indexed) of a specific element in O(log n), returns -1 if element not in set
    int idx(int num)
    {
        if(nums.find(num) == nums.end())
            return -1;
        return sum(bit, num - 1);
    }
    // gets the iterator of the element at a specific index (0-indexed) in O((log n) * (log n)), returns end of set if idx is invalid
    set<int>::iterator at(int idx)
    {
        if(idx < 0 || idx >= nums.size())
            return nums.end();
        return nums.find(bs(0, mx, idx));
    }
};

int main()
{
    ss test = ss(1000);
    test.insert(1);
    test.insert(3);
    test.insert(5);
    test.insert(1);
    test.insert(9);
    test.insert(1000);
    cout << *test.at(1) << "\n";
    test.erase(3);
    cout << *test.at(1) << "\n";
    cout << test.idx(1) << "\n";
    cout << *test.at(-1) << "\n";
}

这个集合存在一些缺陷,因为它标记了二进制索引树中的元素,所以元素不能是负数或非常大的数,除非进行额外的修改,但在某些情况下仍然有用。此外,使用std :: map或某种其他类型的映射可以使该集合与负数、大数以及其他数据类型一起工作,但这将使运行时间增加一个O(log n)的因子,而且您必须预先知道可能出现在集合中的所有元素,以便可以按正确的顺序将它们存储在映射内。
编辑:我刚刚意识到已经有一个名为ordered-set的基于策略的数据结构具有与集合相同的功能,但可以在O(log n)的时间内执行两个操作(获取索引处的元素和获取元素的索引)。在此处阅读更多信息:https://www.geeksforgeeks.org/ordered-set-gnu-c-pbds/。但这可能不适用于所有编译器。

1
这不是STD中的一个bug。在std::set中不存在随机访问。如果需要按索引进行随机访问,可以使用std::vector。

1
好的,有的。默认情况下,它使用 std::less 来排序。 - chris
你是对的,我想到了 std::unordered_set。已经修改过了。 - José D.
1
顺序不依赖于实现。正如Chris所说,它使用std::less,除非另有说明,在int的情况下只是一种花哨的方式来表示< - Steve Jessop
Trollkemada,感谢您的回答,不过这个问题有点像恶作剧问题(请参见评论或Steve的答案)。 - hauron

0
我相信最优的方式,尤其是当这种索引发生在循环中时,就是将其转换为向量。
auto my_vect = std::vector(my_set.begin(), my_set.end()); // O[n]
int output = my_vect[n]; // O[1]

0
有时需要一个可索引的集合是有充分理由的。我最近不得不实现这个功能来支持一个遗留的 API,该 API 有函数返回项数和索引处的项,以便调用者可以枚举这些项。
我解决这个问题的方法是使用 std::vector,并使用 std::equal_range 在集合中查找、插入或删除元素。例如,将新项插入到集合中的代码如下:
    std:vector<std::string> my_set;

    ...

    std::string new_item("test");

    auto range = std::equal_range(my_set.begin(),my_set.end(),new_item);
    if (range.first == range.second)
        my_set.insert(range.first,new_item);

删除操作与插入操作十分相似:使用 equal_range 找到目标项,如果 range.first 不等于 range.second,则删除此范围。


-3
  std::set<int> my_set;
  my_set.insert(0x4A);
  my_set.insert(0x4F);
  my_set.insert(0x4B);
  my_set.insert(0x45);

  int arr[my_set.size()];

  set<int>::iterator it = my_set.begin();
  for (int i = 0; i < my_set.size(); i++) {
    arr[i] = *it;
    it++;
  }
  cout << arr[0];

编辑:已编辑代码。您无法使用索引访问集合,但上述方法将提供一个“索引”i,如果您想将元素从集合复制到数组中,前提是您事先创建了足够大小的数组。


这怎么回答问题了呢?你只是以稍微不同的方式迭代集合。 - Uyghur Lives Matter
@cpburnz 其他答案都没有使用索引迭代集合。在我的示例中,使用索引迭代集合是有用的。 - katta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接