1 线性表定义
线性结构:线性结构中的元素之间的关系是一对一的关系。
线性表(linear list):也称有序表(order list),它的每一个实例都是元素的一个有序集合。每一个实例的形式为(e0,e1,e2,...,en-1),其中n是有穷自然数,ei是线性表的元素,i是线性表元素ei的索引,n是线性表的大小或长度。元素可以被看做原子,它们本身的结构与线性表的结构无关,但是必须是同一种类型。当n为0时,线性表为空;当n>0时,e0是线性表的首元素,en-1是线性表的最后一个元素。可以认为e0先于e1,e1先于e2。除了这种先后关系外,线性表不在有其他关系。一般来说,第一个元素无前驱,最后一个元素无后驱,其他每个元素都有且只有一个前驱和后驱。
1.1 线性表的抽象数据类型
一个线性表可以用一个抽象数据类型(abstract data type,ADT)来说明,既说明它的实例,也说明对他的操作。这里省略了对创建一个实例和撤销一个实例的操作说明。
ADT linearList
{
实例
有限个元素的有限集合
操作
empty();若为空,则返回true,否则返回false
size();返回线性表的大小(表的元素个数)
get(index);返回线性表中索引为index的元素
indexOf(x);返回线性表中第一次出现x的索引。若不存在,则返回-1
erase(index);删除索引为index的元素,索引大于index的元素其索引减1
insert(index,x);把x插入线性表中索引为index的位置上,索引大于等于index的元素其索引加1
};
以下为一个线性表的抽象类
template<class T>
class linearList
{
public:
virtual ~linearList() {};
virtual bool empty() const = 0;
//返回true,当且仅当线性表为空
virtual int size() const = 0;
//返回线性表的元素个数
virtual T& get(int theIndex) const = 0;
//返回索引为theIndex的元素
virtual int indexOf(const T& theElement) const = 0;
//返回元素theElement第一次出现时的索引
virtual void erase(int theIndex) = 0;
//删除索引为theIndex的元素
virtual void insert(int theIndex, const T& theElement) = 0;
//把theElement插入线性表中索引为theIndex的位置上
};
2 线性表的顺序存储结构
指的是用一段地址连续的存储单元依次存储线性表的数据元素。其实用数组来存储线性表的元素。
要创建一个数组类(如vector) ,以实现抽象数据类型linearList,必须首先选择数组element的类型和数组长度。使用模板可以很好解决第一个问题。使用动态数组可以很好地解决第二个问题。首先按照用户估计的长度创建数组,然后在数组空间不足的情况下,动态地增加数组长度。(备注:下次可以学习下vector如何高效动态增加长度)
2.1 类arrayList的定义
template<class T>
class arrayList : public linearList<T>
{
public:
//构造函数,复制构造函数和析构函数
arrayList(int initialCapacity = 10);
arrayList(const arrayList<T>&);
~arrayList() {delete[] element};
//ADT方法
bool empty() const {return listSize == 0;};
int size() const {return listSize;}
T& get(int theIndex) const;
int indexOf(const T& theElement) const;
void erase(int theIndex);
void insert(int theIndex, const T& theElement);
//其他方法
int capacity() const {return arrayLength;}
//给出数组element当前的长度
protected:
void checkIndex(int theIndex) const;
//若索引theIndex无效,则抛出异常
T* element; //存储线性表元素的一维数组
int arrayLength; //一维数组的容量
int listSize; //线性表的元素个数
}
注意:size()和capacity()的区别:
(1)capacity()返回的是当前能容纳元素容量
(2)size()返回的是元素个数
(可以参考vector里resize()和reverse()的用法)
从下面类构造函数也能看出两者区别:
2.2 类arrayList构造函数
template<class T>
arrayList<T>::arrayList(int initialCapacity)
{//构造函数
if(initialCapacity < 1)
{
ostringstream s;
s << "Initial Capacity = " << initialCapacity << "Must be > 0";
throw illegalParameterValue(s.str());
}
arrayLength = initialCapacity;
element = new T[arrayLength];
listSize = 0;
}
template<class T>
arrayList<T>::arrayList(const arrayList<T>& theList)
{
arratLength = theList.arrayLength;
listSize = theList.listSize;
element = new T[arrayLength];
copy(theList.element, theList.element + listSize, element);
//这里用到STL算法copy
}
分析:如果操作符new的时间复杂度为O(1),那么当T是基本类型时,构造函数的基本类型是O(1)。当T是用户自定义类型时,构造函数的时间复杂度是O(initialCapacity),因为在创建数组时,数组每一个位置上的用户自定义类型T都需要调用构造函数。方法empty、size和capacity的时间复杂度都是O(1)。复制构造函数的时间复杂度是O(n),其中n是要复制的线性表的大小。
2.3 arrayList实例化
用数组描述的线性表需要使用下面的语句来创建/实例化。
//创建两个容量为100的线性表
linearList *x = (linearList)new arrayList<int>(100);
arrayList<double> y(100);
//利用容量的缺省值创建一个线性表
arrayList<char> z;
//用线性表y复制创建一个线性表
arrayList<double> w(y);
2.4 arrayList的基本方法
template<class T>
void arrayList<T>::checkIndex(int theIndex) const
{//确定索引在theIndex在0和listSize - 1之间
if(theIndex < 0 || theIndex >= listSize)
{
ostringstream s;
s << "Index = " << theIndex << " size = " << listSize;
throw illegalIndex(s.str());
}
}
template<class T>
T& arrayList<T>::get(int theIndex) const
{//返回索引为theIndex的元素
//若此元素不存在,则抛出异常
checkIndex(theIndex);
return element[theIndex];
}
template<class T>
int arrayList<T>::indexOf(const T& theElement) const
{//返回元素theElement第一次出现的索引
//若该元素不存在,则返回-1
//查找元素theElement
int theIndex = (int)(find(element, element + listSize, theElement) - element);
//这里用到STL算法find
//确定元素theElement是否找到
if(theIndex == listSize)
{
return -1;
}
else
{
return theIndex;
}
}
分析:方法checkIndex和get的时间复杂度都为O(1),indexOf的时间复杂度为O(listSize)。
2.5 插入一个元素
线性表的顺序存储结构-插入算法的思路:
(1)如果插入位置不合理,抛出异常;
(2)如果线性表长度大于等于数组长度(或者说最大长度),则抛出异常或者动态增加容量;
(3)首先将索引从theIndex到listSize-1的元素向右移动一个位置;
(4)将新元素插入索引为theIndex的位置
(5)线性表长度加1。
template<class T>
void arrayList<T>::insert(int theIndex, const T& theElement)
{//从索引theIndex处插入元素theElement
if (theIndex < 0 || theIndex > listSize)
{//无效索引
ostringstream s;
s << "Index = " << theIndex << " size = " << listSize;
throw illegalIndex(s.str());
}
//有效索引,确定数组是否已溢
if (listSize == arrayLength)
{//数组空间已满,数组长度倍增
changeLength1D(element, arrayLength, 2 * arrayLength);
arrayLength *= 2;
}
//把元素向右移动一个位置
copy_backward(element + theIndex, element + listSize, element + listSize + 1);
//STL算法copy_backward
element[theIndex] = theElement;
listSize++;
}
template<class T>
void changeLength1D(T*& a, int oldLength, int newLength)
{
if (newLength < 0)
{
throw illegalParameterValue("new length must be >= 0");
}
T* temp = new T[newLength]; //新数组
int number = min(oldLength, newLength); //需要复制的元素个数
copy(a, a + number, temp);
delete[] a;
a = temp;
}
分析:确认是否抛出异常,时间复杂度为O(1)。数组长度加倍,时间复杂度时O(listSize)。移动数组元素,时间复杂度时O(liseSize-theIndex)。因此,总的时间复杂度时O(listSize)。
分析:changeLength1D这个函数,创建数组为O(1),如果new不抛出异常的话,将源数组复制到目标数组的时间复杂度为O(n)。当数组需要加大数组长度时,数组长度常常是要加倍的。这个过程称为数组倍增。数组倍增的时间,从渐近上考量,不会大于元素插入的总时间,见定理1。
分析:为什么数组长度不是增加1或2,而是要加倍呢?数组长度每次增加1或2,虽然不影响插入操作的最坏时间复杂度(O(listSize)),但是影响连续插入时的渐近时间复杂度。假设从一个长度为1的空表开始,执行n=2^k+1次插入。假设插入的位置都是表尾。于是,插入不需要移动已存在的元素,n次插入的时间是O(n)加上增加数组长度的时间。如果数组长度每次增加1,那么增加数组长度的时间是O(1+2+……n-1) = O(n^2)。于是,n次插入的总时间为O(n^2)。
如果数组长度增倍,那么改变数组长度的时间为O(n)。于是,n次插入时间为O(n)。
定理1:如果我们总按一个乘法因子来增加数组的长度,那么实施一系列线性表的操作所需要的时间与不改变数组长度时相比,至多增加一个常数因子。
2.6 删除一个元素
线性表的顺序存储结构-删除算法的思路:
(1)如果删除位置不合理,抛出异常;
(2)取出删除元素;
(3)删除索引为theIndex的元素时,把索引从theIndex+1,theIndex+2,……,listSize-1的元素向左移动一位;
(4)线性表长度减1。
template<class T>
void arrayList::erase(int theIndex)
{//删除其索引为theIndex的元素
//如果该元素不存在,则抛出异常illegaklIndex
checkIndex(theIndex);
//有效索引,移动其索引大于theIndex的元素
copy(element + theIndex + 1, element + listSize, element + theIndex);
element[--listSize].~T(); //调用析构函数
}
分析:时间复杂度为O(listSize-theIndex)。
(这里要注意内置类型的析构函数其实是不存在的,只是在泛型编程中为了统一,此时若是内置类型则执行一个空函数)
线性表顺序存储结构的插入和删除操作的时间复杂度都为O(n)。
3 顺序存储的优点和缺点
顺序存储的特性,所以可以计算出线性表中的任意位置的地址, 那么我们对每个线性表位置的存入或者取出数据,对计算机都是相等时间,存取时间性能为O(1)。通常把具有这一特点的存储结构称为随机存取结构。
线性表顺序存储结构的优点和缺点:
优点:(1)无需为表示元素之间的逻辑关系而增加额外的存储空间
(2)可以快速地存取表中任一位置的元素
缺点:(1)插入和删除操作需要移动大量元素
(2)当线性表长度变化较大时,难以确定存储空间的容量
(3)造成存储空间的碎片(由于线性表长度有时小于最大容量)
顺序存储结构插入和删除速度慢的原因:由于数据在内存中存储的形式是连续的,但正由于这种连续性,所以存取数据很快。
4 参考
1 《大话数据结构》 程杰著.
2 《数据结构、算法与应用(C++语言描述)》 Sartaj Sahni著.