赞
踩
容器种类 | 功能 |
---|---|
序列容器 | 主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。 |
关联容器 | 包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。关联容器内的元素是排序的。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。 |
哈希容器(散列容器) | C++ 11 新加入 4 种无序容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。 |
连续内存容器:vector、string、deque
基于节点的容器:list、关联容器、哈希容器
序列容器只是一类容器的统称,并不指具体的某个容器,序列容器大致包含以下几类容器:
时间复杂度:
尾部插入(或删除):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的时候,会先建一个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;
- }
- }
插入: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();
是先进后出(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());
- }
- }
是先进先出(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高速缓存命中率高,申请和释放内存空间的次数少。
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;
- }
扮演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 没有迭代器,也不能进行走访功能。
优先队列基于 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函数
主要适用于插入、删除、查找混合一起使用的场景。
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;
- }
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和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和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 个特点:
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<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);
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<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);
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<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 << ' ';
- }
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<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来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。