2 线性表
线性结构,线性结构的特点:(1)是数据元素的非空有限集合;(2)存在唯一的一个被称做“第一个”的数据元素;(3)存在唯一的一个被称做“最后一个”的数据元素;(4)除第一个以外,集合中的每个数据元素均有一个前驱;(5)除最后一个以外,集合中的每个数据元素均有一个后继;
2.1 线性表的类型定义
线性表示最常用也是最简单的数据结构。
简言之,一个线性表示n个数据元素的有限序列。
每个数据元素的具体含义在不同的情况下不同,可以是一个整数,一个符号,甚至一页书,甚至是更复杂的信息。
数据元素可以由若干数据项(item)构成。这种情况下把数据元素称为记录。含有大量记录的线性表称为文件。
线性表中的数据元素可以是各种各样的,但是同一线性表中的元素必定具有相同的特性,即属于同一种数据对象。相邻数据元素之间存在着序偶关系。
线性表中的元素个数n定义为线性表的长度。n=0时称为空表。在非空表中,每个数据元素都有一个确定的位置。i称为数据元素ai在线性表中的位序。
2.2 线性表的顺序表示和实现
线性表的顺序表示:用一组地址连续的存储单元依次存储线性表的数据元素。
线性表的第一个存储位置称为线性表的基地址或起始位置。
线性表的机内表示:称为线性表的顺序存储结构或顺序映像。
换句话就是,以元素在计算机内“物理位置相邻”来表示线性表中数据元素之间的逻辑关系。
每个数据元素的存储位置都和线性表的起始位置相差一个和数据元素在线性表中的位序成正比的常数。
只要确定了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构。
2.3 线性表的链式表示和实现
顺序表的特点:逻辑上相邻的两个元素在物理位置上也相邻。
顺序表得优点:可以随机存取表中任一元素,存储位置可用一个简单的直观的公式来表示。
顺序表的弱点:在做插入或删除操作时,需移动大量元素。
这就要另一种线性表的表示方法:链式存储结构;不要求逻辑上相邻的元素,物理位置上也相邻。所以没有顺序表的弱点,但是也就丧失了顺序表的优点。
2.3.1 线性链表
对于数据元素来说,除了存储其本身的信息外,还要存储其直接后继的信息;
这两部分信息组成了数据元素的存储映像,叫做结点。
结点包含两个域:数据域、指针域;
指针域中存储的信息叫做指针或链;
n个结点链结而成一个链表,称为线性表;
用线性链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的。换句话说,指针为数据元素之间的逻辑关系的映像。逻辑上相邻的两个数据元素在物理位置上不要求相邻。这种存储结构被称为非顺序映像或链式映像。(数据元素的物理位置上没有关系)
//定义结点的结构体
typedef struct
{
ElemType data;
struct LNode *next;
}LNode *LinkList;
LinkList是一个头指针;
头结点的指针域指向第一个元素结点的存储位置;
头结点的数据域可以不存储任何信息。
对于链表而言取得第i个元素必须从头指针出发寻找。链表是非随机存取的存储结构。
//从链表中取第i个元素;
Status GetElem_L(LinkList L, int i, ElemType &e)
{
int j;
LinkList p;
p = L->next;
j=1;
while(p&&j<i)
{
p=L->next;
++j;
}
if(!p || j>i)
return ERROR;
e=p->data;
return OK;
}
//链表中插入元素
Status ListInsert_L(LinkList &L, int i, ElemType &e)//头指针,插入位置(i之前),要插入的数据;
{
int j=0;
LinkList p;
p =L;
while(p&&j<i-1) //寻找第i-1个结点
{
p=p->next;
++j;
}
if(!p||j>i-1) return ERROR;
s =(LinkLikst)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next =s;
return OK;
}
//删除第i个元素
Status ListDelete_L(LinkList &L, int i, ElemType &e)
{
int j=0;
LinkList p q;
p = L;
while(p&&j<i-1)
p=p->next;
++j;
if(!p || j>i-1)
return ERROR;
q=p->next;
p->next=q->next;
e=q->data;
free(q);
}
单链表是一种动态结构,整个可用存储空间可以为多个链表共享。每个链表占用的空间不需要预先分配划定,而是可以根据系统应需求即时生成。
建立链表的过程也就是一个动态生成链表的过程。
//建立链表
void CreateList_L(LinkList &L, int n)
{
L = (LinkList)malloc(sizeof(LNode));
L->Next =NULL;
LinkList p;
int i=0;
while(i=n;i>0;--i)
{
p = (LinkList)malloc(sizeof(LNode));
scanf(&p->data);
p->next=L->Next;
L->Next = p;
}
}
//归并两个单链表
void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc,)
{
LinkList pa,pb,pc;
pa = La->Next;
pb = Lb->Next;
Lc = pc = La; //用La的头结点为Lc的头结点;
while(pa && pb)
{
if(pa->Data<=pb->Data)
{
pc->Next = pa; //第一次时pc和Lc都指向同一个位置。即a1;更新pc的后继
pc =pa; //更新pc,pc后移
pa=pa->next; //pa后移一位
}
else
{
pc->Next=pb; //第一次pc和Lc都指向同一个位置。更新pc的后继;
pc =pb; //更新pc,pc后移
pb=pb->Next; //pb后移
}
}
pc->Next = pa ? pa:pb //插入剩余段
free(Lb) //释放Lb的头结点;
}
以上算法的空间复杂度比较小,不需要另建新表的结点空间。时间复杂度和顺序表一样。只需要将原来的两个链表的结点之间的关系解除,重新按照元素值非递减关系排列即可。
//静态链表
有一种链表,用数组描述;叫做静态链表
#define MAXSIZE 1000
typedef struct{
ElemType data;
int cur;
}component, SLinkList[MAXSIZE];
这种链表仍然需要预先分配较大的空间,但在作线性表得插入和删除操作时不需要移动元素。每个结点里有个叫cur的变量作为游标代替指针指示结点在数组中的相对位置。整型游标代替了动态指针。当cur为0时,表示链表的结尾;数组的首元素被作为类似链表的头结点。
//静态链表定位元素,在SL中查找第一个值为e的元素
int LocateElem_L(SLinkList S, ElemType e)
{
int i;
i=S[0].cur;
while(i&&S[i].data != e)
{
i=S[i].cur; //相当于p=p->Next
}
return i;
}
//将整个数组空间初始化成一个链表
void InitSpace_SL(SLinkList &space)
{
for(i=0;i<MAXSIZE-1;++i)
space[i].cur=i+1;
space[MAXSIZE-1].cur=0;
}
//从备用空间取得一个结点
int Malloc_SL(SLinkList &space)
{
i=space[0].cur;
if(space[0].cur)
space[0].cur =space[i].cur;
return i;
}
//将空间结点链结到备用链表上
void Free_SL(SLinkList &space, int k)
{
space[k].cur = space[0].cur;
space[0].cur = k;
}
//依次输入集合A和集合B的元素,在一维数组space中建立表示集合(A-B)U(B-A)
//的静态链表,S为其头指针。假设备用空间足够大,space[0].cur为其头指针。
void difference(SLinkList &space, int &S)
{
InitSpace_SL(space); //初始化备用空间
S=Malloc_SL(space); //生成备用空间的头结点
r =S; //r指向当前备用空间的最后一个结点;
scanf(m,n) //提供输入A和B的元素个数
for(j=0;j<m;j++)
{
i=Malloc_SL(space); //从备用链表取得一个结点
scanf(space[i].data); //用户输入集合A的元素
space[r].cur=i; //插入到表尾
r=i; //更新r
}
space[r].cur=0; //最后一个元素的指针(游标)为空
//至此S中插入了所有集合A的元素;
for(j=0; j<n ;j++)
{
scanf(b);//需要判断b有没有在A中,遍历A即可;
p=S; //指的是在与集合A的中元素匹配的位置前一个位置,不匹配则为0;初始化
k=space[S].cur; //记录当前用来与b比较的元素在哪,初始化k=第一个节点
while(k!=space[r].cur&&space[k].data !=b)
{
p=k;
k=space[k].cur; //k向后移(逻辑后移)
}
if(k==space[r].cur) //当前表中不存在该元素,则插入到表尾
{
i=Malloc_SL(space);
space[i].cur=space[r].cur;
space[r].cur=i;
space[i].data=b;
}
else //当前表中已有该元素,删除
{
space[p].cur=space[k].cur; k
Free_SL(space,k);
if(r==k) r=p; //如果删除的k是队尾r,需要把更新队尾r,此时p是队尾了(p是k前一个位置)
}
}
}
2.3.2 循环链表
表中最后一个结点的指针域指向头结点;整个链表形成一个环;由此,从表中任一结点出发均可找到表中其他结点。
注意:循环链表的算法中循环条件变成了是否等于头指针。
但是有的链表没有头指针(头结点)。
2.3.3 双向链表
以上的链式存储结构只有一个指示直接后继的指针域。
由此,从某个结点出发只能顺指针往后寻找其他结点。
若要寻找结点的直接前趋,则需从表头指针出发。
换句话说:单链表的NextElem的执行时间为O(1),PriorElem的执行时间为O(n)。
为了克服单链表的这个单向性缺点,可以利用双向链表。
顾名思义,在双向链表中有两个指针域,其一指向直接后继,另一指向直接前趋。
//双向链表存储结构表示
typedef struct DuLNode
{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode, *DuLinkList;
//插入结点
Status ListInsert_DuL(DuLinkList &L, int i, ElemType e)//第i个位置之前插入
{
if(!(p=GetElemP_DuL(L,i)))//确定第i个元素额指针p,如果指针不存在,则报错;
return ERROR;
if(!(s=(DuLinkList)malloc(sizeof(DuLNode)))) return ERROR;
s->date=e;
s->prior=p->prior; p->prior->next=s;
s->next=p;p->prior=s;
return OK;
}
//删除结点
Status ListDelete_DuL(DuLinkList &L, int i, ElemType e)
{
if(!(p=GetElemP_DuL(L,i)))
return ERROR;
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
}
链表的优点:
空间的合理利用;插入和删除时不需要移动;
链表的缺点:
求表长不如顺序表;
链表的位序概念淡化,结点之间的关系用指针来表示;
带头结点的线性链表定义:
Typedef struct LNode //结点类型
{
ElemType data;
Struct LNode *next;
}*Link, *Position;
Typedef struct{ //链表类型
Link head, tail; //分别指向线性链表的头结点和最后一个结点;
int len; //链表长度;
}LinkList