线性表
(一)线性表的定义和基本操作
1.线性表概念
1)定义:是由相同类型的结点组成的有限序列。如:由n个结点组成的线性表
( a1, a2, …, an)
a1是最前结点,an是最后结点。结点也称为数据元素或者记录。
2)线性表的长度:线性表中结点的个数称为其长度。长度为0的线性表称为空表。
3)结点之间的关系:设线性表记为(a1,a2,…ai-1 , ai, ai+1 ,…an),称ai-1是ai的直接前驱结点(简称前驱),ai+1是ai的直接后继结点(简称后继)。
4)线性表的性质:
①线性表结点间的相对位置是固定的,结点间的关系由结点在表中的位置确定。
②如果两个线性表有相同的数据结点,但它们的结点顺序不一致,该两个线性表也是不相等的。
注:线性表中结点的类型可以是任何数据(包括简单类型和复杂类型),即结点可以有多个成分,其中能唯一标识表元的成分称为关键字(key),或简称键。以后的讨论都只考虑键,而忽略其它成分,这样有利于把握主要问题,便于理解。
2.线性表的抽象数据类型
线性表是一个相当灵活的数据结构,其长度可以根据需要增加或减少。从操作上讲,用户不仅可以对线性表的数据元素进行访问操作,还可以进行插入、删除、定位等操作。
1)线性表的基本操作
假设线性表L有
数据对象 D={ai | ai∈ElemSet,i=1,2,3,…,n,n>=0},
数据元素之间的关系R={<ai-1,ai>|ai-1,ai∈D,i=1,2,…,n},
则线性表L的基本操作如下所示:
InitList(&L):其作用是构造一个长度为0的线性表(空线性表);
DestoryList(&L):其作用是销毁当前的线性表L;
l ClearList(&L):清空线性表L,使之成为空表;
l ListLength(L):返回线性表L的长度,即线性表中数据元素的个数;
l ListEmpty(L):判断线性表L是否为空表,是则返回True,否则返回False;
l GetElem(L,i,&e):将线性表L中第i个数据元素的值返回到变量e中;
l LocateELem(L,e,compare( )):判断线性表L中是否存在与e满足compare()条件的数据元素,有则返回第一个数据元素;
l PriorElem(L,cur_e,&pri_e):返回线性表L中数据元素cur_e的前驱结点;
l NextElem(L,cur_e,&next_e):返回线性表L中数据元素cur_e的后继结点;
l ListInsert(&L,i,e):向线性表L的第i个位置之前插入一个数据元素,其值为e;
l ListDelete(&L,i,&e):删除线性表L的第i个数据元素,并将该数据元素的值返回到e中;
l ListTraverse(L,visit()):遍历线性表中的每个数据元素。
2)线性表的操作举例
① 用两个线性表La,Lb分别表示两个集合A、B,现要求两个集合的合集,使得A=AUB。操作如下:依次取出Lb中的元素,然后到La中去找,如果找不到,则将该元素加入La中,同时修改La的长度,如果Lb中的元素同La中的元素相同,那么按照集合的概念,不再加入到La中。算法描述为:
Void union(List &La , List Lb)
{ La_len = length(La) ; Lb_len=length(Lb) ;
for (i = 1 ; i <= Lb_len ; i++)
{ GetElem(Lb,i,e) ; //取出Lb的第i个元素,并将之赋值给e
if (!LocateElem(La,e,equal))
ListInsert(La,++La_len ,e) ;
}
}
② 有序线性表合并问题:利用抽象数据类型实现两个线性表的合并
已知线性表La和Lb中的数据元素按照非递减有序排列,现在要求La和Lb归并为一个新的有序线性表Lc,使得Lc仍然是非递减有序排列。思想如下:
先设Lc为空表,从La、Lb的开头开始,比较La、Lb当前两个元素的大小,将较小者插入到Lc中。为了比较方便,我们辅设两个指针i和j,让它们分别指向La和Lb即将参与比较的元素。
将较小元素插入Lc后,该较小元素所在的线性表上辅设的指针向后移动一个位置(+1),另一个指针不变,继续参与下一轮比较,这样一直比到某一个线性表结束(i>La_length || j>Lb_length)。
最后再将还没有比较完的线性表中剩余的元素全部插入Lc中即可。
算法如下:
void MergeList(List La , List Lb , List &Lc)
{
InitList(Lc) ;
i=j=1 ; //两个指针初始化,i指向La的第一个元素,j指向Lb的第一个元素
k=0;//用于存储Lc当前元素个数,初始为0
La_Length = length(La) ; Lb_Length= length(Lb);
while (i<=La_Length && j<= Lb_Length)
{
GetElem(La,i,ai) ;
GetElem(Lb,j,bj);
if (ai<=bj)
{ ListInsert(Lc,++k ,ai) ;
i++ ;
}
else
{ ListInsert(Lc,++k,bj) ;
j++;
}
} //while
//将La或Lb中剩余所有元素全部插入Lc中,以下两句只可能执行一句。
while (i<=La_len)
{ GetElem(La,i++ ,ai) ; ListInsert(Lc,++k,ai) };
While (j<= Lb_len)
{ GetElem(Lb,j++ , bj)} ; ListInsert(Lc,++k ,bj) );
}
(二)线性表的实现
1.顺序存储结构
线性表有两种存储方式:顺序存储和链式存储。顺序存储利用大数组或分配了连续内存空间的指针实现,链式存储利用链表实现。
1)存储方法:利用一个足够大的数组,从第一个元素开始将线性表的结点依次存储在数组中。我们知道,数组是顺序存储的,利用数组的目的是用数组的顺序存储来体现线性表中结点的先后次序。由此得到的线性表称为顺序表,具有“随机存取”的特点。
2)地址表示及计算:线性表的顺序存储指的是用一组地址连续的存储单元依次存储线性表的数据元素。设每个元素占用L个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储位置,则第i+1个元素的存储位置LOC(ai+1)和第i个元素的存储位置LOC(ai)有如下关系:
LOC(ai+1) = LOC(ai)+L
假设LOC(a1)是线性表第一个数据元素a1的存储位置(线性表的起始位置),则数据元素ai的地址计算公式为:
LOC(ai) = LOC(a1)+ (i-1)×L
假设m>n,则有:
LOC(am) = LOC(an)+ (m-n)×L
3)线性表的顺序存储结构定义:
#define List_INIT_SIZE 100 //初始分配量
# define LISTINCREMENT 10 //分配增量
typedef struct
{ ElemType *elem ; //带有连续地址块的指针变量,相当于一维数组(向量)
int length; //x线性表的当前长度,即当前数据元素的个数,初始值为0
int ListSize ; //线性表当前分配的存储容量(以sizeof(ElemType)为单位)
}SqList;
线性表初始化时,利用下面的语句为指针成员elem分配连续地址空间:
L.elem =(ElemType *)malloc(LIST_INIT_SIZE *sizeof(ElemType)) ;
4)顺序表的各种操作
①初始化线性表
Status Init_Sq(SqList &L)
{ //构造一个空的线性表
L.elem = (ElemType * ) malloc( List_INIT_Size * sizeof(ElemType) ) ;
if (!L.elem) exit(OVERFLOW) ; //存储分配失败
L.length = 0 ;
L.ListSize = List_INIT_Size ;
return OK ;
}
②在第i个元素之前插入一个新元素
需要将第i个元素到第n个元素均向后移动一个单位,插入的新元素成为第i个元素,原来的第i个元素成为第i+1个元素,原来的第n个元素成为第n+1个元素,线性表的长度加1。
Status listInsert_Sq(Sqlist &L,int i , ElemType e)
{ // i 的合法取值范围是:1<=i<=ListLength(L) + 1
if (i<1 || i > L.length +1) return ERROR ;
if (L.length >= L.ListSize) //如果存储空间已满,则增加分配
{ newbase = (ElemType *) realloc(L.elem,
(L.ListSize + LISTINCREMENT)*sizeof(ElemType)) ;
if (!newbase) exit(OVERFLOW) ; // 存储分配失败
L.elem = newbase ; //新基址
L.ListSize += LISTINCREMENT ; //增加存储容量
}
q = &(L.elem[i-1]) ; // q为插入位置
for ( p = &(L.elem[L.length-1]); p>=q ; --p) *(p+1) = *p ;
//插入位置以及以后的元素右移
*q = e ; //插入e
++L.length ; //表的长度增1
return OK ;
}
③删除第i个元素,并返回其值
Status ListDelete_Sq(SqList &L , int i , ElemType &e)
{ if ((i<1) || (i>L.length)) return ERROR ;
P= &(L.elem[i-1]) ;
e = * p ;
q = L.elem + L.length – 1 ; //表尾元素位置
for (++p ; p<= q ; ++p)
*(p-1) = *p ; //被删除元素之后的所有元素左移
--L.length;
return OK ;
}
5)顺序存储方式的特点:
优点:能直接访问线性表的任一结点,访问时间几乎不受结点存储位置的影响,这就是所谓的“随机存取”机制。
缺点:①数组长度固定,动态性太差;
②执行结点插入、删除操作将移动大量数据。
7)一个应用:顺序表的合并
有两个线性表La、Lb,其数据元素均是非递减排列的,要求合并La、Lb为一个新的线性表Lc,且Lc也是非递减排列的。
void MergeList_Sq(SqList La , SqList Lb , SqList &Lc)
{ pa = La.elem ; pb = Lb.elem ; //取得La , Lb的基地址
Lc.ListSize = La.Length + Lb.Length ;
pc = Lc.elem = (ElemType *) malloc(Lc.ListSize*sizeof(ElemType));
if (!Lc.elem) exit(OVERFLOW) ;
pa_Last = pa + La.length -1 ;
pb_Last = pb + Lb.length - 1 ;
while (pa <= pa_Last && pb<= pb_Last)
{
if (*pa<=*pb) *pc++ = *pa++ ;
else *pc++=*pb++ ;
}
while (pa <= pa_Last) *pc++ = *pa++ ;
while (pb <= pb_Last) *pc++ = *pb++ ;
}
由于合并后数据元素要放到Lc中,所以算法的主要操作是“复制”。如果将Lb中的数据插入到La中,则需要移动La的元素。
La、Lb均为顺序表,否则无法进行合并操作。如果原来的线性表不是顺序表,需要用排序算法先进行排序。有关排序算法,后文有专门介绍。
2.链式存储结构
2.1 链式存储结构之一:单链表
利用单链表(也称线性链表)来实现,从链表的第一个数据元素开始,依次将线性表的结点存入。需要注意的是,链表的每个数据元素除了要存储线性表的数据元素信息之外,还要有一个成分存储其后继结点的指针,单链表就是通过这个指针来表明数据元素之间的先后关系的。
单链表在保存时,一般在第一个结点之前辅设一个结点,称为头结点。头结点的数据域可以不存任何信息,也可以存储线性表的长度等附加信息,其指针域中存储指向第一个结点的指针(即第一个元素结点的存储位置)。故单链表的头指针指向头结点,如果头结点的指针域为空,则说明是空表(head->next ==NULL)。
1) 单链表结构
typedef struct LNode{
ElemType data ;
struct LNode * next ;
}LNode , *LinkList ;
2)单链表的各种操作
①取得第i个元素的值
Status GetElem_L(LinkList L , int i , ElemType &e)
{ //L为带头结点的单链表的头指针
p = L.next ; //p指向第一个结点
j = 1 ;
while (p && j<i)
{ p = p->next ;
j++ ;
}
if (!p || j>i) return ERROR ; //不存在
e= p->data ;
return OK ;
}
②单链表的插入操作:
在单链表L的某结点(设该结点由指针p指向)之后插入一个新的数据元素。设该新数据元素由s指向。操作如下:s->next = p->next ; p->next = s;(注意语句顺序)
③单链表的删除操作:
3)链式存储的特点
优点:每个数据元素的实际存储位置可以任意,数据元素的插入、删除变得非常容易;
缺点:①每个数据元素增加了后继指针成分,增加了存储空间,降低了存储密度;
②不能随机访问线性表的任一结点。(同顺序存储恰恰相反)
4)静态链表
有时也可以采用一维数组来描述线性链表,其类型说明如下所示。
#define MAXSIZE 100
tyepdef struct {
ElemType data ;
int cur ;
} component , SLinkList[MAXSIZE] ;
链式存储结构之二:循环链表
表中最后一个结点的指针指向头结点,整个链表形成一个环。其特点为:从表中任何一个结点出发,都可以找到表中其它结点。设p指向最后一个结点(如上图),则循环结束的条件是:p->next == H,H为线性表的头
循环链表在实现时,有时设置尾指针,而不设置头指针,如下图所示。
将B接到A的后面,语法为:
q = rear2.next ;
rear2.next = rear1.next ;
rear1.next = q.next ;
链接后,得到:
此时,B中原来的头结点就可以被释放了,如可用free(q) ;
链式存储结构之三:双向循环链表
单链表的数据结构中只提供一个指向后继的指针域,因此,当需要查找某结点的前驱时,只能从表头指针出发,效果较差。为解决此问题,提出了双向链表的思想。双向链表是为了方便查找结点的前驱而设计的,表中的结点不仅有一个指向其直接后继的指针域,还有一个指向其直接前驱的指针域,如下图所示。
1)双向循环链表的数据结构
Typedef struct DuLNode
{ ElemType data ;
struct DulNode * prior ; //前驱指针
struct DulNode * next ; //后继指针
} DulNode , *DuLinkList ;
设d为指向某结点的指针,则下式成立:
d->next ->prior ==d->prior->next == d
2)双向链表的插入、删除操作
同单链表相比,双向链表的插入、删除操作需同时修改两个指针,因此操作较为复杂。
①插入一个结点
步骤:s->prior = p->prior ;
p->prior->next = s ;
s->next = p ;
p->prior = s ;
②删除一个结点b
步骤:p->prior->next = p->next ;
p->next->prior = p->prior ;
free(p) ;