上篇文章介绍了线性表的顺序储存结构(顺序表),这篇文章接着介绍链表的相关内容。
一、单链表的定义和表示
单链表:线性表链式存储结构,用于存储逻辑关系为 “一对一” 的数据。与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。
单链表的特点:用一组任意的存储单元存储线性表的数据元素。
例如,使用链表存储 {1,2,3},数据的物理存储状态如图所示
这样的话无法表示各元素之间的逻辑关系,对此,链表的解决方案是,每个数据元素在存储时都配备一个指针,用于指向自己的直接后继元素。如图 所示:
这样,数据元素随机存储,并通过指针表示数据之间逻辑关系的存储结构就是链式存储结构。
二、链表的结点
链表中每个数据的存储都由以下两部分组成
- 数据元素本身,其所在的区域称为数据域
- 指向直接后继元素的指针,所在的区域称为指针域
链表中存储各数据元素的结构:也就是结点。如图所示,
指针域中存储的信息称作指针或链。
n个结点链结成一个链表,即线性表。
链表实际存储的是一个个结点,真正的数据元素包含在这些节点之中,如图
用单链表表示线性表是由数据元素之间的逻辑关系是由结点中的指针指示的。
在使用链表时,关心的只是它所表示的线性表中数据元素之间的逻辑顺序,而不是每个数据元素在存储器中的实际位置。
因此,单链表的每个结点需要使用C语言的结构体来实现
//------单链表的存储结构------
typedef struct LNode
{
ElemType data; //结点的数据域
struct LNode *next; //结点的指针域
} LNode,*LinkList; //LinkList为指向结构体LNode的指针类型
三、首元结点、头结点、头指针
.
一个完整的链式结构需要以下几个部分构成
- 头指针:一个普通的指针,它的特点是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据;
- 头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点。对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题;
- 首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义;
一个存储 {1,2,3} 的完整链表结构如图所示
注意:链表中有头节点时,头指针指向头节点;反之,若链表中没有头节点,则头指针指向首元节点。
四、单链表基本操作的实现
1.单链表的初始化
构造一个空表,算法如下:
Status InitList(LinkList &L)
{//构造一个空的单链表L
L=new LNode; //生产新节点作为头结点,用头指针L指向头结点
L->next=NULL; //头结点的指针域置空
return OK;
2.单链表的取值
算法如下:
```clike
int getVal(LinkList L,int pos)
{
if(pos<1||pos>L->data)
{
printf("invalid input\n"); //判断查找的位置是否合理
return 0;
}
LinkList p = L;
for(int i=1;i<pos;i++)
{
p = p->next;
}
return p->next->data; //返回查找的值
3.单链表的查找
int getPos(LinkList L,ElemType val)
{
LinkList p = L;
for(int i=1;i<=L->data;i++)
{
p = p->next;
if(p->data==val)
{
return i; //返回值的位置
}
if(i==L->data)
{
return 0; //若没找到,返回0
}
}
}
4.单链表的插入
同顺序表一样,向链表中增添元素,根据添加位置不同,可分为以下 3 种情况
- 插入到链表的头部(头节点之后),作为首元节点;
- 插入到链表中间的某个位置;
- 插入到链表的最末端,作为链表中最后一个数据元素;
虽然新元素的插入位置不固定,但是链表插入元素的思想是固定的,只需做以下两步操作,即可将新元素插入到指定的位置:
- 将新结点的 next 指针指向插入位置后的结点
- 将插入位置前结点的 next 指针指向插入结点
我们在链表 {1,2,3,4} 的基础上分别实现在头部、中间部位、尾部插入新元素 5,其实现过程如图所示:
.
算法如下:
Status insertList(LinkList L,int local,ElemType &e)
{
if(local<1||local>(L->data+1))
{
printf("invalid input\n"); //判断插入的未知是否有效
return FALSE;
}
LinkList p = L;
for(int i=1;i<local;i++)
{
p = p->next;
}
LinkList s = new Lnode;
s->data = e;
s->next = p->next;
p->next = s;
L->data++; //插入后长度加1
return TRUE;
}
5.单链表的删除
单链表的删除是将存有该数据元素的节点从链表中摘除,要对存储空间负责,对不再利用的存储空间要及时释放。因此,从链表中删除数据元素需要进行以下 2 步操作:
- 将结点从链表中摘下来
- 手动释放掉结点,回收被结点占用的存储空间
从链表上摘除某节点的实现非常简单,只需找到该节点的直接前驱节点 temp,执行一行程序:
temp->next=temp->next->next;
从存有 {1,2,3,4} 的链表中删除元素 3,则此代码的执行效果如图所示:
算法如下:
Status deleteList(LinkList L,int pos)
{
if(pos<1||pos>L->data)
{
printf("invalid input\n");
return FALSE;
}
LinkList p = L;
for(int i=1;i<pos;i++)
{
p = p->next;
}
p->next = p->next->next;
L->data--;
return TRUE;
}
五、单链表的两种创建方式
- 前插法:通过将新结点逐个插入链表的头部(头结点之后)来创建链表,每次申请一个新节点,读入相应的数据元素值,然后将新节点插入到头结点之后。
算法如下:
void CreateList_H(LinkList &L,int n)
{
//逆位序输入n个元素的值,建立带表头结点的单链表L
L=new LNode;
L->next=NULL; //先建立一个带头结点的空链表
for(i = 0; i < n; i ++)
{
p=new LNode; //生成新节点*p
cin>>p->data; //输入元素值赋给新节点*p的数据域
p->next=L->next; L->next=p; //将新节点*p插入到头结点之后
}
}
- 后插法:通过将新节点逐个插入到链表的尾部来创建链表。与前插法不同的是,为了使新节点能够插入到表尾,需要增加一个尾指针r指向链表的尾结点。
算法如下:
void CreatList_R(LinkList &L, int n)
{
//正位序输入n个元素的值,建立带表头结点的单链表L
L=new LNode;
L->next=NULL; //先建立一个带头结点的空链表
r=L; //尾指针r指向头结点
for(i=0;i<n;++i)
{
p=new LNode; //生成新结点
cin>>p->data; //输入元素值付给新结点*p的数据域
p->next=NULL; r->next=p; //将新结点*p插入尾结点*r之后
r=p; //r指向新的尾结点*p
}
}