一.前言
链表:链表是一种物理上存储非连续,数据元素的逻辑顺序(内存存储位置不一定连续)通过链表中的指针链接次序,数显线性存储结构。
链表中的每一个元素称为一个节点,元素分为两部分:数据域、指针域
数据域:通常表示节点所存储的相关数据,比如该节点的元素值等
指针域:节点的连接则是通过指针进行,以指针存储下一个节点的地址,以此找到下一个节点,由于每一个指针不断存储着下一个节点的地址,所以这些连接构成了线性表——链表。
注意:每一个节点的定义动态生成(malloc)。
原因:malloc 是为指针指向的地方开内存,如果指针没有开辟空间,没有首地址,对指针进行操作时会发生运行错误。
二.链表的操作
这里以单向链表为例。
1.链表的构成
typedef struct student{
int num,id;
char name[101]; // 以上均为数据域
struct student *next;
}student; // 链表的构成,next指针 指向下一个节点
2.链表的创建
①尾插法
由指针域依次寻找下一个节点,直至找到链表尾端,然后将新节点加至链表尾端
void link_add(student **phead,student *pnew) // 链表的创建
{
student *pmov = *phead;
// /*定义指针*/ /*对二级指针取*的一级指针*/
if(*phead==NULL) // 如果起始点为空,则第一个输入点为起始点
{
*phead=pnew;
pnew->next=NULL;
}
else
{
while(pmov->next!=NULL) pmov=pmov->next; //通过next指针寻找下一个节点地址
pmov->next=pnew; //创立新节点pnew,并且 pmov的next指针指向pnew地址
pnew->next=NULL;
}
return ;
}
②头插法
void link_add(node **phead,node *pnew)
{
node *pmov=*phead; // 头插头插法
if(*phead==NULL) // 空指针时
{
*phead=pnew;
pnew->next=NULL;
}
else
{
pnew->next=pmov;
*phead=pnew;
}
return ;
}
关于phead指针:在被函数调用的时候,为了改变头指针head的值,需要传递头指针的地址,即使用取地址符(&),因此在调用函数时,phead指针为二级指针(即对指针取地址)。所以,在定义pmov指针时,将其等于 *phead(赋为一级指针操作)。
3.链表的释放
void link_free(student **phead)
{
student *pb=*phead;
while(*phead!=NULL)
{
pb=*phead;
*phead=(*phead)->next; // 先将phead传至下一个节点后再释放pb
free(pb);
pb=NULL; // 释放后 pb = NULL
}
}
4.链表节点的删除
首先找到需要删除的某个节点,同时记录该节点的前一个节点,删除该节点只需要前节点指向删除节点的后节点即可。
void link_delete(student **phead,int num)
{
student *pf=*phead,*pb=*phead;
if(*phead==NULL) return ;
while(pb->num!=num && pb->next!=NULL)
{
pf=pb; pb=pb->next; // pf 为 pb的上一个节点
}
if(pb->num==num)
{
if(pb==*phead) *phead=pb->next; //如果恰好删除头节点,需要将*phead指针向下一个节点移动
else pf->next=pb->next;
}
return ;
}
5.链表的节点插入
图示,先找到插入的位置(即找到两个相邻指针,在其中插入一个新指针)
void link_insert(student **phead,student *pnew)
{
student *pf=*phead,*pb=*phead;
if(*phead==NULL) // 如果起始点为空,则插入的节点即为头节点
{
*phead=pnew; // 将头节点定义为 pnew
pnew->next=NULL;
return ;
}
// 链表中每个节点位置都有可能
while(pnew->num>=pb->num && pb->next!=NULL) {pf=pb,pb=pb->next;} //寻找插入的地方
if(pnew->num>=pb->num) // 找到
{
if(pb==*phead) // 如果插入在头节点前
{
pnew->next=pb;
*phead=pnew;
}
else // 插在中间
{
pf->next=pnew;
pnew->next=pb;
}
}
else // 插在末尾
{
pb->=next=pnew;
pnew->next=NULL;
}
return ;
}
6.链表的查找和遍历
将这两个操作分成一组的原因:这两个操作并不需要改变头指针head的内容,通过形参调用,一级指针即可。
①查找
student *link_search(student *head,int num) // 链表查找
{
student *pmov=head; //一级指针
while(pmov!=NULL)
{
if(pmov->num==num)
{
return pmov;
}
else pmov=pmov->next;
}
return NULL;
}
②遍历
void link_print(student *head) // 遍历链表
{
student *pmov=head;
while(pmov!=NULL)
{
printf("%d %d \n",pmov->id,pmov->num);
pmov=pmov->next;
}
return ;
}
三.哨兵节点
哨兵是一个哑对象,作用就是简化边界条件的处理。
在链表中设置一个对象 L.nil,该对象代表NIL,但具有和其他对象相同的各个属性。对于链表代码中出现的每一处NIL的引用,都代之以对哨兵的L.nil的引用,如图,将一个常规的单向链表转变为有哨兵的单向循环链表
此时插入操作变为(尾插法):
void link_add(node *phead,node *pnew) // phead 为哨兵指针
{
if(phead->next==NULL) // 如果哨兵位下一个没有指针即空指针
{
phead->next=pnew;
pnew->next=NULL;
}
else
{
node *pmov=phead->next;
while(pmov->next!=NULL)
pmov=pmov->next;
pmov->next=pnew;
pnew->next=NULL;
}
return ;
}
删除某个节点操作:
void link_delete(node *phead,int num) // phead 为哨兵指针
{
node *pmov=phead->next,*pf=phead->next;
if(phead->next==NULL) return ;
while(pmov->next!=NULL && pmov->key!=num)
pf=pmov,pmov=pmov->next;
if(pmov->key==num)
{
if(pmov==phead->next) phead->next=pmov->next;
else pf->next=pmov->next;
}
return ;
}
遍历指针操作,从phead->next 开始遍历
oid print(node *phead)
{
node *pmov=phead->next;
while(pmov!=NULL)
{
printf("%d ",pmov->key);
pmov=pmov->next;
}
return ;
}
操作并没有简便很多,该判断表头的还是需要判断,可以与无哨兵指针相比较。
区别在于:哨兵指针调用时不需要二级指针(因为与哨兵指针的元素无关联)