数据结构-链表

一、链表

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

相关推荐
无己心1 天前
面向多平台 AI 搜索引擎的适配层架构设计
前端·人工智能·搜索引擎·ai·1024程序员节
u0103055277 天前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u0103055278 天前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝9 天前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u0103055279 天前
Java图像处理实战指南
人工智能·1024程序员节
u01030552710 天前
Java AWT鼠标事件全解析
人工智能·1024程序员节
u01030552712 天前
Java实用类应用精讲
人工智能·1024程序员节
u01030552713 天前
Java I/O核心操作实战
人工智能·1024程序员节
u01030552714 天前
ArrayList操作详解与实战应用
人工智能·1024程序员节
u01030552714 天前
Java模块化编程实战指南
人工智能·笔记·1024程序员节