数据结构4:链表(下)

文章目录

数据结构4:链表(下)

上一章节我们实现了无头单向不循环链表,也了解链表的概念和分类。本章节我们来给链表收尾。

带头双向循环链表

有的人一看这里又是带头又是双向又是循环的,就感觉带头双向循环链表实现和理解起来会比无头单向不循环链表会更难。恰恰相反带头双向循环链表实际上比无头单向不循环链表简单很多,比如:带头双向循环链表实现尾插时不需要找尾,随机插入/删除可以选择删除pos,pos之前和pos之后的位置不像无头单向不循环链表那样删除pos和pos之前的位置需要辅助链表。

除了上述的优势,双向链表也比单向链表更容易维护。

那么让我们一起来看一下双向链表的成员变量吧

c 复制代码
typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;

	LTDataType data;
}LTNode;

带头双向循环链表有三个参数,分别是保存前面链表的prev,保存后面链表的next和数据,那么头呢?

一般头就是申请一个无任何意义的链表结点而这个结点我们通常叫做哨兵位,然后让其他的链表在它的之前或之后进行插入或删除操作。

双向链表的实现

c 复制代码
#pragma once
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<stdbool.h>

typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;

	LTDataType data;
}LTNode;

// 初始化双向链表
LTNode* LTInit();
// 双向链表销毁
void LTDestroy(LTNode* phead);
// 双向链表打印
void LTPrint(LTNode* phead);
// 双向链表判空
bool LTEmpty(LTNode* phead);

// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x);
// 双向链表尾删
void LTPopBack(LTNode* phead);
// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x);
// 双向链表头删
void LTPopFront(LTNode* phead);

// 查找
LTNode* LTFind(LTNode* phead, LTDataType x);

// pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x);
// pos位置之前删除
void LTErase(LTNode* pos);

初始化

c 复制代码
// 初始化双向链表
LTNode* LTInit();

我们发现这次实现双向链表时怎么没有扩容函数啊?

因为正常来说扩容函数向这类函数是供给其他函数使用的而非用户那么最好就是不要将这种类型的函数接口暴露出来。

然后这次实现的是一个带头双向循环链表所以初始化的时候就是给用户返回一个哨兵位,所以这里和单链表不一样,无头的链表可以直接插入数据而带头的链表就需要先创建一个哨兵位。

c 复制代码
// 扩容
LTNode* BuyListNode(LTDataType x)
{
	LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));

	if (newNode == NULL)
	{
		perror("malloc fail");
		return NULL; // 这里可以直接返回NULL或者直接exit
	}

	newNode->data = x;
	newNode->next = NULL;
	newNode->prev = NULL;
	
	return newNode;
}

// 初始化双向链表
LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1); 

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

	return phead;
}

因为这里扩容操作插入需要用到所以封装成了一个函数接口。初始化一个哨兵位后续进行插入和删除就好写很多了。

尾插和尾删

c 复制代码
// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x);
// 双向链表尾删
void LTPopBack(LTNode* phead);

这里插入会很好搞,因为不需要找尾了在哨兵位的头节点之前插入即可。

尾删操作呢?

尾删我们就需要单独去判断一下了,为什么要单独判断呢?

因为我们这里定义的哨兵位,这个哨兵位本身就是一个链表结点如果全部删除都话后续进行插入和删除就不知道插入到谁的后面了。

不过这里的phead怎么是一级指针?不应该是二级指针吗?

因为我们这里实现的是带头的链表,如果单链表带头同样是一级指针,那么为什么带头就是一级指针呢?因为哨兵位在申请之后直到销毁之前一直都在那里不需要进行传地址进行捕获它,而不哨兵位的就需要每次都检查地址才行,因为不哨兵位的链表在销毁之前可以为空。

判断是否为空

c 复制代码
// 双向链表判空
bool LTEmpty(LTNode* phead);

phead->next == phead; 如果相等那么就证明现在只剩下一个哨兵位。prev也可以看个人喜欢那种方式。

c 复制代码
// 双向链表判空
bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

实现了判空操作,现在再来实现尾插和尾删操作。还是和实现单链表那样先画图再实现。

c 复制代码
// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newNode = BuyListNode(x);

	//phead <-> tail <-> newNode <-> phead
	LTNode* tail = phead->prev;
	tail->next = newNode;
	newNode->prev = tail;
	newNode->next = phead;

	phead->prev = newNode;
}
// 双向链表尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	// phead <-> tail <-> newNode <-> phead
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

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

	free(tail);
	tail = NULL;
}

实现了尾插和尾删,我们就可以进行测试一下功能了。为了方便演示先去实现一个打印函数。

双向链表打印函数

c 复制代码
// 双向链表打印
void LTPrint(LTNode* phead)
{
	assert(phead);

	// 这里可以选择打印哨兵也可以选择不打印哨兵
	LTNode* cur = phead->next;

	while (cur != phead)
	{
		printf("%d-> ", cur->data);
		cur = cur->next;
	}

	printf("\n");
}

测试尾插和尾删功能

c 复制代码
void test1()
{
	LTNode* head = LTInit();
	LTPushBack(head, 1);
	LTPushBack(head, 2);
	LTPushBack(head, 3);
	LTPushBack(head, 4);

	LTPrint(head);

	//先删除4个,然后多删除一个看会不会assert
	LTPopBack(head);
	LTPopBack(head);
	LTPopBack(head);
	LTPopBack(head);

	LTPopBack(head);

}

int main()
{
	test1();
	return 0;
}

头插和头删

c 复制代码
// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x);
// 双向链表头删
void LTPopFront(LTNode* phead);
c 复制代码
// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	
	LTNode* newNode = BuyListNode(x);
	// phead <-> newNode <-> first <-> phead
	LTNode* first = phead->next;

	phead->next = newNode;
	newNode->prev = phead;
	first->prev = newNode;
	newNode->next = first;

}
// 双向链表头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	// phead <-> first <-> node <-> phead
	LTNode* first = phead->next;
	LTNode* firstNext = first->next;

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

	free(first);
	first = NULL;
}

测试头插和头删的功能

c 复制代码
void test2()
{
	LTNode* head = LTInit();
	LTPushFront(head, 1);
	LTPushFront(head, 2);
	LTPushFront(head, 3);
	LTPushFront(head, 4);

	LTPrint(head);

	//先删除4个,然后多删除一个看会不会assert
	LTPopFront(head);
	LTPopFront(head);
	LTPopFront(head);
	LTPopFront(head);

	LTPopFront(head);

}

int main()
{
	//test1();
	test2();
	return 0;
}

随机插入和随机删除

c 复制代码
// pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x);
// pos位置删除
void LTErase(LTNode* pos);
c 复制代码
// 在pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newNode = BuyListNode(x);

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

// 删除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;
}

注:因为LTErase只有一个pos参数这就导致如果pos就是head的话就未定义了,这种写法是默认用户知道将head当pos是错误写法的。或者再给一个参数head就能解决了。

测试insert和erase功能

因为是给pos所以需要搭配find函数进行使用。那么就先实现一下find再来进行测试吧。

双向链表的查找函数

c 复制代码
// 查找
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;
}

测试:

c 复制代码
void test3()
{
	LTNode* head = LTInit();

	LTInsert(head, 1);

	LTNode* pos = LTFind(head, 1);

	if (pos == NULL)
	{
		return;
	}
	LTInsert(pos, 2);

	LTPushBack(head, 3);
	pos = LTFind(head, 3);
	LTInsert(pos, 4);

	LTPrint(head);

	pos = LTFind(head, 1);
	if (pos)
	{
		LTErase(pos);
	}
	LTPrint(head); 

	LTErase(head->next);
	LTPrint(head); 

	LTErase(head->prev);
	LTPrint(head); 

	LTErase(head->next);
	LTPrint(head); 

}

int main()
{
	//test1();
	//test2();
	test3();
	return 0;
}

完整代码

c 复制代码
#include "List.h"

// 扩容
LTNode* BuyListNode(LTDataType x)
{
	LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));

	if (newNode == NULL)
	{
		perror("malloc fail");
		return NULL; // 这里可以直接返回NULL或者直接exit
	}

	newNode->data = x;
	newNode->next = NULL;
	newNode->prev = NULL;
	
	return newNode;
}

// 初始化双向链表
LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1); 

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

	return phead;
}

// 双向链表判空
bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	//LTNode* newNode = BuyListNode(x);

	////phead <-> tail <-> newNode <-> phead
	//LTNode* tail = phead->prev;
	//tail->next = newNode;
	//newNode->prev = tail;
	//newNode->next = phead;

	//phead->prev = newNode;
	LTInsert(phead, x);
}
// 双向链表尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	//// phead <-> tail <-> newNode <-> phead
	//LTNode* tail = phead->prev;
	//LTNode* tailPrev = tail->prev;

	//phead->prev = tailPrev;
	//tailPrev->next = phead;

	//free(tail);
	//tail = NULL;
	LTErase(phead->prev);
}

// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	
	//LTNode* newNode = BuyListNode(x);
	//// phead <-> newNode <-> first <-> phead
	//LTNode* first = phead->next;

	//phead->next = newNode;
	//newNode->prev = phead;
	//first->prev = newNode;
	//newNode->next = first;

	LTInsert(phead->next, x);

}
// 双向链表头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	//// phead <-> first <-> node <-> phead
	//LTNode* first = phead->next;
	//LTNode* firstNext = first->next;

	//phead->next = firstNext;
	//firstNext->prev = phead;

	//free(first);
	//first = NULL;

	LTErase(phead->next);
}

// 在pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newNode = BuyListNode(x);

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

// 删除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;
}


// 查找
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;
}


// 双向链表打印
void LTPrint(LTNode* phead)
{
	assert(phead);

	// 这里可以选择打印哨兵也可以选择不打印哨兵
	LTNode* cur = phead->next;

	while (cur != phead)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}

	printf("NULL\n");
}

顺序表和链表的区别

不同点 顺序表 链表
存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续
随机访问 支持O(1) 不支持:O(N)
任意位置插入或者删除元素 可能需要搬移元素,效率低O(N) 只需修改指针指向
插入 动态顺序表,空间不够时需要扩容 没有容量的概念
应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁
缓存利用率

相关推荐
午彦琳2 小时前
2026.9.9
数据结构·算法·leetcode
我的小卷呀2 小时前
Linux ip 命令底层逻辑与实战指南
linux·运维·c语言·网络协议·tcp/ip·网络安全
luj_17682 小时前
中锋卡位与开放线博弈
c语言·开发语言·网络·经验分享·算法
未来之窗软件服务2 小时前
计算机二级[英文]-C 逻辑运算和fopen—东方仙盟
c语言·开发语言·仙盟创梦ide·东方仙盟
YIN_尹3 小时前
【Linux系统编程】模拟实现一个最简版C语言标准IO库
linux·服务器·c语言
橘子汽水1683 小时前
Leetcode 200,994岛屿数量,腐烂的橘子
数据结构·算法·leetcode
kiracrimson3 小时前
【无标题】
数据结构
zcmodeltech5 小时前
冷链物流沙盘模型控制系统设计——基于STM32与Modbus RTU的冷藏车、冷库全流程动态展示方案
数据结构·stm32·单片机·嵌入式硬件·物联网·能源
智闲电子设计5 小时前
RS485 总线实战:终端电阻、差分信号、半双工方向控制,工程里最容易翻车的几处
c语言·stm32·单片机·嵌入式硬件