正儿八经地谈数据结构(单链表篇)

我们今天来正儿八经的谈谈单链表吧。线性表的链式存储结构也就是指链表,其特点是用一组任意的存储单元存储线性表的数据元素(这里的存储单元可以是连续的,也可以是不连续的),不像顺序表,顺序表必须是连续的。

一、定义和表示

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评个论鼓励我一下呗。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值