二 表、栈和和队列
本章讨论的主题是:
-
介绍抽象数据类型(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表达了链表的一般思想。
图2-1 一个链表
链表是由一系列不必在内存中相连的节点组成。每一个节点均含有表示元素和包含该元素后继元素的节点的链(link)。我们称之为next链。最后一个单元的next链指向NULL。
执行printList()和find(x),与数组一样都使花费线性时间。findKth操作不如数组实现效率高。
remove方法可以通过修改一个next引用来实现。如下图:
图2-2 从链表中删除
insert方法需使用new操作符从系统取得一个新节点,此后执行两次引用调整。其一般想法如下图,其中的虚线表示原来的指针。
图2-3 向链表插入
我们将链表中的每一个节点都添加一个指向上一项的链接。如下图所示,这称双向链表(doubly linked list)。
图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是空双向链表。
图2-5 有表头节点和尾节点的双向链表
图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。。。
}