2.1 线性表的类型定义
1、一个线性表是n个数据元素的有限序列。在稍复杂的线性表中,一个数据元素可以由若干个数据项组成,在这种情况下,常把数据元素称为记录,含有大量记录的线性表称为文件。
2、同一线性表中的元素必定具有相同特性,即属于同一数据对象,相邻数据元素之间存在着序偶关系。
将线性表记为:(a1,…,ai-1,ai,ai+1,…,an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,…,n-1时,ai有且仅有一个直接后继,当i=2,3,…,n时,ai有且仅有一个直接前驱。
线性表中元素的个数n(n>=0)定义为线性表的长度,n=0时称为空表。在非空表中的每个数据元素都有一个确定的位置,ai是第i个数据元素,称i为数据元素ai在线性表中的位序。
注意:位序是从1开始的,数组下标是从0开始的;
3、抽象数据类型线性表的基本操作如下:
InitList(&L); //构造一个空的线性表L
DestoryList(&L); // 线性表L已存在,销毁线性表L
ClearList(&L); //线性表L已存在,将L重置为空表
ListEmpty(L): //线性表L已存在,判断L是否为空,若为空则true,否则为false
ListLength(L); //返回线性表中数据元素的个数
GetElem(L,i,&e); //用e返回第i个数据元素的值,保证i合法
LocateElem(L,i,compare()); //compare()是数据元素判定函数,返回L中第一个与e满足关系compare()的数据元素的位序,若这样的元素不存在,返回0
PriorElem(L,cur_e,&pre_e); //若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败,pre_e无定义
NextElem(L,cur_e,&next_e); //若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的前驱,否则操作失败,next_e无定义
ListInsert(&L,i,e); //在L中第i个位置之前插入新的数据元素e,L的长度加1
ListDelete(&L,I,&e); //删除L的第i个数据元素,并用e返回其值,L的长度减1
ListTraverse(L,visit()); //依次对L中的每个数据元素调用visit()函数,一旦visit()失败,则操作失败
4、例题:
已知线性表LA和LB中的数据元素按值非递减有序排列,现要求将LA和LB归并为一个新的线性表LC,且LC中的数据元素仍按值非递减有序排列。
代码:
void Merge(List la,List lb,List &lc)
{
InitList(lc);
int i=j=1,k=0;
int la_len=ListLength(la);
int lb_len=ListLength(lb);
int ea,eb;
while(i<=la_len&&&j<=lb_len) //当两表都非空时
{
GetElem(la,i,ea);
GetElem(lb,i,eb);
if(ea<=eb)
{
ListInsert(lc,++k,ea);
i++;
}
else
{
ListInsert(lc,++k,eb);
j++;
}
}
while(i<=la_len) //若两表中其中一个表中还有元素时,直接加入lc
{
GetElem(la,i,ea);
ListInsert(lc,++k,ea);
i++;
}
while(j<=lb_len)
{
GetElem(lb,i,eb);
ListInsert(lc,++k,eb);
j++;
}
}
该算法的时间复杂度为:O(ListLength(LA)+ListLength(LB)) ;
2.2 线性表的顺序表示和实现
线性表的顺序表示指的是一组地址连续的存储单元依次存储线性表的数据元素。
假设线性表的每个元素需占用l个存储单元,则一般来说,存在以下关系:
LOC(ai)=LOC(a1)+(i-1)*l
线性表的这种机内表示称作线性表的顺序存储结构或顺序映像,通常,称这种存储结构的线性表为顺序表。它的特点是:以元素在计算机内“物理位置相邻”来表示线性表中数据元素之间的逻辑关系。每一个数据元素的存储位置都和线性表的起始位置相差一个和数据元素在线性表中的位序成正比的常数。所以,只要确定了存储线性表的起始位置,线性表中任一数据元素都可随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构。
线性表的动态分配顺序存储结构为:
#define LIST_INIT_SIZE 100 //线性表存储空间的初始分配量
#define LISTINCREMENT 10 //线性表存储空间的分配增量
typedef struct{
ElemType *elem; //存储空间基址
int length; //线性表当前长度
int listsize; //当前分配的存储容量
}SqList;
顺序表的初始化操作:
Status InitList_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个位置增加元素e:
Status ListInsert_Sq(SqList &L,int i,ElemType e)
{
if(i<1||i>L.length+1) // 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]); //获得需要插入位置的地址,注意位序和下标的关系
for(p=&(L.elem[L.length-1]);p>=q;--p) //后移
{
*(p+1)=*p;
}
*q=e;
++L.length;
return OK;
}
其时间复杂度为:O(n/2);
把线性表第i个位置的元素删除,并将其返回:
Status ListDelete_Sq(SqList &L,int i,ElemType &e)
{
if(i<1||i>L.length) // 1<=i<=L.length
return ERROR;
q=&(L.elem[i-1]); //获得需要删除位置的地址,注意位序和下标的关系
e=*q;
for(++q,p=&(L.elem[L.length-1]);p>=q;++q) //前移
{
*(q-1)=*q;
}
--L.length;
return OK;
}
其时间复杂度为:O(n-1)/2;
2.3 线性表的链式表示和实现
2.3.1 线性链表
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以连续,也可以不连续)。这时为了说明数据元素ai和直接后继数据元素ai+1之间的逻辑关系,对于数据元素ai来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息。这两部分信息组成数据元素ai的存储映像,成为结点。
结点包括两个域:数据域:存储数据元素信息的域;
指针域:存储直接后继存储位置的域;
指针域中存储的信息称作指针或者链。n个结点链结成一个链表,即为线性表(a1,a2,……,an)的链式存储结构。又由于此链表的每个结点中只包含一个指针域,故又称线性链表或单链表。
用线性链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的。换句话说,指针为数据元素之间的逻辑关系的映像,则逻辑上相邻的两个数据元素其存储的物理位置不要求紧邻,由此,这种存储结构为非顺序映像或链式映像。
C语言中可用“结构指针”来描述单链表:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
假设L是单链表的头指针,它指向表中的第一个结点。若L为“空”(L==NULL),则所表示的线性表为“空”表,其长度为0。通常,我们会在单链表的第一个结点之前附设一个结点,称之为“头结点”。头结点的数据域不做限制,但是其指针域存储指向的是第一个结点指针。若线性表为空,则头结点的指针域为“空”。
在单链表中,假设p是指向线性表中第i个数据元素(结点ai)的指针,则p->next是指向第i+1个数据元素(结点ai+1)的指针,即p->data=ai,则p->next->data=ai+1。
单链表是非随机存取的存储结构。
Status GetElem_L(LinkList L,int i,ElemType &e)
{
LinkList p=L->next; //p指向第一个结点
int j=1; //计数器
while(p&&j<i)
{
p=p->next;
j++;
}
if(!p||j>i) //第i个元素不存在
{
return ERROR;
}
e=p->data;
return OK;
}
该算法时间复杂度为O(n);
2.3.2 单链表的插入
在带头结点的单链表L中第i个位置之前插入元素e:(使用的是后插法)
Status ListInsert_L(LinkList &L,int i,ElemType e)
{
LinkList p=L;
int j=0; //计数器
while(p&&j<i-1) //寻找到第i-1个位置
{
p=p->next;
j++;
}
if(!p||j>i-1) //第i个元素不存在
{
return ERROR;
}
LinkList s=(LinkList)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
该算法时间复杂度为O(n);
扩展:对某一个结点进行前插操作:
待插入结点为*s,将*s插入到*p的前面。我们仍然将*s插入到*p的后面,然后将p->data与s->data进行交换,则既满足逻辑关系,又满足时间复杂度为O(1)。
Status ListInsert_L(LinkList &L,LinkList p,ElemType e)
{
LinkList s=(LinkList)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;
int t=p->data;
p->data=s->data;
s->data=t;
return OK;
}
2.3.3 单链表的删除
在带头结点的单链表L中,删除第i个元素,并由e返回;
Status ListDelete_L(LinkList &L,int i,ElemType &e)
{
LinkList p=L;
int j=0;
while(p->next&&j<i-1)
{
p=p->next;
j++;
}
if(!(p->next)||j>i-1)
{
return ERROR;
}
LinkList q=(LinkList)malloc(sizeof(LNode));
q=p->next;
e=q->data;
p->next=q->next;
free(q);
return OK;
}
该算法时间复杂度为O(n);
扩展:删除结点*p;
要删除某个给定的结点*p,通常的做法是先从链表的头结点开始顺序找到其前驱结点,然后执行删除操作,算法的时间复杂度为O(n)。
其实,删除结点*p的操作可以删除*p的后继结点操作来实现,实质就是将其后继结点的值赋予其自身,然后删除后继结点,时间复杂度为O(n)。
Status ListDelete_L(LinkList &L,LinkList p)
{
LinkList q=(LinkList)malloc(sizeof(LNode));
q=p->next;
p->data=q->data;
p->next=q->next;
free(q);
return OK;
}
总结 :对于单链表的插入和删除操作,都必须要找到删除或插入操作的前一个位置;
从上面两个程序中也可以看出,我们使用了malloc和free函数,这就表明,单链表和顺序存储结构不同,它是一种动态结构,建立线性表的链式存储结构的过程其实就是一个动态生成链表的过程。
2.3.3 单链表的建立
1、带头结点的头插法:
void CreatList_L(LinkList &L,int n) //头插法
{
L=(LinkList)malloc(sizeof(LNode));
L->data=0;
L->next=NULL;
for(int i=n;i>0;i--)
{
LinkList p=(LinkList)malloc(sizeof(LNode));
scanf("%d",p->data);
p->next=L->next;
L->next=p;
}
}
2、不带头结点的头插法:
void headjinset(LinkList &L)
{
for(int i=n;i>=1;i--)
{
int x;
cin>>x;
LinkList s=(LinkList)malloc(sizeof(LNode));
s->data=x;
s->next=L;
L=s; //因为再头部插入新结点,所以每次需要将新节点的地址赋给头指针
}
}
3、头插法:
void CreatList_L(LinkList &L,int n) //尾插法
{
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
LinkList end=L; //让尾结点指向头结点
for(int i=0;i<n;i++)
{
LinkList p=(LinkList)malloc(sizeof(LNode));
scanf("%d",p->data);
end->next=p;
end=p;
}
end->next=NULL;
}
4、不带头结点的尾插法:
void CreatList_L(LinkList &L,int n) //尾插法
{
LinkList end=L;
for(int i=0;i<n;i++)
{
int x;
LinkList p=(LinkList)malloc(sizeof(LNode));
scanf("%d",x);
p->data=x;
p->next=NULL;
if(L==NULL)
{
L=p;
end=L;
}
else
{
end->next=p;
end=p;
}
}
}
2.3.4 求单链表的表长
单链表的长度不包含头结点,因此对于不带头结点的单链表,当表为空时,要单独处理。
带头结点的表长:
int length(LinkList L)
{
LinkList p=L;
int len=0;
while(p->next!=NULL)
{
p=p->next;
len++;
}
return len;
}