在C++中设置STL的底层数据结构是什么?

zeb*_*man 42 c++ set

我想知道如何在C++中实现一个集合.如果我在不使用STL提供的容器的情况下实现自己的set容器,那么最好的方法是什么呢?

我理解STL集基于二叉搜索树的抽象数据结构.那么底层数据结构是什么?数组?

另外,如何insert()为一组工作?set如何检查元素是否已经存在?

我在维基百科上读到,实现集合的另一种方法是使用哈希表.这怎么样?

Tol*_*oli 24

正如KTC所说,std::set实现的方式可能会有所不同--C++标准只是指定了一种抽象数据类型.换句话说,标准没有规定应该如何实现容器,只是指定需要支持的操作.但是,据我所知,STL的大多数实现都使用红黑树或其他某种平衡的二叉搜索树(例如,GNU libstdc ++使用红黑树).

虽然理论上你可以将一个集合实现为哈希表并获得更快的渐近性能(分摊O(密钥长度)与O(log n)进行查找和插入),这将需要让用户为他们想要的任何类型提供哈希函数存储(请参阅维基百科在哈希表上的条目,以获得有关它们如何工作的详细解释).至于二叉搜索树的实现,您不希望使用数组 - 正如Raul所提到的,您需要某种Node数据结构.

  • 您可以使用哈希表实现*a*set类型.但是,您不能(至少不容易)实现满足std :: set迭代器要求的那个. (3认同)
  • @ dan04:你甚至不能实现一个满足std :: set lookup和insert的要求.正如Toli所说,你需要一个关键类型的哈希函数,并且标准不要求`std :: set`的用户提供一个.因此,当他们的代码无法使用`找不到hash(MyElement)`进行编译时,这意味着std :: set实现有缺陷. (3认同)

Rau*_*ait 13

您可以通过首先定义Node结构来实现二叉搜索树:

struct Node
{
  void *nodeData;
  Node *leftChild;
  Node *rightChild;
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以使用另一个树定义树的根 Node *rootNode;

二进制搜索树上的维基百科条目有一个很好的例子来说明如何实现插入方法,所以我也建议你检查一下.

就重复而言,它们通常不允许在集合中,因此您可以根据您的规范放弃该输入,抛出异常等.

  • @Shasha99 插入顺序不是“有序容器”中的“有序”所指的。 (3认同)
  • 据我所知, std::set 是一个有序的容器。如果它是一个 BST,如何订购遍历? (2认同)
  • 为了满足标准所承诺的性能规格,需要一棵*平衡*的二叉搜索树。 (2认同)

Cir*_*四事件 12

步骤调试到g++6.4 stdlibc ++源码

您是否知道在Ubuntu的16.04默认g++-6软件包或源代码GCC 6.4版本中,您可以直接进入C++库而无需进一步设置?

通过这样做,我们很容易得出结论,在这个实现中使用了一个红黑树.

这是有道理的,因为std::set可以按顺序遍历,如果使用哈希映射则不会有效.

main.cpp中

#include <cassert>
#include <set>

int main() {
    std::set<int> s;
    s.insert(1);
    s.insert(2);
    assert(s.find(1) != s.end());
    assert(s.find(2) != s.end());
    assert(s.find(3) == s3.end());
}
Run Code Online (Sandbox Code Playgroud)

编译和调试:

g++ -g -std=c++11 -O0 -o main.out main.cpp
gdb -ex 'start' -q --args main.out
Run Code Online (Sandbox Code Playgroud)

现在,如果你踏入s.insert(1)你的话,马上到达/usr/include/c++/6/bits/stl_set.h:

487 #if __cplusplus >= 201103L
488       std::pair<iterator, bool>
489       insert(value_type&& __x)
490       {
491     std::pair<typename _Rep_type::iterator, bool> __p =
492       _M_t._M_insert_unique(std::move(__x));
493     return std::pair<iterator, bool>(__p.first, __p.second);
494       }
495 #endif
Run Code Online (Sandbox Code Playgroud)

这显然只是转发给了_M_t._M_insert_unique.

所以我们在vim中打开源文件并找到以下定义_M_t:

      typedef _Rb_tree<key_type, value_type, _Identity<value_type>,
           key_compare, _Key_alloc_type> _Rep_type;
       _Rep_type _M_t;  // Red-black tree representing set.
Run Code Online (Sandbox Code Playgroud)

所以_M_t是类型_Rep_type,_Rep_type是一个_Rb_tree.

好的,现在这对我来说已经足够了.如果您不相信这_Rb_tree是一棵黑红色的树,请再进一步阅读算法.

unordered_set 使用哈希表

相同的过程,但更换setunordered_set的代码.

这是有道理的,因为std::unordered_set不能按顺序遍历,所以标准库选择哈希映射而不是红黑树,因为哈希映射具有更好的分期插入时间复杂度.

走进insert导致/usr/include/c++/6/bits/unordered_set.h:

415       std::pair<iterator, bool>
416       insert(value_type&& __x)
417       { return _M_h.insert(std::move(__x)); }
Run Code Online (Sandbox Code Playgroud)

所以我们打开源文件vim并搜索_M_h:

      typedef __uset_hashtable<_Value, _Hash, _Pred, _Alloc>  _Hashtable;
      _Hashtable _M_h;
Run Code Online (Sandbox Code Playgroud)

所以哈希表就是这样.

std::mapstd::unordered_map

类似于std::setvs std:unordered_set:C++中的std :: map里面有什么数据结构?

性能特点

您还可以通过计时来推断使用的数据结构.

我们清楚地看到:

  • std::set,对数插入时间
  • std::unordered_set,一个更复杂的模式hashmap模式:

    • 在非缩放的情节中,我们清楚地看到背景动态阵列在巨大的线性增加尖峰上加倍
    • 在缩放的情节中,我们看到时间基本上是恒定的并且朝向250ns,因此比非常快std::map,除了非常小的地图尺寸

      几个条带清晰可见,并且每当阵列加倍时它们的倾斜度变小.

      我相信这是由于平均线性增加的链表行走每个箱子.然后当阵列加倍时,我们有更多的箱子,所以更短的步行.

在此输入图像描述

图表生成:

堆与BST分析:堆与二元搜索树(BST)


jas*_*ine 9

我理解STL集基于二叉搜索树的抽象数据结构.那么底层数据结构是什么?数组?

正如其他人所指出的,它有所不同.一组通常被实现为树(红黑树,平衡树等),但是可能存在其他实现.

另外,insert()如何适用于一组?

这取决于您的集合的底层实现.如果它是作为二叉树实现的,则Wikipedia具有insert()函数的示例递归实现.你可能想看一下.

set如何检查元素是否已经存在?

如果它是作为树实现的,那么它遍历树并检查每个元素.但是,集合不允许存储重复元素.如果你想要一个允许重复元素的集合,那么你需要一个multiset.

我在维基百科上读到,实现集合的另一种方法是使用哈希表.这怎么样?

您可能指的是hash_set,其中使用哈希表实现该集合.您需要提供哈希函数来了解存储元素的位置.当您希望能够快速搜索元素时,此实现非常理想.但是,如果按特定顺序存储元素很重要,那么树实现更合适,因为您可以按顺序遍历它,按顺序或后序.


KTC*_*KTC 7

如何在C++中实现特定容器完全取决于实现.所需要的只是结果满足标准中规定的要求,例如各种方法的复杂性要求,迭代器要求等.

  • 也就是说,大多数(全部?)都是红黑树. (9认同)
  • 鉴于时间复杂性约束以及必须对其进行排序的要求,没有太多空间可以选择不涉及某种平衡树的替代方案. (3认同)