以后咱也是有头有脸的链表了

目录

链表简介

链表的结构定义

要实现的接口函数

哨兵位节点创建

链表打印

链表尾插

链表尾删

链表头插

链表头删

链表查找

链表在pos位置前插入

链表pos位置删除

链表销毁(释放空间)


链表简介

链表(Linked List)是一种常见的数据结构,用于存储和组织数据。与数组不同,链表中的元素在内存中可以不连续存储,而是通过节点之间的指针链接起来

链表由一个个节点组成,每个节点包含两部分:数据部分和指针部分。数据部分用于存储实际的数据,而指针部分则用于指向下一个节点的地址

链表的好处包括:

1. 动态性:链表的长度可以根据需要动态增长或缩小,不像数组那样需要提前定义大小。这使得链表更加灵活,适用于处理未知数量的数据。

2. 插入和删除效率高:由于链表中的节点是通过指针相互连接的,因此在插入和删除节点时只需要修改指针,而不需要移动其他元素。这使得链表在插入和删除操作上具有较高的效率。

3. 空间利用效率高:链表以节点为单位存储数据,不需要像数组那样预先分配固定大小的连续内存空间。这意味着链表可以更好地利用可用的内存空间,避免了内存浪费

4. 灵活性:链表可以轻松地进行节点的添加、删除和移动操作,无需进行大规模的数据搬迁。这在某些特定的应用场景中非常有用,例如实现栈、队列等数据结构。

链表的结构非常多样,以下情况组合起来有8种链表结构:

1.单向或者双向

单向的链表通过每个节点存储的指针部分来进行链接,由于每个节点只存储了下一个节点的地址,因此在实现尾插的时候非常不方便,需要先找到尾节点(非循环链表尾节点指向空),这时候就需要遍历一次链表。双向的链表则是可以分别找到上一个节点和下一个节点。但注意头尾并没有相连,循环链表才会头尾相连。

2.带头或者不带头(指哨兵位)

head这个哨兵位不存储有效数据,只是用来存储下一个节点的地址,在一些oj题中我们手动创建一个哨兵位有时候能省去一些判断空指针的麻烦。

3.循环或者非循环

循环的话这个链表就成一个环状了,没有任何一个节点是指向空的。环状链表在oj题中的考察也非常经典,值得一做。

本次讲解带头双向循环链表。它看似复杂,其实非常简单,这种链表在实践中使用最多,因为它的结构能带来很多优势,往下看就能明白了。

链表的结构定义

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// 接口实现涉及的头文件

typedef int LTDataType; // 方便更改存储数据类型

typedef struct ListNode 
{
	LTDataType val;
	struct ListNode* prev; // prev用来存储上一个节点的地址
	struct ListNode* next; // next用来存储下一个节点的地址
}ListNode;

要实现的接口函数

// 创建返回链表的哨兵位节点
ListNode* ListCreate();
// 链表打印
void ListPrint(ListNode* pHead);
// 链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 链表尾删
void ListPopBack(ListNode* pHead);
// 链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 链表头删
void ListPopFront(ListNode* pHead);
// 链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 链表删除pos位置的节点
void ListErase(ListNode* pos);
// 链表销毁
void ListDestory(ListNode* pHead);

哨兵位节点创建

ListNode* ListCreate()
{
	ListNode* pHead = (ListNode*)malloc(sizeof(ListNode));
	if (pHead == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	pHead->next = pHead;
	pHead->prev = pHead;
	pHead->val = -1;   // 这里随便塞了个值,可以不塞,哨兵位不存储有效数据

	return pHead;
}

这里就创建了一个哨兵位,因为此时只有一个节点,因此让它的prev和next都指向自己即可

链表打印

void ListPrint(ListNode* pHead)
{
	assert(pHead);      // 链表无论如何不会为空,因为前面创建了哨兵位节点 

	printf("哨兵位<=>"); // 视觉上模拟双向箭头,更直观的展示
	ListNode* cur = pHead->next;
	while (cur != pHead) // 让cur从第一个有效节点开始,遇到哨兵位结束 
	{
		printf("%d<=>", cur->val);
		cur = cur->next;
	}
	printf("哨兵位\n");  // 视觉上模拟循环
}

 由于下面的函数经常要涉及新节点的创建,所以写一个用来创建节点的函数,方便后面cv编程

ListNode* CreateNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->val = x;      // x是要添加的数据
	newnode->next = NULL;  // 先置空后面看具体情况来操作
	newnode->prev = NULL;  // 先置空后面看具体情况来操作

	return newnode;
}

链表尾插

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* newnode = CreateNode(x);
    // 下面四步看着有点绕,画画图就理解了
	pHead->prev->next = newnode;
	newnode->prev = pHead->prev;
	newnode->next = pHead;
	pHead->prev = newnode;
}

 这里就能发现尾插变得很简单,单向链表尾插还需要找尾,每尾插一次时间复杂度O(N)。而双向带头循环链表用哨兵位就能找到尾节点。并且单向链表尾插的时候还要判断首节点为不为空,这就需要传入头节点指针的地址,才能在头节点为空时改变头节点,而带哨兵位就没有这个烦恼。

链表尾删

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next != pHead); 
// 没节点还删个毛,用assert暴力检查,敢传只有哨兵位的节点就敢报错
    
    
// 老三步,tmp这个变量用来保存要删除的节点地址,因为后面链表的指向会变
	ListNode* tmp = pHead->prev;
	pHead->prev = pHead->prev->prev;
	pHead->prev->next = pHead;
	free(tmp);
// 释放空间后tmp可以置空也可以不置空,这里tmp是局部变量,函数结束就销毁了
}

链表头插

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* newnode = CreateNode(x);

    // 是不是很眼熟,插入基本都是四步走,只有略微改变
	newnode->next = pHead->next;
	newnode->prev = pHead;
	newnode->next->prev = newnode;
	pHead->next = newnode;
}

链表头删

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next != pHead); // 暴力检查

    // 老三样
	ListNode* tmp = pHead->next;
	pHead->next = pHead->next->next;
	pHead->next->prev = pHead;
	free(tmp);
}

链表查找

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->val == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

 这里返回的是要查找的数据所在的节点的地址,可以配合后面这两个函数来使用

链表在pos位置前插入

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* newnode = CreateNode(x);
	newnode->prev = pos->prev;
	newnode->next = pos;
	newnode->prev->next = newnode;
	pos->prev = newnode;
}

这里的pos指的是链表某个节点的地址

链表pos位置删除

void ListErase(ListNode* pos)
{
	assert(pos);

	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
}

链表销毁(释放空间)

void ListDestory(ListNode* pHead)
{
	assert(pHead);

	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		ListNode* tmp = cur->next;
		free(cur);
		cur = tmp;
	}
    // 前面循环cur不会等于哨兵位节点,所以手动释放一下
	free(pHead);
}

动态开辟的空间切记要释放,以免造成内存泄漏 

看到这再来对比一下单向链表的接口实现,是不是觉得带头双向循环链表很容易了呢

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值