线性链表的基本算法
线性链表一般不具有预先设置好的存储空间以备新的链结点使用。当有新的数据元素加入线性表时(对应的操作就是在线性链表中插入一个新的链结点),需要临时从一个被称为存储库的机构中取得一个空的结点空间,填入必要的信息,然后将它插入到线性链表中。
还应该说明的是,这个存储库并非某一个链表所专用,当任何链表操作的需要新的链结点时都可以从中获取链结点空间(只要还有可用的存储空间);一旦某个链结点链结点不再使用时,可以将其送还到该存储库,通常称这种操作为存储释放或者回收(不释放不再使用的链结点空间,虽然不是很大的错误,但是用完及时释放则是个好习惯)
如果假设p是一个指向LinkList类型的指针变量,则从存储库中得一个新的链结点空间是通过执行赋值语句
p=(LinkList)malloc(sizeof(LNode));
中的调用存储分配库函数malloc得到。该语句的作用是由系统生成一个LinkList类型的链结点,同时将该链结点的地址赋给指针p。不过,在包含该函数的赋值语句之前应该由下面一条include语句。
#include <alloc.h>
将不再使用的链结点空间回收到存储库中则是通过调用函数free( p)实现,其中参数p是LinkList类型的指针变量,它指向要被释放到存储库中不再使用的结点。执行free( p)的结果是:p正在指向的地址未变,但是在该地址的数据此时已经无定义了。因此,系统回收的链结点空间可以再次生成链结点供需要时使用。
后面的讨论中将会看到,链式存储结构除了有合理利用存储空间的特点之外,还具有在表中插入或者删除元素时不需要移动表中其他元素的优点,因此,当对表进行的主要操作作为插入和删除时,它是线性表的首选存储结构。
下面是几个相关线性链表常用的算法。
1.建立一个线性链表
设线性表为A={a1,a2,a3,……,an},下面的算法建立与A对应的线性链表。设线性链表的第1个链结点的指针为list。
建立一个线性链表的过程是一个动态生成链结点并依次将它们链接到链表中的过程。
算法思想比较简单,只需要从线性表的第1个数据元素开始依次获取表中数据元素,每次取得一个数据元素,就为该数据元素生成一个新的链结点,将取得的数据元素的数据信息送新结点的数据域的同时,将新结点的指针域置NULL,然后将新的链结点插入到链表的末尾。当取第1个数据元素时,链表为空,此时直接将新的链结点的地址送list即可。
算法返回链表第1个链结点的位置算法如下。
Linklist CREAT(int n)
{
LinkList p,r,list=NULL;
ElemType a;
int i;
for(i=1;i<=n;i++){
READ(a); /*获取一个数据元素*/
p = (LinkList)malloc(sizeof(*LNode));
p -> data = a;
p ->link =NULL;
if(list==NULL)
list =p;
else
r->link =p;
r=p;
}
return (list);
}
算法中READ(a)表示以某种方式提供一个数据元素a。上述算法的时间复杂度为O(n),n为线性表的长度。
如前面所提到的那样,上述算法中使用了malloc库函数创建一个新的链结点,并返回指向该链结点的指针。但需要说明的是,这里未考虑是都一定能通过malloc库函数创建该链结点,换一句话说,这里假设通过使用该库函数能够建立一个链结点。关于这一说明,同样适用于后面的讨论。
2.求线性链表的长度
线性链表的长度被定义为链表中包含的结点的个数。因此,只需设置一个活动的指针变量和一个计数器,首先让活动指针变量指向链表的第1个链结点,然后遍历该链表,活动指针变量每指向一个链结点,计数器做一次计数。遍历结束后,计数器的内容就是链表的长度。算法如下
int LENGTH(LinkList list)
{
LinkList p =list;
int n=0;
while(p!=NULL){
n++;
p = p->link; /*指针p指向下一个链结点*/
}
return n; /*返回链表长度*/
}
该算法中,问题的规模是链表的结点数n,基本操作是指针p向后移。时间复杂度为O(n)。
由于线性链表是一种递归结构,即每个链结点的指针域均指向一个线性链表(可称之为该链结点的后继线性链表),他所指向的链结点为该单链表的第1个链结点,因此,也可以将上述过程设计成为一个递归算法,算法如下。
int LENGHT(LinkList list)
{
if(list != NULL)
return 1+LENTHG(list->link);
else
return 0;
}
测试线性链表是否为空
算法如下。
int ISEMPTY(LinkList list)
return list==NULL;
需要说明的是,以后将会提出带头结点的链表问题。在带头结点的链表中,空表并非任何结点都没有,而是只有一个头结点,此时list指向头结点,头结点的指针域内为NULL。于是,判断带头结点的链表是否为空,上述算法中的返回语句应该为
return list->link ==NULL;
时间算法为O(1)
4.确定元素item在线性链表中的位置
从链表的第1个链结点开始,从前向后一次比较当前链结点的数据与内容是否与给定值item匹配。若查找成功,算法返回被查找结点的地址,否则,返回NULL;
LinkList FIND(LinkList list,ElemType item)
{
LinkList p=list;
while(p!=NULL &&p->data!=item)
p=p->link;
return p;
}
很显然,算法4和算法2的“问题规模”都是线性链表的长度n,“基本动作”是指针依次向“后”移动。由此可见时间复杂度应为O(n)
5.在非空线性链表的第一个结点前插入一个数据信息为item的链结点
算法步骤是:首先从存储库中申请一个新的链结点p,将数据信息item置于新结点的数据域内,然后将第1个结点的指针list送到新的结点的指针域内,同时将新结点的地址p赋给list,至此,新结点已经插入到链表的最前面,成为了新链表的第1个结点。
void INSERTLINK(LinkList &list,ElemType item)
{
/*list中存放链表的首地址*/
LinkList p;
p = (LinkList)malloc(sizeof(LNode)); /*申请一个新的链结点*/
p->data = item;
p = list->link; /* 将list送新结点的指针域 */
list=p; /* 将地址p送到list */
}
时间复杂度为O(1),因为在链表最前面一个链结点与链表长度无关。
6.在非空线性链表的末尾插入一个数据信息为item的链结点
设置一个指针变量(不妨取名为r),先让r指向链表的第1个结点,然后反复执行 r = r->link直到r->link为NULL,此时r指向链表的末尾结点,将item送入从存储库中申请到的新结点的数据域的同时,将新结点的指针域设置NULL,最后将新结点的地址送入r的指向的链结点的指针域,即完成插入操作,算法如下。
void INSERTLINK2 (LinkList list, ELemType item)
{
/*list中存放链表的首地址*/
LinkList p,r;
r = list;
while(r->link != NULL)
r = r->link; /* 找到链表的末尾结点 */
p =(LinkList)malloc(sizeof(LNode)); /* 申请一个新的链结点 */
p->data = item; /* item送新结点的数据域 */
p->link =NULL; /* 新结点的指针域置NULL*/
r->link = p; /* 插入链表的末尾 */
}
7.线性链表中由指针q指出的链结点后面插入一个数据信息为item的链结点
算法步骤是:先从存储库中申请一个新的空结点p,将item送入新链结点的数据域。若原线性链表为空,则链结点就是结果链表,此时只要把p的内容(新结点的地址)送给list即可;若原链表非空,先将q指向的链结点的下一个链结点的地址(用p->link 表示)送入新链结点的指针域,然后再将新链结点的地址p赋给q结点的指针域即可。当链表非空时,插入过程如下图所示。
下面给出具体算法
void INSERTLINK3(LinkList &list, LinkList q, ElemType item)
{
/* list 存放链表的首地址, item 为被插入元素 */
p =(LinkList)malloc(sizeof(LNode)); /*生成一个新的链结点*/
p->data =item; /* 将item送新结点的数据域*/
if(list->link==NULL){ /* 当链表为空时 */
list =p;
p->link =NULL;
}
else{ /* 当链表非空时 */
p->link=q->link;
q->link =p;
}
}
可以看到,这个算法只能在指定的结点后面插入一个新的链结点,也就是说,在链表中插入一个新结点,必须给出插入的位置,这是因为,在线性表中无法从一个指定结点出发到达它的前驱结点(除非从链表头开始遍历这个链表,直到该指定结点的前驱结点)。类似问题在后面的算法中也会存在。上述算法的时间复杂度为O(1).