当前位置:   article > 正文

STL源码剖析之容器_向量这种容器可以用o(1)时间找到第n个元素吗

向量这种容器可以用o(1)时间找到第n个元素吗
容器种类功能
序列容器主要包括 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)。也就是说,如果你创建了一个存放基类对象的容器,却向其中插入派生类的对象,那么在派生类对象(通过基类的拷贝构造函数)被拷贝进容器时,它所特有的部分(即派生类中的信息)将会丢失。”剥离”问题意味着向基类对象的容器中插入派生类对象几乎总是错误的。使拷贝动作高效、正确,并防止剥离问题发生的一个简单办法是使容器包含指针而不是对象

  1. class Widget {};
  2. class SpecialWidget : public Widget {};
  3. int test_item_3()
  4. {
  5. std::vector<Widget> vw;
  6. SpecialWidget sw;
  7. vw.push_back(sw); // sw作为基类对象被拷贝进vw中,它的派生类特有部分在拷贝时被丢掉了
  8. return 0;
  9. }

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()个调用了。

  1. // 删除c中所有值为1963的元素
  2. std::vector<int> c1{1,1963,4,233,555,1963,2000};
  3. c1.erase(std::remove(c1.begin(), c1.end(), 1963), c1.end()); // 当c1是vector, string或deque时,erase-remove习惯用法是删除特定值的元素的最好办法

具体remove介绍见:https://blog.csdn.net/baidu_16370559/article/details/124795090?csdn_share_tail=%7B"type"%3A"blog"%2C"rType"%3A"article"%2C"rId"%3A"124795090"%2C"source"%3A"baidu_16370559"%7D&ctrtid=hd4YV

延伸:

  1. vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
  2. vec.shrink_to_fit():请求容器降低其capacity和size匹配。
  3. vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。
  4. vector<int> vec33(5, 10);
  5. vector<int>().swap(vec33);
  6. //或者
  7. vector<int> vec34(4, 5);
  8. vec34.clear();
  9. 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():改变容器的大小。如果新的大小小于原来的,容量保持不变,只改容器大小。

  1. std::vector<int> vecint{ 1,5,4,7 };
  2. vecint.push_back(2);
  3. vecint.pop_back();
  4. vector<int>::iterator iter;
  5. iter = vecint.begin();
  6. int nit = *iter;
  7. iter++;
  8. std::sort(vecint.begin(), vecint.end(), [](int a, int b)->bool {return a > b; });

10. assign()可以实现区间赋值。通过迭代器的方式,减少拷贝和构造的次数。

  1. vector<int> vecint;
  2. vector<int> vecint2(10,3);
  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刻意指向尾端的一个空白节点。

  1. iterator begin() {
  2. return (link_type)((*node).next);
  3. }
  4. iterator end() { return node; }

3.list 的缺省构造函数,会调用一个empty_initialize函数,产生一个空链表。

  1. void empty_initialize() {
  2. node = get_node();//底层本质调用alloc配置空间
  3. node->next = node;
  4. node->prev = node;
  5. }

 1.插入操作(insert)和结合操作(splice)都不会导致原有的list迭代器失效。甚至删除操作(erase)也只是被删除的元素的那个迭代器失效,其他迭代器不受影响。 

5.put_back 会调用insert函数。所以指针node很重要,所有在最后新增的节点都是依赖于指针node节点。

  1. void push_back(const T& x)
  2. {
  3. insert(end(), x);
  4. }
  5. iterator insert(iterator position, const T& x)
  6. {
  7. link_type tmp = create_node(x); //先调用get_node函数配置空间,再调用构造函数construct初始化数据
  8. tmp->next = position.node;
  9. tmp->prev = position.node->prev;
  10. (link_type(position.node->prev))->next = tmp;
  11. position.node->prev = tmp;
  12. return tmp;
  13. }

 6.push_front 调用insert(begin(),x);

  1. void push_front(const T& x)
  2. {
  3. insert(begin(), x);
  4. }

7.erase先调整next和prev的node链接,再对当前的节点node进行调用析构函数并删除内存。这点和vector是不一样的。vector是不删除内存的。

  1. iterator erase(iterator position) {
  2. link_type next_node = link_type(position.node->next);
  3. link_type prev_node = link_type(position.node->prev);
  4. prev_node->next = next_node;
  5. next_node->prev = prev_node;
  6. destroy_node(position.node);//先调用析构函数,再释放空间配置器
  7. return iterator(next_node);
  8. }

对list删除节点最好的办法是调用list::remove(). 

8.clear会对所有节点进行析构并释放内存。这点和vector是不一样的

9. 调用empty而不是检查size()是否为0。

empty对所有的标准容器都是常数时间操作,而对一些list实现,size耗费线性时间。

  1. list<int> listint{ 1,3 };
  2. listint.push_back(4);
  3. listint.push_front(2);
  4. listint.pop_back();
  5. listint.pop_front();
  6. listint.insert(listint.begin(), 11);
  7. 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)。

  1. struct MyListNode
  2. {
  3. int nValue;
  4. MyListNode *pPrevNode;
  5. MyListNode *pNextNode;
  6. };
  7. void offertest::deleteNode(MyListNode **pHead, MyListNode *tobeDelete)
  8. {
  9. if (pHead == nullptr || *pHead == nullptr || tobeDelete == nullptr)
  10. return;
  11. if (tobeDelete->pNextNode != nullptr) //要删除的节点不是尾节点
  12. {
  13. MyListNode *pNext = tobeDelete->pNextNode;
  14. tobeDelete->nValue = pNext->nValue;
  15. tobeDelete->pNextNode = pNext->pNextNode;
  16. delete pNext;
  17. pNext = nullptr;
  18. }
  19. else if (*pHead == tobeDelete)//只有一个节点
  20. {
  21. delete tobeDelete;
  22. tobeDelete = nullptr;
  23. *pHead = nullptr;
  24. }
  25. else//链表有多个节点,删除尾结点
  26. {
  27. MyListNode *pcur = *pHead;
  28. while (pcur->pNextNode != tobeDelete)
  29. pcur = pcur->pNextNode;
  30. pcur->pNextNode = nullptr;
  31. delete tobeDelete;
  32. tobeDelete = nullptr;
  33. }
  34. }

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()个调用了。

  1. deque<int> deqint{1,3};
  2. deqint.push_back(4);
  3. deqint.push_front(2);
  4. deqint.pop_back();
  5. deqint.pop_front();
  6. deqint.insert(deqint.begin(), 11);
  7. deqint.erase(deqint.begin());
  8. 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.其他元素无法操作,对应就没有迭代器,也没有走访功能。

 

  1. stack<int> mstack;
  2. mstack.push(1);
  3. mstack.push(2);
  4. int astack = mstack.top();
  5. mstack.pop();

4.也可以list作为stack的底层容器。

  1. stack<int, list<int> > st;
  2. st.push(1);
  3. st.push(2);
  4. int ast = st.top();
  5. st.pop();

 底层实现原理的数据结构

  1. 1)使用数组实现栈: 不能循环
  2. class Stack {
  3. static final int MAX = 1000;
  4. int top;
  5. int a[] = new int[MAX]; // Maximum size of Stack
  6. boolean isEmpty() {
  7. return (top < 0);
  8. }
  9. Stack() {
  10. top = -1;
  11. }
  12. // 入栈
  13. boolean push(int x) {
  14. if (top >= (MAX - 1)) {
  15. System.out.println("Stack Overflow");
  16. return false;
  17. } else {
  18. a[++top] = x;
  19. System.out.println(x + " pushed into stack");
  20. return true;
  21. }
  22. }
  23. //出栈
  24. int pop() {
  25. if (top < 0) {
  26. System.out.println("Stack Underflow");
  27. return 0;
  28. } else {
  29. int x = a[top--];
  30. return x;
  31. }
  32. }
  33. //查看栈顶元素
  34. int peek() {
  35. if (top < 0) {
  36. System.out.println("Stack Underflow");
  37. return 0;
  38. } else {
  39. int x = a[top];
  40. return x;
  41. }
  42. }
  43. void print() {
  44. for (int i = top; i > -1; i--) {
  45. System.out.print(" " + a[i]);
  46. }
  47. }
  48. }
  49. // Driver code
  50. class Main {
  51. public static void main(String args[]) {
  52. Stack s = new Stack();
  53. s.push(10);
  54. s.push(20);
  55. s.push(30);
  56. System.out.println(s.pop() + " Popped from stack");
  57. System.out.println("Top element is :" + s.peek());
  58. System.out.print("Elements present in stack :");
  59. s.print();
  60. }
  61. }
  62. 2)使用链表实现堆栈:
  63. public class StackAsLinkedList {
  64. StackNode root;
  65. static class StackNode {
  66. int data;
  67. StackNode next;
  68. StackNode(int data) {
  69. this.data = data;
  70. }
  71. }
  72. public boolean isEmpty() {
  73. if (root == null) {
  74. return true;
  75. } else {
  76. return false;
  77. }
  78. }
  79. public void push(int data) {
  80. StackNode newNode = new StackNode(data);
  81. if (root == null) {
  82. root = newNode;
  83. } else {
  84. StackNode temp = root;
  85. root = newNode;
  86. newNode.next = temp;
  87. }
  88. System.out.println(data + " pushed to stack");
  89. }
  90. public int pop() {
  91. int popped = Integer.MIN_VALUE;
  92. if (root == null) {
  93. System.out.println("Stack is Empty");
  94. } else {
  95. popped = root.data;
  96. root = root.next;
  97. }
  98. return popped;
  99. }
  100. public int peek() {
  101. if (root == null) {
  102. System.out.println("Stack is empty");
  103. return Integer.MIN_VALUE;
  104. } else {
  105. return root.data;
  106. }
  107. }
  108. // Driver code
  109. public static void main(String[] args) {
  110. StackAsLinkedList sll = new StackAsLinkedList();
  111. sll.push(10);
  112. sll.push(20);
  113. sll.push(30);
  114. System.out.println(sll.pop() + " popped from stack");
  115. System.out.println("Top element is " + sll.peek());
  116. }
  117. }

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高速缓存命中率高,申请和释放内存空间的次数少。

  1. push() 在队尾插入一个元素
  2. pop() 删除队列第一个元素
  3. size() 返回队列中元素个数
  4. empty() 如果队列空则返回true
  5. front() 返回队列中的第一个元素
  6. back() 返回队列中最后一个元素

4.queue没有迭代器,也不提供走访功能。

5.可以list做为queue的底层容器

  1. queue<int, list<int>> que;
  2. que.push(1);
  3. que.push(2);
  4. que.push(3);
  5. que.push(4);
  6. que.pop();
  7. que.front();
  8. que.back();

 队列底层实现数据结构

  1. 1)使用数组实现队列: 使用循环机制
  2. #include <stdio.h>
  3. #define MAX_SIZE 100
  4. typedef struct {
  5. int data[MAX_SIZE];
  6. int front;
  7. int rear;
  8. } Queue;
  9. void initQueue(Queue *queue) {
  10. queue->front = -1;
  11. queue->rear = -1;
  12. }
  13. int isEmpty(Queue *queue) {
  14. return queue->front == -1;
  15. }
  16. int isFull(Queue *queue) {
  17. return (queue->rear + 1) % MAX_SIZE == queue->front;
  18. }
  19. void enqueue(Queue *queue, int element) {
  20. if (isFull(queue)) {
  21. printf("Error: Queue is full\n");
  22. return;
  23. }
  24. if (isEmpty(queue)) {
  25. queue->front = 0;
  26. }
  27. queue->rear = (queue->rear + 1) % MAX_SIZE;
  28. queue->data[queue->rear] = element;
  29. }
  30. int dequeue(Queue *queue) {
  31. if (isEmpty(queue)) {
  32. printf("Error: Queue is empty\n");
  33. return -1;
  34. }
  35. int element = queue->data[queue->front];
  36. if (queue->front == queue->rear) {
  37. queue->front = -1;
  38. queue->rear = -1;
  39. } else {
  40. queue->front = (queue->front + 1) % MAX_SIZE;
  41. }
  42. return element;
  43. }
  44. int front(Queue *queue) {
  45. if (isEmpty(queue)) {
  46. printf("Error: Queue is empty\n");
  47. return -1;
  48. }
  49. return queue->data[queue->front];
  50. }
  51. int main() {
  52. Queue queue;
  53. initQueue(&queue);
  54. enqueue(&queue, 10);
  55. enqueue(&queue, 20);
  56. enqueue(&
  57. queue, 30);
  58. printf("Front: %d\n", front(&queue)); // 输出:Front: 10
  59. printf("Dequeue: %d\n", dequeue(&queue)); // 输出:Dequeue: 10
  60. printf("Dequeue: %d\n", dequeue(&queue)); // 输出:Dequeue: 20
  61. printf("Is Empty: %d\n", isEmpty(&queue)); // 输出:Is Empty: 0
  62. return 0;
  63. }
  64. 2)使用链表实现队列:
  65. #pragma once
  66. #include<stdio.h>
  67. #include<string.h>
  68. #include<assert.h>
  69. #include<stdbool.h>
  70. #include<stdlib.h>
  71. typedef int QDataType;
  72. typedef struct QueueNode//队列的节点结构
  73. {
  74. QDataType date; //节点数据
  75. struct QueueNode* next; //节点指针
  76. }QueueNode;
  77. typedef struct QueuePtr //队列的链式结构
  78. {
  79. QueueNode* phead;//头指针
  80. QueueNode* ptail;//尾指针
  81. int size;
  82. }Q;
  83. // 队列常用接口定义 ///
  84. /* 队列:初始化 */
  85. void QueueInit(Q* q);
  86. /* 队列:销毁 */
  87. void QueueDestroy(Q* q);
  88. /* 队列:尾插 */
  89. void QueuePush(Q* q, QDataType val);
  90. /* 队列:头出 */
  91. void QueuePop(Q* q);
  92. /* 队列:获取头部数据 */
  93. QDataType QueueFront(Q* q);
  94. /* 队列:获取尾部数据 */
  95. QDataType QueueBack(Q* q);
  96. /* 队列:获取队列有效元素个数 */
  97. size_t QueueSize(Q* q);
  98. /* 队列:检查是否为空栈 */
  99. bool QueueEmpty(Q* q);
  100. #define _CRT_SECURE_NO_WARNINGS 1
  101. #include"queue.h"
  102. /* 队列:初始化 */
  103. void QueueInit(Q* q)
  104. {
  105. assert(q);
  106. q->phead = q->ptail = NULL;
  107. q->size = 0;
  108. }
  109. /* 队列:检查是否为空栈 */
  110. bool QueueEmpty(Q* q)
  111. {
  112. return q->size == 0;
  113. }
  114. /* 队列:尾插 */
  115. void QueuePush(Q* q, QDataType val)
  116. {
  117. assert(q);
  118. QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
  119. if (newnode == NULL)
  120. {
  121. perror("malloc fail:");
  122. return -1;
  123. }
  124. newnode->date = val;
  125. newnode->next = NULL;
  126. if (q->phead == NULL)
  127. {
  128. assert(q->ptail==NULL);
  129. q->phead = q->ptail = newnode;
  130. }
  131. else
  132. {
  133. q->ptail->next = newnode;
  134. q->ptail = newnode;
  135. }
  136. q->size++;
  137. }
  138. /* 队列:头出 */
  139. void QueuePop(Q* q)
  140. {
  141. assert(q);
  142. assert(!QueueEmpty(q));
  143. assert(q->size > 0);
  144. QueueNode* cur = q->phead;
  145. QueueNode* ne = q->phead->next;
  146. free(cur);
  147. q->phead = ne;
  148. if (q->phead == NULL)
  149. {
  150. q->ptail = NULL;
  151. }
  152. q->size--;
  153. }
  154. /* 队列:获取头部数据 */
  155. QDataType QueueFront(Q* q)
  156. {
  157. assert(q);
  158. assert(!QueueEmpty(q));
  159. return q->phead->date;
  160. }
  161. /* 队列:获取尾部数据 */
  162. QDataType QueueBack(Q* q)
  163. {
  164. assert(q);
  165. assert(!QueueEmpty(q));
  166. return q->ptail->date;
  167. }
  168. /* 队列:获取队列有效元素个数 */
  169. size_t QueueSize(Q* q)
  170. {
  171. assert(q);
  172. QueueNode* cur = q->phead;
  173. int size = 0;
  174. while (cur)
  175. {
  176. cur = cur->next;
  177. size++;
  178. }
  179. return size;
  180. }
  181. /* 队列:销毁 */
  182. void QueueDestroy(Q* q)
  183. {
  184. assert(q);
  185. QueueNode* cur = q->phead;
  186. while (cur)
  187. {
  188. QueueNode* ne = cur->next;
  189. free(cur);
  190. cur = ne;
  191. }
  192. q->phead = q->ptail = NULL;
  193. }

 

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的最开始位置。

  1. int ia[9] = { 0,1,2,3,4,8,9,3,5 };
  2. vector<int> ivec(ia, ia + 9);
  3. make_heap(ivec.begin(), ivec.end());//将一个现有的数据转化成一个heap
  4. for (int i = 0; i < ivec.size(); ++i)
  5. {
  6. cout << ivec[i] << endl; //958340231
  7. }
  8. ivec.push_back(7);
  9. push_heap(ivec.begin(), ivec.end());

6.pop_heap算法:取走根节点,对于vector而言,其实是移至容器vector最后一个元素。方法和push_heap类似,将最大值移走后(这样形成一个洞),用上述失去生命空间的叶子节点(即原来vector最后面的一个元素)填入到这个洞。同时将该失去生命空间的叶子节点和该洞的2个子节点比较键值,并与较大的节点对调位置。直到键值大于左右两个子节点或者到最后叶子节点。结果是取到键值最大的元素在vector的最后面(即从vector最前面的值赋值给vector最后面的值)

  1. int ia[9] = { 0,1,2,3,4,8,9,3,5 };
  2. vector<int> ivec(ia, ia + 9);
  3. make_heap(ivec.begin(), ivec.end());//将一个现有的数据转化成一个heap/
  4. for (int i = 0; i < ivec.size(); ++i)
  5. {
  6. cout << ivec[i] << endl; //958340231
  7. }
  8. ivec.push_back(7);
  9. push_heap(ivec.begin(), ivec.end());
  10. pop_heap(ivec.begin(), ivec.end());
  11. ivec.pop_back();

7.sort_heap算法:持续对整个heap最pop_heap操作,便可得到一个递增序列。也就是堆排序

  1. int ia[9] = { 0,1,2,3,4,8,9,3,5 };
  2. vector<int> ivec(ia, ia + 9);
  3. make_heap(ivec.begin(), ivec.end());//将一个现有的数据转化成一个heap/
  4. for (int i = 0; i < ivec.size(); ++i)
  5. {
  6. cout << ivec[i] << endl; //958340231
  7. }
  8. ivec.push_back(7);
  9. push_heap(ivec.begin(), ivec.end());
  10. pop_heap(ivec.begin(), ivec.end());
  11. ivec.pop_back();
  12. sort_heap(ivec.begin(), ivec.end());
  13. for (int i = 0; i < ivec.size(); ++i)
  14. {
  15. cout << ivec[i] << endl; //012334578
  16. }

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还是当前的容器中最大值,也就是最大值一直在顶层。

  1. struct cmp {
  2. bool operator() (int a, int b)
  3. {
  4. return a > b;
  5. }
  6. };
  7. int ib[9] = { 0,1,2,3,4,8,9,3,5 };
  8. priority_queue<int> ipq(ib, ib + 9);//默认是大堆,即大到小
  9. priority_queue<int, vector<int>, cmp> ipq1(ib, ib + 9);//小堆,需要自定义仿函数
  10. //或者直接调用 greater<int>
  11. priority_queue<int, vector<int>, greater<int>> ipq2(ib, ib + 9);
  12. cout << ipq.top() << endl; //9
  13. while (!ipq.empty())
  14. {
  15. cout << ipq.top() << endl;
  16. ipq.pop();
  17. }

关联式容器

使用关联式容器存储的元素,都是一个一个的“键值对”( <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)

 

 

  1. int ic[9] = { 0,1,2,3,4,8,9,3,5 };
  2. set<int> iset(ic, ic + 9);
  3. cout << iset.size() << endl;
  4. iset.insert(10);
  5. cout << iset.size() << endl;
  6. iset.insert(1);
  7. cout << iset.size() << endl;
  8. iset.erase(4);
  9. cout << iset.size() << endl;
  10. set<int>::iterator it;
  11. for (it = iset.begin(); it!= iset.end(); it++)
  12. {
  13. cout << *it << endl;
  14. }
  15. for (auto iter:iset)
  16. {
  17. cout << iter << endl;
  18. }

map(映射)

1.所有元素都会按照元素的键值自动被排序,默认是递增排序。

2.元素是pair,同时拥有键值(key)(第一个元素 first)和实值(value)(第二个元素second).

3.不允许2个元素有相同的键值,但可以有相同的实值。

4.不可以通过set的迭代器改变map的键值。但可以改变是map的实值。

5.map底层是RB_tree(红黑树)。所以map的操作其实都是红黑树的操作。具体是用insert_unique.

6.插入和删除元素的时间效率都是o(logN)
 

 

 

  1. map<string, int> simap;
  2. simap["ab"] = 1;
  3. simap["qw"] = 2;
  4. simap["dw"] = 3;
  5. simap.insert(pair<string,int>("fd",6));
  6. map<string, int>::iterator mapitor;
  7. for (mapitor = simap.begin();mapitor != simap.end();++mapitor)
  8. {
  9. cout << mapitor->first << ' ' << mapitor->second << endl;
  10. }
  11. mapitor = simap.find("qw");
  12. mapitor->second = 8;

Multiset

multiset和set唯一的差别是允许键值重复。具体是用底层RB-tree 的insert_equal而非insert_unique。

  1. int ie[9] = { 0,1,2,3,4,8,9,3,5 };
  2. multiset<int> imuset(ie, ie + 9);
  3. cout << imuset.size() << endl;
  4. imuset.insert(2);
  5. cout << imuset.size() << endl;
  6. imuset.insert(1);
  7. cout << imuset.size() << endl;
  8. imuset.erase(4);
  9. cout << imuset.size() << endl;
  10. multiset<int>::iterator muit;
  11. for (muit = imuset.begin(); muit != imuset.end(); muit++)
  12. {
  13. cout << *muit << endl;
  14. }

multimap

multimap和map的唯一区别是允许键值重复。具体是用底层RB-tree 的insert_equal而非insert_unique.

所以;
1.multimap 不支持下标运算符,因为键并不能确定一个唯一元素。即不能使用multimap[key] = value;只能使用insert函数插入元素。

  1. multimap<string, int> musimap;
  2. musimap.insert(pair<string, int>("ab", 1));
  3. musimap.insert(pair<string, int>("fd", 21));
  4. musimap.insert(pair<string, int>("fd", 6));
  5. musimap.insert(pair<string, int>("gd", 7));
  6. for (auto mumapitor = musimap.begin(); mumapitor != musimap.end(); ++mumapitor)
  7. {
  8. cout << mumapitor->first << ' ' << mumapitor->second << endl;
  9. }
  10. auto musetpr = musimap.equal_range("fd");
  11. if (musetpr.first != musimap.end())
  12. {
  13. for (auto iter = musetpr.first; iter != musetpr.second; ++iter)
  14. std:cout << iter->first << " is " << iter->second << std::endl;
  15. }
  16. auto musetpr1 = musimap.find("fd");//返回第一个
  17. musetpr1->second = 10;
  18. 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. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,
  2. 和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1));但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。

hash_set

1.底层使用hashtable, 即其hash_set 结构内部函数hashtable 变量。所有hash_set的操作都是转化成hashtable 的操作。默认桶子的大小为100,相对应最小的质数为197.

2.元素无排序功能。

3.不可以插入相同键值得元素。

  1. hash_set<int> hset;
  2. hset.insert(3);
  3. hset.insert(196);
  4. hset.insert(1);
  5. hset.insert(389);
  6. hset.insert(194);
  7. hset.insert(387);
  8. hset.insert(1);
  9. hash_set<int>::iterator hsetitor;
  10. for (hsetitor = hset.begin();hsetitor != hset.end();++hsetitor)
  11. {
  12. cout << *hsetitor << ' ';
  13. }
  14. 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

  1. unordered_set<int> unset;
  2. unset.insert(3);
  3. unset.insert(196);
  4. unset.insert(1);
  5. unset.insert(389);
  6. unset.insert(194);
  7. unset.insert(387);
  8. unset.insert(1);
  9. unordered_set<int>::iterator unsetitor;
  10. for (unsetitor = unset.begin(); unsetitor != unset.end(); ++unsetitor)
  11. {
  12. cout << *unsetitor << ' ';
  13. }
  14. unsetitor = unset.find(196);

 hash_multiset

1. hash_multiset 和hash_set 的唯一差别是前者元素的插入调用hashtable的insert_equal,后者采用insert_unique.所以hash_multiset可以插入相同键值的元素。

2.元素无排序功能。

 

  1. hash_multiset<int> hset;
  2. hset.insert(3);
  3. hset.insert(196);
  4. hset.insert(1);
  5. hset.insert(389);
  6. hset.insert(194);
  7. hset.insert(387);
  8. hset.insert(1);
  9. hash_set<int>::iterator hsetitor;
  10. for (hsetitor = hset.begin();hsetitor != hset.end();++hsetitor)
  11. {
  12. cout << *hsetitor << ' ';
  13. }
  14. hsetitor = hset.find(196);

unordered_multiset

  1. unordered_multiset<int> unset;
  2. unset.insert(3);
  3. unset.insert(196);
  4. unset.insert(1);
  5. unset.insert(389);
  6. unset.insert(194);
  7. unset.insert(387);
  8. unset.insert(1);
  9. unordered_multiset<int>::iterator unsetitor;
  10. for (unsetitor = unset.begin(); unsetitor != unset.end(); ++unsetitor)
  11. {
  12. cout << *unsetitor << ' ';
  13. }
  14. unsetitor = unset.find(196);

hash_map

1.底层使用hashtable, 即其hash_map结构内部函数hashtable 变量。所有hash_smap的操作都是转化成hashtable 的操作。默认桶子的大小为100,相对应最小的质数为197.

2.元素无排序功能。

3.不可以插入相同键值得元素。

 

 

  1. hash_map<string, int> days;
  2. days["jan"] = 31;
  3. days["feb"] = 28;
  4. days["mar"] = 31;
  5. days["apr"] = 30;
  6. hash_map<string, int>::iterator hmapitor;
  7. for (hmapitor = days.begin(); hmapitor != days.end(); ++hmapitor)
  8. {
  9. cout << hmapitor->first << ' ';
  10. }

unordered_map

  1. unordered_map<string, int> days;
  2. days["jan"] = 31;
  3. days["feb"] = 28;
  4. days["mar"] = 31;
  5. days["apr"] = 30;
  6. unordered_map<string, int>::iterator hmapitor;
  7. for (hmapitor = days.begin(); hmapitor != days.end(); ++hmapitor)
  8. {
  9. cout << hmapitor->first << ' ';
  10. }

 hash_multimap

1. hash_multimap 和hash_map 的唯一差别是前者元素的插入调用hashtable的insert_equal,后者采用insert_unique.所以hash_multiset可以插入相同键值的元素。

2.元素无排序功能。

  1. hash_multimap<string, int> mudays;
  2. mudays.insert(pair<string, int>("jan", 31));
  3. mudays.insert(pair<string, int>("feb", 28));
  4. mudays.insert(pair<string, int>("mar", 31));
  5. mudays.insert(pair<string, int>("apr", 30));
  6. mudays.insert(pair<string, int>("feb", 31));
  7. hash_map<string, int>::iterator hmumapitor;
  8. for (hmumapitor = mudays.begin(); hmumapitor != mudays.end(); ++hmumapitor)
  9. {
  10. cout << hmumapitor->first << ' ';
  11. }

unordered_multimap

  1. unordered_multimap<string, int> mudays;
  2. mudays.insert(pair<string, int>("jan", 31));
  3. mudays.insert(pair<string, int>("feb", 28));
  4. mudays.insert(pair<string, int>("mar", 31));
  5. mudays.insert(pair<string, int>("apr", 30));
  6. mudays.insert(pair<string, int>("feb", 31));
  7. unordered_multimap<string, int>::iterator hmumapitor;
  8. for (hmumapitor = mudays.begin(); hmumapitor != mudays.end(); ++hmumapitor)
  9. {
  10. cout << hmumapitor->first << ' ';
  11. }

 

总结:

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来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/960610
推荐阅读
相关标签
  

闽ICP备14008679号