链表
,采用链式存储结构存储数据元素的线性表
头结点是在链表的开始结点之前附加一个结点。它具有两个优点:
⒈由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作就和在表的其它位置上操作一致,无须进行特殊处理;
⒉无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域空),因此空表和非空表的处理也就统一了。
②带头结点的单链表
注意:
头结点数据域的阴影表示该部分不存储信息。在有的应用中可用于存放表长等附加信息。
上述算法里,动态申请新结点空间时未加错误处理,这对申请空间极少的程序而言不会出问题。但在实用程序里,尤其是对空间需求较大的程序,凡是涉及动态申请空间,一定要加入错误处理以防系统无空间可供分配。
在链表中,即使知道被访问结点的序号i,也不能像顺序表中那样直接按序号i访问结点,而只能从链表的头指针出发,顺链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。
② 查找的思想方法
计数器j置为0后,扫描指针p指针从链表的头结点开始顺着链扫描。当p扫描下一个结点时,计数器j相应地加1。当j=i时,指针p所指的结点就是要找的第i个结点。而当p指针指为null且j≠i时,则表示找不到第i个结点。
注意:
头结点可看做是第0个结点。
③具体算法实现
从开始结点出发,顺着链逐个将结点的值和给定值key作比较,若有结点的值与key相等,则返回首次找到的其值为key的结点的存储位置;否则返回NULL。
②具体算法实现
(3)算法分析
删除运算是将表的第i个结点删去。
具体步骤:
(1)找到a i-1的存储位置p(因为在单链表中结点a i的存储地址是在其直接前趋结点a i-1的指针域next中)
(2)令p->next指向a i的直接后继结点(即把a i从链上摘下)
(3)释放结点a i的空间,将其归还给"存储池"。
(2)具体算法实现
设单链表的长度为n,则删去第i个结点仅当1≤i≤n时是合法的。
当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,它是终端结点。因此被删结点的直接前趋*p存在并不意味着被删结点就一定存在,仅当*p存在(即p!=NULL)且*p不是终端结点(即p->next!=NULL)时,才能确定被删结点存在。
(3)算法分析
算法的时间复杂度也是O(n)。
链表上实现的插入和删除运算,无须移动结点,仅需修改指针。
链式存储方法
用一组任意的存储单元来存放线性表的数据元素(这组存储单元既可以是连续的,也可以是不连续的)
链表的逻辑关系
(1)为了能正确表示数据元素间的逻辑关系,对于每个数据元素来说,
除了储存本身的信息外,还必须存储指示其后继结点的位置信息,
这两部分信息组成数据的一个结点。它包括两个域:数据域(data)和指针域(next)(
每个结点只有一个指针域的称为单链表
)
(2)单链表中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。
(3)由于单链表中最后数据元素没有直接后继,所以将最后一个结点的指针指向NULL。
(4)只要确定了第一个数据元素,就可以确定所有数据元素,由此可见,单链表可以由头指针唯一确定
单链表类型定义
typedef struct LNode
{
ElemType data;
struct LNode * next;
}LNode, * LinkList;
【例】假设p是指向单链表中的第i个数据元素的指针,则
p->data是第i个数据元素的值。
p->next是指向第i+1个数据元素的指针
p->next-data是第i+1个数据元素的值
单链表的算法实现
1.建立单链表
(1) 头插法建表
从一个空表开始,重复读入n个数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表头上。
具体方法【参见动画演示】
LinkList CreateList_L(int n)
{//头插法建表,返回单链表的头指针
LinkList head; //头指针
LinkList p; //临时保存新结点地址
head = NULL; //先建立一个空表
for (; n > 0; n--)
{//在表头插入n元素
p = (LinkList)malloc(sizeof(LNode)); //生成新结点
scanf(&p->data); //输入结点数据的值
p->next = head; //将新结点插入到表头
head->next = p; //修改head指向新的表头
}
return head;
}
LinkList CreateList_L(int n)
{//头插法建表,返回单链表的头指针
LinkList head; //头指针
LinkList p; //临时保存新结点地址
head = NULL; //先建立一个空表
for (; n > 0; n--)
{//在表头插入n元素
p = (LinkList)malloc(sizeof(LNode)); //生成新结点
scanf(&p->data); //输入结点数据的值
p->next = head; //将新结点插入到表头
head->next = p; //修改head指向新的表头
}
return head;
}
(2) 尾插法建表
从一个空表开始,重复读入n个数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表尾上。
具体方法【参见动画演示】
LinkList GreateList_R(int n)
{
LinkList head; //头指针
LinkList r; //尾指针
LinkList p; //临时保存新结点地址
head = NULL; //建立一个空表
r = NULL; //尾指针初始化
for (; n > 0; n--)
{
p = (LinkList)malloc(sizeof(LNode)); //生成新结点
scanf(&p->data); //输入结点数据的值
if (NULL == head) //如果是空表
{
r = head = p; //新结点插入表头
}
else
{//插入表尾
r->next = p;
r = p;
}
}
return head;
}
注意:
- 由于开始结点的位置是存放在头指针(指针变量)中,而其余结点的位置是在其前趋结点的指针域中,插入开始结点时要将头指针指向开始结点。
- 因为对尾部进行插入,所以增加了一个尾指针
(3) 尾插法建带头结点的单链表
①头结点及作用头结点是在链表的开始结点之前附加一个结点。它具有两个优点:
⒈由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作就和在表的其它位置上操作一致,无须进行特殊处理;
⒉无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域空),因此空表和非空表的处理也就统一了。
②带头结点的单链表
注意:
头结点数据域的阴影表示该部分不存储信息。在有的应用中可用于存放表长等附加信息。
LinkList CreatListR1(int n)
{//用尾插法建立带头结点的单链表
LinkList head=(LinkList)malloc(sizeof(LNode)); //生成头结点
LinkList r = head; //初始化尾指针指向头结点
LinkList p; //临时保存新结点地址
for (; n > 0; n--)
{//在表头插入n元素
p = (LinkList)malloc(sizeof(LNode)); //生成新结点
scanf(&p->data); //输入元素的值
r->next = p; //将新结点链接到尾部
r = p; //将尾指针指向新的尾部
}
r->next=NULL; //终端结点的指针域置空,或空表的头结点指针域置空
return head;
}
注意:上述算法里,动态申请新结点空间时未加错误处理,这对申请空间极少的程序而言不会出问题。但在实用程序里,尤其是对空间需求较大的程序,凡是涉及动态申请空间,一定要加入错误处理以防系统无空间可供分配。
2.单链表的查找运算
(1)按序号查找
① 链表不是随机存取结构在链表中,即使知道被访问结点的序号i,也不能像顺序表中那样直接按序号i访问结点,而只能从链表的头指针出发,顺链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。
② 查找的思想方法
计数器j置为0后,扫描指针p指针从链表的头结点开始顺着链扫描。当p扫描下一个结点时,计数器j相应地加1。当j=i时,指针p所指的结点就是要找的第i个结点。而当p指针指为null且j≠i时,则表示找不到第i个结点。
注意:
头结点可看做是第0个结点。
③具体算法实现
LinkList GetNode(LinkList head, int i)
{//在带头结点的单链表head中查找第i个结点,若找到(0≤i≤n),
//则返回该结点的存储位置,否则返回NULL。
int j;
ListNode *p;
p=head;
j=0; //从头结点开始扫描
while(p->next && j < i)
{//顺指针向后扫描,直到p->next为NULL或i=j为止
p=p->next;
j++;
}
if(i==j)
return p; //找到了第i个结点
else
return NULL; //当i<0或i>0时,找不到第i个结点
}
(2) 按值查找
①思想方法从开始结点出发,顺着链逐个将结点的值和给定值key作比较,若有结点的值与key相等,则返回首次找到的其值为key的结点的存储位置;否则返回NULL。
②具体算法实现
LinkList LocateNode (LinkList head, DataType key)
{//在带头结点的单链表head中查找其值为key的结点
ListNode *p=head->next; //从开始结点比较。表非空,p初始值指向开始结点
while(p && p->data!=key) //直到p为NULL或p->data为key为止
p=p->next;//扫描下一结点
return p;//若p=NULL,则查找失败,否则p指向值为key的结点
}
3.插入运算
(1)思想方法
插入运算是将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。
具体步骤:
(1)找到ai-1存储位置p
(2)生成一个数据域为x的新结点*s
(3)令结点*p的指针域指向新结点
(4)新结点的指针域指向结点ai。
void InsertList(LinkList head, DataType x, int i)
{//将值为x的新结点插入到带头结点的单链表head的第i个结点的位置上
LinkList p;
p=GetNode(head,i-1); //寻找第i-1个结点
if (p==NULL) //i<1或i>n+1时插入位置i有错
Error("position error");
s=(ListNode *)malloc(sizeof(ListNode));
s->data=x;
s->next=p->next;
p->next=s;
}
算法的时间主要耗费在查找操作GetNode上,故时间复杂度亦为O(n)。
4.删除运算
(1)思想方法删除运算是将表的第i个结点删去。
具体步骤:
(1)找到a i-1的存储位置p(因为在单链表中结点a i的存储地址是在其直接前趋结点a i-1的指针域next中)
(2)令p->next指向a i的直接后继结点(即把a i从链上摘下)
(3)释放结点a i的空间,将其归还给"存储池"。
(2)具体算法实现
void DeleteList(LinkList head, int i)
{//删除带头结点的单链表head上的第i个结点
ListNode *p,*r;
p=GetNode(head,i-1); //找到第i-1个结点
if (p==NULL || p->next==NULL) //i<1或i>n时,删除位置错
Error("position error");//退出程序运行
r=p->next;//使r指向被删除的结点ai
p->next=r->next; //将ai从链上摘下
free(r); //释放结点ai的空间给存储池
}
注意:
设单链表的长度为n,则删去第i个结点仅当1≤i≤n时是合法的。
当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,它是终端结点。因此被删结点的直接前趋*p存在并不意味着被删结点就一定存在,仅当*p存在(即p!=NULL)且*p不是终端结点(即p->next!=NULL)时,才能确定被删结点存在。
(3)算法分析
算法的时间复杂度也是O(n)。
链表上实现的插入和删除运算,无须移动结点,仅需修改指针。