数据结构——第二章 线性表(2)——链式存储结构

文章详细介绍了线性表的链式存储结构,特别是单向链表的实现,包括不带头结点和带头结点的两种形式。重点讲述了单向链表的初始化、插入、删除、更新、长度计算、定位、遍历和创建等基本操作的算法实现,并讨论了不同创建方法的效率和特点。此外,还介绍了如何就地逆置链表,即将链表中的数据元素顺序反转的方法。
摘要由CSDN通过智能技术生成

1 线性表的链式存储结构

线性表的链式存储结构是指用一组数据类型相同的结点串接成一个单向链表,每一个结点是一个结构体类型的变量,由数据域和指针域组成,其中数据域用于存放数据元素,指针域用于存放直接后继结点的地址。

单链表分为带头结点和不带头结点

1.1不带头结点的单向链表

头指针直接指向第一个结点的地址;
链表是否为空判断标准:头指针指向NULL时为空链表。

1.2 带头结点的单向链表

头指针指向的结点称为头结点(通常数据域为空,不存放数据),头结点的直接后继结点是第一个结点。
判断链表是否为空取决于头结点的指针域是否为空。
在带头结点的单向链表中进行插入和删除是,由于第一个结点和其他结点的前驱邻接节点和后接邻接节点都是相同类型的结点,因此对带头结点的单向链表所做的插入和删除操作不会改变头指针的值,实现的代码比不带头结点的单向链表相对简单。

2 单向链表的基本操作实现

凡是操作单向链表,都必须记住链表的头指针,其余结点通过结点的指针域均可以依序得到。基本链表的基本操作者大致分为两类。一类是以查找为基础的算法设计,比如定位以及根据条件找到相应位置后的插入和删除等;另一类是建表为基础的算法设计,比如将存放在链表中的一组数据逆序存放,对存放在链表中的一组数据进行排序等。

2.1 单向链表的初始化操作

【算法实现】
方法一:将建立的带结点的头指针存到主调函数的某个头指针变量H。调用时主调函数提供存放头指针变量H的地址为被调函数的参数。

int initLinkList1(LinkList* L)
{
	*L = (LinkList)malloc(sizeof(LNode));
	if (*L == NULL)
	{
		return 0;
	}
	return 1;
}

方法二:将建立的带头结点的头指针用返回值返回给主调函数。调用时主调函数用赋值语句接受被调函数返回的头指针。

LinkList initLinkList2()
{
	LinkList L;
	//申请头结点空间,将头结点地址赋值给头指针
	L = (LinkList)malloc(sizeof(LNode));
	//判断是否申请成功
	if (L == NULL)
		return NULL;
	L->next = NULL;
	return L;//返回地址的值
}

2.2 单向链表的插入操作

单向链表的插入操作是将学生数据插入到单向链表的指定位置i。
由于单向链表每个结点的指针域记得是直接后继结点,所以要想使插进入的新结点* s成为第i个结点,即让 *s的指针域记原来ai所在结点的地址,并让原来a-i所在结点的指针域记 * s的地址,实现这些操作的前提是找到ai-1所在结点的指针域记 *的地址,实现这些操作的前提是找到ai-1所在的第i-1个结点。如何找到第i-1个结点呢?用一个工作指针变量p向后继方向移动,在移动的过程中用一个整型变量pos记p指向结点的位置。由于链表只能顺序查找,让p从头结点开始,p=L,pos=0.当p不为空且p未到达第i-1个结点时,条件p!=NULL&&pos<i-1为真,则p=p->next;pos++;直至条件不成立。由于插入位置i给定值,因此插入应考虑i取值的正确性,即如果i<1,则结束操作;如果i>n+1,则p为空,结束操作;如果1<=i<=n+1,则p指向第i-1个结点,pos=i-1,插入新结点 * s。插入的主要代码为:①s->next=p->next;②p->next=s。
分析:因为插入的结点总是在头结点之后,所以插入操作不会引起头指针L的改变,由于插入操作必须知道插入的位置和插入的数据元素,所以对应的应该有三个形参,算法如下。
【算法】:

int insertLinkList(LinkList L, int i, STD x)
{
	LinkList p, s;
	int pos = 0;//pos记头结点的位置0
	p = L;//p的初值为头指针

	if (i < 1)
	{
		printf("插入位置越下界,插入失败\n");
		return 0;
	}

	while (p != NULL && pos < i - 1)
	{
		p = p->next;
		pos++;
	}

	if (p == NULL)
	{
		printf("插入位置越上界,插入失败!\n");
		return 0;
	}

	//生成新的结点
	if ((s = (LinkList)malloc(sizeof(LNode)) == NULL))
	{
		perror("insertLinkList::");
		return 0;
	}

	//将s指向的新结点在指定位置插入
	s->data = x;
	s->next = p->next;
	p->next = s;

	return 1;
}

【算法分析】
寻找插入位置,将数据插进来,需要从第一个结点开始比较。最好的情况是o(1);最坏的情况是O(n);等概率加权平均是O(n)。

2.3. 单链表的删除操作

单链表的删除操作就是将指定位置的学生数据删除。
【算法实现】

int deleteLinkList(LinkList L, int i, STD* x)
{
	LinkList p = L,q;//p的初值为头指针
	int pos = 0;//pos记录结点的位置0

	if (L->next == NULL)
	{
		printf("链表为空,删除失败!\n");
		return 0;
	}
	if (i < 1)
	{
		printf("删除位置越下界,删除失败!\n");
		return 0;
	}
	//让p记住第i-1个结点,pos记住p指向结点的位置
	while (p->next!=NULL&&pos<i-1)
	{
		p = p->next;
		pos++;
	}
	if (p->next == NULL)
	{
		printf("删除位置越上界,删除失败!\n");
		return 0;
	}
	q = p->next;
	p->next = q->next;
	*x = q->data;
	free(q);
	return 1;
}

【算法分析】
寻找删除位置,将数据删除,需要从第一个结点开始比较。最好的情况是O(1);最坏的情况是O(n);等概率加权平均是O(n)。

2.4.单向链表的更新操作

单向链表的更新操作是用新数据元素替换指定位置i处的数据元素。
【算法实现】

int updateLinkList(LinkList L, int i, STD x)
{
	LinkList p;
	int pos;
	if (L->next == NULL)
	{
		printf("链表为空,不能更新!\n");
		return 0;
	}
	if (i < 1)
	{
		printf("更新位置越下界,不能更新!\n");
		return 0;
	}
	//p指向第一个结点,pos记p指向结点的位置
	p = L->next;
	pos = 1;
	while (p != NULL && pos < i)
	{
		p = p->next;
		pos++;
	}
	if (p == NULL)
	{
		printf("更新位置越上界,不能更新!\n");
		return 0;
	}
	p->data = x;
	return 1;//更新成功

}

【算法分析】
寻找更新数据的位置,需要从第一个结点开始比较。最好的情况是O(1);最坏的情况是O(n);等概率加权平均是O(n)。

2.5.单向链表的求长度操作

单向链表的求长度操作是计算单向链表中数据元素个数。
【算法实现】

int LinkListLength(LinkList L)
{
	LinkList p = L->next;
	int n = 0;
	while (p)
	{
		n++;
		p = p->next;
	}
	return n;
}

2.6.单向链表的定位操作

单向链表的定位操作是根据条件得到某个数据元素的地址。定位操作又称为查找操作。
分析:定位操作不会引起头指针的变化,但是必须提供查找的条件,所以定位函数应该有2个形参。如果查找成功,返回结点所在地址;否则返回空。
【算法实现】

LinkList locationLinkList(LinkList L, char* name)//依据姓名查找
{
	LinkList p = L->next;//p指向第一个数据结点
	while (p)
	{
		if (strcmp(p->data.name, name) != 0)
		{
			p = p->next;
		}
		else
		{
			return p;
		}
	}
	return NULL;
}

2.7.单向链表的遍历操作

单向链表的遍历操作是输出单链表中存放的所有数据元素
【算法】

void dispLinkList(LinkList L)
{
	LinkList p = L->next;//p指向第一个数据结点
	while (p)
	{
		printf("%10s%7.2f\n", p->data.name, p->data.score);
		p = p->next;
	}
	return;
}

算法中的指针变量p可以不定义,直接用形参L代替p。但是为了提高算法的可读性,约定这里的L在调用指向链表的头结点,在后序的操作中不要改变L的值。如需寻找链表上的其他结点,建议用另外工作指针去完成相应的操作。

2.8.单向链表的创建操作

单向链表的创建操作是创建一个空链表,并依序插入新的结点。
常见的创建单向链表的算法有三种。分别如下。
(1)用初始化函数和插入函数组合得到,算法如下。
【算法】

void createLinkList(LinkList* L)
{
	int n = 1;
	STD x;
	char yn;
	initLinkList1(L); //调用初始化函数,创建空表
	do
	{
		printf("请输入第%d个学生的姓名和分数,用空格隔开:", n);
		scanf("%s%f", x.name, &x.score);
		getchar();//空读回车
		insertLinkList(*L, n++, x);//调用插入函数,将新节点插入在尾部
		printf("继续请输入吗?Y/N:");
		scanf("%c", &yn);
	} while (yn == 'Y' || yn == 'y');
}

(2)头插法:将新结点插入到头结点之后和原来的第一个结点之前。
分析:为了使新结点是第一个结点,必须使用新结点的指针域记原来的第1个结点,头结点的指针域记新结点。
【头插法算法如下】

int frontCreateLinkList(LinkList* L)
{
	STD x;
	LinkList p;
	char yn;
	int n = 0;

	initLinkList(L);//创建空表
	do
	{
		printf("请输入第%d个学生的姓名和分数,用空格隔开:", ++n);
		scanf("%s%f", x.name, , &x.score);
		getchar();//空读回车
		if ((p = (LinkList)malloc(sizeof(LNode))) == NULL)
		{
			perror("frontCreateLinkList::");
			return 0;
		}
		//将新结点p插入到头结点之后和原来的第一个结点之前
		p->next = (*L)->next;
		(*L)->next = p;
		printf("继续输入吗?Y/N:");
		scanf("%c", &yn);
	} while (yn == 'y' || yn == 'Y');
	return 1;
}

(3)尾插法:将新结点插入到原来的尾结点之后。
分析:原来的单向链表只有头指针。现在进行尾插,必须已知尾结点。因此尾插算法需要一个工作指针记住当前的尾结点(称尾指针)。用原结点的指针域记新插入的结点,尾指针记新的尾结点,使新结点为新的尾结点。
【尾插法算法如下】

int rearCreateLinkList(LinkList* L)
{
	STD x;
	LinkList p, R;
	char yn;
	int n = 0;

	//创建空表
	if ((*L = (LinkList)malloc(sizeof(LNode))) == NULL)
	{
		perror("rearCreateLinkList::");
		return 0;
	}
	(*L)->next = NULL;
	R = *L;//R是尾指针
	do
	{
		printf("请输入第%d个学生的姓名和分数,用空格隔开:", ++n);
		scanf("%s%f", x.name, &x.score);
		getchar();//空读回车

		//创建新结点
		if ((p = (LinkList)malloc(sizeof(LNode))) == NULL)
		{
			perror("rearCreateLinkList::");
			return 0;
		}
		p->data = x;
		p->next = NULL;

		//将新结点p插入到原来的尾结点之后,R记录新的尾结点p
		R->next = p;
		R = p;

		printf("继续输入吗?Y/N:");
		scanf("%c", &yn);
	} while (yn == 'y' || yn == 'Y');
	return 1;
}

创建链表的三种算法的比较如下:由于调用函数需要花费系统开销,因此多次调用插入函数insertLinkList()创建链表的效率较低;头插法创建链表的数据元素顺序与输入数据元素的顺序相反;尾插法创建链表与输入数据元素的顺序相同。

2.9.基于建表算法的就地逆置操作

所谓的就地逆置指的是,原来的一组数据已经存放在一个带头结点的单向链表中,现在将这组数据逆序存放,结点的存储空间数原来的,只是改变了结点的指向。这相当于重新做一次创建链表的头插法。
分析:首先将原链表置成空表,再将原链表的每个数据结点依次做头插即可。需要注意的是要用两个辅助指针变量p和q协助完成,其中p记每次待插入的第一个结点,q记p的直接后继结点,直至所有结点插入完成。
【算法实现如下】

void inverLink(LinkList L)
{
	LinkList p, q;
	if (L->next == NULL)
	{
		printf("表空!\n");
		return;
	}
	p = L->next;//p记链表的第一个结点
	L->next = NULL;//置L为空链表
	while (p != NULL)
	{
		q = p->next;//q记p的直接后继结点

		//对*p做头插
		p->next = L->next;
		L->next = p;

		//p记下一个待插入的结点
		p = q;
	}
}

对已知链表进行排序,与就地逆置的算法思想相似,只不过对每一个待排序的结点不是做头插,而是根据排序的要求先找插入位置,重新作用做一次插入的创建链表。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杰深入学习计算机

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值