数据结构与算法学习(二)

二 表、栈和和队列

  本章讨论的主题是:

  •  介绍抽象数据类型(ADT)的概念。

  •  阐述如何对表进行高效的操作。

  •  介绍栈ADT及其在实现递归方面的应用。

  •  介绍队列ADT及其在操作系统和算法设计中的应用。

  本章给出两个库类vector和list的重要子集实现代码。

  1 抽象数据类型(ADT)

  抽象数据类型(abstract data type,ADT)是带有一组操作的一些对象的集合。是数学抽象的,定义中并没有提到这组操作如何实现。表、集合、图以及它们各自的操作一起形成的这些对象都可以看作是ADT。对于集合ADT可以有加(add)、删除(remove)、大小(size)以及包含(contains)这些操作。当然也可以只要两种操作:并(union)和查找(find),这两种操作又在该上定义了一种不同的ADT。

  2 表ADT

  我们将处理形如A0,A1,A2,…,AN-1的一般的表。这个表的大小是N。我们将称大小为0的表为空表(empty list)。

  对于空表外的任何表,我们说Ai后继Ai-1(或继Ai-1之后)并称Ai-1(i<N)前驱Ai(i>1)。表中第一个元素是A0,而最后一个元素是AN-1。而我们将不指定A0的前驱元,也不定义AN-1的后继元。元素Ai在表中的位置(position)为i。

  与这些“定义”相关的是我们要在表ADT上进行操作的集合。printList和makeEmpty是常用的操作,其功能显而易见;find返回项首次出现的位置;insert和remove一般是从表的某个位置插入和删除一些元素;而findKth则返回某个位置上(作为参数而被指定)的元素。

 

   2.1 表的简单数组实现

  对表的所有操作都可以使用数组来实现。数组实现使得printList以线性的时间执行,findkth则花费常数时间。而插入和删除的花费却可能是昂贵的,这两种操作最坏的情况为Ο(N)。一般表是通过在末尾插入元素来建立的,这种情况适合数组。而插入和删除在整个表中都发生,特别是表的前端发生,数组就不是个好的选择。

 

   2.2 简单的链表

  为了避免插入和删除的线性开销,我们需要允许表可以不连续储存。图2-1表达了链表的一般思想。

205342_L8Wd_2537915.jpg

图2-1 一个链表

  链表是由一系列不必在内存中相连的节点组成。每一个节点均含有表示元素和包含该元素后继元素的节点的链(link)。我们称之为next链。最后一个单元的next链指向NULL。

  执行printList()和find(x),与数组一样都使花费线性时间。findKth操作不如数组实现效率高。

  remove方法可以通过修改一个next引用来实现。如下图:

210310_kzti_2537915.jpg

图2-2 从链表中删除

  insert方法需使用new操作符从系统取得一个新节点,此后执行两次引用调整。其一般想法如下图,其中的虚线表示原来的指针。

210644_h4Vd_2537915.jpg

图2-3 向链表插入

  我们将链表中的每一个节点都添加一个指向上一项的链接。如下图所示,这称双向链表(doubly linked list)。

211359_0Vmp_2537915.jpg

图2-4 双向链表

 

   2.3 STL中的向量和表

  表ADT就是C++的标准模板库(Standard Template Library,STL)中实现的数据结构之一。一般来说这些数据结构称为集合(collection)或者容器(container)。

  表ADT有两个流行的实现。vector给出了表ADT的可增长数组实现,优点在于其在常数的时间里是可索引的,缺点是插入或删除已有项的代价昂贵,除非是这些操作发生在vector尾部。list提供了表ADT的双向链表实现,使用list的优点是,如果变化发生的位置已知,插入新项和删除已有项的代价很小,缺点是list不容易索引。vector和list在查找时效率都很低。list为双向链表。

  vector和list两者都使用其包含项的类型来实例的类模板。所有前三个方法实际上对所有STL容器都适用:

int size() const; //返回容器内的元素个数
void clear(); //删除容器中所有的元素
bool empty(); //如果容器没有元素,返回true;否则返回false

  vector和list两者都支持在常量的时间内在表的末尾添加或删除项,以及访问表的前端的项:

void push_back(const Object &x); //在表的末尾添加x
void pop_back(); //删除表的末尾的对象
const Object &back() const; //返回表的末尾对象(也提供返回引用的修改函数)
const Object &front() const; //返回表的前端对象(也提供返回引用的修改函数)

  双向链表list支持在表的前端进行高效的改变:

void push front(const Object &x); //在list的前端添加x
void pop_front(); //在list的前端删除对象

  vector有两个方法可以高效索引,还有两个方法可以观察和改变vector的内部容量;

Object & operator[] (int idx); //返回vector中idx索引位置的对象,包含边界检测(也提供返回常数引用的访问函数)
Object & at(); //返回vector中idx索引位置的对象,包含边界检测(也提供返回常数引用的访问函数)
int capacity() const; //返回vector内部容量
void reserve (int new Capacity); //设定vector的新容量,如果已有良好的估计的话,这可以避免对vector进行扩展。

 

    2.3.1 迭代器

  在开始的时候,需要处理三个问题:第一,如何得到迭代器;第二,迭代器可以执行什么操作;第三,哪些表ADT方法需要迭代器作为形参。

 

     2.3.1.1 获得迭代器

  对第一个问题,STL表(包括其他STL容器)定义了一对方法:

iterator begin(); //返回指向容器的第一项的一个适合的迭代器
iterator end(); //返回指向容器的终止标志(容器中最后一个项的后面的位置)的一个适当的迭代器。

  end方法的返回迭代器指向容器的“边界之外”。

 

     2.3.1.2 迭代器方法

  除了复制之外,迭代器最常见的操作如下:

itr++和++itr; //推进迭代器itr至下一个位置。前缀和后缀两种形式都使允许的。
*itr; //返回储存在迭代器itr指定位置的对象的引用。返回的引用或许能、或许不能被修改
itr1=itr2; //如果itr1和itr2都指向不同位置就返回true,否则,返回false
itr1!=itr2; //如果itr1和itr2都指向不同位置就返回true,否则,返回false

 

     2.3.1.3 需要迭代器的容器操作

  对于最后一个问题,需要使用迭代器的三个流行方法。

iterator insert(iterator pos,const Object &x); //添加x到表中迭代器pos所指位置之前的位置。这对list是常数
时间操作,但对vector则不是。返回值是一个指向插入项位置的迭代器。
iterator eraset(iterator pos); //删除迭代器所给出位置的对象。这对list来说是常数时间操作,但对vector则不是。
返回值是调用之前pos所指向元素的下一个元素的位置。这个操作使pos失效。pos不再有用,因为它所指向的容器变量已经被
删除。
iterator erase(iterator start, iterator end); //删除所有的从位置start开始,直到位置end(但是不包括end)
的所有元素。注意,整个表的删除可以调用c.erase(c.begin(),c.end())。

 

    2.3.2 示例:对表使用erase

  这里给出一个例子,从表的起始项开始间隔地删除项。为了比较vector和list的效率,下面的函数是一个模板,对于400000项的list,程序将花费0.062s;而800000项的list,程序将花费0.125s,这是个线性时间例子。而对vector,400000项花费差不多两分半钟,800000项超过十分钟,输入两倍,时间增长四倍,这是二次算法的表征。

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

    2.3.3 const_iterator

  *itr的结果不只是迭代器指向的项的值,也是该项本身。为了研究其优点,假设要将一个集合里的所有项都改为一个特殊的值,下面是一个用于vector和list的线性时间泛型代码:

template<typename Container,typename Object>
void change(Container &c, const Object &newValue)
{
  typename Container::iterator itr = c.begin();
  while(itr != c.end())
  {
    *itr++ = newValue;
  }
}

  iterator类型可能改变所指的值。STL提供的解决方案是每个集合同时包含嵌套的const_iterator类型。和前者的区别在于:const_iterator的operator*返回常量引用,这样就不能被赋值了。

  更进一步,编译器还会要求必须使用const_iterator类遍历常量集合,如下:

iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;

  下面的代码是使用const_iterator打印任意集合的例子。集合的项打印在括号中,并用逗号隔开:

template<typename Container>
void printCollection(const Container &c, ostream &out = cout)
{
  if( c.empty() )
    out << "{empty}"
  else
  {
    typename Container::const_iterator itr = c.begin();
    out << "[" << *itr++;  //Print first item
    
    while( itr != c.end() )
      out << "," << *itr++;
    out << "]" << endl;
  }
}

   2.4 向量的实现

  数组的一些重要特性:

  •  数组就是指向一块内存的指针变量;实际的数组的大小必须由程序员单独确定。

  •  内存块可以通过new[]来分配,但是相应地也就必须用delete[]来释放。

  •  内存块的大小不能改变(但是可以定义一个新的具有更大内存块的数组,并且用原来的数组来将其初始化,然后原来的内存块就可以释放了)。

  为了避免与库函数类混淆,我们的类模板命名为Vector。其主要细节概括如下:

  •  Vector将仍然是基本数组(通过一个指针变量来指向分配的内存块)。数组的容量和当前的数组项数目储存在Vector里。

  •  Vector将通过实现“三个函数”,为复制构造函数和operator=提供深复制,同时也提供析构函数来回收基本数组。

  •  Vector将提供resize方法来改变Vector的大小(通常是更大的数);提供reserve方法来改变Vector的容量(通常是更大的数)。容量的改变是通过为基本数组分配一个新的内存块,然后复制原内存块的内容到新块中,再释放原块的内存来实现的。

  •  Vector将提供operator[]的实现(典型实现有访问和修改函数两个版本)

  •  Vector将提供基本的方法,如size、empty、clear(以上都是典型的方法)、back、pop_back、push_back。如果大小和容量都是一样的话,push_back方法将调用reserve来增大Vector的容量。

  •  Vector将支持嵌套的iterator和const_iterator类型,并提供begin和end方法。

  作为STL的副本,Vector也有有限的错误检查。

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 ) : objects( NULL )
    { operator= ( rhs ); }
    ~Vector()
    { delete [] objects; }
    
    const  Vector & operator= ( const Vector & rhs )
    {
      if( this != &rhs )
      {
        delete [] objects;
        theSize = rhs.size();
        theCapacity = rhs.theCapacity;
        
        objects = new Object[ capacity() ];
        for( int k = 0; k < size(); k++ )
            objects[ k ] = rhs.objects[ k ];          
      }
      return *this;
    }
    
    void resize( int newSize )
    {
      if( newSize > theCapacity )
        reserve( newSize * 2 + 1 );
        theSize = newSize;
    }
    
    void reserve( int newCapacity )
    {
      if( newCapacity < theSize )
        return;
        
      Object *oldArray = objects;
      
      objects = new Object[ newCapacity ];
      for( int k = 0; k < theSize; k++ )
        objects[ k ] = oldArray[ x ];
        
      theCapacity = newCapacity;
      
      delete[] oldArray;      
    }
    
    Object & operator[] ( int index )
    { return objects[ index ]; }    
    const object & operator[] ( int index ) const
    { return object[ 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 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() ]; }
    
    enum ( SPARE_CAPACITY = 16 );
    
  private:
    int theSize;
    int theCapacity;
    Object *objects;   
}

 

   2.5 表的实现

  为了与库类区分,我们命名表模板为List。为了设计需要,我们提供以下4个类:

  •  List类本身。包含连接到表两端的链接、表的大小以及一系列的方法。

  •  Node类。该类看起来像是私有的嵌套类。一个节点包含数据和用来指向其前和其后的节点指针,以及适当的构造函数。

  •  const_iterator类。该类抽象了位置的概念,是一个公有的嵌套类。const_iterator存储指向当前节点的指针,并且提供基本迭代器操作的实现,以及所有的重载操作符,例如=、==、!=、++。

  •  iterator类。该类抽象了位置的概念,是一个公有的嵌套类。除了operator*操作返回所指向项的引用,而不是该项的常量引用的功能外,iterator具有与const_iterator相同的功能。一个重要的技术点是iterator可以用任何需要是要const_iterator的方法里,反之则不然。

  在表的和前端和尾部生成一个额外的节点表示开始标志和尾部标志。这些额外的节点有时被称为哨兵节点,特别的,头部的称为表头节点(header node),尾部的称为尾节点(tail node)。使用这些额外的节点的好处是可以去掉很多特例,可极大简化程序代码。图2-5是带表头和尾节点的双向链表,图2-6是空双向链表。

123722_qfLB_2537915.jpg

图2-5 有表头节点和尾节点的双向链表

123857_8tQi_2537915.jpg

图2-6 具有表头节点和尾节点的空双向链表 

template <typename Object>
class List
{
  private:
    struct Node
    {
      Object data;
      Node * prev;
      Node * next;
      
      Node( const Object & d =Object(), Node *p = NULL, Node *n = NULL )
      : data( d ), prev( p ), next( n ) { } 
    };
    
  public:
    class const_iterator
    {
      public:
        const_iterator( ) : current( NULL )
        { }
        
        const Object & operator* () const
        { return retieve( ); }
        
        const_iterator & operator++ ()
        {
          current = current->next;
          return *this;
        } 
        
        const_iterator & operator++ ( int )
        {
          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 : const_iterator
    {
      public:
        iterator();
        { }
        
        Object & operator* ()
        { return retrieve(); }
        const Object & operator* () const
        { return const_iterator::operator*(); }
        
        const_iterator & operator++ ()
        {
          current = current->next;
          return *this;
        } 
        
        const_iterator & operator++ ( int )
        {
          const_iterator old = *this;
          ++( *this );
          return old;
        }       
      protected:
        iterator( Node *p ) : const_iterator( p )
        { }
        
        friend class List<Object>;
    }
    
  public:
    //to be continue。。。
}


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  


 

 

 

 

   

 

 

 

 

 

 

转载于:https://my.oschina.net/u/2537915/blog/637954

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值