线性表的链式表示和实现
线性链表: 用一组任意的存储单元来依次存放线性表的结点,其存储单元可以是任意位置的.因此,链表种结点的逻辑次序和物理次序不一定相同. 每一个结点值及一个指针两部分组成了链表种的结点结构.
Data
|
link
|
其中,data域是数据域,用来存放结点值. Next是指针域,存放结点的直接后继的地址.
Typedef
char datatype;
Typedef struct node{
datatype data;
struct node *next;
}listnode;
typedef listnode *linklist;
listnode *p;
linklist head;
注意区分指针变量和结点变量这两个不同的概念。
P为动态变量,它是通过标准函数生成的,即
p=(listnode*)malloc(sizeof(listnode));
函数malloc分配了一个类型为listnode的结点变量的空间,并将其首地址放入指针变量p中。一旦p所指的结点变量不再需要了,又可通过标准函数
free(p)
释放所指的结点变量空间。
头插法建表:
linklist createlistf(void)
{
char ch;
linklist head;
listnode *p;
head=null;
ch=getchar( );
while (ch!=‵/n′{
p=(listnode*)malloc(sizeof(listnode));
p–>data=ch;
p–>next=head;
head=p;
ch=getchar( );
}
return (head);
}
listlink createlist(int n)
{ int data;
linklist head;
listnode *p
head=null;
for(i=n;i>0;--i){
p=(listnode*)malloc(sizeof(listnode));
scanf((〝%d〞,&p–>data);
p–>next=head;
head=p;
}
return(head);
}
尾插法建表
头插法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。若希望二者次序一致,可采用尾插法建表。该方法是将新结点插入到当前链表的表尾上,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点。例:
linklist creater(
)
{
char ch;
linklist head;
listnode *p,*r;
//(, *head;)
head=NULL;r=NULL;
while((ch=getchar( )!=‵/n′){
p=(listnode *)malloc(sizeof(listnode));
p–>data=ch;
if(head=NULL)
head=p;
else
r–>next=p;
r=p;
}
if (r!=NULL)
r–>next=NULL;
return(head);
}
无论链表是否为空,其头指针是指向头结点在的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就统一了。
其算法如下:
linklist createlistr1( ){
char ch;
linklist head=(linklist)malloc(sizeof(listnode));
listnode *p,*r
r=head;
while((ch=getchar( ))!=‵/n′{
p=(listnode*)malloc(sizeof(listnode));
p–>data=ch;
p–>next=p;
r=p;
}
r–>next=NULL;
return(head);
}
上述算法里动态申请新结点空间时未加错误处理,可作下列处理:
p=(listnode*)malloc(sizeof(listnode))
if(p==NULL)
error(〝No space for node can be obtained〞);
return ERROR;
以上算法的时间复杂度均为O(n)。
查找算法
在链表中,即使知道被访问结点的序号i,也不能象顺序表中那样直接按序号i访问结点,而只能从链表的头指针出发,顺链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构。
设单链表的长度为n,要查找表中第i个结点,仅当1≦i≦n时,i的值是合法的。但有时需要找头结点的位置,故我们将头结点看做是第0 个结点,其算法如下:
Listnode * getnode(linklist head , int i)
{
int j;
listnode * p;
p=head;j=0;
while(p–>next && j<I){
p=p–>next;
j++;
}
if (i==j)
return p;
else
return NULL;
}
按值查找
按值查找是在链表中,查找是否有结点值等于给定值key的结点,若有的话,则返回首次找到的其值为key的结点的存储位置;否则返回NULL。查找过程从开始结点出发,顺着链表逐个将结点的值和给定值key作比较。其算法如下:
Listnode * locatenode(linklist head,int key)
{
listnode * p=head–>next;
while( p && p–>data!=key)
p=p–>next;
return p;
}
插入运算:
具体算法如下:
void insertnode(linklist head,datetype x,int i)
{
listnode * p,*q;
p=getnode(head,i-1);
if(p==NULL)
error(〝position error〞);
q=(listnode *)malloc(sizeof(listnode));
q–>data=x;
q–>next=p–next;
p–>next=q;
}
设链表的长度为n,合法的插入位置是1≦i≦n+1。注意当i=1时,getnode找到的是头结点,当
i=n+1时,getnode找到的是结点an。因此,用i-1做实参调用getnode时可完成插入位置的合法性检查。
删除运算
删除运算是将表的第i个结点删去。因为在单链表中结点ai的存储地址是在其直接前趋结点a a i-1的指针域next中,所以我们必须首先找到
a i-1的存储位置p。然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间,将其归还给“存储池”。此过程为:
具体算法如下:
void deletelist(linklist head, int i)
{
listnode * p, *r;
p=getnode(head,i-1);
if(p= =NULL || p–>next= =NULL)
return ERROR;
r=p–>next;
p–>next=r–>next;
free( r ) ;
}
设单链表的长度为n,则删去第i个结点仅当1≦i≦n时是合法的。注意,当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,它是终端结点。因此被删结点的直接前趋*p存在并不意味着被删结点就一定存在,仅当*p存在(即p!=NULL)且*p不是终端结点
(即p–>next!=NULL)时,才能确定被删结点存在。
循环链表
循环链表时一种头尾相接的链表。其特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
单循环链表:在单链表中,将终端结点的指针域NULL改为指向表头结点的或开始结点,就得到了单链形式的循环链表,并简单称为单循环链表。
例、在链表上实现将两个线性表(a1,a2,a3,…an)和(b1,b2,b3,…bn)链接成一个线性表的运算。
linklist connect(linklist heada,linklist headb)
{
linklist p=heada—>next;
heada—>next=(headb—next)—>next
free(headb—>next);
headb—>next=p;
return(headb);
}
和单链表类似,双链表一般也是由头指针唯一确定的,增加头指针也能使双链表上的某些运算变得方便,将头结点和尾结点链接起来也能构成循环链表,并称之为双向链表。
设指针p指向某一结点,则双向链表结构的对称性可用下式描述:
(p—>prior)—>next=p=(p—>next)—>prior
即结点*p的存储位置既存放在其前趋结点*(p—>prior)的直接后继指针域中,也存放 在它的后继结点*(p—>next)的直接前趋指针域中。
双向链表的前插操作算法如下:
void dinsertbefor(dlistnode *p,datatype x)
{
dlistnode *q=malloc(sizeof(dlistnode));
q—>data=x;
q—>prior=p—>prior;
q—>next=p;
p—>prior—>next=q;
p—>prior=q;
}
void ddeletenode(dlistnode *p)
{
p–>prior–>next=p–>next;
p–>next–>prior=p–>prior;
free(p);
}
注意:与单链表的插入和删除操作不同的是,在双链表中插入和删除必须同时修改两个方向上的指针。