数据结构之链表及其代码详解

1. 链表的概念及其结构

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。下图便是链表在内存中存储的样子。

 链表的特点:

  1. 链表是由一系列节点组成的,每个节点包含一个数据元素和一个指向下一个节点的指针。
  2. 链表中的节点可以在内存中不连续的位置上存储,因此插入和删除节点时不需要移动其他节点。
  3. 相比于数组,链表在插入和删除操作上更高效,因为只需要修改节点的指针,而不需要移动其他元素。
  4. 链表内部的元素可以动态分配内存,可以根据需要灵活地进行插入和删除操作。
  5. 链表的访问效率较低,因为要访问特定位置的元素需要从头开始遍历链表,时间复杂度为O(n)。
  6. 链表可以用于实现其他数据结构,如栈和队列,以及高级数据结构,如图和树。
  7. 链表可以是单向链表,即每个节点只有一个指向下一个节点的指针;也可以是双向链表,每个节点既有一个指向下一个节点的指针,又有一个指向前一个节点的指针。
  8. 链表的长度可以动态变化,可以根据需要增加或减少节点。
  9. 链表不需要预先指定大小,可以根据需要灵活地分配内存。

2. 链表的分类

1. 单向链表或双向链表

2. 带哨兵位头解点和不带哨兵位头解点

3.循环链表和非循环链表

PS:虽然有这么多类型,但是很多时候我们会把他们结合在一起使用,比如双向带头循环链表(这是我个人比较喜欢的一种类型),因为使用方便且可以减少很多错误。

3. 链表的优势

1.灵活的内存使用:适合数据量大小不固定的情况。

2. 高效的局部操作:如在已知位置附近进行插入和删除。

4. 链表的劣势

1.随机访问效率低:不能像数组那样通过索引直接访问特定位置的元素。

2.额外的内存开销:每个节点需要存储指针,增加了空间复杂度。

5. 链表的实现

我们以链表中较为复杂的双向带头循环链表来讲解链表,以下是我们在链表中常常使用的一些函数:

1. 初始化链表,返回头节点指针

BuyListNode这个函数我们并不会在text文件里面使用,我们之所以写他是因为他可以使我们更方便,更快速的写好其他代码,同时提高代码的可读性。

BuyListNode这个函数的主要目的是创建一个新的 LTNode 类型的节点,并进行必要的初始化操作。如果内存分配失败,会打印错误信息并终止程序。最后函数返回新创建的节点的指针。在LTInit中可以理解为用于给phead也就是头节点初始化。

LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		//return NULL;
		exit(-1);
	}
	node->next = NULL;
	node->prev = NULL;
	node->data = x;

	return node;
}

这段代码的作用通常是初始化一个双向带头循环链表的头节点。通过将头节点的 next 和 prev 指针都指向自身,形成一个只有头节点的环形结构。

LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

2. 销毁链表

首先使用 assert 宏检查传入的头节点指针 phead 是否为空。如果为空,程序会在调试时终止并报错。然后获取头节点的下一个节点,并将其指针存储在 cur 中,准备从第一个非头节点开始释放内存,通过一个 while 循环,保存当前节点的下一个节点指针到 next 中。释放当前节点 cur 所占用的内存。将 cur 更新为 next ,以便在下一次循环处理下一个节点。循环结束后,释放头节点 phead 所占用的内存。最后将头节点指针 phead 设置为 NULL,以避免出现空指针,防止后续代码误使用已释放的内存。总的来说,LTDestroy 函数通过遍历链表并依次释放每个节点的内存,实现了对整个链表的安全销毁,同时进行了必要的指针处理和错误检查,以保证程序的正确性和稳定性。

void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

3. 打印链表

函数开始用 assert 确保传入的头节点指针 phead 有效。接着打印 <=head=> 表示开头。然后获取头节点的下一个节点给 cur ,只要 cur 不是头节点,就打印其数据并跟 <=> ,再更新 cur 指向下一节点。循环结束后打印 NULL 表示链表结束并换行。整个函数实现了对链表内容的有序打印。

void LTPrint(LTNode* phead)
{
	assert(phead);
	printf("<=head=>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("NULL");
	printf("\n");
}

4.判断链表是否为空

首先通过 assert 检查头节点指针的有效性,然后根据链表为空的特定条件(头节点的下一个节点是自身)返回相应的布尔值(即true或者false)。

bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

5.在链表尾部插入节点

首先创建一个带有数据 x 的新节点 newnode 。接着找到链表的尾节点 tail (即头节点 phead 的前一个节点)。然后,依次更新尾节点、新节点和头节点的指针:让尾节点的 next 指向新节点,新节点的 prev 指向尾节点,新节点的 next 指向头节点,头节点的 prev 指向新节点。这样,新节点就成功添加到了链表的尾部,维持了链表双向链接的结构。

void LTPushBack(LTNode* phead, LTDataType x)
{
	LTNode* newnode = BuyListNode(x);
	LTNode* tail = phead->prev;
	tail->next = newnode;
	newnode -> prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

6. 删除链表尾部节点

首先,通过 assert 断言确保头节点指针 phead 非空且链表不为空。然后,找到尾节点 tail 及其前一个节点 tailprev 。接着,修改 tailprev 的 next 指针使其指向头节点,更新头节点的 prev 指针使其指向 tailprev ,从而将尾节点从链表中移除。最后,释放尾节点的内存,并将尾节点指针置为 NULL ,防止出现空指针。

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;
	tailprev->next = phead;
	phead->prev = tailprev;
	free(tail);
	tail = NULL;
}

7. 在链表头部插入节点

函数先通过 assert 确保头节点指针 phead 非空。接着创建一个新节点 newnode 并初始化数据。然后获取头节点的下一个节点 first 。之后,让新节点的 next 指向 first ,first 的 prev 指向新节点,头节点的 next 指向新节点,新节点的 prev 指向头节点。这样就成功把新节点插入到了链表头部,保持了链表的双向链接关系。

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode= BuyListNode(x);
	LTNode* first = phead->next;
	newnode->next = first ;
	first ->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

8. 删除链表头部节点

首先,通过两个 assert 断言确保头节点指针 phead 非空且链表不为空。然后定义指针 second 指向链表的第二个节点。接着修改 second 的 prev 指针指向头节点,头节点的 next 指针指向 second ,从而将首节点从链表的链接关系中剔除。之后定义指针 first 并使其指向首节点。最后使用 free 释放 first 所指首节点的内存。

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	second=phead->next->next;
    second->prev=phead;
    phead->next=second;
    first=phead->next;
    free(first);
}

9. 在指定位置插入节点

首先,通过断言确保 pos 不为空。接着获取 pos 前一个节点 prev ,创建新节点 newnode 并初始化。然后,让 prev 的 next 指向新节点,新节点的 prev 指向 prev ,新节点的 next 指向 pos ,pos 的 prev 指向新节点。这样就完成了在指定位置前插入新节点的操作,保证了链表结构的正确。

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode*newnode= BuyListNode(x);
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

10. 删除指定位置的节点

首先,使用 assert 断言来确保传入的节点指针 pos 非空。这是为了防止在后续操作中因空指针而导致错误。然后,获取指定节点 pos 的前一个节点 p 和后一个节点 n 。接下来,通过修改指针的指向来完成节点的删除操作。将前一个节点 p 的 next 指针指向后一个节点 n ,后一个节点 n 的 prev 指针指向前一个节点 p ,这样就将指定节点从链表的链接关系中移除。最后,使用 free 函数释放指定节点所占用的内存,并将 pos 指针置为 NULL ,以避免出现悬空指针的问题。

void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* p = pos->prev;
	LTNode* n = pos->next;
	p->next = n;
	n->prev = p;
	free(pos);
	pos = NULL;
}

11.查找指定x的位置

首先,通过 assert 断言确保传入的头节点指针 phead 不为空。然后,从链表的第一个有效节点(phead->next)开始,将其指针存储在 cur 中。通过一个 while 循环遍历链表,只要当前节点 cur 不等于头节点 phead ,就继续查找。在循环内部,如果当前节点 cur 的数据等于要查找的指定数据 x ,则返回当前节点的指针,表示找到了匹配的节点。如果遍历完整个链表都没有找到匹配的数据,循环结束后返回 NULL ,表示未找到。

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

总结:

链表是一种重要的数据结构,由一系列节点组成。每个节点包含数据域和指针域,通过指针相互连接。链表的优势在于能灵活地动态分配内存,可方便地进行节点的插入和删除操作,尤其在中间位置时,只需修改相关节点的指针,无需大规模的数据移动。然而,链表也有不足,如随机访问元素的效率较低,因为必须从头或尾依次遍历才能找到指定节点,且每个节点的指针会带来额外的空间开销。它常用于数据量不固定、频繁增删元素的场景。

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

啊吧怪不啊吧

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

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

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

打赏作者

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

抵扣说明:

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

余额充值