【C语言】实现双向链表


目录

(一)头文件

[(二) 功能实现](#(二) 功能实现)

(1)初始化

(2)打印链表

[(3) 头插与头删](#(3) 头插与头删)

(4)尾插与尾删

(5)指定位置之后插入

(6)删除指定位置的数据

(7)链表的销毁


正文开始:

在实际应用中,常用的双向链表是双向带头循环链表,本文考虑到实际应用,目的也是实现双向带头循环链表。

(一)头文件

命名"List.h"

本文不加解释的给出头文件,按照头文件来实现双向链表的基本功能:包括打印链表,链表的初始化,头插与头删,尾插与尾删,在指定位置之后插入数据,删除指定位置的数据,链表的销毁等共九个功能。

cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//链表的数据类型
typedef int LTtype;
//双向链表的节点
typedef struct ListNode
{
	LTtype data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

//双向链表带有哨兵位,插入数据之前先插入哨兵位
//初始化》传入头节点
void LTInit(LTNode** pphead);


//初始化2>返回头节点
LTNode* LtInit0();

//销毁链表
void LTDestory(LTNode** pphead);

//尾插
void LTtail_push(LTNode* phead,LTtype x);

//头插
//是在头节点之后插入,在头节点之前插入等价于尾插
void LThead_push(LTNode* phead,LTtype x);

//打印便于调试
void Print(LTNode* phead);

//头删
void LTpophead(LTNode* phead);

//尾删
void LTpoptail(LTNode* phead);

//查找
//为了找到pos位置
LTNode* LTfind(LTNode* phead, LTtype x);

//在pos之后插入数据
void LTinsert(LTNode* pos, LTtype x);

//删除pos处的数据
void LTerase(LTNode* pos);

(二) 功能实现

带头相对于不带头有诸多优势:

带头与不带头链表:

带头链表是指在链表的开头增加一个额外的节点,该节点不存储具体的数据,仅用于辅助操作链表。带头链表的带头节点可以简化链表的操作,提高代码的可读性和可维护性。

意义:

带头节点可以避免链表为空的特殊情况处理。在没有带头节点的链表中,如果链表为空,添加、删除等操作都需要额外的处理逻辑。而带头节点的链表中,带头节点一直存在,无论链表是否为空,操作逻辑都是一致的,可以减少代码的复杂性。

局限:

造成额外的空间浪费。

(1)初始化

初始化有两种方法,具体实现如下:

传址初始化

初始化传递链表的地址(二级指针【通过传址,将形参与实参建立联系】),初始化要插入头指针哨兵位(不保存有效的数据),便于简化代码逻辑。

cpp 复制代码
//双向链表带有哨兵位,插入数据之前先插入哨兵位
void LTInit(LTNode** pphead)
{
	*pphead = (LTNode*)malloc(sizeof(LTNode));
	if (*pphead == NULL)
	{
		perror("malloc");
		exit(1);
	}
	(*pphead)->data = -1;
	(*pphead)->next = (*pphead)->prev = *pphead;
}

接受返回值初始化

在函数内申请头节点,最后返回到主函数内:

cpp 复制代码
//初始化2>返回头节点
LTNode* LtInit0()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		perror("malloc");
		exit(1);
	}
	phead->data = -1;
	phead->next = phead->prev = phead;
	return phead;
}

(2)打印链表

以便于调试,理解代码;

与打印单链表一样,不同的是:while的跳出条件是 pcur != phead;

cpp 复制代码
//打印链表
void Print(LTNode* phead)
{
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

(3) 头插与头删

插入操作需要申请新节点:

cpp 复制代码
//获取新节点
LTNode* LTbuynewnode(LTtype x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = NULL;
	return newnode;
}

头插与头删

头插,是在头节点之后插入,在头节点之前插入等价于尾插;

先对新节点进行操作,因为新节点与原节点没有联系,不会因为对指针进行操作而丢失节点,造成数据丢失和内存泄漏;

我们可以直接得到phead,因此先让phead的下一个节点的前驱指针指向newnode,再改变phead的next指针;(如果反过来,先改变phead的next指向newnode,此时newnode后面的节点就找不到了。)

cpp 复制代码
//头插,是在头节点之后插入,在头节点之前插入等价于尾插
void LThead_push(LTNode* phead, LTtype x)
{
	assert(phead);

	LTNode* newnode = LTbuynewnode(x);

    //先对新节点进行操作
	newnode->next = phead->next;
	newnode->prev = phead;


	phead->next->prev = newnode;
	phead->next = newnode;

}

头删

断言传过来的指针不为空,链表不为空,通过assert实现;

通过创建del指针(要被删除的节点),保存旧头节点;

通过创建next指针,保存旧头节点的next节点;

这样命名后进行头删,逻辑清晰,不易出错;

cpp 复制代码
//头删
void LTpophead(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* del = phead->next;//被删除的节点
	LTNode* next = del->next;//del的后置节点

	phead->next = next;
	next->prev = phead;

	free(del);
	del = NULL;
}

(4)尾插与尾删

尾插

与头插的逻辑完全相同

cpp 复制代码
//尾插
void LTtail_push(LTNode* phead, LTtype x)
{
	assert(phead);

	LTNode* newnode = LTbuynewnode(x);

	newnode->next = phead;
	newnode->prev = phead->prev;

	phead->prev->next = newnode;
	phead->prev = newnode;
}

尾删

与头删的逻辑完全相同

cpp 复制代码
//尾删
void LTpoptail(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* del = phead->prev;//被删除的节点
	LTNode* prev = del->prev;//del的前置节点

	prev->next = phead;
	phead->prev = prev;

	free(del);
	del = NULL;
}

(5)指定位置之后插入

查找

查找数据值为x的节点,找到返回此节点,找不到返回NULL;

cpp 复制代码
//查找
LTNode* LTfind(LTNode* phead,LTtype x)
{
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

在指定位置之后插入数据

根据find找到的pos位置,申请新节点,插入到pos之后

cpp 复制代码
//在pos位置之后插入数据
void LTinsert(LTNode* pos,LTtype x)
{
	assert(pos);

	LTNode* newnode = LTbuynewnode(x);
	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;
}

(6)删除指定位置的数据

删除指定位置的数据,需要的指针有pos的前驱节点,pos节点。

cpp 复制代码
//删除pos处的数据
void LTerase(LTNode* pos)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* next = pos->next;

	prev->next = next;
	next->prev = prev;

	free(pos);
	pos = NULL;
}

(7)链表的销毁

链表的销毁共有两种方法:

传址销毁

cpp 复制代码
//销毁链表
void LTDestory(LTNode** pphead)
{
	assert(pphead);
	//哨兵位不为空
	assert(*pphead);

	LTNode* pcur = (*pphead)->next;

	while (pcur != *pphead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//释放哨兵位
	free(pphead);
	//此时可以改变哨兵位的
	pphead = NULL;

}

传值销毁

一级指针phead传递的是值,不是phead的地址,所以在函数中改变phead并不会改变phead的实际值
函数返回后,仍需要手动phead = NULL;

cpp 复制代码
//推荐使用一级,为了保持接口的一致性
void LTDestory0(LTNode* phead)
{
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;//无效的置空,当做是习惯
}
//一级指针phead传递的是值,不是phead的地址,所以在函数中改变phead并不会改变phead的实际值
//函数返回后,仍需要手动phead = NULL;

完~

未经作者同意禁止转载

相关推荐
陌小呆^O^13 分钟前
Cmakelist.txt之win-c-udp-client
c语言·开发语言·udp
daiyang123...1 小时前
测试岗位应该学什么
数据结构
kitesxian1 小时前
Leetcode448. 找到所有数组中消失的数字(HOT100)+Leetcode139. 单词拆分(HOT100)
数据结构·算法·leetcode
qystca3 小时前
洛谷 B3637 最长上升子序列 C语言 记忆化搜索->‘正序‘dp
c语言·开发语言·算法
薯条不要番茄酱3 小时前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea
盼海5 小时前
排序算法(五)--归并排序
数据结构·算法·排序算法
网易独家音乐人Mike Zhou8 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
‘’林花谢了春红‘’9 小时前
C++ list (链表)容器
c++·链表·list
搬砖的小码农_Sky11 小时前
C语言:数组
c语言·数据结构
先鱼鲨生13 小时前
数据结构——栈、队列
数据结构