容器种类 | 功能 |
---|---|
序列容器 | 主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。 |
关联容器 | 包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。关联容器内的元素是排序的。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。 |
哈希容器(散列容器) | C++ 11 新加入 4 种无序容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。 |
连续内存容器:vector、string、deque
基于节点的容器:list、关联容器、哈希容器
序列容器
序列容器只是一类容器的统称,并不指具体的某个容器,序列容器大致包含以下几类容器:
- vector<T>(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);
- deque<T>(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;
- list<T>(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,可以前后双向迭代。在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list<T> 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
- stack<T> 和 queue<T> 本质上也属于序列容器,只不过它们都是在 deque 容器的基础上改头换面而成,通常更习惯称它们为容器适配器。
- 还有heap 、priority_queue(优先队列) 底层本质就是vector和二叉树算法。
- string 其实就是vector<char>
vector
时间复杂度:
尾部插入(或删除):O(1)
随机插入(或删除):O(N)
随机查看:O(1)
适用于:查找操作基本不与添加删除操作混在一起使用。
1.vector维护的是一个单向连续性空间。
2.vector增加新元素,即push_back函数。如果超过当时的容量,则容量就会扩充到原来的2倍,容量的扩充需要经历重新配置、元素移动、释放源空间。配置空间是调用alloc配置,元素移动是调用拷贝构造函数,底层是uninitialized_copy。释放空间是调用析构函数和deallocate.不是在原地址上扩充,而是在新地址。
拷贝用的是uninitialized_copy,如果拷贝的是POD(标量型别,也就是trivial)调用的是copy(自己去看STL 的copy实现),如果是non-POD使用for循环遍历,调用construct,一个一个的构造,针对char*和wchar_t*,uninitialized_copy直接用memmove来执行复制行为,更加快。这些都是深拷贝。
vector = 操作符默认是深拷贝,调用的是拷贝构造函数。
只有当移动构造函数声明为noexcept
的时候才会调用,否则将统一调用拷贝构造函数。(就算调用移动构造函数也没有,不能直接按指针赋值,因为也会执行析构函数,导致数据被delete,内存泄漏)
std::vector扩容时为何进行深复制?_fl2011sx的博客-CSDN博客
问题:32.vector内存拷贝问题,什么情况下会出现内存拷贝,如何解决这个问题使其更加高效?
不知道怎样提高效率.
push_back和 emplace_back的区别
如果要将一个临时变量push到容器的末尾,push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程。
注意:在存在继承关系的情况下,拷贝动作如push_back、insert会导致剥离(slicing)。也就是说,如果你创建了一个存放基类对象的容器,却向其中插入派生类的对象,那么在派生类对象(通过基类的拷贝构造函数)被拷贝进容器时,它所特有的部分(即派生类中的信息)将会丢失。”剥离”问题意味着向基类对象的容器中插入派生类对象几乎总是错误的。使拷贝动作高效、正确,并防止剥离问题发生的一个简单办法是使容器包含指针而不是对象。
class Widget {};
class SpecialWidget : public Widget {};
int test_item_3()
{
std::vector<Widget> vw;
SpecialWidget sw;
vw.push_back(sw); // sw作为基类对象被拷贝进vw中,它的派生类特有部分在拷贝时被丢掉了
return 0;
}
3.vector缺省使用alloc作为空间配置器。
4. vector(int n, const T& value) { fill_initialize(n, value); } 解析
a.先调用alloc配置n个空间(参考空间配置器),本质是malloc
b.调用构造函数进行初始化。在刚配置的空间使用的是fill。
5.pop_back 本质只是调用destroy析构函数,并不会释放内存。
6.erase本质就是先调用copy函数把后面的数据移动到前面来,再调用destroy析构函数。并不会释放内存。同理可知clear函数也只是通过调用erase函数从而调析构函数,而不会释放内存。所以vector的内存空间一般只能由其生命周期结束后,调用vector析构函数并释放内存空间。
但如果要删除vector中多个元素,如相同的元素。应该使用erase-remove,效率最高。原因是最多只调用vector.size()个拷贝构造函数。如果使用循环调用erase 就会有vector.size() * vector.size()个调用了。
// 删除c中所有值为1963的元素
std::vector<int> c1{1,1963,4,233,555,1963,2000};
c1.erase(std::remove(c1.begin(), c1.end(), 1963), c1.end()); // 当c1是vector, string或deque时,erase-remove习惯用法是删除特定值的元素的最好办法
延伸:
vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
vec.shrink_to_fit():请求容器降低其capacity和size匹配。
vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。
vector<int> vec33(5, 10);
vector<int>().swap(vec33);
//或者
vector<int> vec34(4, 5);
vec34.clear();
vec34.shrink_to_fit();
7.insert比较复杂,涉及到如果空间不足,需要重新配置;涉及到copy函数(原有的数据)和fill函数(插入的数据)
8.bool empty() const {return begin() == end();}
size_type size()const { return size_type(end() - begin()); }
size_type capacity()const { return size_type(end_of_storage - begin()); }//表示容量
延伸:调用empty而不是检查size()是否为0
8. reserve(): 新的大小大于原来的才设置,改变容器的容量大小。
9.resize():改变容器的大小。如果新的大小小于原来的,容量保持不变,只改容器大小。
std::vector<int> vecint{ 1,5,4,7 };
vecint.push_back(2);
vecint.pop_back();
vector<int>::iterator iter;
iter = vecint.begin();
int nit = *iter;
iter++;
std::sort(vecint.begin(), vecint.end(), [](int a, int b)->bool {return a > b; });
10. assign()可以实现区间赋值。通过迭代器的方式,减少拷贝和构造的次数。
vector<int> vecint;
vector<int> vecint2(10,3);
vecint.assign(vecint2.begin() + vecint2.size() / 2, vecint2.end());
延伸:
std::copy(vecint2.begin() + vecint2.size() / 2, vecint2.end(),std::back_inserter(vecint));// 但效率不如assign
list
环状双向链表的形式组织元素,可以前后双向迭代。
最大的特点是创建一个空list的时候,会先建一个node空白节点,并让该node的pre、next指针都指向自己。然后插入节点都是围绕着node节点展开。
插入:O(1)
随机查看:O(N)
删除:O(1)
1.缺省使用alloc为配置器。
2.link_type node 这个指针,表示整个环状双向链表。该node指针有pre、next、data。让指针node刻意指向尾端的一个空白节点。
iterator begin() {
return (link_type)((*node).next);
}
iterator end() { return node; }
3.list 的缺省构造函数,会调用一个empty_initialize函数,产生一个空链表。
void empty_initialize() {
node = get_node();//底层本质调用alloc配置空间
node->next = node;
node->prev = node;
}
1.插入操作(insert)和结合操作(splice)都不会导致原有的list迭代器失效。甚至删除操作(erase)也只是被删除的元素的那个迭代器失效,其他迭代器不受影响。
5.put_back 会调用insert函数。所以指针node很重要,所有在最后新增的节点都是依赖于指针node节点。
void push_back(const T& x)
{
insert(end(), x);
}
iterator insert(iterator position, const T& x)
{
link_type tmp = create_node(x); //先调用get_node函数配置空间,再调用构造函数construct初始化数据
tmp->next = position.node;
tmp->prev = position.node->prev;
(link_type(position.node->prev))->next = tmp;
position.node->prev = tmp;
return tmp;
}
6.push_front 调用insert(begin(),x);
void push_front(const T& x)
{
insert(begin(), x);
}
7.erase先调整next和prev的node链接,再对当前的节点node进行调用析构函数并删除内存。这点和vector是不一样的。vector是不删除内存的。
iterator erase(iterator position) {
link_type next_node = link_type(position.node->next);
link_type prev_node = link_type(position.node->prev);
prev_node->next = next_node;
next_node->prev = prev_node;
destroy_node(position.node);//先调用析构函数,再释放空间配置器
return iterator(next_node);
}
对list删除节点最好的办法是调用list::remove().
8.clear会对所有节点进行析构并释放内存。这点和vector是不一样的
9. 调用empty而不是检查size()是否为0。
empty对所有的标准容器都是常数时间操作,而对一些list实现,size耗费线性时间。
list<int> listint{ 1,3 };
listint.push_back(4);
listint.push_front(2);
listint.pop_back();
listint.pop_front();
listint.insert(listint.begin(), 11);
listint.erase(listint.begin());
扩展:
1.在 O(1) 时间内删除链表节点
① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。
② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。
综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1,其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数,N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N ~ 2,因此该算法的平均时间复杂度为 O(1)。
struct MyListNode
{
int nValue;
MyListNode *pPrevNode;
MyListNode *pNextNode;
};
void offertest::deleteNode(MyListNode **pHead, MyListNode *tobeDelete)
{
if (pHead == nullptr || *pHead == nullptr || tobeDelete == nullptr)
return;
if (tobeDelete->pNextNode != nullptr) //要删除的节点不是尾节点
{
MyListNode *pNext = tobeDelete->pNextNode;
tobeDelete->nValue = pNext->nValue;
tobeDelete->pNextNode = pNext->pNextNode;
delete pNext;
pNext = nullptr;
}
else if (*pHead == tobeDelete)//只有一个节点
{
delete tobeDelete;
tobeDelete = nullptr;
*pHead = nullptr;
}
else//链表有多个节点,删除尾结点
{
MyListNode *pcur = *pHead;
while (pcur->pNextNode != tobeDelete)
pcur = pcur->pNextNode;
pcur->pNextNode = nullptr;
delete tobeDelete;
tobeDelete = nullptr;
}
}
deque:
插入:O(N)
随机查看:O(1)
删除:O(N)
deque<int> dq;创建一个双端队列dq
deque是一个双向的连续空间,可以在头部和尾部插入和删除数据。所谓的连续空间是分段连续空间。是逻辑上的连续,不是真实物理的上的连续。所以在插入,删除操作涉及到内存创建、数据移动赋值、析构、内存销毁等复杂的操作。
1.既然是分段连续空间,就需要一个中控器进行控制。
2.缺省使用alloc配置器。
3.deque模板
重点在于确定第一个迭代器(start)和最后一个迭代器(finish)。其实第一个迭代器(start)和最后一个迭代器(finish)分别对应中控区(节点)的起始地址和终点地址。
4.deque 迭代器的结构
用于cur、first、last 用于联系存储的数据。node用于联系中控器(节点)。
5.构造及内存管理。在定义使用deque结构时,分配内存(先分配中控区(节点内存),再分配缓冲区内存)包括处理好各位置,及数据赋值初始化。
注意分配节点数最少8个,最多所需节点数加2.缓冲区大小默认为512字节。
在deque构造函数会调用fill_initialize进行初始化。
6.push_back.要考虑后面是否有足够的备用空间,如果没有,需要重新配置新缓冲区空间,甚至可能需要重新分配(中控器)节点空间。同理push_front也是一样的原理。
reserve_map_at_back()会调用reallocate_map进行重新分配中控器(节点)
7.pop_back和pop_back. 注意有可能需要释放缓冲区内存空间。pop_back和pop_back原理都是一样。
8.clear. 注意需要保留一个缓冲区,其他的缓冲区内存全被释放掉(这点和list一样)。而中控区(节点)内存不做任何处理?
9.insert 。如果是最前面插入调用push_front.如果是最后面插入调用push_back。其他情况调用下面的函数。 insert内部也是通过最后(最前面)一个元素push_back和push_front ,中间一段元素copy移动,要插入元素赋值。
10.erase。删除一个点或者多个点都可能涉及释放缓冲区和中控器(节点)的内存空间,重新设置起始和终点位置。 erase内部是先把删除点下一个位置到最后位置的元素copy到当前删除点,然后通过pop_back删掉最后的一个元素。或者把最前面位置到删除点上一个位置的元素copy到最开始的的下一个位置上,然后pop_front最前面的一个元素。
但如果要删除deque中多个元素,如相同的元素。应该使用erase-remove。和vector一样的。原因是最多只调用deque.size()个拷贝构造函数。如果使用循环调用erase 就会有deque.size() * deque.size()个调用了。
deque<int> deqint{1,3};
deqint.push_back(4);
deqint.push_front(2);
deqint.pop_back();
deqint.pop_front();
deqint.insert(deqint.begin(), 11);
deqint.erase(deqint.begin());
deqint.clear();
stack(栈)
是先进后出(FILO).基于 deque 实现 。只有一个出口。只允许操作(新增、移除、取得)最顶端的元素,其他元素无法操作。
插入:O(1)
查看栈顶:O(1)
删除:O(1)
缺点:只能后进先出,不能随机删改查询
1.缺省情况以deque为底部结构。所以stack操作其实都是deque容器的操作。也可以是list、vector作为底部结构.
相对于vector实现stack:deque的扩容代价低,扩容不用拷贝数据,空间浪费较少。
相对于list实现stack:deque不用频繁申请和释放小块内存空间,CPU高速缓存命中率高,申请和释放内存空间的次数少。
2.只有最顶层的操作,对应的函数是push、pop、top函数。
3.其他元素无法操作,对应就没有迭代器,也没有走访功能。
stack<int> mstack;
mstack.push(1);
mstack.push(2);
int astack = mstack.top();
mstack.pop();
4.也可以list作为stack的底层容器。
stack<int, list<int> > st;
st.push(1);
st.push(2);
int ast = st.top();
st.pop();
底层实现原理的数据结构
(1)使用数组实现栈: 不能循环
class Stack {
static final int MAX = 1000;
int top;
int a[] = new int[MAX]; // Maximum size of Stack
boolean isEmpty() {
return (top < 0);
}
Stack() {
top = -1;
}
// 入栈
boolean push(int x) {
if (top >= (MAX - 1)) {
System.out.println("Stack Overflow");
return false;
} else {
a[++top] = x;
System.out.println(x + " pushed into stack");
return true;
}
}
//出栈
int pop() {
if (top < 0) {
System.out.println("Stack Underflow");
return 0;
} else {
int x = a[top--];
return x;
}
}
//查看栈顶元素
int peek() {
if (top < 0) {
System.out.println("Stack Underflow");
return 0;
} else {
int x = a[top];
return x;
}
}
void print() {
for (int i = top; i > -1; i--) {
System.out.print(" " + a[i]);
}
}
}
// Driver code
class Main {
public static void main(String args[]) {
Stack s = new Stack();
s.push(10);
s.push(20);
s.push(30);
System.out.println(s.pop() + " Popped from stack");
System.out.println("Top element is :" + s.peek());
System.out.print("Elements present in stack :");
s.print();
}
}
(2)使用链表实现堆栈:
public class StackAsLinkedList {
StackNode root;
static class StackNode {
int data;
StackNode next;
StackNode(int data) {
this.data = data;
}
}
public boolean isEmpty() {
if (root == null) {
return true;
} else {
return false;
}
}
public void push(int data) {
StackNode newNode = new StackNode(data);
if (root == null) {
root = newNode;
} else {
StackNode temp = root;
root = newNode;
newNode.next = temp;
}
System.out.println(data + " pushed to stack");
}
public int pop() {
int popped = Integer.MIN_VALUE;
if (root == null) {
System.out.println("Stack is Empty");
} else {
popped = root.data;
root = root.next;
}
return popped;
}
public int peek() {
if (root == null) {
System.out.println("Stack is empty");
return Integer.MIN_VALUE;
} else {
return root.data;
}
}
// Driver code
public static void main(String[] args) {
StackAsLinkedList sll = new StackAsLinkedList();
sll.push(10);
sll.push(20);
sll.push(30);
System.out.println(sll.pop() + " popped from stack");
System.out.println("Top element is " + sll.peek());
}
}
queue
是先进先出(FIFO).默认是基于 deque 实现,也可以是list、vector作为底部结构
插入:O(1)
查看队首:O(1)
删除:O(1) //删除只能是删除头,如果是O(1),意味着头的位置一直在变动,头位置一直往后走。
缺点:只能先进先出,不能随机删改查询
1.queue可以访问两端.即调用front、back函数。
2.只允许从最头端删除,从最尾端添加。即调用pop、push函数。
3.queue<Type, Container> (<数据类型,容器类型>)
初始化时必须要有数据类型,容器可省略,省略时则默认为deque 类型。
也可以是vector、list。相对于list实现queue:deque不用频繁申请和释放小块内存空间,CPU高速缓存命中率高,申请和释放内存空间的次数少。
- push() 在队尾插入一个元素
- pop() 删除队列第一个元素
- size() 返回队列中元素个数
- empty() 如果队列空则返回true
- front() 返回队列中的第一个元素
- back() 返回队列中最后一个元素
4.queue没有迭代器,也不提供走访功能。
5.可以list做为queue的底层容器
queue<int, list<int>> que;
que.push(1);
que.push(2);
que.push(3);
que.push(4);
que.pop();
que.front();
que.back();
队列底层实现数据结构
(1)使用数组实现队列: 使用循环机制
#include <stdio.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front;
int rear;
} Queue;
void initQueue(Queue *queue) {
queue->front = -1;
queue->rear = -1;
}
int isEmpty(Queue *queue) {
return queue->front == -1;
}
int isFull(Queue *queue) {
return (queue->rear + 1) % MAX_SIZE == queue->front;
}
void enqueue(Queue *queue, int element) {
if (isFull(queue)) {
printf("Error: Queue is full\n");
return;
}
if (isEmpty(queue)) {
queue->front = 0;
}
queue->rear = (queue->rear + 1) % MAX_SIZE;
queue->data[queue->rear] = element;
}
int dequeue(Queue *queue) {
if (isEmpty(queue)) {
printf("Error: Queue is empty\n");
return -1;
}
int element = queue->data[queue->front];
if (queue->front == queue->rear) {
queue->front = -1;
queue->rear = -1;
} else {
queue->front = (queue->front + 1) % MAX_SIZE;
}
return element;
}
int front(Queue *queue) {
if (isEmpty(queue)) {
printf("Error: Queue is empty\n");
return -1;
}
return queue->data[queue->front];
}
int main() {
Queue queue;
initQueue(&queue);
enqueue(&queue, 10);
enqueue(&queue, 20);
enqueue(&
queue, 30);
printf("Front: %d\n", front(&queue)); // 输出:Front: 10
printf("Dequeue: %d\n", dequeue(&queue)); // 输出:Dequeue: 10
printf("Dequeue: %d\n", dequeue(&queue)); // 输出:Dequeue: 20
printf("Is Empty: %d\n", isEmpty(&queue)); // 输出:Is Empty: 0
return 0;
}
(2)使用链表实现队列:
#pragma once
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>
typedef int QDataType;
typedef struct QueueNode//队列的节点结构
{
QDataType date; //节点数据
struct QueueNode* next; //节点指针
}QueueNode;
typedef struct QueuePtr //队列的链式结构
{
QueueNode* phead;//头指针
QueueNode* ptail;//尾指针
int size;
}Q;
// 队列常用接口定义 ///
/* 队列:初始化 */
void QueueInit(Q* q);
/* 队列:销毁 */
void QueueDestroy(Q* q);
/* 队列:尾插 */
void QueuePush(Q* q, QDataType val);
/* 队列:头出 */
void QueuePop(Q* q);
/* 队列:获取头部数据 */
QDataType QueueFront(Q* q);
/* 队列:获取尾部数据 */
QDataType QueueBack(Q* q);
/* 队列:获取队列有效元素个数 */
size_t QueueSize(Q* q);
/* 队列:检查是否为空栈 */
bool QueueEmpty(Q* q);
#define _CRT_SECURE_NO_WARNINGS 1
#include"queue.h"
/* 队列:初始化 */
void QueueInit(Q* q)
{
assert(q);
q->phead = q->ptail = NULL;
q->size = 0;
}
/* 队列:检查是否为空栈 */
bool QueueEmpty(Q* q)
{
return q->size == 0;
}
/* 队列:尾插 */
void QueuePush(Q* q, QDataType val)
{
assert(q);
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
perror("malloc fail:");
return -1;
}
newnode->date = val;
newnode->next = NULL;
if (q->phead == NULL)
{
assert(q->ptail==NULL);
q->phead = q->ptail = newnode;
}
else
{
q->ptail->next = newnode;
q->ptail = newnode;
}
q->size++;
}
/* 队列:头出 */
void QueuePop(Q* q)
{
assert(q);
assert(!QueueEmpty(q));
assert(q->size > 0);
QueueNode* cur = q->phead;
QueueNode* ne = q->phead->next;
free(cur);
q->phead = ne;
if (q->phead == NULL)
{
q->ptail = NULL;
}
q->size--;
}
/* 队列:获取头部数据 */
QDataType QueueFront(Q* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->phead->date;
}
/* 队列:获取尾部数据 */
QDataType QueueBack(Q* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->ptail->date;
}
/* 队列:获取队列有效元素个数 */
size_t QueueSize(Q* q)
{
assert(q);
QueueNode* cur = q->phead;
int size = 0;
while (cur)
{
cur = cur->next;
size++;
}
return size;
}
/* 队列:销毁 */
void QueueDestroy(Q* q)
{
assert(q);
QueueNode* cur = q->phead;
while (cur)
{
QueueNode* ne = cur->next;
free(cur);
cur = ne;
}
q->phead = q->ptail = NULL;
}
heap(堆)
扮演priority queue的助手,允许以任何次序将任何元素插入容器,但取出时一定是从数组最高的元素开始。
1.使用的原理是binary heap.所谓binary heap就是一种完全二叉树。
2.binary heap的特点有某个节点处在vector的i处,其左子节点必位于2i处,其右节点必位于2i+1处、其父节点必位于i/2(表示取整)。
3.heap本质就是一个vector和一个heap算法。STL提供的是max-heap方式(每个节点的键值都大于或等于其子节点键值)。在heap算法的二叉树上,从顶层往底层方向,值越小。
max-heap: 每个节点的键值都大于或等于其子节点键值
min-heap: 每个节点的键值都小于或等于其子节点键值
所以在不管添加还是删除元素后,还是要保持该性质要求。
4.make_heap算法://将一个现有的数据转化成一个heap。满足max_heap的特点。
5.push_heap算法:把新元素插入在底层vector的end()出处。通过调整其所在的位置(方法是:将新节点和其父节点比较,如果其键值大于父节点,则和父子节点对换位置,如此一致上溯,直到不需要对换或到根节点为止),使每个节点的键值都大于或等于其子节点键值。结果是:最大值(根节点)在树的最上层,对应底层vector的最开始位置。
int ia[9] = { 0,1,2,3,4,8,9,3,5 };
vector<int> ivec(ia, ia + 9);
make_heap(ivec.begin(), ivec.end());//将一个现有的数据转化成一个heap
for (int i = 0; i < ivec.size(); ++i)
{
cout << ivec[i] << endl; //958340231
}
ivec.push_back(7);
push_heap(ivec.begin(), ivec.end());
6.pop_heap算法:取走根节点,对于vector而言,其实是移至容器vector最后一个元素。方法和push_heap类似,将最大值移走后(这样形成一个洞),用上述失去生命空间的叶子节点(即原来vector最后面的一个元素)填入到这个洞。同时将该失去生命空间的叶子节点和该洞的2个子节点比较键值,并与较大的节点对调位置。直到键值大于左右两个子节点或者到最后叶子节点。结果是取到键值最大的元素在vector的最后面(即从vector最前面的值赋值给vector最后面的值)。
int ia[9] = { 0,1,2,3,4,8,9,3,5 };
vector<int> ivec(ia, ia + 9);
make_heap(ivec.begin(), ivec.end());//将一个现有的数据转化成一个heap/
for (int i = 0; i < ivec.size(); ++i)
{
cout << ivec[i] << endl; //958340231
}
ivec.push_back(7);
push_heap(ivec.begin(), ivec.end());
pop_heap(ivec.begin(), ivec.end());
ivec.pop_back();
7.sort_heap算法:持续对整个heap最pop_heap操作,便可得到一个递增序列。也就是堆排序了
int ia[9] = { 0,1,2,3,4,8,9,3,5 };
vector<int> ivec(ia, ia + 9);
make_heap(ivec.begin(), ivec.end());//将一个现有的数据转化成一个heap/
for (int i = 0; i < ivec.size(); ++i)
{
cout << ivec[i] << endl; //958340231
}
ivec.push_back(7);
push_heap(ivec.begin(), ivec.end());
pop_heap(ivec.begin(), ivec.end());
ivec.pop_back();
sort_heap(ivec.begin(), ivec.end());
for (int i = 0; i < ivec.size(); ++i)
{
cout << ivec[i] << endl; //012334578
}
6.heap 没有迭代器,也不能进行走访功能。
priority_queue
优先队列基于 vector、heap实现
插入:O(logN)
查看最值:O(1)
删除:O(logN)
1.priority_queue 是自动按照元素的最高值排在最前面。所以top函数得到容器的最大值。
2.缺省情况下,priority_queue 利用max-heap 完成, 那底层本质是vector容器。那是没有迭代器,也不提供遍历功能。
3.其实这些都容器适配器。
4.利用push在尾部增加元素,pop在开始处删除元素。即使push或者pop操作后,top还是当前的容器中最大值,也就是最大值一直在顶层。
struct cmp {
bool operator() (int a, int b)
{
return a > b;
}
};
int ib[9] = { 0,1,2,3,4,8,9,3,5 };
priority_queue<int> ipq(ib, ib + 9);//默认是大堆,即大到小
priority_queue<int, vector<int>, cmp> ipq1(ib, ib + 9);//小堆,需要自定义仿函数
//或者直接调用 greater<int>
priority_queue<int, vector<int>, greater<int>> ipq2(ib, ib + 9);
cout << ipq.top() << endl; //9
while (!ipq.empty())
{
cout << ipq.top() << endl;
ipq.pop();
}
关联式容器
使用关联式容器存储的元素,都是一个一个的“键值对”( <key,value> ),这是和序列式容器最大的不同。除此之外,序列式容器中存储的元素默认都是未经过排序的,而使用关联式容器存储的元素,默认会根据各元素的键值的大小做升序排序。
它的功能是在使用关联式容器的过程中,如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式。
关联式容器名称 | 特点 |
---|---|
map(映射表) | 定义在 <map> 头文件中,使用该容器存储的数据,其各个元素的键必须是唯一的(即不能重复),该容器会根据各元素键的大小,默认进行升序排序(调用 std::less<T>)。 |
set(集合) | 定义在 <set> 头文件中,使用该容器存储的数据,各个元素键和值完全相同,且各个元素的值不能重复(保证了各元素键的唯一性)。该容器会自动根据各个元素的键(其实也就是元素值)的大小进行升序排序(调用 std::less<T>)。 |
multimap | 定义在 <map> 头文件中,和 map 容器唯一的不同在于,multimap 容器中存储元素的键可以重复。 |
multiset | 定义在 <set> 头文件中,和 set 容器唯一的不同在于,multiset 容器中存储元素的值可以重复(一旦值重复,则意味着键也是重复的)。 |
1.关联式容器的底层实现采用的树存储结构,更确切的说是红黑树结构;
2.关联溶蚀没有所谓的头尾(只有最大值、最小值)。也就没有push_back,push_front、begin、end这样的操作。
树的介绍见:二叉树、平衡二叉树、红黑树_小飞侠hello的博客-CSDN博客
set、multiset、map、multimap都是基于红黑树, 的时间复杂度一样,都是
插入:O(logN)
查看:O(logN)
删除:O(logN) 删除函数都是各自容器的erase函数
主要适用于插入、删除、查找混合一起使用的场景。
set(集合)
1.set的键值就是实值,实值就是键值。
2.不允许2个元素有相同的键值。
3.不可以通过set的迭代器改变set的元素。即set的迭代器是const_iterator。可通过const_cast去掉const属性。
4.set底层是RB_tree(红黑树)。所以set的操作其实都是红黑树的操作。具体是用insert_unique.
5.缺省采用递增排序。
6.插入和删除元素的时间效率都是o(logN)
int ic[9] = { 0,1,2,3,4,8,9,3,5 };
set<int> iset(ic, ic + 9);
cout << iset.size() << endl;
iset.insert(10);
cout << iset.size() << endl;
iset.insert(1);
cout << iset.size() << endl;
iset.erase(4);
cout << iset.size() << endl;
set<int>::iterator it;
for (it = iset.begin(); it!= iset.end(); it++)
{
cout << *it << endl;
}
for (auto iter:iset)
{
cout << iter << endl;
}
map(映射)
1.所有元素都会按照元素的键值自动被排序,默认是递增排序。
2.元素是pair,同时拥有键值(key)(第一个元素 first)和实值(value)(第二个元素second).
3.不允许2个元素有相同的键值,但可以有相同的实值。
4.不可以通过set的迭代器改变map的键值。但可以改变是map的实值。
5.map底层是RB_tree(红黑树)。所以map的操作其实都是红黑树的操作。具体是用insert_unique.
6.插入和删除元素的时间效率都是o(logN)
map<string, int> simap;
simap["ab"] = 1;
simap["qw"] = 2;
simap["dw"] = 3;
simap.insert(pair<string,int>("fd",6));
map<string, int>::iterator mapitor;
for (mapitor = simap.begin();mapitor != simap.end();++mapitor)
{
cout << mapitor->first << ' ' << mapitor->second << endl;
}
mapitor = simap.find("qw");
mapitor->second = 8;
Multiset
multiset和set唯一的差别是允许键值重复。具体是用底层RB-tree 的insert_equal而非insert_unique。
int ie[9] = { 0,1,2,3,4,8,9,3,5 };
multiset<int> imuset(ie, ie + 9);
cout << imuset.size() << endl;
imuset.insert(2);
cout << imuset.size() << endl;
imuset.insert(1);
cout << imuset.size() << endl;
imuset.erase(4);
cout << imuset.size() << endl;
multiset<int>::iterator muit;
for (muit = imuset.begin(); muit != imuset.end(); muit++)
{
cout << *muit << endl;
}
multimap
multimap和map的唯一区别是允许键值重复。具体是用底层RB-tree 的insert_equal而非insert_unique.
所以;
1.multimap 不支持下标运算符,因为键并不能确定一个唯一元素。即不能使用multimap[key] = value;只能使用insert函数插入元素。
multimap<string, int> musimap;
musimap.insert(pair<string, int>("ab", 1));
musimap.insert(pair<string, int>("fd", 21));
musimap.insert(pair<string, int>("fd", 6));
musimap.insert(pair<string, int>("gd", 7));
for (auto mumapitor = musimap.begin(); mumapitor != musimap.end(); ++mumapitor)
{
cout << mumapitor->first << ' ' << mumapitor->second << endl;
}
auto musetpr = musimap.equal_range("fd");
if (musetpr.first != musimap.end())
{
for (auto iter = musetpr.first; iter != musetpr.second; ++iter)
std:cout << iter->first << " is " << iter->second << std::endl;
}
auto musetpr1 = musimap.find("fd");//返回第一个
musetpr1->second = 10;
musimap.erase("fd");//如果有相同的键,相同的键的元素删除
哈希容器(无序容器)
这类容器的实现底层原理是散列表(哈希表)hashtable.
散列表内部是有一个vector做桶子和一个link list 做节点组成的。其中该list 是单向,只有往后的迭代器。
具体见:散列表(哈希表)_小飞侠hello的博客-CSDN博客
无序容器 | 功能 |
---|---|
unordered_map (hash_map) | 存储键值对 <key, value> 类型的元素,其中各个键值对键的值不允许重复,且该容器中存储的键值对是无序的。 |
unordered_multimap(hash_multimap) | 和 unordered_map 唯一的区别在于,该容器允许存储多个键相同的键值对。 |
unordered_set(hash_set) | 不再以键值对的形式存储数据,而是直接存储数据元素本身(当然也可以理解为,该容器存储的全部都是键 key 和值 value 相等的键值对,正因为它们相等,因此只存储 value 即可)。另外,该容器存储的元素不能重复,且容器内部存储的元素也是无序的。 |
unordered_multiset(hash_multiset) | 和 unordered_set 唯一的区别在于,该容器允许存储值相同的元素。 |
以后使用还是使用unordered_ 这样形式的容器。
上述四种容器采用哈希表实现,不同操作的时间复杂度为:
插入:O(1),最坏情况O(N)。
查看:O(1),最坏情况O(N)。
删除:O(1),最坏情况O(N)。
哈希容器提供的常数查找能力优于关联容器提供的对数查找能力。
基于底层实现采用了不同的数据结构,因此和关联式容器相比,无序容器具有以下 2 个特点:
- 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,
- 和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1));但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。
hash_set
1.底层使用hashtable, 即其hash_set 结构内部函数hashtable 变量。所有hash_set的操作都是转化成hashtable 的操作。默认桶子的大小为100,相对应最小的质数为197.
2.元素无排序功能。
3.不可以插入相同键值得元素。
hash_set<int> hset;
hset.insert(3);
hset.insert(196);
hset.insert(1);
hset.insert(389);
hset.insert(194);
hset.insert(387);
hset.insert(1);
hash_set<int>::iterator hsetitor;
for (hsetitor = hset.begin();hsetitor != hset.end();++hsetitor)
{
cout << *hsetitor << ' ';
}
hsetitor = hset.find(196);
在vs2017中,hash_map是C++非标准STL,因为标准化的推进,hash_map属于非标准容器,未来将要用unordered_map替代之。建议我们使用unorder_map替代hash_map,解决办法
(1)使用<unordered_map>替换<hash_map> 或者
(2)加宏定义忽略这个错误
#define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS //添加这个宏定义即不报错
#ifndef _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS
static_assert(false, "<hash_map> is deprecated and will be REMOVED. "
"Please use <unordered_map>. You can define "
"_SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS "
"to acknowledge that you have received this warning.");
#endif /* _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS */
unordered_set
unordered_set<int> unset;
unset.insert(3);
unset.insert(196);
unset.insert(1);
unset.insert(389);
unset.insert(194);
unset.insert(387);
unset.insert(1);
unordered_set<int>::iterator unsetitor;
for (unsetitor = unset.begin(); unsetitor != unset.end(); ++unsetitor)
{
cout << *unsetitor << ' ';
}
unsetitor = unset.find(196);
hash_multiset
1. hash_multiset 和hash_set 的唯一差别是前者元素的插入调用hashtable的insert_equal,后者采用insert_unique.所以hash_multiset可以插入相同键值的元素。
2.元素无排序功能。
hash_multiset<int> hset;
hset.insert(3);
hset.insert(196);
hset.insert(1);
hset.insert(389);
hset.insert(194);
hset.insert(387);
hset.insert(1);
hash_set<int>::iterator hsetitor;
for (hsetitor = hset.begin();hsetitor != hset.end();++hsetitor)
{
cout << *hsetitor << ' ';
}
hsetitor = hset.find(196);
unordered_multiset
unordered_multiset<int> unset;
unset.insert(3);
unset.insert(196);
unset.insert(1);
unset.insert(389);
unset.insert(194);
unset.insert(387);
unset.insert(1);
unordered_multiset<int>::iterator unsetitor;
for (unsetitor = unset.begin(); unsetitor != unset.end(); ++unsetitor)
{
cout << *unsetitor << ' ';
}
unsetitor = unset.find(196);
hash_map
1.底层使用hashtable, 即其hash_map结构内部函数hashtable 变量。所有hash_smap的操作都是转化成hashtable 的操作。默认桶子的大小为100,相对应最小的质数为197.
2.元素无排序功能。
3.不可以插入相同键值得元素。
hash_map<string, int> days;
days["jan"] = 31;
days["feb"] = 28;
days["mar"] = 31;
days["apr"] = 30;
hash_map<string, int>::iterator hmapitor;
for (hmapitor = days.begin(); hmapitor != days.end(); ++hmapitor)
{
cout << hmapitor->first << ' ';
}
unordered_map
unordered_map<string, int> days;
days["jan"] = 31;
days["feb"] = 28;
days["mar"] = 31;
days["apr"] = 30;
unordered_map<string, int>::iterator hmapitor;
for (hmapitor = days.begin(); hmapitor != days.end(); ++hmapitor)
{
cout << hmapitor->first << ' ';
}
hash_multimap
1. hash_multimap 和hash_map 的唯一差别是前者元素的插入调用hashtable的insert_equal,后者采用insert_unique.所以hash_multiset可以插入相同键值的元素。
2.元素无排序功能。
hash_multimap<string, int> mudays;
mudays.insert(pair<string, int>("jan", 31));
mudays.insert(pair<string, int>("feb", 28));
mudays.insert(pair<string, int>("mar", 31));
mudays.insert(pair<string, int>("apr", 30));
mudays.insert(pair<string, int>("feb", 31));
hash_map<string, int>::iterator hmumapitor;
for (hmumapitor = mudays.begin(); hmumapitor != mudays.end(); ++hmumapitor)
{
cout << hmumapitor->first << ' ';
}
unordered_multimap
unordered_multimap<string, int> mudays;
mudays.insert(pair<string, int>("jan", 31));
mudays.insert(pair<string, int>("feb", 28));
mudays.insert(pair<string, int>("mar", 31));
mudays.insert(pair<string, int>("apr", 30));
mudays.insert(pair<string, int>("feb", 31));
unordered_multimap<string, int>::iterator hmumapitor;
for (hmumapitor = mudays.begin(); hmumapitor != mudays.end(); ++hmumapitor)
{
cout << hmumapitor->first << ' ';
}
总结:
1) 如果需要随机访问,用vector
2) 如果存储元素的数目已知,用vector
3) 需要任意位置随机插入删除,用list
4) 只有需要更多在容器的首部尾部插入删除元素,用deque
5) 如果需要在一组动态的数据中随时取最值,用priority_queue
6) 如果操作是基于键值,用set map
7)stack更适用于DFS,queue更适用于BFS
迭代器失效的情况
(1)插入操作
对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效; 对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效; 对于list和forward_list,所有的iterator,pointer和refercnce有效。
(2)删除操作
对于vector和string,删除点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的; 对于deque,如果删除点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效; 对于list和forward_list,所有的iterator,pointer和refercnce有效。 对于关联容器map来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。