一文读懂简单的数据结构——链表

专栏目录(数据结构与算法解析):https://blog.csdn.net/qq_40344524/article/details/107785323

链表

链表是一种物理存储单元上非连续、非顺序的存储结构,链表中的数据随机的分布在内存中的各个位置,没有特定限制,这种存储结构又被称为线性表的链式存储。

单链表

单链表又称为单向列表,该链表的访问顺序是单向的,由于链表中各元素是分散存储的,所以为了能够体现出元素之间的逻辑关系,在单链表中每个数据元素在存储的同时,要配备一个指针,这些配备有指针的结构我们会统称为结点,在单链表中每个结点都会包含一个数据域和一个指针域,数据域用于存储各结点的元素数据,指针域用于指向它的直接后继结点,即每一个数据元素都指向下一个数据元素(最后一个指向NULL)。

双链表

双链表即双向链表,它的特性与单链表相同,区别在于在双链表中各个结点中都会包含两个指针域,分别指向当前结点的前驱结点和后继节点,双向链表的访问要比单链表的访问更灵活一些,它既可以向后访问下个结点,也可以向前访问上一个结点。

接下来我会分别对单链表和双链表从结点构成,创建,遍历,操作等方面进行详细的说明:

单链表

结点的构成

之前有介绍到每个结点都包含数据域和指针域两部分,多个这样的结点串联起来就构成了一个单链表,

图:结点的构成

图:结点的构成

图:含有5个结点的链表

图:含有5个结点的链表

由于链表中存放的不是一个基本数据类型,所以通常需要一个结构体来对它进行描述

typedef struct Node{
    int data;	//代表数据域
    struct Node * next;	//代表指针域,指向直接后继结点
}node;
头结点

有时候可以在单链表的第一个结点之前附设一个结点,它没有直接前驱,称之为头结点,头结点的数据域可以不存储任何信息,头结点的指针域存储指向第一个结点的指针,它的作用是使所有链表(包括空表)的头指针非空,并使对单链表的插入、删除操作不需要区分是否为空表或是否在第一个位置进行,从而与其他位置的插入、删除操作一致。可以有效的降低链表操作的复杂性。

头指针

链表中第一个结点的存储位置叫做头指针,整个链表的存取必须是从头指针开始进行,在链表中可以没有头结点,但是必须有头指针。

单链表的相关定义介绍到这里,接下来开始结合C语言代码对链表的操作进行说明。

1、 链表的创建
node * init()	//创建链表(1,2,3,4)
{
	node * p = (node*)malloc(sizeof(node));//创建一个头结点,此时已经创建了一个空链表,之后别刻意通过头插法或尾插法向链表中插入结点了

	
	node * temp = p;//声明一个指针指向头结点,用于遍历链表
	//头插法 生成链表
	for (int i = 1; i<5; i++) {
		node *a = (node*)malloc(sizeof(node));
		a->data = i;
		a->next = temp->next;
		temp->next = a;
	}
	/*尾插法生成链表
	for (int i = 1; i<5; i++) {
		node *a = (node*)malloc(sizeof(node));
		a->data = i;
		a->next = NULL;
		temp->next = a;
		temp = temp->next;
	}
	*/
	return p;
}
2、 插入结点
/*链表中插入结点的方法可以总结为两步:
  1、将新结点的next指针指向插入位置后的结点;
  2、将插入位置前的结点的next指针指向插入结点;
*/
node * insertData(node * p, int elem, int add)	//插入结点 (p——原始链表,elem——要插入的值,add——插入位置)
{
	node * temp = p;//创建临时结点temp
	//首先找到要插入位置的上一个结点
	for (int i = 1; i<add; i++) {
		if (temp == NULL) {
			printf("插入位置无效\n");
			return p;
		}
		temp = temp->next;
	}
	//创建插入结点c
	node * c = (node*)malloc(sizeof(node));
	c->data = elem;
	//向链表中插入结点
	c->next = temp->next;
	temp->next = c;
	return  p;
}
3、 遍历链表
void traversingList(node * L)	//遍历链表
{
	node * temp = L;
	temp = temp->next;
	while (temp)
	{
		printf("%d", temp->data);
		if (temp->next)
			printf(" ");
		temp = temp->next;
	}
}
4、 查找链表中的某个结点
/*链表通常只能通过头结点或者头指针进行访问,所以实现查找某结点最常用的方法就是对链表中的结点进行逐个遍历*/
int selectElem(node * p, int elem)	//查找链表中的某个结点
{
	node * t = p;
	int i = 1;
	while (t->next) {
		t = t->next;
		if (t->data == elem) {
			return i;
		}
		i++;
	}
	return -1;
}
5、 修改链表中结点的数据
/*在链表中修改结点的数据域时,需要通过遍历的方法找到该结点,然后直接更改数据域的值*/
node *updataElem(node * p, int add, int newElem)	//修改数据,add——修改结点在链表中的位置,newElem——新的数据域的值
{
	node * temp = p;
	temp = temp->next;//在遍历之前,temp指向首元结点
	//遍历到被删除结点
	for (int i = 1; i<add; i++) {
		temp = temp->next;
	}
	temp->data = newElem;
	return p;
}
6、 删除结点

node * delData(node * p, int add)	//删除结点
{
	node * temp = p;
	//temp指向被删除结点的上一个结点
	for (int i = 1; i<add; i++) {
		temp = temp->next;
	}
	node * del = temp->next;//单独设置一个指针指向被删除结点,以防丢失
	temp->next = temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域
	free(del);//手动释放该结点,防止内存泄漏
	return p;
}
总结

通过以上代码可以看出在对链表进行查找和修改操作时资源占用明显比之前一节讲到的数组要多,而在插入和删除操作中链表却有明显的优势,另外由于链表存储空间不连续,在没有足够大的连续的内存空间时使用链表存储可解决很多问题,同时对空间大的利用率也会大大提高。

双链表

结点的构成

双链表中每个结点中所包含内容比之单链表要多一部分(前驱指针),这可以为双链表提供双向访问的能力。

双链表链表结构

图:双链表链表结构

双链表的结点结构通常用如下结构体表示:

typedef struct Node{
	struct Node * pro;	//代表指针域,指向直接前驱结点
	int data;	//代表数据域
	struct Node * next;	//代表指针域,指向直接后继结点
}node;

通过以上图片和结点结构体的展示可以直观的了解到双链表和单链表的差别,双链表中同样有头结点和头指针,这里就不进行赘述了,由于双链表比之单链表多了反向操作的功能,因此操作复杂性有所提升,接下来继续通过代码对双链表的操作进行详细说明,

1、 双链表的创建
/*
  创建双向链表的过程中,每一个新节点都要和前驱节点之间建立两次链接,分别是:
  将新节点的 pro 指针指向直接前驱节点;
  将直接前驱节点的 next 指针指向新节点;
*/
Node* initLine(node * head) {
	int i = 0;
	node * list = NULL;
	//创建一个首元节点,链表的头指针为head
	head = (node*)malloc(sizeof(node));
	//对节点进行初始化
	head->pro = NULL;
	head->next = NULL;
	head->data = 1;
	//声明一个指向首元节点的指针,方便后期向链表中添加新创建的节点
	list = head;
	for (i = 2; i <= 5; i++) {
		//创建新的节点并初始化
		node * body = (node*)malloc(sizeof(node));
		body->pro = NULL;
		body->next = NULL;
		body->data = i;
		//新节点与链表最后一个节点建立关系
		list->next = body;
		body->pro = list;
		//list永远指向链表中最后一个节点
		list = list->next;
	}
	//返回新创建的链表
	return head;
}
2、 插入结点
/*
  插入结点简单来说就是重置结点的前驱和后置指针指向的位置,可根据插入位置的不同分为三种情况
  1、添加至链表头:(1)将新结点后继指向头结点,头结点前驱指向新结点,(2)将头结点重新移动到新结点
  2、添加到中部:(1)新结点先与其直接后继结点建立双向逻辑关系,(2)新结点的直接前驱结点与之建立双向逻辑关系
  3、添加到尾部:(1)找到双链表中最后一个结点,(2)让新节点与最后一个结点建立双向逻辑关系
*/
node * insertLine(node * head, int data, int add)	//data 为要添加的新数据,add 为添加到链表中的位置
{
	//新建数据域为data的结点
	node * temp = (node*)malloc(sizeof(node));
	temp->data = data;
	temp->pro = NULL;
	temp->next = NULL;
	//插入到链表头,要特殊考虑
	if (add == 1) {
		temp->next = head;
		head->pro = temp;
		head = temp;
	}
	else {
		int i = 0;
		node * body = head;
		//找到要插入位置的前一个结点
		for (i = 1; i < add - 1; i++) {
			body = body->next;
			if (body == NULL) {
				printf("插入位置有误\n");
				break;
			}
		}
		if (body) {
			//判断条件为真,说明插入位置为链表尾
			if (body->next == NULL) {
				body->next = temp;
				temp->pro = body;
			}
			else {
				body->next->pro = temp;
				temp->next = body->next;
				body->next = temp;
				temp->pro = body;
			}
		}
	}
	return head;
}
3、 正向遍历双链表
/*正序遍历是从头结点开始依次向后指向后继结点,并读取响应结点的数据域值*/
void ergodic(node *pH)//正序遍历,pH——头结点
{
	int cnt = 0;
	node *p = pH;
	while (NULL != p->next)
	{
		cnt++;
		p = p->next;
		printf("第%d节点数据为为%d\n", cnt, p->data);
	}
}

4、 逆向遍历双链表
/*逆序遍历*/
void re_ergodic(node *pT)//逆序遍历,pT——尾结点
{
	int cnt = 0;
	node *p = pT;
	while (NULL != p->pro)
	{
		cnt++;
		printf("第%d节点数据为为%d\n", cnt, p->data);
		p = p->pro;
	}
}
5、 查找链表中的某个结点
/*双链表查找指定元素的实现同单链表类似,都是从表头依次遍历表中元素*/
int selectElem(node * head, int data)	//head为原双链表,data表示被查找元素
{
	//新建一个指针t,初始化为头指针 head
	node * temp = head;
	int i = 1;
	while (temp) {
		if (temp->data == data) {
			return i;
		}
		i++;
		temp = temp->next;
	}
	//程序执行至此处,表示查找失败
	return -1;
}
6、 修改链表中结点的数据
/*修改双链表中指定结点数据域是通过遍历找到存储有该数据元素的结点,直接更改其数据域*/
node *updataElem(node * p, int add, int newElem)		//更新函数,add——更改结点在双链表中的位置,newElem——新数据
{
	int i = 0;
	node * temp = p;
	//遍历到被删除结点
	for (i = 1; i < add; i++) {
		temp = temp->next;
		if (temp == NULL) {
			printf("更改位置有误!\n");
			break;
		}
	}
	if (temp) {
		temp->data = newElem;
	}
	return p;
}
7、 删除结点
/*双链表删除结点时,只需遍历链表找到要删除的结点,然后将该节点从表中移除即可*/
node * delLine(node * head, int data) // 删除结点的函数,data为要删除结点的数据域的值
{
	node * temp = head;
	//遍历链表
	while (temp) {
		//判断当前结点中数据域和data是否相等,若相等,摘除该结点
		if (temp->data == data) {
			temp->pro->next = temp->next;
			temp->next->pro = temp->pro;
			free(temp);
			return head;
		}
		temp = temp->next;
	}
	printf("链表中无该数据元素\n");
	return head;
}
总结

通过以上学习我们不难看出双链表和单链表在逻辑上基本没有区别。二者主要的区别是结构有所不同,由于双链表有两个指针域,在插入和删除结点是难免会比单链表要稍显复杂,但是也正是由于双链表比单链表多了一个前驱结点,使双链表在查找和遍历等方面要更灵活一些。

优点

链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;

缺点

因为含有大量的指针域,占用空间较大;
查找元素需要遍历链表来查找,非常耗时。

适用场景

数据量较小,需要频繁增加,删除操作的场景


链表就讨论到这里,下一节详解另一个很有意思也很重要的数据结构——栈,它是一种特殊的线性表。

以上是我的一些粗浅的见解,有表述不当的地方欢迎指正,谢谢!


表述能力有限,部分内容讲解的不到位,有需要可评论或私信,看到必回…

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

书山客

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

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

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

打赏作者

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

抵扣说明:

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

余额充值