我想知道如何在C ++中实现一个集合。如果我在不使用STL提供的容器的情况下实现自己的set容器,那么最好的方法是什么呢?
我理解STL集基于二叉搜索树的抽象数据结构。那么底层数据结构是什么?数组?
另外,insert()
如何适用于一组? set如何检查元素是否已经存在?
我在维基百科上读到,实现集合的另一种方法是使用哈希表。这将如何运作?
答案 0 :(得分:23)
正如KTC所说,std::set
的实现方式可能有所不同--C ++标准只是指定了一种抽象数据类型。换句话说,标准没有规定应该如何实现容器,只是指定需要支持的操作。但是,据我所知,STL的大多数实现都使用red-black trees或其他某种平衡的二叉搜索树(例如,GNU libstdc ++使用红黑树)。
虽然理论上你可以将一个集合实现为哈希表并获得更快的渐近性能(分摊O(密钥长度)与O(log n)进行查找和插入),这将需要让用户为任何东西提供哈希函数他们想要存储的类型(请参阅Wikipedia's entry on hash tables以获得有关其工作原理的详细说明)。至于二叉搜索树的实现,您不希望使用数组 - 正如Raul所提到的,您需要某种Node
数据结构。
答案 1 :(得分:12)
您可以通过首先定义Node
结构来实现二叉搜索树:
struct Node
{
void *nodeData;
Node *leftChild;
Node *rightChild;
}
然后,您可以使用另一个Node *rootNode;
Binary Search Tree上的维基百科条目有一个很好的例子,说明如何实现插入方法,所以我也建议你检查一下。
就重复而言,它们通常不允许在集合中,因此您可以放弃该输入,抛出异常等,具体取决于您的规范。
答案 2 :(得分:8)
我理解STL集基于二叉搜索树的抽象数据结构。 那么底层数据结构是什么?数组?
正如其他人所指出的,它有所不同。一组通常被实现为树(红黑树,平衡树等),但可能存在其他实现。
另外,insert()如何用于a 组?
这取决于您的集合的底层实现。如果它是作为二叉树实现的,Wikipedia具有insert()函数的示例递归实现。你可能想看一下。
该集如何检查是否 元素已经存在于其中?
如果它是作为树实现的,那么它遍历树并检查每个元素。但是,集合不允许存储重复元素。如果你想要一个允许重复元素的集合,那么你需要一个多集合。
我在维基百科上读到另一种方式 实现一个集合是用哈希 表。这将如何运作?
您可能指的是hash_set,其中该集合是使用哈希表实现的。您需要提供哈希函数来了解存储元素的位置。当您希望能够快速搜索元素时,此实现非常理想。但是,如果按特定顺序存储元素很重要,那么树实现更合适,因为您可以按顺序遍历它,按顺序或后序。
答案 3 :(得分:7)
如何在C ++中实现特定容器完全取决于实现。所需要的只是结果符合标准中规定的要求,例如各种方法的复杂性要求,迭代器要求等。
答案 4 :(得分:6)
逐步调试到g++
6.4 stdlibc ++源代码中
您知道吗,在Ubuntu 16.04默认的g++-6
软件包或GCC 6.4 build from source上,您可以无需任何进一步设置即可进入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());
}
编译和调试:
g++ -g -std=c++11 -O0 -o main.out main.cpp
gdb -ex 'start' -q --args main.out
现在,如果您踏入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
显然只是转发到_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.
因此_M_t
的类型为_Rep_type
,_Rep_type
的类型为_Rb_tree
。
好的,现在对我来说已经足够了。如果您不相信_Rb_tree
是黑红色的树,请再走一步,然后阅读算法。
unordered_set
使用哈希表
相同的过程,但是在代码上将set
替换为unordered_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)); }
因此,我们在vim
中打开源文件并搜索_M_h
:
typedef __uset_hashtable<_Value, _Hash, _Pred, _Alloc> _Hashtable;
_Hashtable _M_h;
所以是哈希表。
std::map
和std::unordered_map
与std::set
和std:unordered_set
类似:What data structure is inside std::map in C++?
性能特征
您还可以通过计时来推断使用的数据结构。
我们清楚地看到:
std::set
,对数插入时间 std::unordered_set
,更复杂的模式哈希图模式:
在放大的图上,我们看到时间基本上是恒定的,接近250ns,因此比std::map
快得多,除了非常小的地图尺寸
几个条带清晰可见,每当阵列加倍时,它们的倾斜度就会变小。
我相信这是由于每个bin的平均链表遍历线性增加。然后,当数组增加一倍时,我们会有更多的垃圾箱,因此走步更短。
生成的图形:
堆vs BST分析位于:Heap vs Binary Search Tree (BST)
答案 5 :(得分:1)
集合通常以红黑树的形式实现。
我检查了一下,libc++
和libstdc++
都为std::set
使用了红黑树。
std::unordered_set
是通过libc++
中的哈希表实现的,我认为libstdc++
中的哈希表也是相同的,但是没有检查。
编辑:显然我的话不够好。
答案 6 :(得分:1)
对此进行说明,因为我没有看到任何人明确提及它……C ++标准没有指定用于std :: set和std :: map的数据结构。但是,它指定的是各种操作的运行时复杂性。插入,删除和查找操作对计算复杂性的要求或多或少迫使实现使用平衡树算法。
有两种实现平衡二叉树的常用算法:红黑算法和AVL算法。在这两种方法中,Red-Black的实现稍微简单一些,每个树节点只需要少一点的存储空间(这没关系,因为无论如何您都将在一个简单的实现中对其烧掉一个字节),并且在节点删除方面比AVL快一点(这是由于对树平衡的要求更加宽松)。
所有这些都与std :: map要求将键和数据存储在std :: pair中的要求结合在一起,在没有明确命名必须用于容器的数据结构的情况下,将所有这些强加给您。
这又与容器的c ++ 14/17补充功能复合,这些功能允许将节点从一棵树拼接到另一棵树。