第二章 线性表
2.1 线性表的逻辑结构
2.1.1 线性表的定义
线性表简称表,是n(n≥0)个具有相同类型的数据元素的有限序列,线性表中数据元素的个数称为线性表的长度。长度等于零时称为空表。
一个非空表通常记为 L=(a1,a2,…,an)
线性表具有 有限性 相同性 顺序性
在序列中,元素a1无前驱,元素an无后继,其他每个元素有且仅有一个前驱和后继。
2.1.2 线性表的抽象数据类型定义
ADT List
Data
线性表中的数据元素具有相同类型,相邻元素具有前驱和后继关系。
Operation
InitList
前置条件:线性表不存在
输入:无
功能:线性表的初始化
输出:无
后置条件:一个空的线性表
DestroyList
前置条件:线性表已存在
输入:无
功能:销毁线性表
输出:无
后置条件:释放线性表所占用的空间
Length
前置条件:线性表已存在
输入:无
功能:求线性表的长度
输出:线性表中数据元素的个数
后置条件:线性表不变
Get
前置条件:线性表已存在
输入:元素的序号i
功能:按位查找,在线性表中查找序号为i的数据元素
输出:如果序号合法,返回序号为i的元素值,否则抛出异常
后置条件:线性表不变
Locate
前置条件:线性表已存在
输入:数据元素x
功能:按值查找,在线性表中查找值为i的元素
输出:如果查找成功,返回元素x在表中的序号,否则返回0
后置条件:线性表不变
Insert
前置条件:线性表已存在
输入:插入位置为i;待查元素x
功能:插入操作,在线性表的第i个元素处插入一个新元素x
输出:若插入不成功,抛出异常
后置条件:若插入成功,表中增加一个元素
Delete
前置条件:线性表已存在
输入:删除位置i
功能:删除操作,删除线性表中的第i个元素
输出:若删除成功,返回被删元素,否则抛出异常
后置条件::若删除成功,表中减少一个元素
Empty
前置条件:线性表已存在
输入:无
功能:判空操作,判断线性表是否为空
输出:若是空表,返回1,否则返回0
PrintList
前置条件:线性表已存在
输入:无
功能:遍历操作,按序号依次输出线性表中的元素
输出:线性表的各个数据元素
后置条件:线性表不变
endADT
2.2 线性表的顺序存储结构及实现
2.2.1
线性表的顺序存储结构称为顺序表。
顺序表是用一段地址连续的存储单元一次存储线性表的数据元素。C++中数组的下标是从0开始的,而线性表中元素的序号是从1开始的,也就是说,线性表中第i个元素存储在数组中下标为i-1的位置。
2.2.2 顺序表的实现
const int MaxSize =100;
template
class SeqList
{
public:
SeqList(){length=0;} //无参构造函数,建立一个空的顺序表
SeqList(DataType a[],int n); //有参构造函数,建立一个长度为n的顺序表
~SeqList(){ } //析构函数为空
int Length(){ } //求线性表的长度
DataType Get (int i); //按位查找,在线性表中查找第i个元素
int Locata (DataType x); //按值查找,在线性表中查找值为x的元素序号
void Insert(int i,DataType x); 插入操作,在线性表中第i个位置插入值为x的元素
DataType Delete (int i);//删除操作,删除线性表的第i个元素
void PrintList ();//遍历操作,按序号一次输出各元素
private:
DataType data[MaxSize];//存放数据元素的数组
int length;//线性表的长度
}
1.构造函数
无参构造函数SeqList()创建一个空的顺序表,只需简单的将顺序表的长度length初始化为0.
有参构造函数SeqList(DataType a[ ],int n)创建一个长度为n的顺序表,需要将给定的数组元素作为线性表的数据元素传入顺序表中,并将传入的元素个数作为顺序表的长度。
顺序表有参构造函数SeqList
template
SeqList
{
if(n>MaxSize)throw'参数非法";
for(i=0;i data[i]=a[i]; length=n; } 2.求线性表的长度 求线性表的长度只需返回成员变量length的值。 3.查找操作 (1)按位查找 顺序表中第i个元素存储在数组中下标为i-1的位置,所以,容易实现按位查找。显然,按位查找算法的时间复杂度为O(1)。 顺序表按位查找算法Get template DataType SeqList { if(i<1&&i>length)throw "查找位置非法"; else return data[i-1]; } (2)按值查找 在顺序表中实现按值查找操作,需要对顺序表中的元素依次进行比较。如果查找成功,返回元素的序号(注意不是下标);如果查找不成功,返回查找失败的标志“0”。 顺序表按值查找算法Locate template int SeqList< DataType >::Locate (DataType x) { for(int i=0;i if(data[i]==x) return i+1; return 0; } 查找算法的问题规模是表长n,基本语句是for循环中元素比较的语句。按位查找算法的平均时间性能是O(n)。 4.插入操作 插入操作是在表的第i(1≤i≤n+1)个位置插入一个新元素x,使长度为n的线性表(a1,…,ai-1,ai, …,an)变成长度为n+1的线性表(a1, …,ai-1,x,ai,v,an),插入后,元素ai-1和ai之间的逻辑关系发生了变化并且存储位置要反映这个变化。 顺序表插入算法Insert template void SeqList< DataType >::Insert(int i, DataType x) { if(length>=MaxSize)throw"上溢"; if(i<1||i>length+1)throw"位置非法"; for(int j=length;j>=i;j--) data[j]=data[j-1]; data[i-1]=x; length++; } 该算法的问题规模是表的长度n,基本语句是for循环中元素后移的语句。 5.删除操作 删除操作是将表的第i(1≤i≤n)个元素删除,使长度为n的线性表(a1,…,ai-1,ai,ai+1, …,an)变成长度为n-1的线性表(a1, …,ai-1,ai+1, …,an),删除后元素ai-1和ai+1之间的逻辑关系发生了变化并且存储位置也要反映这个变化。 顺序表删除算法Delete template DataType SeqList< DataType >::Delete (int i) { if(length==0)throw"下溢"; if(i<1||i>length)throw"位置非法"; x=data[i-1]; for(int j=i;j data[j-1]=data[j]; length--; return x; } 算法的问题规模是表长n,基本语句是for循环中元素前移的语句。 6.遍历操作 在顺序表中,遍历操作是按下标依次输出各元素。 顺序表遍历算法PrintList template void SeqList< DataType >::PrintList () { for(int i=0;i cout<
} 2.3 线性表的链接存储结构及实现 顺序表利用数组元素在物理位置(即数组下标)上的邻接关系来表示线性表中数据元素之间的逻辑关系,这使得顺序表具有以下缺点。 (1)插入和删除操作需移动大量数据。 (2)表的容量难以确定。 (3)造成存数空间的“碎片”。 造成顺序表上述缺点的根本原因是静态存储分配,为了克服顺序表的缺点,可以采用动态存储分配来存储线性表,也就是采用链接存储结构。 2.3.1 单链表 1.单链表的存储方法 单链表是用一组任意的存储单元存放线性表的元素,这组存储单元可以连续也可以不连续,甚至可以零散分布在内存中的任意位置。为了能正确表示元素之间的逻辑关系,每个存储单元在存储数据元素的同时,还必须存储其后继元素所在的地址信息,这个地址信息称为指针,这两部分组成了数据元素的存储映像,称为结点。 2.单链表的实现 将线性表的抽象数据类型定义在单链表存储结构下用C++的类实现。由于线性表的数据元素类型不确定,所以采用C++的模板机制。 template class LinkList { public: LinkList();//无参构造函数,建立只有头节点的空链表 LinkList(DataType a[],int n);//有参构造函数,建立有n个元素的单链表 ~LinkList();//析构函数 int Length();//求单链表的长度 DataType Get(int i);//按位查找。在单链表中查找第i个结点的元素值 int Locate(DataType x);//按位查找。在单链表中查找值为x的元素序号 void Insert(int i,DataType x);//插入操作,在第i个位置插入元素值为x的结点 DataType Delete(int i);//删除操作,在单链表中删除第i个结点 void PrintList();//遍历操作,按序号依次输出各元素 private: Node }; 1.遍历操作 所谓遍历单链表是指按序号依次访问单链表中的所有结点且仅访问一次。 遍历单链表需要将单链表扫描一遍,因此时间复杂度为O(n)。 单链表遍历算法PrintList template void LinkList { p=first->next; while(p!=NULL) { cout< p=p->next; } 需要强调的是,工作指针P后移不能写作p++,因为单链表的存储单元可能不连续,因此p++不能保证工作指针p指向下一个结点。 2.求线性表的长度 由于单链表类中没有存储线性表的长度,因此,不能直接求得线性表的长度。考虑采用“数数”的方法来求其长度,即从第一个结点开始数,一直数到尾。 在单链表中求线性表的长度需要将单链表扫描一遍,因此时间复杂度为O(n)。 求线性表长度算法Length template int LinkList { p=first->next; count=0; while (p!=NULL) { p=p->next; count++; } return count; } 3.查找操作 (1)按位查找 单链表按位查找算法Get template DataType LinkList { p=first->next; count=1; while (p!=NULL&&count
{ p=p->next; count++; } if(p==NULL)throw "位置"; else return p->data; } 查找算法的基本语句是工作指针p后移,该语句执行的次数与被查结点在表中的位置有关。在查找成功的情况下,若查找位置为i(1≤i≤n),则需要执行i-1次,等概率情况下,平均时间性能为O(n)。因此,单链表是顺序存取结构。 (2)按值查找 在单链表中实现按值查找操作,需要对单链表中的元素依次进行比较,如果查找成功,返回元素的序号,如果查找不成功,返回0表示查找失败。 单链表按值查找算法Locate template int LinkList { p=first->next; count=1; while (p!=NULL) { if (p->data==x) return count; p=p->next; count++; } return 0; } 按值查找的基本语句是将结点p的数据域与待查值进行比较,具体的比较次数与待查值结点在单链表中的位置有关。在等概率情况下,平均时间性能为为O(n)。 4.插入操作 单链表的插入操作是将值为x的新结点插入到单链表的第i个位置,即插入到ai-1与ai之间。 插入算法的时间主要耗费在查找正确的插入位置上,故时间复杂度为O(n)。 单链表插入算法Insert template void LinkList { p=first; count=0; while (p!=NULL&&count { p=p->next; count++; } if(p==NULL) throw"位置"; else { s=new Node; s->data=x;s->next=p->next; p->next=s; } } 5.构造函数 首先讨论无参构造函数LinkList( ),也就是生成只有头结点的空链表,算法如下: 无参构造函数LinkList template LinkList { first=new Node; first->next=NULL; } 下面讨论有参构造函数LinkList(DataType a[ ],int n),也就是生成一个有n个结点的单链表,有两种方法:头插法和尾插法。 (1)头插法 头插法是每次将新申请的结点插在头结点的后面。 头插法建立单链表LinkList template LinkList { first=new Node; first->next=NULL; for (int i=0;i { s=new Node; s->data=a[i]; s->next=first->next; first->next=s; } } (2)尾插法 尾插法就是每次将新申请的结点插在终端节点的后面。 尾插法建立单链表LinkList template LinkList { first=new Node; r=first; for (int i=0;i { s=new Node; s->data=a[i]; r->next=s; r=s; } r->next=NULL; } 6.删除操作 删除操作是将单链表的第i个结点删去。 删除算法的时间主要耗费在查找正确的删除位置上,故时间复杂度亦为O(n)。 单链表删除算法Delete template DataType LinkList { p=first; count=0; while (p!=NULL&&count { p=p->next; count++; } if(p==NULL||p->next==NULL) throw "位置"; else { q=p->next;x=q->data; p->next=q->next; delete q; return x; } } 7.析构函数 单链表类中的结点是用运算符new申请的,在释放单链表类的对象时无法自动释放这些结点的存储空间,所以,析构函数应将单链表中结点(包括头结点)的存储空间释放。 单链表析构函数算法~LinkList template LinkList { while(first!=NULL) { q=first; first=first->next; delete q; } } 2.3.2 循环链表 在单链表中,如果将终端节点的指针域由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相连的单链表称为循环单链表,为了是空表和非空表的处理一致,通常也附设一个头结点。 在用头指针指示的循环链表中,找到开始结点的时间是O(1),然而要找到终端节点,则需从头指针开始遍历整个循环链表,其时间是O(n)。 循环链表没有增加任何存储量,仅对单链表的链接方式稍作改变,因而其抽象数据类型同单链表相同。循环链表的基本操作的实现与单链表相似,不同之处仅在于循环条件不同。 2.3.3双链表 在单链表的每个节点中再设置一个指向其前驱的指针域,这样就形成了双链表。 其中data为数据域,存放数据; Prior为前驱指针域,存放该结点的前驱结点的地址; Next为后继指针域,存放该结点的后继结点的地址。 1. 插入 在结点p的后面插入一个新结点s,需要修改4个指针 (1)s->prior=p; (2)s->next=p->next; (3)p->next->prior=s; (4)p->next=s; 2.删除 设指针p指向待删除结点,删除语句可通过下述两条语句完成 (1)(p->prior)->next=p->next; (2) (p->next)->prior=p->prior; 这两个语句的顺序可以颠倒。而且执行完操作后将结点p所占的存储空间释放。