【数据结构】STL中的vector和list

来源:互联网 发布:淘宝网桌面图标 编辑:程序博客网 时间:2024/06/09 04:48

表的ADT有两种流行的实现方法,vector提供表ADT的一种可增长的数组实现。使用vector的有点在与它是以常数时间可索引(indexabel)的。缺点是插入新项和删除现有项的代价高昂,除非变化发生在vector的尾端。而list则提供表ADT的双向链表实现。使用list的有点事插入新项和删除现有项代价地炼,但是假设变动的位置是已知的。缺点是list不用一被索引,vector和list两者在执行查找时都是低效的。
在STL中,vector和list都是类模板,他们用其所存储的项的类型来实例化而成为具体的类。二者有几个方法是共有的。下面所示的3中方法对所有的STL容器都是可用的:

  • int size() const; //返回容器中元素的个数
  • int clear(); //从容其中删除所有元素
  • bool empty() const; //若容器不含有元素则返回true,否则返回false。

在STL中,vector和list都是类模板,他们用其所存储的项的类型来实例化而成为具体的类。二者有几个方法是共有的。下面所示的3中方法对所有的STL容器都是可用的,:

  • int size() const; //返回容器中元素的个数
  • int clear(); //从容其中删除所有元素
  • bool empty() const; //若容器不含有元素则返回true,否则返回false。

vector和list都支持以常数时间向表的尾端添加和从表的尾端进行删除操作。vector和list两者都支持以常数时间访问表的前端项。这些操作时:

  • void push_back(const Object & x); //把x添加到表的尾端

  • void pop_back(); //删除位于表的尾端的对象

  • const Object & back() const; //返回位于表的尾端处的对象(还提供一个返回引用的修改函数)
  • const Object & front() const; //返回位于表的前端处的对象(还提供一个返回引用的修改函数)

因为双端链表在其前端可以进行高效的移动,但是vector却不能,因此下面的两个方法只有list有:

  • void push_front(const Object &x); //把x添加到表的前端
  • void pop_front(); //删除位于表前端处的对象

但是vector有它自己的方法集,这些操作list并不具备,有两个操作可以进行高效的索引操作,而其余的两个方法允许程序猿查看和改变内部容量。这些方法是:

  • Object & operator[ ](int idx); //返回vector中下标为idx的对象,不带界限检查(还提供一个返回常量引用的访问函数)
  • Object & at(int idx); //返回vector中下标为idx的对象,带有界限检查(还提供一个返回常量引用的访问函数)
  • int capacity() const; //返回vector的内部容量
  • void reserve(int newCapacity); //设置新的容量。如果有好的估计可用,那么它可以避免扩展vector。

关于两种容器的迭代器。

对表的一些操作,尤其是那些在表的中间进行精密的插入和删除的操作,需要位置的概念。在STL中,位置由内嵌类型iterator来表示。特别的是,对于list,位置由类型
list::iterator表示,对于vector,位置有vector::iterator类表示,等等。在描述一些方法时,我们将直接使用iterator作为简记,但是在编写代码时还是使用具体的内嵌类名。


获取迭代器

STL表(以及所有的STL容器)定义了一对方法L

  • iterator begin(); //返回一个适当的迭代器,表示容器中的第一项。

  • iterator end(); //返回一个适当的迭代器,表示容器中的尾端(终端)标记(endmarker)(也就是容器中最后一项之后的位置)

迭代器方法:

  • itr++和 ++itr :将迭代器推进到下一个位置。前缀形式和后缀形式都是可以使用的。
  • *itr:返回对存储在迭代器itr位置上对象的引用。所返回的引用可以允许修改,也可以不允许修改。
  • itr1 == itr2:如果两个迭代器指向了同一个位置,则返回true,否则返回false
  • itr1 != itr2:如果两个迭代器指向了不同的位置,则返回true,否则返回false

需要对迭代器的容器操作
需要使用迭代器的三个最流行的方法是那些从表(vector 或 list)的指定位置上进行添加或删除的操作L•

  • iterator insert(iterator pos, const Object &x);//把x添加到表中由迭代器pos所给定的位置上。这是对list(不是vector)的常数时间的操作。返回值是指向被插入项的位置的一个迭代器
  • iterator erase(iterator pos); //删除由迭代器所给定的位置上的对象。这是对list(不是vector)的常数时间的操作。返回值是调用之前pos的后级元素所在的位置。这个操作使得pos失效,现在它是多余的了,因为它正在指向的容器项已经被删除。
  • iterator erase(iterator start, iterator end); //删除从位置start开始直到(但不包括)位置end,为止的所有的项,注意,整个表的删除可以通过调用c.erase(c.begin(),c.end())来进行删除。

例子,对表使用erase
对于一个给定的表,每隔一项删除表中的一项,所以,假设原始表中含有{6,5,1,4,2}这五个元素,在调用方法之后,将会只包含5,4。通过遍历整个表并对每个第2项使用erase方法完成操作,对于list,这是一个线性时间的例程,因为每次调用erase都是花费常数时间。但是,对于vector,将会花费二次时间,因为每次调用erase都是低效率的,需要时间O(N)。因此,我们通常只为list编写代码。具体代码如下所示:

template <typename Container>  void removeEveryOtherItem(Container & lst)  {      auto itr = lst.begin();      while(iter != lst.end())      {             iter = lst.erase(itr);          if(itr != lst.end())              ++itr;      }  }  

需要说明的是,如果运行这个程序,传递一个list,那么对于800000项的list将会花费0.039s,而对于1600000项的list将会花费0.073s。
可以很明显的看出,这是一个线性的例程,因为运行时间是按照与输入大小的相同增长因子增长的。
当我们传递一个vector时,该例程对于800000项的vector则花费几乎5分钟的运行时间,而对于1600000项的vector,花费大约为20分钟。当输入增长两倍时运行时间增长4倍,这个二次的行为是一致的。


const_iterators

在使用STL的容器时,编译器还要求我们使用consr_iterator来遍历一个常量集合。这个功能通过提供两种版本的begin和两种版本的end来完成遍历:

  • iterator begin()

  • const_iterator begin() const

  • iterator end()
  • const_iterator end()const

这两种形式的begin和end可以在同一个类中出现是因为函数的定常性(const-ness)也就是访问函数还是修改函数,被认为是特征(signature)的一部分。
如果是对非常量容器调用begin,那么返回一个iterator的“修改函数”版的begin被调用,然而,如果对常量容器调用begin,那么所返回的则是const_iterator,并且返回值不可以赋给iterator,如果我们试图去对它进行赋值,则会报错。
如果使用auto进行迭代器的声明,则编译器将会推算出它替代的是iterator还是const_iterator。这在很大程度上缓解了程序猿必须牢记正确的迭代器类型的负担,而这也是auto的预期使用的目的之一。再有,像vector和list这样一些如上所述提供迭代器的库类,与基于范围的for循环是兼容的,就像用户定义的那些类一样。


vector的实现

下面进行实现STL中的vector,这个vector将会是第一类型(first-class type),含义是:不同于C++中的原始数组(primitive array),该vector的对象可以被复制,并且其所用的内存可以(通过它的析构函数)被自动回收。
为了避免和STL中的vector相互混淆,这里讲这个类定义为Vector,在写代码之前,概述主要细节如下所示:

  1. Vector将(通过一个指向所分配的内存块的指针变量)保留原始的数组、数组容量以及数组当时存储在Vector中的项数

  2. 该Vector将实现五大函数以提供为拷贝构造函数和operator=的深层开背(deep-copy)功能。并将提供一个析构函数用来挥手原始的数组。此外,还将实现C++11的移动功能。

  3. Vector将会提供改变Vector的大小(一般改成更大的数)的resize例程,以及reserve例程,后者将会改变Vector的容量(一般是更大的数)这个容量通过为原始数组获取新的内存块、把老内存块赋值到新内存块并回收老内存块而得以更新。
  4. Vector将提供operator[]的实现。
  5. Vector还提供诸如size,empty,clear(通常他们是一行代码),back,pop_back,和push_back等基本例程,如果大小和容量相同,则push_back将会调用reserve函数
  6. 对于内嵌类型iterator和const_iterator,Vector也会提供支持,并且提供相关联的begin方法和end方法。

具体实现代码如下所示:

//  // Created by 大幕 on 2017/9/13.  //  #ifndef VECTOR_VECTOR_H  #define VECTOR_VECTOR_H  #include <algorithm>  template <typename Object>  class Vector{  public:      explicit Vector(int initSize = 0):theSize{initSize},                                        theCapacity{initSize + SPARE_CAPACITY}      {          objects = new Object[theCapacity];      }      Vector(const Vector & rhs):theSize(rhs.theSize),                                 theCapacity(rhs.theCapacity),objects(rhs.objects)      {          objects = new Object[theCapacity + 1];          for(int k = 0;k < theSize;k++)          {              objects[k] = rhs.objects[k];          }      }      Vector & operator= (const Vector & rhs)      {          Vector copy = rhs;          std::swap(*this, copy);          return *this;      }      ~Vector()   //析构函数      {          delete []objects;      }      Vector(Vector && rhs):theSize(rhs.theSize),theCapacity(rhs.theCapacity),              objects(rhs.objects)      {          rhs.objects = nullptr;          rhs.theSize = 0;          rhs.theCapacity = 0;      }      Vector & operator= (Vector && rhs)      {          std::swap(theSize, rhs.theSize);          std::swap(theCapacity,rhs.theCapacity);          std::swap(objects,rhs.objects);          return *this;      }      void resize(int newSize)      {          if(newSize > theCapacity)          {              reserve(newSize * 2);   //将原来的存储空间翻倍          }          theSize = newSize;      }      void reserve(int newCapacity)      {          if(newCapacity < theSize)              return;          Object *newArray = new Object[newCapacity];          for(int k = 0;k < theSize;k++)          {              newArray[k] = std::move(objects[k]);           }          theCapacity = newCapacity;          std::swap(objects, newArray);          delete []newArray;      }      Object & operator[](int index)   //索引函数      {          return objects[index];      }      const Object & operator[](index)const      {          return objects[index];      }      bool empty()const      {          return size() == 0;      }      int size()const      {          return theSize;      }      int capacity()const      {          return theCapacity;      }      void push_back(const Object &x)      {          if(theSize == theCapacity)              reserve(2 * theCapacity + 1);          objects[theSize++] = x;      }      void push_back(Object && x)      {          if(theSize == theCapacity)              reserve(2 * theCapacity + 1);          Object[theSize++] = std::move(x);      }      void pop_back()      {          --theSize;      }      const Object & back()const      {          return objects[theSize - 1];      }      // 创建迭代器      typedef Object * iterator;      typedef const Object * const_iterator;      iterator begin()      {          return &objects[0];      }      const_iterator begin()const      {          return &objects[0];      }      iterator end()      {          return &objects[size()];      }      const_iterator end() const      {          return &objects[size()];      }      static const int SPARE_CAPACITY = 16;  private:      int theSize;      int theCapacity;      Object *objects;  };  #endif //VECTOR_VECTOR_H  

在resize函数中,这个代码段直接设置theSize的数据承运,但是确实在有可能拓展容量之后进行。容量扩展的代价是非常高昂的。因此,如果容量被扩展,那么它就要变成原来的两倍大小,以避免容量的再次改变,除非大小戏剧性地增加。扩展容量是通过reverse历程完成的。这个函数的代码段也可以用于收缩原数组,不过只能在指定的新容量至少和原大小相同的情况下才行。否则,reserve要求会被忽略。
[]取值运算通过operator[]来实现并不复杂,通过确保index不越出0和size()-1的范围,包括不在该范围则抛出异常,我们很容易添加对错误的检测。
需要说明的是
在迭代器使用++运算符时,++的定位很重要:在前缀++运算符下,*++iter先推进iter,然后使用心得iter来确定指向哪一项,同样objects[++theSize]将使得theSize增1,然后使用所得到的新值来给数组确定下标(但这不是我们想要的)。
pop_back和bcak函数通过错误检验得以实现,当大小是0的时候会抛出异常。
最后,需要说明的是,
在vector类型中,迭代器和指针之间具有一致性,这种一致性意味着,不适用C++数组而是用vector,资源消耗可能会稍高。
正如前面所说,它的不足之处在于程序没有对错误的检测。如果迭代器中iter超出重点标记,++iter和*iter都不需要发出错误信息。想要修改这个问题,就需要iterator和const_iterator都是内嵌类型而不是简单的指针变量。同时需要说明,在程序设计中,使用内嵌类型要常见的多,这也正是下面实现list实用的方法。

list的实现

我们知道,List类将作为双向量表被实现,而且需要将保留只想表两端的两个指针。这样就使得只要每次操作都发生在已知的位置上,那就总是花费常数时间的开销。

这个已知的位置可以在表的端点,也可以在迭代器指定的位置上。

在考虑设计时,需要提供下面四个类:

  1. List类本身,它包含连接到表两端的链,表的大小,以及一些方法。

  2. Node类,它很可能是一个私有的内嵌类。一个节点包含数据和指向前后两个节点的指针,以及适当的构造函数。

  3. const_iterator类,它抽象了位置的概念,而且是一个公共的内嵌类。该const_iterator存储当前一个指向“当前”节点的指针,并且提供基本迭代器操作的实现,所有的操作,包括=,==,!=和++等,这些操作都以重载运算符的形式出现。

  4. iterator类,它抽象了位置的概念,而且是一个共有的内嵌类。该iterator有着和const_iterator相同的功能,但是operator* 返回的是所指项的引用,而不是对该项常量引用。一个重要的技术问题是,iterator可以用在任何需要const_iterator的例程中,但是反之不一定成立。

因为迭代器类存储一个指向“当前节点”的指针,并且终端标记是一个合法的位置,所以在表的终端创建一个表示终端标记(endmaker)的附加节点是有意义的。此外,还可已在表的前端创建一个附加节点,在逻辑上表示开始标志。这类附加节点有时候称为标记节点(setinel node);特别的,前端节点有时候也叫做头结点(header node),而在尾端的节点有时候就叫做尾节点(tail node)


List程序实现:

template <typename Object>class List{  private:    struct Node //可以看到这个内嵌的结构体是私有的    {      Object data;      //数据域      Node *prev;       //前向指针      Node *next;       //后向指针      Node(const Object &d = Object {}, Node * p = nullptr,           Node * n = nullptr):data{d},prev{p},next{n} {}      Node(Object && d,Node * p = nullptr, Node * n = nullptr)        :data{std::move(d)},prev{ p }, next{ n }{ }    };  public:    class const_iterator    //公有内嵌类,const_iterator    {      public:        const_iterator( ):current{ nullptr }          { }        const Object & operator *()const        { return retrieve; }    //取出当前迭代器指向的值        const_iterator & operator++ ()      //++iter        {            current = current -> next;            return *this;        }        const_iterator operator++ (int) //iter++        {            const_iterator old = *this;            ++( *this);            return old;        }        bool operator==( const const_iterator & rhs)const        { return current == rhs.current; }        bool operator!=( const const_iterator & rhs)const        { return !(*this == rhs); }      protected:        Node *current;        Object & retrieve()const        {   return current-> data; }        const_iterator(Node *p):current{p}        {}        friend class List<Object>;  //声明友元类      }    class iterator: public const_iterator   //公有继承    {      public:        iterator( )        { }        Object & operator*( )        { return const_iterator::retrieve; }        const Object & operator*( )const        { return const_iterator::operator*( ); }        iterator & operator++ ( )        {            this->current = this->current->next;            return *this;        }        iterator operator++ ( )        {            iterator old = *this;            ++( *this );            return *this;        }      protected:        iterator( Node *p ) : const_iterator{ p }        { }        friend class List<Object>;    }  public:    List()    { init( ); }    List( const List & rhs)    {       init( );       for( auto & x : rhs)         push_back( x );    }    ~List()    {       clear( );       delete head;       delete tail;    }    List & operator= (const List & rhs)    {       List copy = rhs;       std::swap( *this, copy);       return *this;    }    List (List && rhs)      : theSize{ rhs.theSize }, head{ ths.head }, { rhs.tail }    {       rhs.thesize = 0;       rhs.head = nullptr;       rhs.tail = nullptr;    }    List & operator = (List && rhs)    {       std::swap( theSize, rhs.theSize );       std::swap( head, rhs.head );       std::swap( tail, rhs.tail );    }    iterator begin()    { return {head -> next}; }    const_iterator begin() const    { return {head -> next}; }    iterator end()    { return {tail}; }    const_iterator end() const    { return {tail}; }    int size() const    { return theSize;}    bool empty() const    { return size() == 0;}    void clear()    {        while( !empty() )          pop_front();    }    Object & front()    { return *begin(); }    const Object & front() const    { return *begin(); }    Object & back()    { return *--end(); }    const Object & back() const    { return *--end(); }    void push_front( const Object & x)    { insert( begin(), x); }    void push_front( Object && x)    { insert( begin(), std::move( x )); }    void push_back( const Object & x)    { insert( end(), x); }    void push_back(Object && x)    { insert( end(), std::move( x )); }    void pop_front()    { erase( begin() ); }    void pop_end()    { erase( --end() ); }    // 在itr之前插入 x    iterator insert( iterator itr,const Object & x )    {       Node *p = itr.current;       theSize++;       // 注意对下面这句话的理解       return { p->prev = p->prev->next = new Node{ x, p->prev, p}};    }    // 在itr之前插入 x    iterater insert( iterator itr,Object && x )    {       Node *p = itr.current;       theSize++;       // 注意对下面这句话的理解       return { p->prev = p->prev->next                         = new Node{ std::move( x ), p->prev, p}};    }    // 删除在itr处的项    iterator erase( iterator itr )    {       Node *p = itr.current;       iterator retVal { p->next };       p->prev->next = p->next;       p->next->prev = p->prev;       delete p;       theSize--;       return retVal;    }    // 删除从 from 到 to 处的值    iterator erase( iterator from, iterator to) //删除迭代器范围内的元素    {       for( iterator itr = from; itr != to;)         itr = erase( itr );       return to;    }  privateint theSize;    Node *head;    Node *tail;    void init()    {      theSize = 0;      head = new Node;      tail = new Node;      head->next = tail;      tail->prev = head;    }}

在上面程序中一开始的部分我们可以看到私有内嵌的节点类生命的开头部分。这里并没有使用class关键词,而是使用struct。在C++中,struct是C语言遗留下来的语法。在C++中struct基本上就可以当做class来使用,只不过是其中的成员默认是共有的。而class中的成员默认是私有的。

需要说明的是,在程序中,一般使用struct来表示一种主要包含被直接存取而不是通过函数访问的数据的类。在这里的Node类中,让每个成员都是共有的并不是问题,因为Node类本身是私有的,而List类的外部是不可对其进行访问的。

上面程序中第二部分是公有内嵌类const_iterator以及公有继承这个类的iterator类。这里继承的作用是用来说明,iterator具有和const_iterator完全相同的功能,并且可以用在任何需要const_operator类的地方。但是使用继承的结构在最一般的情况下,会导致代码出现严重的语法负担(会导致大量的 virtual 关键词出现在程序中)。

在下面这两句话中

protected:    Node *current;

const_iterato r存储了一个指向“当前”节点的指针作为它单独的数据成员。正常情况下,这个成员是私有的,然而如果它是私有,那么 iterator 久无权访问它了。所以把 const_iterator 的成员标记为 protected 使得从 const_iterator 继承来的那些类有权访问这些成员,但是不允许其他类有这种访问权限。

在下面这两句话中

const_iterator( Node *p ) : current{ p }{ }

在前面我们看到在 List 类中 begin 和 end 的实现用过的const_iterator 的构造函数,但是我们不想让所有的类都看到这个构造函数(从指针变量显式地构造迭代器是不允许的),因此它不能是公有的,可是我们还想要 iterator 类看到它。因此在下面我们将 List 类声明为友元类

friend class List<Object>;

而这也就赋予了 List 类访问 const_iterator 的非公有成员的权利。

const_iterator 中的公有方法都用到了运算符重载。operator==,operator!= 以及operator* 都比较简单。主要需要说明的是 operator++ 的实现。因为 operator++ 的前缀形式和后缀形式在语义(以及优先级)上是完全不同的,因此需要为每种情况分别编写程序。而这就涉及到了C++中的知识,C++中规定,通过为前缀形式指定空参数表而为后缀形式指定(匿名的)单参数 int 来赋予它们不同的特征。所以,++iter调用的是零参数的operator++,而iter++调用的是单参数的operator++函数。通过实现的方法我们可以看出:在选用前缀 ++ 和后缀 ++ 的许多场合下,前缀形式要比后缀形式更快。

在下面这段程序中,保护型构造函数用一个初始化形参列表来初始化继承来的当前节点。因此就不必重新实现 operator== 和 operator!= ,因为它们是继承来的,是不变的。

protected:    iterator( Node *p ) : const_iterator{ p }    { }

但是我们提供了一对新的 operator++ 的实现(因为返回类型改变了),这两个运算符重载的实现隐藏了 const_iterator 原有的实现,而且还为 operator* 提供了一对访问函数/修改函数。如下所示:

const Object & operator* ( ){ return const_iterator::operator*(); }

直接使用了与 const_iterator 完全相同的实现。这个访问函数显式地在 iterator 中实现,因为否则原始的实现会被新添加的新版修改函数所隐藏。

在实现构造函数时,因为零参数构造函数和拷贝构造函数二者都必须配置头结点和尾结点,所以提供了私有的 init 函数。init 函数用来创建一个空的 List。析构函数回收头结点和尾结点;当析构函数调用 clear 时,所有其他节点都没回收。类似的,拷贝构造函数通过调用一些公有方法而非通过尝试低水平的指针操作而实现。

重新看上面所述的全部代码,能够看到一些可能发生的错误,因为着了没有提供对他们的检查。例如,传递给 erase 和 insert 的迭代器可能是未被初始化的,或者初始化用的是错误的表。当迭代器已经指向终端标志或者尚未初始化的时候,不排除对其使用 ++ 或 * 操作的可能性。

未被初始化的迭代器将会让 current 指向 nullptr,以便条件容易得到检测。终端标记的 next 指针指向 nullptr,因此对于终端标记条件进行 ++ 或 * 操作的测试也很容易。然而,为了确定传递给 erase 或 insert 的迭代器是否正确的表的迭代器,这个迭代器必须存储一个额外的表示指向 List 的指针的数据成员,其中的 List 就是构造迭代器所用的双向链表。

实现方法是在 const_iterator 类中,添加一个指向 List 的指针,并且修改受保护的构造函数以该 List 作为一个参数。此外,还可以添加一些方法,在某些要求得到不满足的时候将会抛出异常。程序经过修订的受保护部分看起来有些像下面的代码。这是所有对 iterator 和 const_iterator 构造函数的调用本来以前他们只需要一个参数,但是现在都需要两个参数,如下代码所示:

const_iterator begin() const{    const_iterator itr{ *this, head};    return ++itr;}

const_iterator 经过修改后的受保护部分的代码,添加了执行附加的错误检测能力。

protected:    const List<Object> *theList;    Node *current;    const_iterator( const List<Object> & lst, Node *p)                    : theList{ &lst }, { p }    { }    void assertIsValue( ) const    {        if( theList == nullptr || current == nullptr || current == theList->head)          throw IteratorOutOfBoundsException{ };    }

带有附加错误检测的 List 的 insert 函数如下:

// 在itr前面插入 xiterator insert( iterator itr, const Object & x){    itr.assertIsValid( );    if( itr.theList != this)      throw IteratorMismatchException{ };    Node *p = itr.current;    theSize++;    return {*this, p->prev = p->prev->next = new Node{ x,p->prev,p} };}