二 线性表
2.1 线性表的定义
线性表(Linear List) :是由n(n≧0)个数据元素(结点)a1,a2, …an组成的有限序列。该序列中的所有结点具有相同的数据类型。其中数据元素的个数n称为线性表的长度。
当n=0时,称为空表。
当n>0时,将非空的线性表记作: (a1,a2,…an)
a1称为线性表的第一个(首)结点,an称为线性表的最后一个(尾)结点。
a1,a2,…ai-1都是ai(2≦i≦n)的前驱,其中ai-1是ai的直接前驱;
ai+1,ai+2,…an都是ai(1≦i ≦n-1)的后继,其中ai+1是ai的直接后继。
2.2 抽象数据类型(ADT)
ADT List{
数据对象:D = { ai| ai∈ElemSet, i=1,2,…,n, n≧0}
数据关系:R ={<ai-1, ai> | ai-1, ai∈D, i=2,3,…,n }
基本操作:
InitList(&L )
操作结果:构造一个空的线性表L;
ListLength( L)
初始条件:线性表L已存在;
操作结果:若L为空表,则返回TRUE,否则返回FALSE;
GetElem( L,i, &e )
初始条件:线性表L已存在,1≦i≦ListLength(L);
操作结果:用e返回L中第i个数据元素的值;
ListInsert (L, i, &e )
初始条件:线性表L已存在,1≦i≦ListLength(L) ;
操作结果:在线性表L中的第i个位置插入元素e;
…
} ADT List
2.3 逻辑结构
<a1,a2,a3,……….> 序偶
2.4 顺序存储
2.4.1顺序存储
把线性表的结点按逻辑顺序依次存放在一组地址连续的存储单元里。用这种方法存储的线性表简称顺序表。
2.4.2顺序存储的线性表的特点
◆线性表的逻辑顺序与物理顺序一致;
◆数据元素之间的关系是以元素在计算机内“物理位置相邻”来体现。
物里地址计算方式:
LOC(ai+1)=LOC(ai)+l (ai ,ai+1)的地址关系
LOC(ai)=LOC(a1)+(i-1)*l (a1, ai)的地址关系
2.4.3顺序存储基本操作算法
2.4.3.1 顺序线性表初始化
思想:
实现:
Status Init_SqList( SqList *L)
{
L->elem_array=( ElemType *)malloc(MAX_SIZE*sizeof( ElemType ) ) ;
If ( !L -> elem_array ) return ERROR ;
else { L->length= 0; return OK ; }
}
时间与空间
2.4.3.2 顺序线性表的插入
思想:
(1) 将线性表L中的第i个至第n个结点后移一个位置。
(2) 将结点e插入到结点ai-1之后。
(3) 线性表长度加1。
实现:
Status Insert_SqList(Sqlist *L,int i ,ElemType e)
{
int j ;
if ( i<0||i>L->length-1) return ERROR ;
if (L->length>=MAX_SIZE)
{ printf(“线性表溢出!\n”); return ERROR ; }
for ( j=L->length–1; j>=i-1; --j )
L->Elem_array[j+1]=L->Elem_array[j];
/* i-1位置以后的所有结点后移 */
L->Elem_array[i-1]=e; /* 在i-1位置插入结点 */
L->length++;
return OK ;
}
时间与空间
设在线性表L中的第i个元素之前插入结点的概率为Pi,不失一般性,设各个位置插入是等概率,则Pi=1/(n+1),而插入时移动结点的次数为n-i+1。
总的平均移动次数: Einsert=∑pi*(n-i+1) (1≦i≦n)
∴ Einsert=n/2 。
即在顺序表上做插入运算,平均要移动表上一半结点。当表长n较大时,算法的效率相当低。因此算法的平均时间复杂度为O(n)。
2.4.3.3 顺序线性表的删除
思想
(1) 在线性表L查找值为x的第一个数据元素。
(2) 将从找到的位置至最后一个结点依次向前移动一个位置。
(3) 线性表长度减1。
实现
Status Locate_Delete_SqList(Sqlist *L,ElemType x)
/* 删除线性表L中值为x的第一个结点 */
{
int i=0 , k ;
printf(“线性表L为空!\n”);return ERROR;
while (i<L->length) /*查找值为x的第一个结点*/
{
if (L->Elem_array[i]!=x ) i++ ;
else
{ for ( k=i+1; k<L->length; k++)
L->Elem_array[k-1]=L->Elem_array[k];
L->length--; break ;
}
}
if (i>L->length)
{ printf(“要删除的数据元素不存在!\n”);
return ERROR; }
return OK;
}
时间与空间:
时间主要耗费在数据元素的比较和移动操作上。
首先,在线性表L中查找值为x的结点是否存在;
其次,若值为x的结点存在,且在线性表L中的位置为i ,则在线性表L中删除第i个元素。
设在线性表L删除数据元素概率为Pi,不失一般性,设各个位置是等概率,则Pi=1/n。
◆比较的平均次数: Ecompare=∑pi*i (1≦i≦n)
∴ Ecompare=(n+1)/2 。
◆删除时平均移动次数:Edelete=∑pi*(n-i) (1≦i≦n)
∴ Edelete=(n-1)/2 。 平均时间复杂度:Ecompare+Edelete=n ,即为O(n)
2.5 链式存储(单链表)
2.5.1 链式存储
用一组任意的存储单元存储线性表中的数据元素。用这种方法存储的线性表简称线性链表。
2.5.2 顺序链表的特点
单链表: 每一个结只包含一个指针域的链表
typedef struct Lnode
{
ElemType data; /*数据域,保存结点的值 */
struct Lnode *next; /*指针域*/
}LNode; /*结点的类型 */
2.5.2基本操作示意图
2.5.4基本操作
2.5.4.1头插入法建表
思想
从一个空表开始,重复读入数据,生成新结点,将读入数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。即每次插入的结点都作为链表的第一个结点
伪码
LNode *create_LinkList(void)
/* 头插入法创建单链表,链表的头结点head作为返回值 */
{
int data ;
LNode *head, *p;
head= (LNode *) malloc(sizeof(LNode));
head->next=NULL; /* 创建链表的表头结点head */
while (1)
{
scanf(“%d”,&data) ;
if(data==32767) break ;
p=(LNode *)malloc(sizeof(LNode));
p–>data=data; /* 数据域赋值 */
p–>next=head–>next; head–>next=p ;
/* 钩链,新创建的结点总是作为第一个结点 */
}
return (head);
}
时间空间
2.5.4.2尾插入法建表
思想
头插入法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。若希望二者次序一致,可采用尾插法建表。该方法是将新结点插入到当前链表的表尾,使其成为当前链表的尾结点。
伪码:
LNode *create_LinkList(void)
/* 尾插入法创建单链表,链表的头结点head作为返回值 */
{ int data ;
LNode *head, *p, *q;
head=p=(LNode *)malloc(sizeof(LNode));
p->next=NULL; /* 创建单链表的表头结点head */
while (1)
{ scanf(“%d”,& data);
if (data==32767) break ;
q= (LNode *)malloc(sizeof(LNode));
q–>data=data; /* 数据域赋值 */
q–>next=p–>next; p–>next=q; p=q ;
/*钩链,新创建的结点总是作为最后一个结点*/
}
return (head);
}
时间空间
2.5.4.3 单链表的查找
思想
对于单链表,不能象顺序表中那样直接按序号i访问结点,而只能从链表的头结点出发,沿链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构
伪码
LNode *Locate_Node(LNode *L,int key)
/* 在以L为头结点的单链表中查找值为key的第一个结点 */
{
LNode *p=L–>next;
while ( p!=NULL&& p–>data!=key)
p=p–>next;
if (p–>data==key) return p;
else {
printf(“所要查找的结点不存在!!\n”);
retutn(NULL);
}
}
时间空间
平均时间复杂度为O(n)。
2.5.4.4 单链表的插入
思想
插入运算是将值为e的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。因此,必须首先找到ai-1所在的结点p,然后生成一个数据域为e的新结点q,q结点作为p的直接后继结点。
伪码
void Insert_LNode(LNode *L,int i,ElemType e)
/* 在以L为头结点的单链表的第i个位置插入值为e的结点 */
{
int j=0; LNode *p,*q;
p=L–>next ;
while ( p!=NULL&& j<i-1)
{
p=p–>next;
j++;
}
if (j!=i-1) printf(“i太大或i为0!!\n ”);
else
{
q=(LNode *)malloc(sizeof(LNode));
q–>data=e;
q–>next=p–>next;
p–>next=q;
}
}
设链表的长度为n,合法的插入位置是1≦i≦n。算法的时间主要耗费移动指针p上,故时间复杂度亦为O(n)。
2.5.4.5 单链表的删除
思想
为了删除第i个结点ai,必须找到结点的存储地址。该存储地址是在其直接前趋结点ai-1的next域中,因此,必须首先找到ai-1的存储位置p,然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间,将其归还给“存储池”
伪码
void Delete_LinkList(LNode *L,int key)
/* 删除以L为头结点的单链表中值为key的第一个结点 */
{ LNode *p=L, *q=L–>next;
while ( q!=NULL&& q–>data!=key)
{ p=q; q=q–>next; }
if (q–>data==key)
{ p->next=q->next; free(q); }
else
printf(“所要删除的结点不存在!!\n”);
}
时间空间
时间复杂度也是O(n)
2.5.4.6 单链表的合并
思想
算法中pa ,pb分别是待考察的两个链表的当前结点,pc是合并过程中合并的链表的最后一个结点。
伪码
LNode *Merge_LinkList(LNode *La, LNode *Lb)
/* 合并以La,Lb为头结点的两个有序单链表 */
{ LNode *Lc, *pa , *pb , *pc, *ptr ;
Lc=La ; pc=La ; pa=La->next ; pb=Lb->next ;
while (pa!=NULL && pb!=NULL)
{ if (pa->data<pb->data)
{ pc->next=pa ; pc=pa ; pa=pa->next ; }
/* 将pa所指的结点合并,pa指向下一个结点 */
if (pa->data>pb->data)
{ pc->next=pb ; pc=pb ; pb=pb->next ; }
/* 将pa所指的结点合并,pa指向下一个结点 */
if (pa->data==pb->data)
{ pc->next=pa ; pc=pa ; pa=pa->next ;
ptr=pb ; pb=pb->next ; free(ptr) ; }
/* 将pa所指的结点合并,pb所指结点删除 */
}
if (pa!=NULL)
pc->next=pa;
else
pc->next=pb; /*将剩余的结点链上*/
free(Lb) ;
return(Lc) ;
}
时间空间
若La ,Lb两个链表的长度分别是m,n,则链表合并的时间复杂度为O(m+n)。
2.6 链式存储(循环链表)
2.6.1 循环链表
循环链表(Circular Linked List):是一种头尾相接的链表。其特点是最后一个结点的指针域指向链表的头结点,整个链表的指针域链接成一个环。
2.6.2循环链表的操作
对于单循环链表,除链表的合并外,其它的操作和单线性链表基本上一致,仅仅需要在单线性链表操作算法基础上作以下简单修改:
⑴ 判断是否是空链表:head->next==head ;
⑵ 判断是否是表尾结点:p->next==head ;
2.7 链式存储 (双向链表)
双向链表(Double Linked List) :指的是构成链表的每个结点中设立两个指针域:一个指向其直接前趋的指针域prior,一个指向其直接后继的指针域next。这样形成的链表中有两个方向不同的链,故称为双向链表。
typedef struct Dulnode
{
ElemType data ;
structDulnode *prior , *next ;
}DulNode ;
双向链表结构具有对称性,设p指向双向链表中的某一结点,则其对称性可用下式描述:
(p->prior)->next=p=(p->next)->prior;结点p的存储位置存放在其直接前趋结点p->prior的直接后继指针域中,同时也存放在其直接后继结点p->next的直接前趋指针域中。
2.7.1 双向链表插入
① 插入时仅仅指出直接前驱结点,钩链时必须注意先后次序是: “先右后左” 。部分语句组如下:
S=(DulNode *)malloc(sizeof(DulNode));
S->data=e;
S->next=p->next; p->next->prior=S;
p->next=S; S->prior=p; /* 钩链次序非常重要 */
② 插入时同时指出直接前驱结点p和直接后继结点q,钩链时无须注意先后次序。部分语句组如下:
S=(DulNode *)malloc(sizeof(DulNode));
S->data=e;
p->next=S; S->next=q;
S->prior=p; q->prior=S;
2.7.2 双向链表删除
设要删除的结点为p ,删除时可以不引入新的辅助指针变量,可以直接先断链,再释放结点。部分语句组如下:
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);