单链表

由于顺序表的插入、删除操作需要移动大量的元素,影响了运行效率,由此引入了线性表的链式存储。链式存储线性表时,不需要使用地址连续的存储单元,即它不要求逻辑上相邻的两个元素在物理位置上也相邻。它通过“链”建立起数据元素之间的逻辑关系,因此对线性表的插入、删除不需要移动元素,而只需要修改指针,

单链表的定义

线性表的链式存储又称单链表,它是通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还要存放一个指向其后继的指针。data为数据域,存放数据元素;next为指针域,存放其后继结点的地址。

单链表中结点类型的描述如下:

typedef struct LNode{//定义单链表结点类型
	ElemType data;//数据域
	Struct LNode *next;	//指针域 
}LNode,*LinkList;

利用单链表可以解决顺序表需要大量连续存储空间的缺点,但单链表附加指针域,也存在浪费存储空间的缺点。由于单链表的元素是离散的分布在存储空间中的,所以单链表是非随机存取的存储结构,即不能直接找到表中某个特定结点,查找某个特定节点时,需要从表头开始遍历,依次查找。

通常用头指针来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。此外,为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点,头结点的数据域可以不设任何信息,也可以记录表长等相关信息。头结点的指针域指向线性表的第一个元素结点。

头结点和头指针的区分

不管带不带头结点,头指针始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内部通常不存储信息。

引入头结点后,可以带来两个好处:

  1. 由于开始节点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
  2. 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理就得到了统一

单链表上基本操作的实现

用头插法建立单链表

该方法从一个空表开始,生成一个新结点,并将读取到的数据存放在新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后

头插法建立单链表的算法如下:

LinkList List_HeadInsert(LinkList &L){
	//从表尾到表头逆向建立单链表L,每次均在头结点之后插入元素
	LNode *s;
	int x;
	L=(LinkList)malloc(sizeof(LNode));//创建头结点
	L->next=NULL;
	scanf("%d",&x);//输入结点的值
	while(x!=9999){
		//输入9999表示结束
		s=(LNode *)malloc(sizeof(LNode));//创建新结点
		s->data=x;
		s->next=L->next;
		L->next=s;
		scanf("%d",&x); 
	} 
	return L;
} 

采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。每个结点插入的时间为O(1),设单链表长为n,则总的时间复杂度为O(n);

采用尾插法建立单链表

头插法建立单链表的算法虽然简单,但生成的链表中的结点的次序和输入数据的顺序不一致。若希望两者一致,则采用尾插法,该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾节点

尾插法建立单链表的算法如下:

LinkList List_TailInsert(LinkList &L){
	//从表头到表尾正向建立单链表L,每次均在表尾插入元素	
	int x;
	L=(LinkList)malloc(sizeof(LNode));//创建头结点
	LNode *s,*r=L;//r为表尾指针 
	scanf("%d",&x);//输入结点的值
	while(x!=9999){
		//输入9999表示结束
		s=(LNode *)malloc(sizeof(LNode));//创建新结点
		s->data=x;
		r->next=s;
		r=s;//r指向新的表尾结点 
		scanf("%d",&x); 
	} 
	r->next=NULL;//尾节点指针置为空 
	return L;
} 

因为附设了一个指向表尾结点的指针,故时间复杂度和头插法的相同

按序号查找结点值

在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个结点的指针域NULL;

按序号查找结点的值的算法如下:

LNode *GetElem(LinkList L,int i){
	//本算法取出单链表L(带头结点)中第i个位置的结点指针
	int j=1;//计数,初始为1
	LNode *p=L->next;//头结点指针赋给p
	if(i==0) return L;//若i等于0,则返回头结点
	if(i<1) return L;//若i无效,返回NULL
	while(p&&j<i){
		//从第1个结点开始找,查找第i个结点
		p=p->next;
		j++; 
	} 
	return p;;//返回第i个结点的指针,如果i大于表长
			  //p=NULL,直接返回p即可 
} 

按序号查找操作的时间复杂度为O(n)

按值查找表结点

从单链表的第一个节点开始,从前往后依次比较表中各结点数据域的值,若某节点数据域的值等于给定值e,则返回该结点的指针;若整个单链表中没有这样的结点,则返回NULL

按值查找表结点的算法如下:

LNode *LocateElem(LinkList L,ElemType e){
	LNode *p=L->next;
	while(p!=NULL&&p->data!=e)
		p=p->next;
	return p;
} 

按值查找操作的时间复杂度是O(n)

插入结点操作

插入节点操作将值为x的新结点插入到单链表的第i个位置上,先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后面插入新结点

算法首先调用按序号查找算法GetElem(L,i-1),查找第i-1个结点,假设返回的第i-1个结点为*p,然后令新结点*s的指针域指向*p的后继结点,再令结点*p的指针域指向新插入的结点*s,

实现插入结点的代码如下:

p=GetElem(L,i-1);
s->next=p->next;
p->next=s;

扩展:对某一结点进行前插操作

前插操作说指在某节点的前面插入一个新结点,后插操作的定义刚好与之相反。在单链表的插入算法中,通常都采用后插操作

以上面的算法为例,首先调用GetElem()找到第i-1个结点,即插入结点的前驱结点后,再对其进行后插操作。由此可知,对结点的前插操作均可转化为后插操作,前提是从单链表的头结点开始顺序查找到其前驱结点,时间复杂度为O(n)

此外,可采用另一种方式将其转化为后插操作来实现。设插入结点为*s,将*s插入到*p的前面,我们仍然将*s插入到*p的后面,然后将p->data与s->data交换,这样既满足了逻辑关系,又能使得时间复杂度为O(1),算法的代码片段如下:

//将*s结点插入到*p之前的主要代码片段
s->next=p->next;
p->next=s;
temp=p->data;
p->data=s->data;
s->data=temp; 

删除结点操作

删除结点操作是将单链表的第i个结点删除,先检查删除位置的合法性,然后查找表中第i-1个结点,即被删除结点的前驱结点,再将其删除

代码片段如下:

p=GetElem(L,i-1);//查找删除位置的前驱结点
q=p->next;
p->next=q->next;
free(q);//释放结点的存储空间 

和插入算法一样,该算法的时间复杂度也消耗在查找操作上,时间复杂度为O(n)

扩展,删除结点*p

其实,删除结点*p的操作也可用删除*p的后继结点的操作来实现,实质就是将其后继节点的值赋予自身,然后删除后继结点,也使得时间复杂度为O(1)

实现上述操作的代码如下:

q=p->next;
p->data=p->next->data;
p->next=q->next;
free(q);//释放后继节点的存储空间 

求表长操作

求表长操作是计算单链表中数据节点(不含头结点)的个数,需要从第一个结点开始顺序依次访问表中的每个结点,为此需要设置一个计数器变量,每访问一个结点,计数器加1,直到访问到空结点为止,算法的时间复杂度为O(n)

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值