• C++标准库(体系结构与内核分析)(侯捷第一讲)(标准库介绍、STL、六大部件、算法、容器分类、Array、Vector、List、Forward-List、Deque、stack、queue、multiset、multimap、分配器)


    一、C++标准库介绍

    C++标准库:C++ Standard Library

    C++标准库与STL有什么关系:

      STL:Standard Template Library

      STL包含6大部件,基本占标准库的80%左右内容,而另外20%是一些好用的零碎的东西,所以说C++标准库包含STL。

    • 编译器一定带着一个C++标准库,是以头文件(header files)的形式提供的,并不是编译好的文件,而是源代码。
    • C++标准库的头文件不带扩展名(.h),例如#include <vector>。
    • 对于C语言的标准库,新式的C语言标准库头文件也去除了扩展名(.h),例如#include <cstdio>。
    • 对于某些编译器,旧式的头文件也可以使用,例如#include <stdio.h>。
    • 新式头文件内的组件都封装在命名空间(namespace)std中,例如using namespace std;

     常用写法:

    #include <iostream>
    #include <string>
    #include <vector>
    #include <algorithm>
    #include <functional>
    //打开std命名空间
    using namespace std;

    二、STL六大部件(video2)

    STL的六大部件分别是:

    • 容器:Containers
    • 分配器:Allocator
    • 算法:Algorithms
    • 迭代器:Iterator
    • 适配器:Adapters
    • 仿函数:Functors

    其中容器和算法是最重要的两大部件。以前有句老话叫做“数据结构加算法就等于程序”,在这句话中,容器就是数据结构,是一个优秀的团队将常用的数据结构都在STL中实现了。同样的,常用算法也在STL算法部件中实现。所以掌握了C++标准库的时候,可以避免重复造轮子的问题,而且库中提供的功能都经过无数次的优化,性能有很好的保证。

    六大部件简要介绍:

    容器:容器是一堆常用数据结构的实现,主要用户存放和读取数据,屏蔽了底层的内存分配和释放的问题,所以能够让用户更方便和高效的使用。

    分配器:容器对数据结构的封装,要处理内存分配和释放的问题,就用到了分配器。

    算法:既然容器用来存取数据,相当于一个数据的仓库,那么算法就是用来处理这些数据的工具。在STL中,算法和容器是分开设计的,这和面向对象(OO)有点不符,在面向对象中,我们一般将数据和方法封装在一个类中,而在STL中,使用的是模板式编程,是另外一条路线。

    迭代器:我们的算法要操作容器中的数据。如何将两者结合起来,就需要一个桥梁,这个桥梁就是迭代器。迭代器可以看成是一种泛化的指针。

    仿函数:比较抽象,后面解释。

    适配器:用于做一些转换,例如容器适配器、仿函数适配器、迭代器适配器。

    三、六大部件示例(video2)

    上图中程序解释:(其中将六大部件综合在一起展示)

    1.在第11行,定义一个容器vector,模板参数为<int, allocator<int>>,其中第一个int代表vector要装的数据类型,后面的allocator<int>是一个内存分配器,用于vector的内存分配。这个分配器是一个可选项,如果不写,标准库源码中有一个默认的分配器给vector,分配器也是一个模板,模板参数必须和容器的第一模板参数匹配,这里是int。

    2.定义vector变量vi,构造函数选用的是将数组ia的第一个位置和最后一个位置作为参数。

    3.在第13行,选用了一个叫做count_if()的算法来处理容器vi。这个算法的作用是对vi中满足一定条件的元素进行计数。

    4.count_if()的参数分别是指向容器开头的迭代器(泛化指针)、指向容器结尾的迭代器以及对元素的过滤条件。vi.begin()返回的是指向开头的迭代器,vi.end()返回的是指向结尾的迭代器。

    5.less<int>()是一个仿函数,用于比较大小。

    6.bind2nd(less<int>(), 40)是一个适配器,意思是将第二个参数也就是整数40,绑定到less<int>()。意思就是小于40的整数。

    7.not1()函数也是一个适配器,意思是取反义,也就是将小于40,变为大于等于40。

    8.最后的输出就是vi中大于等于40的元素的个数,结果为4。

    四、算法效率介绍(video2)

    每个人都想用效率最高的东西和方法,那位什么标准库还要提供十个八个容器和一大堆算法呢?

    因为每个人的需求不同,例如数据分布、数据排列方式、数据处理需求都不一样。没有一个特定的容器或算法能够适应所有的需求。所以我们必须根据不同的需求来选择不同的容器和算法。

    如何评价容器或算法的效率,我们经常会使用复杂度(Complexity)或O()(big-oh)来衡量。

    主要的复杂度有以下一些:

    1.O(1)或O(c):常数时间(constant time)

    2.O(n):线性时间(linear time)

    3.O(log2n):以2为底n的对数,次线性时间(sub-linear time)

    4.O(n2):n的平方,平方时间(quadratic time)

    5.O(n3):n的立方,立方时间(cubic time)

    6.O(2n):2的n次方,指数时间(exponential time)

    7.O(nlog2n):介于线性与平方的中间模式。

    这里面的n必须是一个很大的数(几十万甚至更大)才有实际的意义,因为当n很大时,各个复杂度之间的效率千差万别。而如果n很小,例如一些玩具程序,那对于计算机的计算速度,效率之间差距就没什么意义了。

    五、区间表示法(video2)

    我们对一个范围的表示一般有三种方式:

    1.[  ]:闭区间,即包含最前和最后的元素。

    2.(  ):开区间,即最前最后元素都不包含。

    3.[  ):前闭后开区间,即包含最前面的元素,不包含最后面的元素。

    在C++标准库中,选择第3种作为区间表示,也就是前闭后开

    如上图所示,c是一个容器对象,c.begin()返回一个迭代器(泛化指针,兼容指针几乎所有操作),这个迭代器指向容器的第一个元素的地址。c.end()也返回一个迭代器,这个迭代器指向容器最后一个元素的下一个地址,但是那个地址所保存的东西,根本不是这个容器所拥有的。所以,使用*(c.end())所取到的数据是没有意义的(并且可能导致程序崩溃)。

    上图中展示了如何使用迭代器和for循环来遍历容器中的元素。代码如下:

    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    //定义容器,将数组arr的数据放入容器中
    vector<int> vi(arr, arr + 10);
    //获取容器开头的迭代器
    vector<int>::iterator bg = vi.begin(); //可以使用auto bg = vi.begin()
    //当bg不等于vi.end()时,也就是说还有滑到容器的最后,打印bg指向的数据,并滑向下一个位置
    for (; bg != vi.end(); bg++) {
        cout << *bg << endl;
    }

    另一种遍历的做法(C++11新特性):(参照笔记:C++程序设计2里的第十四节 基于range的for循环)

    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    //定义容器,将数组arr的数据放入容器中
    vector<int> vi(arr, arr + 10);
    //使用auto可以让编译器自己推导类型,当然也可以写for(int i : vi){}
    for (auto i : vi) {
        cout << i << endl;
    }

    六、容器结构与分类(video3)

    容器分为两类:

    1.序列式容器:Sequence Container

    2.关联式容器:Associative Container,其中又可以分为有序的和无序的(Unordered Container)。

    序列式容器,Sequence Containers:

    Array:C++2.0才加入的容器,是对语言自带原始数组的类封装,大小不能扩展。一般初始化时有大小限制(根据机器和内存大小的不同),注意异常。

    Vector:大小可以扩展的数组,当容量不够时,Vector里的分配器会自动进行内存的扩展,可以放心使用。

    Deque:双向队列,念做/dek/。前后都可以扩展容量,可以从两段放入数据,也可从两段取数据。Queue是单向队列,由Deque封装出来的,所有有些人也不把queue看做容器,而是看成容器适配器。

    List:标准库中的List是双向环状链表。

    Forward-List:C++2.0加入的容器,单向链表。(占用空间肯定比List小,应为每个node都少一个指向前面node的指针)

    关系式容器,Assiociative Containers: 

    包含set和map。虽然标准库没有规定用什么样的数据结构来实现,但各家的编译器都默认采用了红黑树(RB-Tree)来实现,因为红黑树是一种特殊的二叉树,这种二叉树又叫做高度平衡二叉树,意思是会自动调整左右臂的元素数量,使得不会出现最坏查找(即刚好遇到需要查找的元素处于最深的那条臂)。

    set:set中的元素没有key,查找元素的时候直接用value进行查找,因为在set中,key就是value,value就是key。

    map:map中元素的key和value是一一对应的,查找元素时用key作为查找的条件。

    Multiset:set中元素可以重复。

    Multimap:map中元素可以重复。

    无序容器,Unordered Containers:

    Unordered set和map的底层是通过哈希表(HashTable)来实现的。对于各家编译器来说,都默认采用了公认最好的Separate Chaining哈希表,即图中右侧的结构。

    U set:通过Hash算法将value映射到某个单元格对应的链表,并存放在其中。

    U map:通过Hash算法将key映射到某个单元格对应的链表,并存放其中。

    注意:hash表中的每一个格子对应的链表不能太长,因为链表查找元素的效率是比较低的,所以在设计set和map的时候应该使用一些规则来限制对应链表的长度。

    七、Array容器示例(video4)

    namespace zz1 {
        //用于对比两个类型为Long的数,该函数提供给C标准库函数qsort用于快速排序
        int compareLongs(const void* a, const void* b) {
            return (*(long *)a - *(long*)b);
        }
    
    
        void use_array() {
            cout << "Test Array Container" << endl;
            //使用static是为了避免在局部内存区域分配大小过大,程序不运行
            static array<long, MAX_NUM> arr;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个数,放入arr
            for (auto& i : arr) {
                i = rand();
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "arr.size() = "<<arr.size() << endl;
            //打印第一个元素
            cout << "arr.front() = " << arr.front() << endl;
            //打印最后一个元素
            cout << "arr.back() = " << arr.back() << endl;
            //打印数据开头的指针,可以用来使用指针遍历数据
            cout << "arr.data() = " << arr.data() << endl;
            
            //从数组中找一个元素
            start_time = clock();
            qsort(arr.data(), MAX_NUM, sizeof(long), compareLongs);
            //查找的目标数为30000
            long target = 30000;
            //使用二分查找
            long* pItem = (long *)bsearch(&target, arr.data(), MAX_NUM, sizeof(long), compareLongs);
            if (pItem != NULL) {
                cout << "Found : " << *pItem << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
        }
    }

    注意:当Array定义时长度总大小超过0x7fffffff时,报错:

    运行输出:

    八、Vector容器示例(video4)

    namespace zz2 {
        void use_vector() {
            cout << "Test Vector Container" << endl;
            //使用static是为了避免在局部内存区域分配大小过大,程序不运行
            static vector<long> vec;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个数,放入vec
            for (size_t i = 0; i < MAX_NUM; i++) {
                try {
                    vec.push_back(rand());
                }
                catch (exception& p) {
                    //看最大能存放多少数据,内存不足以分配,异常为std::bad_alloc
                    cout << "when i = " << i << ",error : " << p.what() << endl;
                    //出错,使用abort终止程序
                    abort();
                }
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "arr.size() = " << vec.size() << endl;
            //打印第一个元素
            cout << "arr.front() = " << vec.front() << endl;
            //打印最后一个元素
            cout << "arr.back() = " << vec.back() << endl;
            //打印数据开头的指针,可以用来使用指针遍历数据
            cout << "arr.data() = " << vec.data() << endl;
            //返回分配器分配的空间大小,一定大于目前数据占用的空间
            cout << "arr.capacity() = " << vec.capacity() << endl;
    
    //查找一个元素(使用find算法,循序查找,看运气)
            long target = 30000;
            start_time = clock();
            auto pItem = ::find(vec.begin(), vec.end(), target);
            if (pItem != vec.end()) {
                cout << "Found : " << *pItem << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "find查询元素花费时间:" << clock() - start_time << "ms" << endl;
    
    //查找一个元素(使用排序+二分查找)
            start_time = clock();
            //先排序
            sort(vec.begin(), vec.end());
            cout << "排序花费时间:" << clock() - start_time << "ms" << endl;
            //使用二分查找
            long target2 = 23456;
            start_time = clock();
            long* pItem2 = (long *)bsearch(&target2, vec.data(), vec.size(), sizeof(long) ,compareLongs);
            if (pItem2 != NULL) {
                cout << "Found : " << *pItem2 << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "bsearch查询元素花费时间:" << clock() - start_time << "ms" << endl;
        }
    }

    在上述代码中,往vector中放入数据,使用push_back()方法。每次数据都从最后加入容器,为什么不能从前面push呢,因为vector是一个只能从尾巴扩张空间的容器,如果要从前面push,那就势必每次都将已存在的时候全部往后面挪动,这是相当花时间的事情。

    运行结果:

    从结果中可以看出,使用顺序查找(O(n)),当运气不错时,可以很快找到需要的数据。但是使用二分查找必须先进行排序,排序消耗了2726ms,相当耗时,而二分查找(O(log2n))确实是很快的。

    当MAX_NUM很大时,运行到中途抛出异常(内存不足):

    在vector中,空间分配是以2倍进行的,例如初始状态空间为0,存入第一个数据,空间分配为1,存入第二个数据,空间分配为2,存入第三个数据,空间分配为4,存入第5个数据,空间分配为8,存入第9个数据,空间分配为16,以此类推。所以vec.capacity(总容量)始终大于vec.size()。

    如何扩张的:

    当空间不足时,分配器要进行多一倍空间的扩展,但是不可能在原本的空间后面扩展一倍。那么,只能找一个是原来空间2倍的新的空间,然后将原本的数据全部复制过去。所以,空间的扩展也是比较消耗性能的。

    九、List容器示例(video5)

    namespace zz3 {
        void use_list() {
            cout << "Test List Container" << endl;
            list<long> lst;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个数,放入lst
            for (long i = 0; i < MAX_NUM; i++) {
                lst.push_back(rand());
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "arr.size() = "<<lst.size() << endl;
            //打印第一个元素
            cout << "arr.front() = " << lst.front() << endl;
            //打印最后一个元素
            cout << "arr.back() = " << lst.back() << endl;
            //双向链表竟然有一个max_size,按理说应该根据内存大小可以无限扩展(后面研究)
            cout << "arr.max_size() = " << lst.max_size() << endl;
            
            //从链表中找一个元素
            start_time = clock();
            long target = 30000;
            auto pItem = ::find(lst.begin(), lst.end(), target);
            if (pItem != lst.end()) {
                cout << "Found : " << *pItem << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
    
            //对链表进行排序
            start_time = clock();
            lst.sort();
            cout << "排序花费时间:" << clock() - start_time << "ms" << endl;
        }
    }

    运行结果:

    从结果中可以看出,链表存放数据以及排序是很慢的,而且排序还使用了链表自己提供的sort,而不是全局的sort。如果一个容器自己提供了sort,那么说明这个sort效率高于全局的sort,尽量选择自己提供的算法。

    十、Forward-List容器示例(video5)

    namespace zz4 {
        void use_flist() {
            cout << "Test Forward-List Container" << endl;
            forward_list<long> flst;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个数,放入arr
            for (long i = 0; i < MAX_NUM; i++) {
                flst.push_front(rand());
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小(单向链表没有这个方法)
            //cout << "flst.size() = " << flst.size() << endl;
            //打印第一个元素
            cout << "flst.front() = " << flst.front() << endl;
            //打印最后一个元素(单向链表没有这个方法)
            //cout << "flst.back() = " << flst.back() << endl;
            //单向链表竟然有一个max_size,按理说应该根据内存大小可以无限扩展(后面研究)
            cout << "flst.max_size() = " << flst.max_size() << endl;
    
            //从链表中找一个元素
            start_time = clock();
            long target = 30000;
            auto pItem = ::find(flst.begin(), flst.end(), target);
            if (pItem != flst.end()) {
                cout << "Found : " << *pItem << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
    
            //对链表进行排序
            start_time = clock();
            flst.sort();
            cout << "排序花费时间:" << clock() - start_time << "ms" << endl;
        }
    }

    单向链表和双向链表相似,但单向链表没有size()和back()方法,并且插入数据只有使用push_front(),也就是只能从一端插入。

    运行结果:

    十一、deque容器示例(video5)

    deque是双向队列,前后都可以扩充。我们知道一个内存要扩充,是没办法在原地扩充的,像vector在尾巴上扩充,也是另外找一个2倍大的空间,整体移动过去。而deque是怎么做到双向扩充的呢?如下图所示:

    deque由一个map和若干个buffer组成,map中保存的是每个buffer的指针,一个buffer可以存放多个元素。当从最后插入元素,刚好遇到最后那个buffer的空间用完,则会在后面另外申请一个buffer,新的buffer的头指针存放在map的最后一格中。从前面插入也是同样的道理。

    namespace zz5 {
        void use_deque() {
            cout << "Test Deque Container" << endl;
            deque<long> dq;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个数,放入dq
            for (long i = 0; i < MAX_NUM; i++) {
                dq.push_back(rand());
                //dq.push_front(rand());
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "dq.size() = " << dq.size() << endl;
            //打印第一个元素
            cout << "dq.front() = " << dq.front() << endl;
            //打印最后一个元素(单向链表没有这个方法)
            cout << "dq.back() = " << dq.back() << endl;
            //双向队列竟然有一个max_size,按理说应该根据内存大小可以无限扩展(后面研究)
            cout << "dq.max_size() = " << dq.max_size() << endl;
    
            //从双向队列中找一个元素
            start_time = clock();
            long target = 30000;
            auto pItem = ::find(dq.begin(), dq.end(), target);
            if (pItem != dq.end()) {
                cout << "Found : " << *pItem << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
    
            //对双向队列进行排序
            start_time = clock();
            ::sort(dq.begin(),dq.end());
            cout << "排序花费时间:" << clock() - start_time << "ms" << endl;
        }
    }

    双向队列没有自己的sort()方法,只能调用全局的sort方法。

    运行结果:

    从结果可以看出,双向队列的max_size非常大,排序比较慢。

    十二、stack容器示例(video5)

    stack是一个先进后出的容器,和下一节要说到的queue(单向队列)相反。但是和queue都是deque的一种特殊形态。如图:

    当一个deque永远都只从其中一端放数据和取数据,他就变成了一个stack。所以stack只提供了一个放元素的函数,叫push()。

    namespace zz6 {
        void use_stack() {
            cout << "Test Stack Container" << endl;
            stack<long> sk;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个数,放入sk
            for (long i = 0; i < MAX_NUM; i++) {
                sk.push(rand());
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "sk.size() = " << sk.size() << endl;
            //打印最顶上的元素
            cout << "sk.top() = " << sk.top() << endl;
            //取出一个元素
            sk.pop();
            //再次打印总大小
            cout << "sk.size() = " << sk.size() << endl;
            //再次打印最顶上的元素
            cout << "sk.top() = " << sk.top() << endl;
        }
    }

    运行结果:

    十三、queue容器示例(video5)

    queue和stack类似,但是取数据的方向和stack不一样,queue是先进先出,是单向队列。deque的特殊情况。

    namespace zz7 {
        void use_queue() {
            cout << "Test Queue Container" << endl;
            queue<long> qu;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个数,放入qu
            for (long i = 0; i < MAX_NUM; i++) {
                qu.push(rand());
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "qu.size() = " << qu.size() << endl;
            //打印最前面的元素
            cout << "qu.front() = " << qu.front() << endl;
            //打印最后面的元素
            cout << "qu.back() = " << qu.back() << endl;
            //取出一个元素
            qu.pop();
            //再次打印总大小
            cout << "qu.size() = " << qu.size() << endl;
            //打印最前面的元素
            cout << "qu.front() = " << qu.front() << endl;
            //打印最后面的元素
            cout << "qu.back() = " << qu.back() << endl;
        }
    }

    运行结果:

    从结果中可以看出,当pop出一个元素后,queue的front数据变了,但back的没变。

    十四、stack和queue说明(video5)

    说明:stack和queue是以deque双向队列为底层来实现的,所以很多人不把stack和queue当成真正的容器,而是将其看做一种容器的适配器。

    他们一个是先进先出,一个先进后出,所以并没有提供iterator,所以也不会使用find算法去查找数据。

    十五、multiset容器示例(video6)

    set和multiset是一种关系式容器,底层是红黑树(RB-Tree)。所以他的元素天生就是有序的,并且每个元素插入的位置都是有规律可循的,所以这个容器放入数据是比较慢的,但因为其底层是平衡二叉树结果,他的查询数据是非常快的。set和multiset的区别在于能够放入相同的元素。这里采用multiset来示范放入100W个元素(肯定有重复)。

    namespace zz8 {
        void use_multiset() {
            cout << "Test Multiset Container" << endl;
            multiset<long> mset;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个数,放入mset
            for (long i = 0; i < MAX_NUM; i++) {
                mset.insert(rand());
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "mset.size() = " << mset.size() << endl;
            //Multiset竟然有一个max_size,按理说应该根据内存大小可以无限扩展(后面研究)
            cout << "mset.max_size() = " << mset.max_size() << endl;
    
            //从链表中找一个元素(用全局find)
            start_time = clock();
            long target = 30000;
            auto pItem = ::find(mset.begin(), mset.end(), target);
            if (pItem != mset.end()) {
                cout << "Found : " << *pItem << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
    
            //从链表中找另一个元素(用自身提供的find)
            start_time = clock();
            long target2 = 30001;
            auto pItem2 = mset.find(target2);
            if (pItem2 != mset.end()) {
                cout << "Found : " << *pItem2 << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
        }
    }

    运行结果:

    从结果可看出,multiset的插入速度相当慢,因为每次插入都要通过排序找准数据放置在树中的位置。使用全局::find()查找数据也比较慢,所以他自己提供了一个find()算法,结果可以看出,速度相当快。

    十六、multimap容器示例(video6)

    multimap和multiset的区别在于,里面存的数据是key-value对,key作为索引用于查找。在multiset中,只有value,所以查找时将value当成key来用。但在multimap中就将其分开了。

    namespace zz9 {
        void use_multimap() {
            cout << "Test Multimap Container" << endl;
            multimap<long,long> mmap;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个键值对,放入mmap
            for (long i = 0; i < MAX_NUM; i++) {
                //将i作为key,不会重复
                mmap.insert(pair<long,long>(i,rand()));
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "mmap.size() = " << mmap.size() << endl;
            //Multiset竟然有一个max_size,按理说应该根据内存大小可以无限扩展(后面研究)
            cout << "mmap.max_size() = " << mmap.max_size() << endl;
    
            //从链表中找一个元素(用自身提供的find)
            start_time = clock();
            long target = 30000;
            auto pItem = mmap.find(target);
            if (pItem != mmap.end()) {
                cout << "Found : key = " << (*pItem).first<<"  value = "<<(*pItem).second << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
        }
    }

    在multimap中,查找元素是通过key来查找的。

    运行结果:

    十七、Unordered multiset容器示例(video6)

    namespace zz10 {
        void use_unorder_multiset() {
            cout << "Test Unorder Multiset Container" << endl;
            //头文件#include <unordered_set>
            unordered_multiset<long> umset;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个键值对,放入umset
            for (long i = 0; i < MAX_NUM; i++) {
                umset.insert(rand());
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "umset.size() = " << umset.size() << endl;
            cout << "umset.max_size() = " << umset.max_size() << endl;
            cout << "umset.bucket_count() = " << umset.bucket_count() << endl;
            cout << "umset.load_factor() = " << umset.load_factor() << endl;
            cout << "umset.max_load_factor() = " << umset.max_load_factor() << endl;
            cout << "umset.max_bucket_count() = " << umset.max_bucket_count() << endl;
    
            //从链表中找一个元素(用自身全局find)
            start_time = clock();
            long target = 30000;
            auto pItem = ::find(umset.begin(),umset.end(),target);
            if (pItem != umset.end()) {
                cout << "Found : " << *pItem << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
            //从链表中再找一个元素(用自身提供的find)
            start_time = clock();
            long target2 = 30001;
            auto pItem2 = umset.find(target2);
            if (pItem2 != umset.end()) {
                cout << "Found : " << *pItem2 << endl;
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
        }
    }

    运行结果:

    从结果可以看出,bucket_count 大于size,原因如下述。负荷系数 load_factor = size / bucket_count。

    Unordered Multiset是使用hash映射+链表作为底层数据结构的。hash算法先将数据映射到不同的bucket,每个bucket保存的是一个链表的头指针。当一个bucket中有多个元素时(即链表有多个node,每个node保存一个元素),那么会使用顺序查找的方式来遍历链表,所以链表一定不能太长,否者会影响查询效率。

    如何让每个bucket下挂的链表不至于太长呢,可以使用大于数据量的bucket数量来解决,当数据增加到等于bucket数量时,Unordered multiset会自动扩展bucket数量(两倍数量),并将数据打散重新hash到不同的bucket下挂链表中。这样,就以空间换取了时间复杂度的提升。

    十八、Unordered multimap容器示例(video6)

    namespace zz11 {
        void use_unorder_multimap() {
            cout << "Test Unorder Multiset Container" << endl;
            //头文件#include <unordered_map>
            unordered_multimap<long,long> ummap;
            //产生随机种子seed
            srand(time(0));
            //记录开始时间戳
            clock_t start_time = clock();
            //随机产生MAX_NUM个键值对,放入ummap
            for (    long i = 0; i < MAX_NUM; i++) {
                ummap.insert(pair<long,long>(i,rand()));
            }
            //打印消耗时间
            cout << "存放元素花费时间:" << clock() - start_time << "ms" << endl;
            //打印总大小
            cout << "ummap.size() = " << ummap.size() << endl;
            cout << "ummap.max_size() = " << ummap.max_size() << endl;
    
            //从链表中找一个元素(用自身提供的find)
            start_time = clock();
            long target = 30001;
            auto pItem = ummap.find(target);
            if (pItem != ummap.end()) {
                cout << "Found : key = " << (*pItem).first << " value = " << (*pItem).second << endl;
    
            }
            else {
                cout << "Not found." << endl;
            }
            //打印花费时间
            cout << "查询元素花费时间:" << clock() - start_time << "ms" << endl;
        }
    }

    运行结果:

    Unordered multimap和前面一节的Unordered multiset差不多,只是存的数据使用key-value对。

    十九、Set、Map、Unordered set、Unordered map(video6)

    这四个容器和前面4节所描述的multiset、multimap、unordered multiset和unordered multimap差不多,唯一的差别就是这四个容器存放的数据的key不能重复

    对于set和map来说,key就是value,value就是key,那么key不能重复的意思就是value不能重复。

    而对于unordered set和unordered map来说,就是存放数据的key不能重复,而value是可以重复的。

    在四种map中(map、multimap、unordered map、unordered multimap),map和unordered map是可以使用“[]”的方式取值的,例如:

    map<long,long> m;
    m[55] = 12389;
    m[66] = 4394509;
    unordered_map<long, long> um;
    um[77] = 436345;
    um[88] = 123;
    
    cout << m[55] << endl;
    cout << m[66] << endl;
    cout << um[77] << endl;
    cout << um[88] << endl;

    因为在map和unordered map中,key不能重复。所以,一个key一定能够唯一表示一个value。那么这个key可以作为index来取值。

    而在multimap和unordered multimap中,key可以重复。所以,可能存在多个key一致的问题,那么使用key作为index就存在问题。

    二十、一些非标容器(video6)

    存在一些在C++1.0中未纳入标准库的容器:

    slist

    hash_set

    hash_map

    hash_multiset

    hash_multimap

    这些在C++1.0未纳入标准库,可能是因为时间的问题,但是由于他们很重要,各家编译器都将他们实现了。

    在C++2.0中这些都已经被纳入标准库,只是名字做了修改。

    二十一、分配器(video7)

    GNU C++编译器下测试多个allocator效率:

    导入头文件:

    #include <list>
    #include <stdexcept>
    #include <string>
    #include <cstdlib>         //abort()
    #include <cstdio>          //snprintf()
    #include <algorithm>     //find()
    #include <iostream>
    #include <ctime> 
    
    #include <cstddef>
    #include <memory>    //內含 std::allocator  
    //欲使用 std::allocator 以外的 allocator, 得自行 #include <ext...> 
    //以下allocator只能在GNUC g++中使用
    #ifdef __GNUC__        
    #include <extarray_allocator.h>
    #include <extmt_allocator.h>
    #include <extdebug_allocator.h>
    #include <extpool_allocator.h>
    #include <extitmap_allocator.h>
    #include <extmalloc_allocator.h>
    #include <ext
    ew_allocator.h>  
    #endif
    namespace zz12 {
        void use_allocator() {
            cout << "Test Allocators" << endl;
    
            list<long, allocator<long>> c1;                        
            list<long, __gnu_cxx::malloc_allocator<long>> c2;  
            list<long, __gnu_cxx::new_allocator<long>> c3;         
            list<long, __gnu_cxx::__pool_alloc<long>> c4;          
            list<long, __gnu_cxx::__mt_alloc<long>> c5;     
            list<long, __gnu_cxx::bitmap_allocator<long>> c6; 
    
            srand(time(0));
    
            clock_t timeStart = clock();
            for (long i = 0; i < MAX_LOOP; ++i){
                c1.push_back(rand());
            }
            cout << "allocator花费时间:" << clock() - timeStart << "ms" << endl;
    
            timeStart = clock();
            for (long i = 0; i < MAX_LOOP; ++i) {
                c2.push_back(rand());
            }
            cout << "malloc_allocator花费时间:" << clock() - timeStart << "ms" << endl;
    
            timeStart = clock();
            for (long i = 0; i < MAX_LOOP; ++i) {
                c3.push_back(rand());
            }
            cout << "new_allocator花费时间:" << clock() - timeStart << "ms" << endl;
    
            timeStart = clock();
            for (long i = 0; i < MAX_LOOP; ++i) {
                c4.push_back(rand());
            }
            cout << "__pool_alloc花费时间:" << clock() - timeStart << "ms" << endl;
    
            timeStart = clock();
            for (long i = 0; i < MAX_LOOP; ++i) {
                c5.push_back(rand());
            }
            cout << "__mt_alloc花费时间:" << clock() - timeStart << "ms" << endl;
    
            timeStart = clock();
            for (long i = 0; i < MAX_LOOP; ++i) {
                c6.push_back(rand());
            }
            cout << "bitmap_allocator花费时间:" << clock() - timeStart << "ms" << endl;
        }
    }

    二十二、直接使用分配器(video7)

    在GNU C++编译器下测试直接使用不同的allocator分配空间以及deallocator归还空间:

    namespace zz13 {
        void use_allocator_directly(){
            //test all allocators' allocate() & deallocate();
            int* p;
            allocator<int> alloc1;
            p = alloc1.allocate(1);
            alloc1.deallocate(p, 1);
    
            __gnu_cxx::malloc_allocator<int> alloc2;
            p = alloc2.allocate(1);
            alloc2.deallocate(p, 1);
    
            __gnu_cxx::new_allocator<int> alloc3;
            p = alloc3.allocate(1);
            alloc3.deallocate(p, 1);
    
            __gnu_cxx::__pool_alloc<int> alloc4;
            p = alloc4.allocate(2);    //分配2个int变量的空间
            alloc4.deallocate(p, 2);     //归还2个int变量的空间
    
            __gnu_cxx::__mt_alloc<int> alloc5;
            p = alloc5.allocate(1);
            alloc5.deallocate(p, 1);
    
            __gnu_cxx::bitmap_allocator<int> alloc6;
            p = alloc6.allocate(3);    //分配3个int变量的空间
            alloc6.deallocate(p, 3);      //归还3个
        }
    }

    不建议直接使用allocator,建议直接使用标准容器。如果需要分配小空间内存,那么建议使用熟悉的new、delete和malloc、free,因为这几个函数只需要关心分配的大小,而不用关心归还的大小,更方便,不容易出错。

  • 相关阅读:
    Js 30 BOM
    js面向对象
    js模态窗口
    js默认行为(也称默认事件)
    框架的控件隐藏
    20150706 js之定时器
    sublime快捷方式和node.js
    js回调函数2
    Hibernate 多对一
    Hibernate入门之配置文件
  • 原文地址:https://www.cnblogs.com/leokale-zz/p/11100566.html
Copyright © 2020-2023  润新知