[C++ STL] 各容器简单介绍

什么是STL?

1、STL(Standard Template Library),即标准模板库,是一个高效的C++程序库。

2、包含了诸多常用的基本数据结构和基本算法。为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。

3、从逻辑层次来看,在STL中体现了泛型化程序设计的思想(generic programming)

4、从实现层次看,整个STL是以一种类型参数化(type parameterized)的方式实现的

1 STL组件

STL有六大组件,但主要包含容器、迭代器和算法三个部分。

  • 容器(Containers):用来管理某类对象的集合。每一种容器都有其优点和缺点,所以,为了应付程序中的不同需求,STL 准备了七种基本容器类型。
  • 迭代器(Iterators):用来在一个对象集合的元素上进行遍历动作。这个对象集合或许是个容器,或许是容器的一部分。每一种容器都提供了自己的迭代器,而这些迭代器了解该种容器的内部结构。
  • 算法(Algorithms):用来处理对象集合中的元素,比如 Sort,Search,Copy,Erase 那些元素。通过迭代器的协助,我们只需撰写一次算法,就可以将它应用于任意容器之上,这是因为所有容器的迭代器都提供一致的接口。

STL的基本观念就是将数据和操作分离。数据由容器进行管理,操作则由算法进行,而迭代器在两者之间充当粘合剂,使任何算法都可以和任何容器交互运作。这一篇博客暂时只介绍容器,下一篇介绍迭代器。


2 容器(Containers)

容器用来管理某类对象。为了应付程序中的不同需求,STL 准备了七种基本容器类型:

示意图如下图所示:

1075214-20190111125204355-653568826.png

总的来说,容器可分为两类:

  • vector头部与中间插入删除效率较低,在尾部插入与删除效率高。
  • deque是在头部与尾部插入与删除效率较高。
  • 需要频繁在序列中间位置上进行插入和/或删除操作且不需要过多地在序列内部进行长距离跳转,应该选择list。
  • 关联式容器自动对其元素排序,这并不意味它们就是用来排序的。你也可以对序列式容器的元素加以手动排序。自动排序带来的主要优点是,当你搜寻元素时,
    可获得更佳效率。更明确地说你可以放心使用二分搜寻法,该算法具有对数复杂度,而非线性复杂度,这什么意思呢?如果你想在1000个元素中搜寻某个元素,平均而言只需10次比较,而非500次比较。


2.1 序列式容器 (Sequence Containers)

STL提供了三个序列式容器:向量(vector)、双端队列(deque)、列表(list),此外你也可以把 string 和 array 当做一种序列式容器。

1、vector

概述

vector(向量): 是一种序列式容器,事实上和数组差不多,但它比数组更优越。一般来说数组不能动态拓展,因此在程序运行的时候不是浪费内存,就是造成越界。而vector正好弥补了这个缺陷,它的特征是相当于可分配拓展的数组(动态数组),它的随机访问快,在中间插入和删除慢,但在末端插入和删除快。

特点

  • 拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随机存取,即[]操作符,但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝,另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。
  • 对头部和中间进行插入删除元素操作需要移动内存,如果你的元素是结构或类,那么移动的同时还会进行构造和析构操作,所以性能不高。
  • 对最后元素操作最快(在后面插入删除元素最快),此时一般不需要移动内存,只有保留内存不够时才需要。

优缺点和适用场景

例子

以下例子针对整型定义了一个vector,插入6个元素,然后打印所有元素:

#include "stdafx.h"
#include <iostream>
#include <vector>

using namespace std;

int main(int argc, char* argv[])
{
    vector<int> vecTemp;

    for (int i = 0; i<6; i++)
    {
        vecTemp.push_back(i);
    }

    for (int i = 0; i<vecTemp.size(); i++)
    {
        cout << vecTemp[i] <<" ";
    }

    return 0;
}

 

2、deque

概述

所谓 deque (发音为['dek]),是"double-ended queue” 的缩写。它是一个动态数组(dynamic array),可以向两端发展,因此不论在尾部或头部安插元素都十分迅速。 在中间部分安插元素则比较费时,因为必须移动其它元素。

特点

  • 按页或块来分配存储器的,每页包含固定数目的元素。
  • deque是list和vector的折中方案。兼有list的优点,也有vector随机线性访问效率高的优点。

优缺点和适用场景

例子

以下例子声明了一个浮点类型的dque,并在容器头部插入6个元素,最后打印出所有元素。

#include "stdafx.h"
#include <iostream>
#include <deque>

using namespace std;

int main(int argc, char* argv[])
{
    deque<float> dequeTemp;

    for (int i = 0; i<6; i++)
    {
        dequeTemp.push_back(i);
    }

    for (int i = 0; i<dequeTemp.size(); i++)
    {
        cout << dequeTemp[i] << " ";
    }

    return 0;
}

 

3、list

概述

List由双向链表(doubly linked list)实现而成,元素也存放在堆中,每个元素都是放在一块内存中,他的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随机存取变得非常没有效率,因此它没有提供[]操作符的重载。但是由于链表的特点,它可以很有效率的支持任意地方的插入和删除操作。

特点

  • 没有空间预留习惯,所以每分配一个元素都会从内存中分配,每删除一个元素都会释放它占用的内存。
  • 在哪里添加删除元素性能都很高,不需要移动内存,当然也不需要对每个元素都进行构造与析构了,所以常用来做随机插入和删除操作容器。
  • 访问开始和最后两个元素最快,其他元素的访问时间一样。

优缺点和适用场景

例子

以下例子产生一个空list,准备放置字符,然后将 'a' 至 'z' 的所有字符插入其中,利用循环每次打印并移除集合的第一个元素,从而打印出所有元素:

#include "stdafx.h"
#include <iostream>
#include <list>

using namespace std;

int main(int argc, char* argv[])
{
    list<char> listTemp;

    for (char c = 'a'; c <= 'z'; ++c)
    {
        listTemp.push_back(c);
    }

    while (!listTemp.empty())
    {
        cout <<listTemp.front() << " ";
        listTemp.pop_front();
    }

    return 0;
}

成员函数empty()的返回值告诉我们容器中是否还有元素,只要这个函数返回 false,循环就继续进行。循环之内,成员函数front()会返回第一个元素,pop_front()函数会删除第一个元素。

注意:list完全是性能最低的做法,还不如直接使用list或使用vector好,因为指针没有构造与析构,也不占用很大内存。

 

总结

\vectorlistdeque
特点快速的随机存取,快速的在最后插入删除元素可以快速的在任意位置添加删除元素,只能快速的访问最开始和最后面的元素在开始和最后添加删除元素一样快,并且提供了随机访问的方法
适用需要高效的随机存取,不在于插入删除的效率需要大量的插入和删除操作,不关心随机存取需要随机存取,也需要高效的在两端进行插入删除操作

综上所述三者的相同区别点如下:

相同点:三者都能实现resize()来重新调整容器的大小。

不同点:

  • vector能实现随机存取,即[]操作,而list不能,deque是二者的结合体,也能够实现[]操作,但效率没有vector高。
  • vector适合在文件的尾部实现插入与删除操作,在头部或中部时效率非常低下。而list可以在容器的任何位置实现插入与删除操作。


2.2 关联式容器

关联式容器依据特定的排序准则,自动为其元素排序。排序准则以函数形式呈现,用来比较元素值(value)或元素键(key)。缺省情况下以operator<进行 比较,不过你也可以提供自己的比较函数,定义出不同的排序准则。

通常关联式容器由二叉树(binary tree)实现。在二叉树中,每个元素(节 点)都有一个父节点和两个子节点;左子树的所有元素都比自己小,右子树的所有元素都比自己大。关联式容器的差别主要在于元素的类型以及处理重复元素时的方式。

STL预先定义好的关联式容器有:集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)。

 

1、set

概述

set(集合)由红黑树实现,其内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复。(红黑树是平衡二叉树的一种)

特点

  • set中的元素都是排好序的。
  • set集合中没有重复的元素。
  • map和set的插入删除效率比用其他序列容器高,因为对于关联容器来说,不需要做内存拷贝和内存移动。

优缺点和适用场景

例子

下面的例子演示set(集合)的两个特点:

#include "stdafx.h"
#include <iostream>
#include <set>

using namespace std;

int main(int argc, char* argv[])
{
    set<int> setTemp;

    setTemp.insert(3);
    setTemp.insert(1);
    setTemp.insert(2);
    setTemp.insert(1);

    set<int>::iterator it;
    for (it = setTemp.begin(); it != setTemp.end(); it++)
    {
        cout << *it << " ";
    }

    return 0;
}

输出结果:1 2 3。一共插入了4个数,但是集合中只有3个数并且是有序的,可见之前说过的set集合的两个特点,有序和不重复。

 

当set集合中的元素为结构体时,该结构体必须实现运算符‘<’的重载:

#include "stdafx.h"
#include <iostream>
#include <set>
#include <string>

using namespace std;

struct People
{
    string name;
    int age;

    bool operator <(const People p) const
    {
        return age < p.age;
    }
};

int main(int argc, char* argv[])
{
    set<People> setTemp;

    setTemp.insert({"张三",14});
    setTemp.insert({ "李四", 16 });
    setTemp.insert({ "隔壁老王", 10 });

    set<People>::iterator it;
    for (it = setTemp.begin(); it != setTemp.end(); it++)
    {
        printf("姓名:%s 年龄:%d\n", (*it).name.c_str(), (*it).age);
    }

    return 0;
}

/*
输出结果
姓名:王二麻子 年龄:10
姓名:张三 年龄:14
姓名:李四 年龄:16 
*/ 

可以看到结果是按照年龄由小到大的顺序排列。另外string要使用c_str()转换一下,否则打印出的是乱码。


2、multiset

概述

Multiset和set相同,只不过它允许重复元素,也就是说multiset可包括多个数值相同的元素。这里不再做过多介绍。


3、map

概述

Map由红黑树实现,其元素都是“键值/实值”所形成的一个对组(key/value pairs)。每个元素有一个键,是排序准则的基础。每一个键只能出现一次,不允许重复。

Map主要用于资料一对一映射(one-to-one)的情况,map内部自建一颗红黑树(平衡二叉树中的一种),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的。比如一个班级中,每个学生的学号跟他的姓名就存在着一对一映射的关系。

特点

  • 自动建立Key - value的对应。key 和 value可以是任意你需要的类型。
  • 根据key值快速查找记录,查找的复杂度基本是O(logN),如果有1000个记录,二分查找最多查找10次(1024),1,000,000个记录,二分查找最多查找20次。
  • 增加和删除节点对迭代器的影响很小,除了那个操作节点,对其他的节点都没有什么影响。
  • 对于迭代器来说,可以修改实值,而不能修改key。

优缺点和适用场景

例子

#include "stdafx.h"
#include <iostream>
#include <map>
#include <string>

using namespace std;

int main(int argc, char* argv[])
{
    map<int, string> mapTemp;

    mapTemp.insert({ 5,"张三" });
    mapTemp.insert({ 3, "李四"});
    mapTemp.insert({ 4, "隔壁老王" });

    map<int, string>::iterator it;
    for (it = mapTemp.begin(); it != mapTemp.end(); it++)
    {
        printf("学号:%d 姓名:%s\n", (*it).first, (*it).second.c_str());
    }

    return 0;
}
/*
输出结果:
学号:3 姓名:李四
学号:4 姓名:隔壁老王
学号:5 姓名:张三
*/


4、multiset

概述

multimap和map相同,但允许重复元素,也就是说multimap可包含多个键值(key)相同的元素。这里不再做过多介绍。


2.3 容器配接器

除了以上七个基本容器类别,为满足特殊需求,STL还提供了一些特别的(并且预先定义好的)容器配接器,根据基本容器类别实现而成。包括:

1、stack

名字说明了一切,stack 容器对元素采取LIFO(后进先出)的管理策略。

2、queue

queue 容器对元素采取FIFO(先进先出)的管理策略。也就是说,它是个普通的缓冲区(buffer)。

3、priority_queue

priority_queue 容器中的元素可以拥有不同的优先权。所谓优先权,乃是基于程序员提供的排序准则(缺省使用operators)而定义。Priority queue的效果相当于这样一个buffer: “下一元素永远是queue中优先级最高的元素”。如果同时有多个元素具备最髙优先权,则其次序无明确定义。


3 各容器的共性和区别

各容器一般来说都有下列函数:默认构造函数、复制构造函数、析构函数、empty()、max_size()、size()、operator=、operator<、operator<=、operator>、operator>=、operator==、operator!=、swap()。

顺序容器和关联容器都共有下列函数:


4 扩展

5.1 Vector与数组的异同点

数组是c++中类似vector的数据结构,它们都可以对一种类型进行储存,既都是容器。虽说两者有相似之处,但也有显著的区别,c++ primer的作者说到,在实际的编程中,我们作为程序员应该避免用到低级数组和指针,而更应该多用高级的vector和迭代器。在程序强调速度的情况下,我们程序员可以在类类型的内部使用数组和指针。下面我对vector和数组进行了总结。两者的相同点如下:

  • 都是对同一种类型的数据进行储存。
  • 都可以用下标操作进行处理。
  • 都可以用迭代器进行操作(在c++中每个容器都配有各自的迭代器,数组也是种容器)

两者的区别如下:

  • vector可以用size获取vector的长度,而数组不可以。
  • vector长度不固定,可以随时增加,而数组长度固定,在定义之后就不可以更改。
  • vector可以在末尾增加元素(用push_back),而数组不能增加在长度以外的长度。
  • 可以确定长度,节约空间,不能确定长度,必须在定义时定义一个很大的空间留给数组,造成内存的浪费。


5.2 C++ vector和list的区别

  • vector和数组类似,拥有一段连续的内存空间,而list是由双向链表实现的,因此内存空间是不连续的。
  • vector支持高效的随机存取,时间复杂度为o(1),而list随机存取效率很低,时间复杂度为o(n),只能通过指针访问。
  • vector只有在尾部插入和舔加的效率高,而list在任何位置都能高效地进行插入和删除。


5 小结

在实际使用过程中,到底选择这几种容器中的哪一个,应该根据遵循以下原则:


引用:

C++标准库 - 侯捷》中的5.2节-容器

转载于:https://www.cnblogs.com/linuxAndMcu/p/10254542.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值