Skip to content

Latest commit

 

History

History
516 lines (454 loc) · 25.5 KB

容器.md

File metadata and controls

516 lines (454 loc) · 25.5 KB

1. 顺序容器 内的元素按其位置存储和访问 (sequential container) vector deque list forward_list array string

  容器(container),顾名思义表示对象的集合,这些对象的类型都相同,
  每个对象都有一个自己的索引,用这个索引我们就可以方便的访问该对象了。
  只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
  vector   可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢。
           vector<int> vi(10, -1); // 10个-1

  deque    双端队列。支持快速快速访问。在头尾位置插入/删除速度很快。
           deque<double> deqd(10); // 10个元素 0

  list     双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快。
           list<int> li(5,1); // 5个1

  forward_list 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快。 
           forward_list<string> fls(10); // 10个元素 0

  array    固定大小数组。支持快速随机访问,不能添加或删除元素。
           array<int, 42> ia;  // 42个int的数组, 支持拷贝构造
       array<int, 42> ia2 = ia;
           而内置数组不支持拷贝构造.
       std::array也提供了 at() 函数。

  string   与vector相似的容器,但专门用于保存字符。随机访问快,在尾部插入/删除都很快。

以下是一些选择容器的基本原则(具体视情况而定,这里只是一般情况):

  1、通常vector是最好的选择,除非你有很好的理由选择其他容器;
  2、如果程序有很多小的元素,且空间额外开销很重要,则不要使用 list 或 forward_list 容器;
  3、如果程序要求随机访问,应使用 vector 或 deque 容器;
  4、如果程序要在头尾位置插入/删除,且不会在中间位置进行插入/删除操作,则应使用deque容器。

a. vector 连续存储结构,每个元素在内存上是连续的 尾部插入 push_back() 与 尾部插删除 pop_back()

  vector<vector<int>> vvi;//相当于二维数组
  STL中的vector容器就是动态大小数组。
  vector<int> v;
  v.push_back(k); 在后面插入 k 这个元素
  v.size()	返回容器中实际元素的个数
  v.resize()	重新设定容器的大小
  v.at(index)	返回索引位置的元素
  v.begin()	返回第一个元素的迭代器 vector<T>::iterator 
  v.end()	返回最后一个元素后面位置的迭代器
  v.front()	返回第一个元素
  v.back()	返回最后一个元素
  v.empty()	返回1为空,0为非空
  v.swap()	交换两容器
  rbegin()	返回逆向容器中的第一个元素的迭代器
  rend()	返回逆向容器中的最后一个元素后面位置的迭代器
  clear()	清除容器中所有元素
  insert()	插入
  max_size()	返回最大数据量
  v.erase()	擦除元素

  #include <vector>       //头文件包含  
  using namespace std;  //或者using std::vector;  
  //vector定义并初始化  
  vector<int> v1;  
  for (int i=0;i<10;i++)  
    v1.push_back(i+1);  // 从后面一次插入元素
  //使用迭代器遍历vector  
  vector<int>::iterator iter = v1.begin();  
  for (;iter!=v1.end();iter++)  
   {  
    cout<<*iter<<" ";  
   } 
  vector容器中存储的元素在内存中是连续存储的。
  假如容器中没有空间容纳新元素,此时由于元素必须连续存储以便索引访问,
  所以不能在内存中随便找个地方存储这个新的元素,于是vector必须重新分配空间 v.resize(10),
  用于存放原来的元素和新添加的元素:
  存放在旧容器中的元素被复制到新的容器中,接着插入新的元素,最后撤销旧的存储空间。
  为了使vector容器实现快速的内存分配,
  其实际分配的容量要比当前所需的空间多一些,vector容器预留这些空间,用于存放新的元素。

  v.push_back(val)
  先将容器c中的元素拷贝到新的内存空间中,
  然后在将val值拷贝到新空间的末尾,
  最后析构掉原始空间。
  当push_back检测到空间不足时,将自动以 2倍 的方式扩展空间。
  对于大量数据来说,这是一个弊端,可以使用可以使用 vector::reserve 方法来改善。
  push_back()使用 vector::insert 方法实现,在容器尾部insert来实现的。
  void push_back(_Elem _Ch)  
  {   // insert element at end  
   insert(end(), _Ch);  // 算法
  } 
  pop_back() 使用vector::earse,擦除最后一个位置元素来实现的。
  删除容器尾部元素,同时c.size()会减少。
  void pop_back()  
  {   // erase element at end  
   erase(this->_Mysize - 1);    // throws if _Mysize == 0  
  } 
  
  v.shrink_to_fit();
  //  为了不让vector浪费太多的内存,我们在最后调用了shrink_to_fit。
  // 该函数会为元素分配足够的空间,将剩余的元素移到该空间内,并且删除之前那个比较大的内存空间。
  // 会将多余的空间归还给系统

vector 优点:

随机存取,即 vi[i] 操作 和 vi.at(i)。

at() 函数会检查给定的索引值是否越界,如果越界则返回一个异常。
因为检查越界要花费一些时间,所以at函数会让程序慢一些。
当需要非常快的索引成员时,并能保证索引不越界,我们会使用[]快速访问vector实例。
很多情况下,at() 函数在牺牲一点性能的基础上,有助于发现程序内在的bug。
默认使用 at() 函数时一个好习惯。
当代码的性能很差,但有没有bug存在时,可以使用性能更高的操作符[]来替代at函数。

动态操作, 尾部插入 vi.push_back() 与 尾部插删除 vi.pop_back()
自动调整内存,节省空间

try{} catch(){} 捕获 vector访问越界 异常

try{
   temp = v.at(k);
}
catch(std::out_of_range &e){
   std::cout << e.what() << std::endl;
}

vector缺点:

  实现插入与删除操作效率低
  只能在尾部插入与删除,在头部插入与删除消耗时间规模与容器大小成正比,它会将后面的元素依次前移。
  当动态添加的数据超过默认内存大小时,要进行整体的重新分配,拷贝与释放。

快速删除未排序的vector中的元素,最后一个元素替换到需要删除的地方,再 pop_back() 删除最后一个元素

  注意此操作会改变数组顺序,对于未排序的数组没什么问题,对于已排序的数组不可用,或者删除之后再排序
// 输入 vector,和要删除的 元素的索引 idx
template <typename T>
void quick_remove_at(std::vector<T> &v, std::size_t idx)
{
    if (idx < v.size()) // 索引为出界
    {
        v[idx] = std::move(v.back());// 最后的元素移动到要删除的位置
	// std::move(v.back()); // 避免拷贝,相当于只是拷贝了指针
	// *it = v.back();      // 会进行拷贝
        v.pop_back();           // 删除最后的元素
    }
}

// 输入 vector,和要删除的 元素的迭代器 it
template <typename T>
void quick_remove_at(std::vector<T> &v,
			typename std::vector<T>::iterator it)
{
  if (it != std::end(v))// 迭代器未出界 
  {
        *it = std::move(v.back());// 最后的元素移动到要删除的位置
        v.pop_back();             // 删除最后的元素
  }
}

b. list 双端链表 非连续存储结构,具有双链表结构,支持排序 sort排序

list参考

  list容器中添加元素时,只需要创建一个新的元素,
  然后将该元素连接到已经存在的链表中,
  不需要重新分配存储空间,也不用复制任何已存在的元素。

  非连续存储结构,具有双链表结构,每个元素维护一对前向和后向指针,因此支持前向/后向遍历。
  支持高效的随机插入/删除操作,但随机访问效率低下,且由于需要额外维护指针,开销也比较大。
  每一个结点都包括一个信息快Info、一个前驱指针Pre、一个后驱指针Post。
  可以不分配必须的内存大小方便的进行添加和删除操作。
  使用的是非连续的内存空间进行存储。   
  list<char> lit;   
  //-----------链表可以从两端插入-------------------        
  lit.push_back('b');  
  lit.push_front('d');
  //-----------链表打印------------------- 
  list<char>::iterator it;    
  for(it=lit.begin();it!=lit.end();it++)  
  {  
    cout<<*it<<"  ";  
  }  
  cout<<endl;  
  //-----------链表可以从两端删除-------------------   
  lit.pop_back();    
  lit.pop_front();  
  //-------------删除所有的a---------------------------------  
  //lit.remove('a');  //删除所有的a;  
  //-----------将list里的数据倒序排列---------------  
  lit.reverse();  
  //-------------移除连续且相同的a,只剩下一个;--------------------------------  
  lit.unique();  //移除连续且相同的a,只剩下一个;  
  list<char> lit1; 
  list<char>::iterator it1; 
  //-------------将一个链表插入到另一个链表---------------------------------  
  it1=find(lit.begin(),lit.end(),'f');  //先的找到要插入的位置,在该位置的前一个插入;  
  ////lit.splice(it1,lit1); //将第二个链lit1表插入到第一个链表lit中;合并后的链表就没了,因为传的是&
  // 打印函数
  void printList(const list<int>& a)
  {
  // 注意形参中是const list,所以下面也需要用const_iterator
  // 否则distance无法使用
  list<int>::const_iterator iter;
  for (iter = a.begin(); iter != a.end(); ++iter)
  {// 计算数组下标,distance包含在algorithm中 #include<algorithm>
    size_t index = distance(a.begin(), iter);
    cout << "a[" << index << "] = " << *iter << endl;
  }
  cout << endl;
  }

      list<int> a;
  // push_front、push_back插入数据
  a.push_front(4);
  a.push_front(3);
  a.push_front(2);
  a.push_front(1);
  a.push_back(50);
  printList(a);

  // insert插入数据
  list<int>::iterator iter;
  iter = a.begin();
  a.insert(iter, 0);
  a.insert(++iter, 10);
  a.insert(++iter, 4, 20); // 插入4个20
  printList(a);


  // sort排序
  cout << "sort排序" << endl;
  a.sort();
  printList(a);

  // reverse逆序
  cout << "reverse逆序" << endl;
  a.reverse();
  printList(a);

  // erase删除指定元素
  cout << "erase删除指定元素" << endl;
  a.erase(iter);
  printList(a);

  // erase删除指定区间的元素
  cout << "erase删除指定区间元素" << endl;
  a.erase(++a.begin(), --a.end());
  printList(a);

list 优点:

  1)不使用连续内存完成动态操作。
  2)在内部方便的进行插入和删除操作 动态操作,插入与删除效率高
  3)可在两端进行push、pop 

list缺点:

  1) 不能进行内部的随机访问,即不支持[ ]操作符和vector.at()
  2) 不能进行内部的随机访问,即不支持[ ]操作符和vector.at()

c. deque 双端队列 连续存储结构

  deque连续存储结构,即其每个元素在内存上也是连续的,类似于vector,
  不同之处在于,deque提供了两级数组结构, 第一级完全类似于vector,代表实际容器;
  另一级维护容器的首位地址。
  这样,deque除了具有vector的所有功能外,还支持高效的首/尾端插入/删除操作。
  deque   双端队列 double-end queue
  deque是在功能上合并了vector和list。
  //
  deque<int> a;
  // 在末尾插入数据
  a.push_back(3);
  a.push_back(4);
  a.push_back(5);
  // 在开头插入数据
  a.push_front(2);
  a.push_front(1);
  a.push_front(0);
  // 以数组方式输出
  for (size_t n = 0; n < a.size(); ++n)
    cout << "a[" << n << "] = " << a[n] << endl;
  cout << endl;
  //删除末尾的数据
  a.pop_back();
  //删除开头的数据
  a.pop_front();
  // 以迭代器方式输出
  deque<int>::iterator iter;
  for (iter = a.begin(); iter < a.end(); ++iter)
  {
    // 计算数组下标,distance包含在algorithm中 #include<algorithm>
    int index = distance(a.begin(), iter);
    cout << "a[" << index << "] = " << *iter << endl;
  }

deque 优点:

  1)随机访问,即[]操作和deque.at()
  2) 在内部方便的进行插入和删除操作
  3)可在双端进行pop,push

deque 缺点:

  占用内存多。

vector、deque、list三种容器的特点

  vector:支持快速随机访问、可高效的在vector容器 尾部    添加删除数据
  deque:支持快速随机访问、可高效的在deque容器 头部和尾部 添加删除数据
  list:支持顺序访问,但是在任何位置插入删除元素都很快。
  相同点:三者都能实现resize()来重新调整容器的大小。 
  不同点:
        1)vector能实现随即存取,即[]操作,而list不能,deque是二者的结合体,也能够实现索引操作[],但效率没有vector高。
        2)vector适合在文件的末尾实现删除元素的操作pop_back()与插入操作push_back(),在中间时效率非常低下。
           而list可以在容器的任何位置实现插入与删除操作。
        原因:vector的元素地址连续,如果在中间插入与删除操作,可能会导致原来的内存地址不足以存储当前的元素,
              需要重新分配内存地址,这就需要将原来的所有元素拷贝到新内存,释放旧的内存地址等操作,操作代价高昂。
              而list元素内存地址不连续,用指针操作,其本身是一个双向链表,它的高效率体现在插入,删除以及排序等移动大量元素的操作。

       a. vector与deque的迭代器支持算术运算,list的迭代器只能进行++/--操作,不支持普通的算术运算。
       b. 向量中的iterator在使用后就释放了,但是链表list不同,它的迭代器在使用后还可以继续用;链表特有的;

选择容器类型的准则:

  1)如果需要随机访问一个容器,vector 比 list好
  2)如果经常 需要 插入或删除 容器元素,list比vector好
  3)如果既要随机存取,又要关心两端数据的插入与删除,则选择 deque
  a、若需要随机访问操作,则选择vector;
  b、若已经知道需要存储元素的数目,则选择vector;
  c、若需要随机插入/删除(不仅仅在两端),则选择list
  d、只有需要在首端进行插入/删除操作的时候,还要 兼顾 随机访问效率,才 选择deque, 否则都选择vector。
  e、若既需要随机插入/删除,又需要随机访问,则需要在vector与list间做个折中-deque。
  f、当要存储的是大型负责类对象时,list要优于vector;
     当然这时候也可以用vector来存储指向对象的指针,
     同样会取得较高的效率,但是指针的维护非常容易出错,因此不推荐使用。

2. 关联容器 按关键字保存和访问 map (key-value键值对,类似字典的概念) set(只包含一个关键字(关键字即值))

  map<名字,电话号码>    利用名字来查询电话号码类似电话簿
  set<bad_checks_name>  定义一个曾经开过空头支票的客户的名字,在接收一张支票前查询其名字是否在其中。
  只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
  #include <map>  
  #include <ext/hash_map>  
  #include <tr1/unordered_map>  
  using namespace std;  
  using namespace std::tr1;  
  //typedef map<int,int> MapKey;          //采用map  
  //typedef hash_map<int,int> MapKey;     //采用hash_map  
  typedef unordered_map<int,int> MapKey;  //采用unordered_map  

有序集合 RB树实现 红黑树

  map      关联数组:保存关键字-值对
  set      关键字即值,只保存关键字的容器
  multimap 关键字可重复出现的 map
  multiset 关键字可重复出现的 set 

无序集合 哈希函数实现

  unordered_map 哈希函数组织的map
  unordered_set 哈希函数组织的set
  unordered_multimap 哈希函数组织的multimap, 关键字可以重复出现
  unordered_multiset 哈希函数组织的multiset, 关键字可以重复出现

无序集合 基于hash table(哈希表)

  hash_map
  hash_set
  hash_multimap
  hash_multiset

结果分析

  运行效率方面:unordered_map最高,hash_map其次,而map效率最低
  占用内存方面:hash_map内存占用最低,unordered_map其次,而map占用最高

使用map

使用map统计每个单词在输入中出现的次数  单词按字典序列 排序

  map<string, size_t> word_count;//string 到 size_t 的空map
  string word;
  while(cin >> word) ++word_count[word];//提取word的计数器并将其加1
  for(const auto &w : word_count)       //对map中的每一个元素  pair类型 first成员保存关键字 second成员保存对应的值
        cout << w.first << " occurs " << w.second

                << ((w.second > 1)? "times" : "time") << endl;// 报告每个单词出现的次数 单词按 字典排序

map 创建一张百万富翁的列表

使用 set

使用set保存需要忽略统计的单词

  map<string, size_t> word_count;//string 到 size_t 的空map
  set<string> exclude = {"The", "But", "And", "Or", "An", "A",
                         "the", "but", "and", "or", "an", "a"};//需要剔除的单词
  string word;
  while(cin >> word)
        if(exclude.find(word) == exclude.end()) ++word_count[word];// 统计合理的词的出现次数
    // exclude.find(word) word没有在exclude中出现时,返回 exclude.end()

     

初始化关联容器

  //map初始化为空
  map<string, size_t> word_count;//string 到 size_t 的空map
  //set的列表初始化
  set<string> exclude = {"The", "But", "And", "Or", "An", "A",
                         "the", "but", "and", "or", "an", "a"};//需要剔除的单词
  //map的列表初始化 三个元素 将姓 映射为 名字
  map<string, string> authers = {{"Jyly", "Jeam"},
                                 {"Aust", "Amy"},
                                 {"Dickens", "Charles"} };
  // multimap multisetde初始化,一个单词对应多个相互关联的词义,就是说 关键字可以重复出现
  vector<int> ivec;
  for(vector<int>::size_type i = 0; i != 10; ++i){
        ivec.push_back(i);
        ivec.push_back(i);// 每个数重复保存一次
        }
  // iset包含来自ivec的不重复的元素 含有10个元素
  set<int> iset(ivec.cbegin(), ivec.cend());
  // multiset 包含ivec的所有20个元素
  multiset<int> miset(ivec.cbegin(), ivec.cend());

pair类型 是用来生成 特点类型 的模板 first 和 second 来访问  map的元素是 pair类型   make_pair(v1, v2)

  pair<string, string> anon;//
  pair<string, size_t> word_count;//
  pair<string, vector<int>> line;//保存 string 和 vector<int> 
  pair<string, string> author{"James", "Joyce"};//列表初始化
  // pair类型的返回值类型函数
  pair<string, int> process(vector<string> &v){
        if(!v.empty())//back()	返回最后一个元素
              return {v.back(), v.back().size()};//列表初始化
              // return pair<string, int>(v.back(), v.back().size());
              // return make_pair(v.back(), v.back().size()):
        else 
              return pair<string, int>();//隐式构造返回值
        }

关联容器类型 key_type value_type 对于set是一样的 而对于map不一样 还有一个 mapped_type

  set<string>::value_type va;//va 是一个string
  set<string>::key_type   vb;//vb 也是一个string
  map<string, int>::key_type vc;//vc 是一个string
  map<string, int>::mapped_type vd;//vd 是一个int
  mep<string, int>::value_type ve;//ve map元素的类型 是pair类型  pair<const string, int>

关联容器迭代器

  map<string, size_t> word_count;//string 到 size_t 的空map
  auto map_it = word_count.begin();//迭代器指针
  // *map_it 是一个指向 pair<const string, size_t>类型对象的 引用
  cout << map_it->first;//打印关键字
  cout << " " << map_it->second;//
  map_it->first = "new key";//错误 map的关键字类型 是 const的 不能改变 可以读取
  ++map_it->second;//正确 值类型不是const 可以改变
  //set的关键字也是只读的即const,不能被改变的
  set<int> iset = {0,1,2,3,4,5,6,7,8,9};//列表初始化
  set<int>::iterator set_it = iset.begin();
  if( set_it != iset.end()){
        *set_it = 42;// 错误 不能修改set的关键字
        cout << *set_it << endl;//可以读取set的关键字

           }

遍历关联容器

  map<string, size_t> word_count;//string 到 size_t 的空map
  string word;
  while(cin >> word) ++word_count[word];//提取word的计数器并将其加1
  // 范围for访问
  for(const auto &w : word_count)       //对map中的每一个元素  pair类型 first成员保存关键字 second成员保存对应的值
        cout << w.first << " occurs " << w.second
             << ((w.second > 1)? "times" : "time") << endl;// 报告每个单词出现的次数
   //迭代器访问
   auto map_it = word_count.cbegin();//迭代器指针
   // *map_it 是一个指向 pair<const string, size_t>类型对象的 引用
   while(mep_it != word_count.cend()){
   cout << map_it->first << " occurs "
        << map_it->second 
        << ((w.second > 1)? "times" : "time") << endl;// 报告每个单词出现的次数
        ++map_it;//递增迭代器,移动到下一个元素

      }

向map中添加元素

  word_count.insert({word, 1});// c++11标准最方便
  word_count.insert(make_pair(word, 1));
  word_count.insert(pair<string, size_t>(word, 1));
  word_count.insert(map<string, size_t>::value_type(word, 1));

向multimap中添加元素

  multimap<string, string> authors;
  authors.insert({"Breath, John", "Sot-Weed Factor"});//插入第一个元素,关键字为 Breath, John
  authors.insert({"Breath, John", "Lost in the funhouse"});//插入第二个元素,关键字也为 Breath, John

multimap的访问

  string search_item("Breath, John");
  auto cnt = authors.count(search_item);//关键字出现的次数
  auto iter = authors.find(search_item);//关键字指向的 值序列
  while(cnt){
        cout << iter->second << endl;//打印作者的每一个书名
        ++iter;//前进到下一步书
        --cnt;//记录剩余未打印的书本数量
  }
  // 迭代器lower_bound  范围for 访问
  for(auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item);
                                beg != end; ++beg)
        cout << beg->second <<endl;//打印作者的每一个书名
  // 范围for equal _range() 访问
  for(auto pose = authors.equal _range(search_item); pose.first != pose.second; ++pose.first)
        cout << pose.first->second << endl;//打印作者的每一个书名

无序容器   无序 存储

  unordered_map<string, size_t> word_count;//string 到 size_t 的空unordered_map 无序map
  string word;
  while(cin >> word) ++word_count[word];//提取word的计数器并将其加1
  for(const auto &w : word_count)       //对map中的每一个元素  pair类型 first成员保存关键字 second成员保存对应的值
        cout << w.first << " occurs " << w.second
             << ((w.second > 1)? "times" : "time") << endl;// 报告每个单词出现的次数 单词 无序

无序容器的存储方式

管理桶

无序容器在存储组织上为一组桶(bucket),每个桶保存零个或多个元素,使用一个hash function(哈希函数)将元素映射到桶,

为了访问元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶,容器将具有固定哈希值的所有元素都保存在相应的桶中。

对于存储自定义类型的 无序容器   需要自己定义哈希函数 和比较函数(==) 来实现存储

  //哈希函数
  size_t hasher(const Sales_data &sd){
        return hash<string>()(sd.isbn());//h哈希类型
  }
  //比较函数
  bool eqOp(const Sales_data &lhs, const Sales_data &rhs){
    return   lhs.isbn() == rhs.isbn();// 比较函数 
  }
  // 定义特定类的 unordered_multiset
  using = sd_unordered_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>// 两个函数指针