我们今天来正儿八经的谈谈单链表吧。线性表的链式存储结构也就是指链表,其特点是用一组任意的存储单元存储线性表的数据元素(这里的存储单元可以是连续的,也可以是不连续的),不像顺序表,顺序表必须是连续的。
一、定义和表示
1.定义
单链表是一种链式存取的数据结构,我们用一组任意的存储单元存放线性表中的数据元素。
链表中的数据是以结点来表示的,每个结点由两部分组成:1.数据域,也就是用来存储数据信息的部分 2.指针域,用来指示后继元素存储位置。由若干个这样的结点链接起来就形成了一个链表。由于这样的链表的每一个结点只有一个指针域,所以我们把这样的链表叫做单链表。用单链表来表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的。
2.存储结构
上面介绍了单链表的定义,可能有很多读者还是不太理解,那我们来看一下它具体是怎样的一个单链表,(图有点丑陋,大家不要介意)
这就是一个简单的单链表的逻辑状态了,我们将链表画成箭头相链接的结点的序列。一个数据域+指针域就构成了一个结点,由若干个这样的结点就构成一个单链表了。要知道的是每个结点的指针指向下一个结点的数据域。看到图可能会有读者问,为什么前面会有一个头指针呢,这个头指针是干嘛用的呢。因为要对整个单链表的存取我们必须通过指针来达到目的,所以我们需要一个头指针来指向这个单链表,自然而然我们就把头指针指向单链表中的第一个结点。同样的,因为最后一个数据元素没有直接后继,也就是没有下一个结点数据可以指向了,所以单链表的最后一个结点的指针域是空的。
知道了单链表的逻辑状态,我们可以知道单链表可由头指针唯一确定,我们用结构指针来描述单链表的存储结构:
typedef struct LNode
{
ElemType data;//结点的数据域
struct LNode *next;//结点的指针域
}LNode,*LinkList;//LinkList为指向结构体LNode的指针类型
这里需要强调以下几点:
(1)这里定义的是单链表中每个结点的存储结构。一个结点包括两个部分,数据域和指针域,这里数据域的类型用通用类型标识符ElemType表示,指针域的类型是指针类型LNode*
(2)单链表是由表头指针唯一确定的,因此单链表可以用头指针的名字来命名。
(3)注意区分指针变量和结点变量。例如:LinkList p(等价于LNode *p) ,这里的p为指向某结点的指针变量,表示该结点的地址;而 * p为对应的结点变量,表示该结点的名称。
3.头结点和首元结点
一般情况下,我们都会在单链表的第一个结点之前附加一个结点,这个结点叫做头结点。那首元节点又是什么呢,首元结点是指链表中存储的第一个结点。具体的我们来看图就知道了。
由图可知头结点是在首元结点之前附加的一个结点,头结点的指针域指向首元结点。头结点的数据域可以不放任何东西,也就是说它的数据域可以是空的,同样它也可存放数据。例如:当数据元素为整型时,头结点的数据域中可存放该线性表的长度。这里再次说明一下,头指针是指向链表中第一个结点的指针。若链表设有头结点,那么头指针就指向头结点;若链表不设头结点,那么头指针就指向首元结点。
我们为什么要增加头结点呢?理由如下:
(1)便于首元结点的处理。增加了头结点后,首元结点的地址保存在头结点的指针域中,使得首元结点就像其他结点一样,就不用再对其进行特殊处理了。
(2)便于空表和非空表的统一处理。增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。
二、基本操作实现
1.初始化
单链表的初始化操作就是构造一个空表,如图:
我们需要进行如下步骤:
(1)生成新结点作为头结点,用头指针L指向头结点。(2)把头结点的指针域置为空。
Status InitList(LinkList &L)
{//构造一个空的单链表L
L=new LNode;//生成新的结点作为头结点,用头指针L指向头结点
L->next=NULL;//头结点的指针域置空
return OK;
}
2.创建
以上的初始化操作我们只是创建了一个只有头结点的空链表,那我们要如何建立一个包含若干个结点的链表呢?链表和顺序表不同,它是一种动态结构。每个链表占用的存储空间不需要预先分配,而是在需要的时候才由系统自动分配。也就是说这是一个动态生成链表的过程,即从空表的初始状态起,依次建立各元素结点,并逐个插入链表。根据插入位置的不同,链表的创建方法又可分为前插法和后插法。
前插法
步骤如下:
(1)创建一个只有头结点的空链表。
(2)生成一个新结点 *p, 输入元素值给新结点 *p 的数据域,将新结点 *p 插入到头结点之后。
(3)根据需要创建的链表长度重复循环操作(2)
代码如下:
void CreateList_H(LinkList &L,int n)
{//逆位序输入n个元素的值,建立带表头结点的单链表L
L=new LNode;
L->next=NULL;//建立一个带头结点的空链表
for(int i=0;i<n;i++)
{
p=new LNode;//生成新结点 *p
cin>>p->data;//输入元素值赋给新结点*p的数据域
p->next=L->next;//将新结点*p插入到头结点之后
L->next=p;
}
}
后插法
步骤如下:
(1)创建一个只有头结点的空链表。
(2)尾指针 r 初始化,指向头指针。
(3)生成一个新结点 p; 输入元素值给 * p的数据域;将新结点p插入尾结点 *r之后;尾指针 r 指向新的尾结点 * p。
(4)根据需要创建的链表长度重复循环操作(3)。
代码如下:
void CreateList_R(LinkList &L,int n)
{//正位序输入n个元素的值,建立带表头节点的单链表L
L=new LNode;
L->next=NULL;//建立一个带头结点的单链表
r=L;//尾指针r指向头结点
for(int i=0;i<n;++i)
{
p=new LNode;//生成新结点
cin>p->data;//输入元素值赋给新结点*p的数据域
p->next=NULL;
r->next=p;//将新结点*p插入尾结点*r之后
r=p;//r指向新的尾结点*p
}
}
3.取值
我们已经介绍了单链表的初始化以及创建了,接下来就是对单链表的操作了。我们先来介绍单链表的取值,跟顺序表不一样,链表中的相邻节点并不是顺序排列的,因此在链表中的取值不能像顺序表那样随机访问,而是只能从链表的首元结点出发,顺着单链表的指针域next逐个结点向下访问。
取值步骤如下:
(1)用指针 p 指向首元结点,用 j 做计数器并赋初值为1
(2)从首元结点依次顺着单链表结点的指针域 next 向下访问,只要当前结点的指针 p 不为空,并且没有达到序号 i 的结点,则循环执行以下操作: 1. p 指向下一个结点
2.计数器的值相应加1
(3)退出循环时,如果指针 p 为空,或者计数器 j 大于 i,说明指定的序号 i 不合法,取值失败返回ERROR;相反,则取值成功,此时 j=i ,p所指的结点就是要找的第 i 个结点,用参数 e 保存当前结点的数据域,返回OK。
代码如下:
int GetElem(LinkList L,int i)//单链表的取值
{//
p=L->next;
int j=1;
while(p&&j<i)
{
p=p->next;
++j;
}
if(!p||j>i)return ERROR;
// e=p->data;
return p->data;
}
4.查找
单链表中按值查找的过程和顺序表类似,从链表的首元结点出发,依次将节点值与给定值 e 进行比较,返回查找结果。
步骤如下:
(1)用指针 p 指向首元结点。
(2)从首元结点开始顺着链域 next 向下查找,只要指向当前结点的指针 p 不空,并且 p 所指结点的数据域不等于要查找的值 e , 则 p 指向下一个结点。
(3)返回 p。若查找成功,p 此时即为结点的地址值,若查找失败,p 的值即为NULL。
代码如下:
LNode *LocateElem(LinkList L,ElemType e)//单链表的按值查找
{//在带头结点的单链表L中查找值为e的元素
p=L->next;//初始化,p指向首元结点
while(p&&p->data!=e)//顺着单链表一值向后扫描,直到p为空或p所指结点的数据域等于e
{
p=p->next;//p指向下一个结点
return p;//查找成功返回值为e的地址p,查找失败p为NULL
}
}
要注意的是,这里返回的是值为e的地址,也就是说返回的是一个地址。
5.插入
单链表的插入操作不需要像顺序表的插入操作那样需要移动元素。如果我们要插入元素 x 到单链表中,首先就要生成一个数据域为 x 的结点,将指针 s 指向该节点,然后插入到单链表中。单链表插入的具体描述语句如下:
s->next=p->next;
p->next=s;
详细代码如下:
Status ListInsert(LinkList &L,int i,ElemType e)//单链表的插入
{//在带头结点的单链表L中第i个位置插入值为e的新结点
LinkList s;
p=L;
int j=0;
while(p&&(j<i-1))//查找第i-1个结点,p指向该结点
{
p=p->next;
j++;
}
if(!p||j>i+1)return ERROR;//i>n+1或者i<1
s=new LNode;//生成新结点*s
s->data=e;//将结点*s的数据域置为e
s->next=p->next;//将结点*s的指针域指向p所指向的结点
p->next=s;//将结点*p的指针域指向结点*s
return OK;
}
6.删除
要删除单链表中指定位置的元素,同插入元素一样,首先应该找到该位置的前驱结点。然后将前驱结点指向要删除结点的后继结点。单链表删除的具体描述语句如下:
p->next=p->next->next;
或者
q=p->next;
p->next=q->next;
delete q;
详细代码如下:
Status ListDelete(LinkList &L,int i)//单链表的删除
{//在带头结点的单链表L中,删除第i个元素
p=L;
int j=0;
while((p->next)&&(j<i-1))//查找第i-1个结点,p指向该结点
{
p=p->next;
++j;
}
if(!(p->next)||(j>i-1))return ERROR;//当i>n或i<1时,删除位置不合理
q=p->next;//临时保存被删除结点的地址以备释放
p->next=q->next;//改变删除结点前驱结点的指针域
delete q;//释放删除结点的空间
return OK;
}
三、基本应用
为了让读者更加直观的看到单链表操作的结果,我把单链表所有的功能都实现了一遍,代码如下:
(代码有点长,感兴趣的读者可以认真阅读并亲自上机体验)
#include<iostream>
using namespace std;
typedef int ElemType;
#define OVERFLOW -2
#define OK 1
#define ERROR 0;
#define MAXSIZE 100 //最大长度
typedef int Status;
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode,*LinkList;LinkList p,q,r;
void CreateList_H(LinkList &L, int n)// 前插法创建单链表
{ //逆位序输入n个元素的值,建立到头结点的单链表L
L=new LNode;
L->next=NULL;//先建立一个带头结点的空链表
cout<<"请输入 "<<n<<" 个数:\n";
for(int i=1;i<=n;i++)
{
p=new LNode;//生成新结点
cin>>p->data;//输入元素值
p->next=L->next;
L->next=p; //插入到表头
}
}
void CreateList_R(LinkList &L,int n)//后插法创建单链表
{
L=new LNode;
L->next=NULL;
r=L;
cout<<"请输入 "<<n<<" 个数:\n";
for(int i=0;i<n;++i)
{
q=new LNode;
cin>>q->data;
q->next=NULL;
r->next=q;
r=q;
}
}
void disp(LinkList L)//显示单链表中的数据
{
p=L->next;
while(p)
{
cout<<p->data<<" ";
p=p->next;
}
cout<<endl;
}
Status ListInsert(LinkList &L,int i,ElemType e)//单链表的插入
{//在带头结点的单链表L中第i个位置插入值为e的新结点
LinkList s;
p=L;
int j=0;
while(p&&(j<i-1))//查找第i-1个结点,p指向该结点
{
p=p->next;
j++;
}
if(!p||j>i+1)return ERROR;//i>n+1或者i<1
s=new LNode;//生成新结点*s
s->data=e;//将结点*s的数据域置为e
s->next=p->next;//将结点*s的指针域指向p所指向的结点
p->next=s;//将结点*p的指针域指向结点*s
return OK;
}
int GetElem(LinkList L,int i)//单链表的取值
{
p=L->next;
int j=1;
while(p&&j<i)
{
p=p->next;
++j;
}
if(!p||j>i)return ERROR;
return p->data;
}
LNode *LocateElem(LinkList L,ElemType e)//单链表的按值查找
{//在带头结点的单链表L中查找值为e的元素
p=L->next;//初始化,p指向首元结点
while(p&&p->data!=e)//顺着单链表一值向后扫描,直到p为空或p所指结点的数据域等于e
{
p=p->next;//p指向下一个结点
return p;//查找成功返回值为e的地址p,查找失败p为NULL
}
}
Status ListDelete(LinkList &L,int i)//单链表的删除
{//在带头结点的单链表L中,删除第i个元素
p=L;
int j=0;
while((p->next)&&(j<i-1))//查找第i-1个结点,p指向该结点
{
p=p->next;
++j;
}
if(!(p->next)||(j>i-1))return ERROR;//当i>n或i<1时,删除位置不合理
q=p->next;//临时保存被删除结点的地址以备释放
p->next=q->next;//改变删除结点前驱结点的指针域
delete q;//释放删除结点的空间
return OK;
}
void main()
{
int x,n;
while(true){
do
{
cout<<"1.前插法创建新的单链表"<<endl;
cout<<"2.后插法创建新的单链表"<<endl;
cout<<"3.查看单链表的数据"<<endl;
cout<<"4.将元素e插入到单链表的i位置"<<endl;
cout<<"5.查找位置j的元素的值"<<endl;
cout<<"6.请输入需要删除的下标"<<endl;
cout<<"7.按值查找单链表的地址值"<<endl;
cout<<"请输入数字选择你需要的功能:"<<endl;
cout<<"注意:1和2只能选择其中一个!"<<endl;
cin>>x;
switch(x)
{
LinkList LL;
case 1:
cout<<"请输入单链表长度n "<<endl;
cin>>n;
CreateList_H(LL,n);
break;
case 2:
cout<<"请输入单链表长度n "<<endl;
cin>>n;
CreateList_R(LL,n);
break;
case 3:
disp(LL);
cout<<endl;
break;
case 4:
int i,e;
cout<<"请输入需要插入的位置"<<endl;
cin>>i;
cout<<"请输入需要插入的元素"<<endl;
cin>>e;
ListInsert(LL,i,e);
cout<<"插入元素后的单链表: ";
disp(LL);
break;
case 5:
int j;
cout<<"请输入序号j"<<endl;
cin>>j;
cout<<GetElem(LL,j)<<endl;
break;
case 6:
int k;
cout<<"请输入需要删除的元素下标"<<endl;
cin>>k;
ListDelete(LL,k);
cout<<"删除元素后的单链表: ";
disp(LL);
break;
case 7:
int a;
cout<<"请输入要查找的下标a"<<endl;
cin>>a;
cout<<LocateElem(LL,a);
default:
break;
}cout<<endl;
}while(true);
}
}
运行结果如下:
四、结束语
敬请批评指正!对于单链表的一些简单应用还没有举例子,以后有时间会跟大家一起分享的。非常感谢大家的支持。创作不易呀,点个赞or评个论鼓励我一下呗。