6 对链表的删除操作
已有一个链表,希望删除其中某个结点。怎样考虑此问题的算法呢?先打个比方:一队小孩(A、B、C、D、E)手拉手,如果某一小孩(C)想离队有事,而队形仍保持不变。只要将 C的手从两边脱开,B改为与D拉手即可,见图11.19。图11.19(a)是原来的队伍,图11.19(b)是C离队后的队伍。
与此相仿,从一个动态链表中删去一个结点,并不是真正从内存中把它抹掉,而是把它从链表中分离开来,只要撤消原来的链接关系即可。
例11.10 写一函数以删除动态链表中指定的结点。
以指定的学号作为删除结点的标志。例如,输人99103表示要求删除学号为99103的结点。解题的思路是这样的:从p指向的第一个结点开始,检查该结点中的num值是否等于输人的要求删除的那个学号。如果相等就将该结点删除,如不相等,就将p后移一个结点,再如此进行下去,直到遇到表尾为止。
可以设两个指针变量p1和p2,先使p1指向第一个结点(图11.20(a))。如果要删除的不是第一个结点,则使p1后指向下一个结点(将p1->next 赋给 p1),在此之前应将p1的值赋给p2,使p2指向刚才检查过的那个结点,见图11.20(b)。如此一次一次地使 p后移,直到找到所要删除的结点或检查完全部链表都找不到要删除的结点为止。如果找到某一结点是要删除的结点,还要区分两种情况:①要删的是第一个结点(p1的值等于 head 的值,如图11.20(a)那样),则应将p1->next赋给head。见图11.20(c)。这时head指向原来的第二个结点。第一个结点虽然仍存在,但它已与链表脱离,因为链表中没有一个结点或头指针指向它。虽然p1还指向它,它仍指向第二个结点,但仍无济于事,现在链表的第一个结点是原来的第二个结点,原来第一个结点已“丢失”,即不再是链表中的一部分了。②如果要删除的不是第一个结点,则将p1->next 赋给p2->next,见图11.20(d).p2->next原来指向p1指向的结点(图中第二个结点),现在p2->next改为指向p1 ->next所指向的结点(图中第三个结点)。p1所指向的结点不再是链表的一部分。
还需要考虑链表是空表(无结点)和链表中找不到要删除的结点的情况。
图11.21表示解此题的算法。
删除结点的函数 del 如下:
struct student * del (struct student* head ,long num)
{struct student * pl,*p2;
if (head==NULL){printf("\nlist null! \n");goto end;}
pl=head;
while(num! =p1->num &&.p1->next!==NULL)
/*p1指向的不是所要找的结点,并且后面还有结点*/
{p2=pl;p1=pl->next;} /*p1后移一个结点*/
if(num==pl->num) /*找到了*/
{if(p1==head)head=p1->next;
/*若pI指向的是首结点,把第二个结点地址赋予head*/
else p2->next=pl->next;
/*否则将下一结点地址赋给前一结点地址*/
printf("delete:%ld\n",num);
n=n-1;
}
else printf(" %ld not been found! \n",num);/*找不到该结点*/
return(head);
}
函数的类型是指向struct student类型数据的指针,它的值是链表的头指针。函数参数为 head 和要删除的学号num.head的值可能在函数执行过程中被改变(当删除第一个结点时)。
7 对链表的插入操作
对链表的插人是指将一个结点插入到一个已有的链表中。
若已有一个学生链表,各结点是按其成员项num(学号)的值由小到大顺序排列的。今要插人一个新生的结点,要求按学号的顺序插人。
为了能做到正确插入,必须解决两个问题:①怎样找到插入的位置;②怎样实现插入。
如果有一群小学生,按身高顺序(由低到高)手拉手排好队。现在来了一名新同学,求按身高顺序插入队中。首先要确定插到什么位置。可以将新同学先与队中第1名小学生比身高,若新同学比第1名学生高,就使新同学后移一个位置,与第2名学生比,如果仍比第2名学生高,再往后移,与第3名学生比……直到出现比第i名学生高,比第i+1名学生低的情况为止。显然,新同学的位置应该在第i名学生之后,在第i+1名学生之前,在确定了位置之后,让第i名学生与第i+1名学生的手脱开,然后让第i名学生的手去拉新同学的手,让新同学另外一只手去拉第i+1名学生的手。这样就完成了插入,形成了新的队列。
根据这个思路来实现链表的插人操作。先用指针变量p0指向待插入的结点,p1指向第一个结点。见图11.22(a)。将p0->num与pl->num相比较,如果p0->num>p! ->num,则待插入的结点不应插在p1所指的结点之前。此时将p1后移,并使p2指向刚才p1所指的结点,见图11.22(b)。再将pl->num 与p0->num比。如果仍然是p0一>num 大,则应使p1继续后移,直到p0->num≤p1->num为止。这时将p0所指的结点插到p1所指结点之前。但是如果pl所指的已是表尾结点,则p1就不应后移了。如果,p0->num比所有结点的num 都大,则应将p0所指的结点插到链表末尾。
如果插入的位置既不在第一个结点之前,又不在表尾结点之后,则将p0的值赋给p2 ->next,使p2->next指向待插入的结点,然后将p1的值赋给p0->next,使得p0 ->next指向pl指向的变量。见图11.22(c)。可以看到,在第一个结点和第二个结点之间已插入了一个新的结点。
如果插入位置为第一个结点之前(即p1等于head时),则将p0 赋给 head,将p1赋给 p0->next。见图11.22(d)。如果要插到表尾之后,应将p0赋给pl->next ,NULL赋给 p0->next,见图 11.22(e)。
以上算法可用图11.23表示。
例11.11 插入结点的函数insert如下。
struct student * insert(struct student*head, struct student* stud)
{struct student * p0,*p1,*p2;
p1=head; /*使p1指向第一个结点*/
p0=stud; /*p0指向要插入的结点*/
if(head==NULL) /*原来的链表是空表*/
{head=p0;p0->next=NULL;} /*使p0指向的结点作为头结点*/
else
{while((p0->num>pl->num)&&(p1->next! =NULL))
{p2=p1; /*使p2指向刚才p1指向的结点*/
pl=pl->next;} /*p1 后移一个结点*/
if(p0->num<=p1->num)
{if(head==p1) head=p0 /*插到原来第一个结点之前*/
else p2->next=p0; /*插到p2指向的结点之后*/
p0->next=p1;}
Else
{p1->next=p0;p0->next=NULL;}}/*插到最后的结点之后*/
n=n+1; /*结点数加1*/
return (head);
}
函数参数是head 和stud。stud也是一个指针变量,从实参传来待插入结点的地址给 stud。语句"p0=stud;"的作用是使p0指向待插入的结点。
函数类型是指针类型,函数值是链表起始地址head。