数据结构-链表

一、链表

1.1 链表的概念以及结构

链表是一种物理上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针依次链接实现的

逻辑结构是为了方便理解想象出来的,物理结构是实际内存中真实的存储方式。

链表的逻辑结构:

链表的物理结构:

注:链表在逻辑结构上是连续的,但在物理结构上并不一定连续。并且链表的每个节点一般都是用malloc从堆区里申请出来的。

1.2 链表的分类

实际中链表的结构非常多样,不同的组合共有8种:

1.单向或双向

2.带头节点或不带头节点

3.循环或不循环

最常用的两种结构:

1.3 无头单向不循环链表的接口实现

结构:

cpp 复制代码
typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;		//节点的数据域,用于存放数据
	struct SListNode* next;	//节点的指针域,用于存放指向下一个节点的指针
}SLTNode;		//对结构体的重命名

链表的打印:

cpp 复制代码
//链表的打印
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while(cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

注:此处的cur是一个临时的结构体指针变量,用于存储结构体的地址,cur=cur->next相当于把cur指向的下一个节点的地址赋给cur这个临时变量,进行访问下一个节点。

动态申请一个节点:

cpp 复制代码
//动态申请一个节点
SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

链表头插:

cpp 复制代码
void SLTPushFront(SLTNode** pphead, SLTDataType x);
cpp 复制代码
//链表头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

注:链表的头插需要改变的是结构体指针phead,因此在传参时需要传入结构体指针的地址。

链表尾插:

cpp 复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x);
cpp 复制代码
//链表尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	//链表为空
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//链表不为空
		SLTNode* tail = *pphead;
		//先找到尾节点
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

注:尾插空链表时需要改变的是phead,是一个结构体指针类型,因此传参需要传结构体指针的指针pphead。当尾插的链表不是空链表时,尾插改变的是尾节点的next,改变的是一个结构体类型,只需要结构体指针就行

链表头删:

cpp 复制代码
void SLTPopFront(SLTNode** pphead);
cpp 复制代码
//链表头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	//当链表为空,不能继续进行删除操作
	assert(*pphead);
	//链表不为空
	SLTNode* del = *pphead;
	*pphead = del->next;
	free(del);
	del = NULL;
}

链表尾删:

cpp 复制代码
void SLTPopBack(SLTNode** pphead);
cpp 复制代码
//链表尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	//当链表为空,不能继续进行删除操作
	assert(*pphead);
	//当链表只剩下一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		//当链表还剩多个节点
		//先找到尾节点的前一个节点
		SLTNode* prev = *pphead;
		while (prev->next->next != NULL)
		{
			prev = prev->next;
		}
		free(prev->next);
		prev->next = NULL;
	}
}

链表查找:

cpp 复制代码
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
cpp 复制代码
//链表查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pos = phead;
	//找到了返回节点的地址,找不到返回NULL
	while (pos)
	{
		if (pos->data != x)
		{
			pos = pos->next;
		}
		else
		{
			return pos;
		}
	}
	return NULL;
}

链表在pos位置之前插入x:

情况1:pos刚好是第一个节点

情况2:pos不是第一个节点

cpp 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
cpp 复制代码
//在pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);	//pos不能传空指针
	SLTNode* newnode = BuySLTNode(x);
	//当pos是第一个节点时
	if (*pphead == pos)
	{
		*pphead = newnode;
		newnode->next = pos;
	}
	else
	{
		//当pos不是第一个节点时,需要先找到pos的前一个节点
		SLTNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = newnode;
		newnode->next = pos;
	}
}

链表在pos位置后面插入x:

cpp 复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
cpp 复制代码
//在pos位置后面插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

注:此处必须先把newnode->next设置为pos->next,如果先把pos->next设置为newnode的话,会出现循环链表的情况

如下:

链表删除pos位置的值:

情况1:pos刚好是第一个节点

情况2:pos不是第一个节点

cpp 复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos);
cpp 复制代码
//删除pos位置的值
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	//情况1:如果pos是第一个节点
	if (*pphead == pos)
	{
		*pphead = pos->next;
		free(pos);
		pos = NULL;
	}
	else
	{
		//情况2:pos不是第一个节点
		//先找到pos的前一个节点
		SLTNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

链表删除pos位置后面的值:

cpp 复制代码
void SLTEraseAfter(SLTNode* pos);
cpp 复制代码
//删除pos位置后面的值
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);	//pos的下一个值不能为NULL
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

单链表的销毁:

cpp 复制代码
void SLTDestroy(SLTNode** pphead);
cpp 复制代码
//单链表销毁
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

1.4 带头双向循环链表的接口实现

带头双向循环链表的结构:

cpp 复制代码
typedef int LTDataType;
typedef struct LTNode
{
	struct LTNode* prev;	//指向前一个节点的指针
	struct LTNode* next;	//指向下一个节点的指针
	LTDataType data;		//用于保持节点的数据
}LTNode;		//对结构体的重命名

初始化:

cpp 复制代码
LTNode* LTInit();
cpp 复制代码
LTNode* LTInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	phead->next = phead;
	phead->prev = phead;
    return phead;
}

打印:

cpp 复制代码
void LTPrint(LTNode* phead);
cpp 复制代码
//打印
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	printf("哨兵位<==>");
	while (cur != phead)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

申请一个节点:

cpp 复制代码
//申请一个节点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = newnode;
	newnode->prev = newnode;
	return newnode;
}

尾插:

cpp 复制代码
void LTPushBack(LTNode* phead, LTDataType x);
cpp 复制代码
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//双向循环链表必定会有一个头结点head,头结点的指针不可能为空
	LTNode* newnode = LTBuyNode(x);
	//找尾
	LTNode* tail = phead->prev;
	//newnode和尾链接
	tail->next = newnode;
	newnode->prev = tail;
	//newnode和头链接
	newnode->next = phead;
	phead->prev = newnode;
}

头插:

cpp 复制代码
void LTPushFront(LTNode* phead, LTDataType x);
cpp 复制代码
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//保存头节点的下一个节点next
	LTNode* next = phead->next;
	//链接头结点和newnode
	phead->next = newnode;
	newnode->prev = phead;
	//链接newnode和next
	newnode->next = next;
	next->prev = newnode;
}

尾删:

cpp 复制代码
void LTPopBack(LTNode* phead);
cpp 复制代码
//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);	//链表的有效节点个数不能为0
	//先找到尾节点的前一个节点
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	//链接tailPrev和phead
	tailPrev->next = phead;
	phead->prev = tailPrev;
	//释放尾节点
	free(tail);
}

头删:

cpp 复制代码
void LTPopFront(LTNode* phead);
cpp 复制代码
//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);	//链表的有效节点个数不能为0
	//先找第一个节点和第二个节点
	LTNode* first = phead->next;
	LTNode* second = first->next;
	//链接头结点和第二个节点
	phead->next = second;
	second->prev = phead;
	//释放第一个节点
	free(first);
}

查找:

cpp 复制代码
LTNode* LTFind(LTNode* phead, LTDataType x);
cpp 复制代码
//查找
LTNode* LTFind(LTNode* phead,LTDataType x)
{
	assert(phead);
	//找到就返回节点的地址,找不到返回NULL
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

注:查找同时也可以代表修改。

在pos位置的前面插入:

cpp 复制代码
void LTInsert(LTNode* pos, LTDataType x);
cpp 复制代码
//在pos前面插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);	//确保pos不能为空
	//记录pos的前一个节点
	LTNode* posPrev = pos->prev;
	LTNode* newnode = LTBuyNode(x);
	//链接posPrev和newnode
	posPrev->next = newnode;
	newnode->prev = posPrev;
	//链接pos和newnode
	newnode->next = pos;
	pos->prev = newnode;
}

删除pos位置的值:

cpp 复制代码
void LTErase(LTNode* pos);
cpp 复制代码
//删除pos位置的值
void LTErase(LTNode* pos)
{
	assert(pos);
	//保存pos的前一个节点和后一个节点
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;
	//链接posPrev和posNext
	posPrev->next = posNext;
	posNext->prev = posPrev;
	//释放pos
	free(pos);
}

注:pos的值也不能等于phead,这一点可以进行另外的判断。

销毁:

cpp 复制代码
void LTDestroy(LTNode* phead);
cpp 复制代码
//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

注:销毁需要使用者在外部将phead置空,和free的使用方法相似。

双向带头循环链表的头插、尾插和头删、尾删可以复用Insert和Erase

二、顺序表和链表的区别

|--------------|---------------------------------------------------------------------------|--------------------|
| 不同点 | 顺序表 | 链表 |
| 存储空间上 | 一块连续的物理空间 | 逻辑上连续,但在物理空间上不一定连续 |
| 随机访问 | 支持 | 不支持 |
| 任意位置的插入或删除元素 | 可能需要挪动元素,效率为O(N) | 只需要修改指针的指向 |
| 插入 | 动态顺序表空间不够时需要扩容,扩容会有两个缺点1.扩容一般是以2倍的形式进行扩,会有扩容所需要的代价;2.扩容后的插入数据一般还会伴随着空间的浪费 | 按需申请空间 |
| 应用场景 | 元素高效存储+频繁访问 | 频繁在任意位置插入删除 |
| 缓存利用率 | CPU高速缓存的缓存命中率会更高 | CPU高速缓存的缓存命中率会更低 |

有关CPU缓存相关的详细文章:

与程序员相关的CPU缓存知识 | 酷 壳 - CoolShell

相关推荐
第七序章7 小时前
【C + +】C++11 (下) | 类新功能 + STL 变化 + 包装器全解析
c语言·数据结构·c++·人工智能·哈希算法·1024程序员节
努力学习的小廉10 小时前
初识MYSQL —— 基本查询
数据库·mysql·1024程序员节
码力引擎12 小时前
【零基础学MySQL】第四章:DDL详解
数据库·mysql·1024程序员节
少林码僧13 小时前
1.1 大语言模型调用方式与函数调用(Function Calling):从基础到实战
人工智能·ai·语言模型·自然语言处理·llm·1024程序员节
胖虎113 小时前
Swift项目生成Framework流程以及与OC的区别
framework·swift·1024程序员节·swift framework
纵有疾風起19 小时前
C++—string(1):string类的学习与使用
开发语言·c++·经验分享·学习·开源·1024程序员节
kitsch0x971 天前
论文学习_LLM4Decompile: Decompiling Binary Code with Large Language Models
1024程序员节
hazy1k1 天前
51单片机基础-继电器实验
stm32·单片机·嵌入式硬件·51单片机·1024程序员节
Brianna Home1 天前
大模型如何变身金融风控专家
人工智能·深度学习·机器学习·自然语言处理·stable diffusion·1024程序员节