Set容器
1、Set
(1)定义
set/multiset会根据待定的排序准则,自动将元素排序。两者不同在于前者不允许元素重复,而后者允许。
set,顾名思义是“集合”的意思,用来存储同一数据类型的数据类型。
在set中元素都是唯一的,而且默认情况下会对元素自动进行升序排列,支持集合的交(set_intersection),差(set_difference) ,并(set_union),对称差(set_symmetric_difference) 等一些集合上的操作,如果需要集合中的元素允许重复那么可以使用multiset 。
C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作。
vector封装数组,list封装了链表,map和set封装了二叉树等,在封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入、排序、删除、查找等。让用户在STL使用过程中,并不会感到陌生。
应该注意的是set中数元素的值不能直接被改变。
(2)底层实现
C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。
RB树的统计性能要好于一般平衡二叉树,所以被STL选择作为了关联容器的内部结构,插入删除操作时仅仅需要指针操作节点即可完成,不涉及到内存移动和拷贝,所以效率比较高。
(3)模板原型
set模板原型:
从原型可以看出,可以看出比较函数对象及内存分配器采用的是默认参数,因此如果未指定,它们将采用系统默认方式。
set的标准形式是set<Key, Compare, Alloc>:
(4)特点
1) 不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,则插入新元素;
2) 不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取,而且从迭代器角度来看,元素值是常数;
3) 元素比较动作只能用于型别相同的容器(即元素和排序准则必须相同)。
(5)性能分析
(1)为何map和set的插入删除效率比用其他序列容器高?
因为对于关联容器来说,不需要做内存拷贝和内存移动。set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。结构图可能如下:
A
/ \
B C
/ \ / \
D E F G
插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点即可。这里的一切操作就是指针换来换去,和内存移动没有关系。
(2)为何每次insert之后,以前保存的iterator不会失效?
iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。
相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。
特别是在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。
(3)当数据元素增多时,set的插入和搜索速度变化如何?
在set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。这样就可以安心往里面放入元素了。
2、成员函数
(1)构造函数
(2)插入操作
示例:
(3)删除操作
示例:
(4)定位查找
示例:
(5)数据大小
示例:
(6)交换操作
示例:
(7)并交差
示例: