3.数据结构与算法——链表

知识回顾:
顺序表的特点:以物理位置相邻表达逻辑关系。
线性表的优点:数据元素随机存取。
线性表的缺点:插入、删除操作需要移动大量元素;存储空间不灵活。

1.线性表的链式存储结构

1.1 链式存储结构的介绍

链式存储结构(又称非顺序映像或链式映像):结点在存储器中的顺序是任意的,即可以连续,也可以不连续,甚至是零散分布在内存的任意位置。(链表中元素的逻辑次序与物理次序不一定相同)
那么问题来了,既然结点的存储是任意的,那我们应该如何确定节点的顺序呢?我们可以在每个节点上开辟一块空间来存储下一个节点的地址,这样就把整个链表串起来了。
例1:线性表:(赵,钱,孙,李,周,吴,郑,王)
在这里插入图片描述
例2:26个小写字母的表的链式存储结构
在这里插入图片描述
通过以上两例我们不难发现,每个结点都是由数据域(存储元素数值数据)和指针域(存储后继结点的存储位置)组成的。

1.2 链式存储结构的相关术语

1.结点:数据元素的存储映像。由数据域和指针域两部分组成;
2.链表:n个结点由指针链组成一个链表。(它是线性表的链式存储映像,称为线性表的链式存储结构);
在这里插入图片描述
3.单链表、双链表和循环链表:
①单链表:结点只有一个指针域的链表;
②双向链表:在单链表的每个结点中,再设置一个指向其前驱结点的指针域;
③循环链表:尾结点的指针域指向头结点的链表。
在这里插入图片描述
4.头指针、头指针和首元结点
头指针:指向链表第一个结点的指针;
首元结点:链表存储元素的第一个结点;
头结点:在头结点前附设的一个节点。
在这里插入图片描述
带头结点与不带头结点的链表存储结构示意图:
在这里插入图片描述
讨论:
1.如何表示空表?
①不带头结点:头指针为空时表示空表;
②带头结点:头结点的指针域为空时表示空表。
2.在链表中设置头结点的好处?
①便于首元结点的处理:首元结点的地址存储在头结点之中,所以在链表上的第一个位置的操作与其他位置一致,无需进行特殊处理;
②便于空表和非空表的处理:无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就变得统一了。
3.头结点的数据域内可以装什么?
可以为空,也可以存放链表的长度等附加信息,但此结点不能计入链表长度值。

1.3 链式存储结构的特点

1.结点在存储器中的位置是随意的;
2.访问时只能通过头结点进入链表,并通过每个结点的指针域依次扫描其他结点。(称为顺序存取,顺序表为随机存取)

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

在这里插入图片描述
定义链表结构体:

typedef struct LNode   //声明节点的类型和指向结点的指针类型
{
	ElemType data;   //结点的数据域
	LNode* next;   //结点的指针域
}LNode,*LinkList;   //LinkList为指向结点的指针类型,后续使用LNode创建结点,用LinkList创建链表,这样做可以避免产生不必要的混淆,使得结点与链表分明

int main()
{
	LinkList L;  //创建链表L
	LNode *p;  //定义结点指针p
	p = (LNode*)malloc(sizeof(LNode));   //分配空间
	assert(p != NULL);
	return 0;
}

举例:存储学生学号、姓名、成绩的单链表结点类型的定义
在这里插入图片描述

2.1 单链表的初始化(带头结点)

在这里插入图片描述

Status InitList_L(LinkList& L)
{
	L = (LNode*)malloc(sizeof(LNode));   //分配空间
	assert(L != NULL);
	L->next = NULL;
	return OK;
}

2.2 判断链表是否为空

在这里插入图片描述

int EmptyList_L(LinkList L)
{
	if (L->next)
		return 0;  //不为空,返回0
	else
		return 1;  //为空,返回1
}

2.3 销毁单链表

在这里插入图片描述

Status DestroyList_L(LinkList& L)
{
	LNode* p ;
	while (L)
	{
		p = L;
		L = L->next;
		free(p);
	}
	return OK;
}

2.3 清空单链表

在这里插入图片描述

Status ClearList_L(LinkList& L)
{
	LNode* p, * q;
	p = L->next;
	while (p)   //while(q)也可
	{
		q = p->next;
		free(p);
		p = q;
	}
	L->next = NULL;
	return OK;
}

2.4 求链表的表长

算法思想:从首元结点开始,依次计数所有结点。

int ListLength_L(LinkList L)
{
	LNode* p;
	p = L->next;
	int num = 0;
	while (p)
	{
		num++;
		p = p->next;
	}
	return num;
}

在这里插入图片描述

2.5 取值(取链表中第i个元素的内容)

在这里插入图片描述
算法思想:定义指针p,从头开始遍历到第i个位置,然后保存第i个位置的数据域到e即可。
算法分析:提起遍历,我们肯定就要使用循环。那么应该使用for循环还是使用while 循环呢?那么就要根据问题具体分析了。
问题分析:
1.输入的位置非法。
当输入的 i<0 或 i>表长 时,就会出现问题。
解决思路:
由于无法确定表长,因此 i>表长 的问题不好判断。仔细想想,当i大于表长时,p遍历到最后一个元素时继续遍历,p不就指向NULL了吗?因此,我们可以使用根据 p是否为空 来判断i是否大于表长。如果使用for循环,那么只需在for循环内部添加条件判断(p是否为空)即可。而对于 i<0 的问题,我们遍历链表时肯定会定义整型变量j,而循环的条件肯定是 j<i 。因此,循环的条件刚好可以解决 i<0 的问题。接下来,我们一起梳理一下算法步骤!
算法步骤:
在这里插入图片描述

Status GetElem_L(LinkList L, int i, ElemType& e)
{
	LNode* p;
	int j = 1;
	p=L->next;
	while (j < i && p)
	{
		j++;
		p = p->next;
	}
	if (!p || j > i)
		return ERROR;  //位置不合法
	e = p->data;
	return OK;
}

从这个算法我们可以看输出,顺序表的取值算法可比链表的取值算法简单的多。因此,顺序表是随机存取,而链表确实顺序存取。

2.6 按值查找

算法思想:通过遍历数组来比较每个结点的值是否与e相等。
算法步骤:
在这里插入图片描述

int LocateElem_L(LinkList L, ElemType e)
{
	LNode* p;
	p = L->next;
	int j = 1;
	while (p->data != e && p)
	{
		p = p->next;
		j++;
	}
	if (p)
		return j;   //找到返回元素位置
	else
		return 0;   //未找到返回0

}

2.7 插入算法

算法思想:定位到指针指向要插入的位置的前一个结点,然后插入即可。
算法步骤:
在这里插入图片描述

Status ListInsert_L(LinkList& L, int i, ElemType e)
{
	LNode* p, * s;
	p = L->next;
	int j = 1;
	while (j < i-1 && p)   //移动p,使其指向要插入结点的前一个结点
	{
		p = p->next;
		j++;
	}
	if (p || j > i-1)     //如果输入的位置不合法(i>表长 或 i<1),则返回ERROR
		return ERROR;
	s = (LNode*)malloc(sizeof(LNode));  //创建新节点并初始化
	assert(s != NULL);
	s->data = e;
	s->next = p->next;   //插入
	p->next = s;
	return OK;
}

2.8 删除算法

算法思想:
算法步骤:
在这里插入图片描述

Status ListDelete_L(LinkList& L, int i, ElemType& e)
{
	LNode* p, * q;
	p = L->next;
	int j = 1;
	while (j < i - 1 && p)   //移动p,使其指向要删除结点的前一个结点
	{
		p = p->next;
		j++;
	}
	if (p || j > i - 1)     //如果输入的位置不合法(i>表长 或 i<1),则返回ERROR
		return ERROR;
	q = p->next;
	p->next = q->next;
	e = q->data;
	free(q);
	return OK;
}

2.9 建立单链表

2.9.1 头插法

建立单链表,元素插入在头部。
在这里插入图片描述

void CreateList_H(LinkList& L, int n)
{
	LNode* p;
	L = (LinkList)malloc(sizeof(ElemType));
	assert(L != NULL);
	L->next = NULL;
	for (int i = n; i > 0; i--)
	{
		p = (LNode*)malloc(sizeof(ElemType));
		assert(p != NULL);
		scanf_s("%d", &(p->data));
		p->next = L->next;
		L->next = p;
	}
}

时间复杂度为O(n);

2.9.2 尾插法

建立单链表,元素插入在尾部。
在这里插入图片描述

void CreateList_R(LinkList& L, int n)
{

	L = (LinkList)malloc(sizeof(ElemType));
	assert(L != NULL);
	L->next = NULL;
	LNode* p, * r;     //创建指针p,r  p用来产生新节点,r指向链表尾部元素
	r = L;
	for (int i = 0; i < n; i++)
	{
		p = (LNode*)malloc(sizeof(ElemType));
		assert(p != NULL);
		scanf_s("%d", &(p->data));
		p->next = NULL;
		r->next = p;
		r = p;    //r指向表尾
	}
}

时间复杂度为O(n);

3. 循环链表

循环链表:是一种头尾相接的链表(表中最后一个节点的指针域指向头结点)
在这里插入图片描述
思考一下,我们在遍历单链表时的循环结束条件是指针指向NULL,那么使用循环链表遍历时的循环结束条件是什么?当然是遍历链表的指针是否与头指针相等啦!
注意:
在这里插入图片描述

3.1 带尾指针的循环链表的合并

在这里插入图片描述

LinkList Connect(LinkList &Ta, LinkList &Tb)
{
	LNode* p;
	p = Ta->next;    //p保存Ta的头结点地址
	Ta->next = Tb->next->next;    //Ta表尾连接Tb的首元结点
	free(Tb->next);    //释放Tb的头结点
	Tb->next = p;    //Tb的表尾结点连接Ta的头结点
	return Tb;    //返回Tb的地址
}

该操作的时间复杂度为O(1)。

4.双向链表

定义:每个结点中还有一个指针域指向其前驱结点的单链表。
双向链表可以解决普通的单链表无法找其前驱结点的问题。
在这里插入图片描述
双向链表的结构定义如下:

typedef struct DuNode
{
	ElemType data;
	DuNode* prior, * next;
}DuNode, * DuLinkList;

4.1 双向循环链表

在这里插入图片描述

4.2 双向链表的插入算法

在这里插入图片描述

int ListInsert_DuL(LinkList& L, int i, ElemType e)
{
	DuNode* p, * s;
	if (!(p=GetElem_DuL(L, e)))
		return ERROR;
	s = (DuNode*)malloc(sizeof(DuNode));
	s->data = e;
	p->prior->next = s;  //p结点的前一个结点的next域指向s
	s->prior = p->prior;  //s结点的prior域指向p结点的前一个结点
	s->next = p;  //s结点的next域指向p
	p->prior = s;  //p结点的prior域指向s
	return OK;
}

4.3 双向链表的删除算法

在这里插入图片描述

int ListInsert_DuL(LinkList& L, int i, ElemType e)
{
	DuNode* p, * s;
	if (!(p=GetElem_DuL(L, e)))
		return ERROR;
	p->prior->next = p->next;
	p->next->prior = p->prior;
	free(p);
	return OK;
}

5.效率比较

5.1 单链表、循环链表和 双向链表的时间效率比较

在这里插入图片描述
存取相同的数据,双向链表所占用的空间更大,但双向链表的操作所需的时间却更少。这是因为双向链表有两个指针域,所以会产生更多的空间开销。而花费的时间更少,是因为双向链表增加的指针域便利了操作,所以时间复杂度更小(以空间换时间)。

5.2 顺序表与链表的比较

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

浮沉丿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值