STL源码剖析之容器

容器种类功能
序列容器主要包括 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习惯用法是删除特定值的元素的最好办法

具体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

延伸:

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

  1. push() 在队尾插入一个元素
  2. pop() 删除队列第一个元素
  3. size() 返回队列中元素个数
  4. empty() 如果队列空则返回true
  5. front() 返回队列中的第一个元素
  6. 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 个特点:

  1. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,
  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来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值