数据结构---线性表--链表

1.链表

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

与顺序表不同的是,链表里的数据是放在一个一个的节点(结点)里面的,节点是单独申请的

节点的组成有两个部分:当前节点要保存的数据和保存下一个节点地址(指针变量)

2.链表的分类

链表的结构非常多样,以下情况组合起来就有8种(2*2*2)

具体结构如下:

链表的结构很多,但是实际应用种最常用的还是两种结构:无头单向非循环链表(单链表)双向带头循环链表

  1. 无头单向非循环链表:结构简单,一般不用来单独存放数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
  2. **带头双向循环链表:**结构最复杂,一般用于单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。虽然结构复杂,但是将代码实现完之后,会带来很多优势。

3.单链表

I.基本结构

图中指针变量plist保存的是吧第一个节点的地址,称为"哨兵位",指向第一个节点,不存放数据。

链表中每个节点都是独立申请的(即需要插入数据时采取申请一个节点的空间),需要指针变量来保存下一个节点的位置才能从当前节点找到下一个节点

假设要保存的节点为整型

cs 复制代码
struct SListNode
{
    int data;//节点数据
    struct SListNode*next;//指针变量用于保存下一个节点的地址
};

当保存整型数据的时候,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,还要保存下一个节点的位置(当下一个节点为空时保存的地址为空)。当需要遍历链表的时候,只需要依次在前一个结点拿下一个下一个节点的位置即可。

注:

  1. 链式结构在逻辑上是连续的,在物理结构上不一定连续
  2. 节点一般是从堆上申请的
  3. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能来连续,可能不连续

II.接口实现

cs 复制代码
typedef int SLNodeDataType;//以整型数据为例实现
typedef struct SLNode
{
	SLNodeDataType data;
	struct SLNode* next;
}SLNode;

每次存储数据都需要申请空间,也就是申请一个节点

cs 复制代码
//申请节点声明
SLNode* GetNode(SLNodeDataType x);
//申请节点实现
SLNode* GetNode(SLNodeDataType x)
{
	SLNode* tmp = (SLNode*)malloc(sizeof(SLNode));//malloc动态申请
	if (tmp == NULL)
	{
		perror("malloc error:\n");
		exit(1);
	}
	tmp->data = x;//存放数据
	tmp->next = NULL;//存放下一个节点的地址
	return tmp;
}

插入数据有四种方式:头插、尾插、指定位置之前插入、指定位置之后插入

cs 复制代码
//头插声明
void PushFront(SLNode** pphead, SLNodeDataType x);
//头插实现
void PushFront(SLNode** pphead, SLNodeDataType x)
{
	assert(pphead);//phead不能为空
	SLNode* new_node = GetNode(x);//申请新的节点
	new_node->next = *pphead;//新的节点的下一个节点指向头节点
	*pphead = new_node;//将新的节点置为头节点
}
cs 复制代码
//尾插声明
void PushBack(SLNode** pphead, SLNodeDataType x);
//尾插实现
void PushBack(SLNode** pphead, SLNodeDataType x)
{
	assert(pphead);//&phead不能为空,不能没有phead
	SLNode* new_node = GetNode(x);//申请新节点
	if (*pphead == NULL)//空链表,直接头节点就等于新节点
	{
		*pphead = new_node;
	}
	else
	{
	    //找到尾节点
		SLNode* ptail = *pphead;
		while ((ptail->next))
		{
			ptail = ptail->next;
		}
        //连接节点
		ptail->next = new_node;
	}
}
cs 复制代码
//指定位置之后插入数据声明
void InsertBackNode(SLNode* pos, SLNodeDataType x);
//指定位置之后插入数据实现
void InsertBackNode(SLNode* pos, SLNodeDataType x)
{
	assert(pos);
	SLNode* new_node = GetNode(x);
	new_node->next = pos->next;//先将新节点的下一个节点连接到指定位置的下一个节点
	pos->next = new_node;//再将新节点连接在指定位置的后面
}
cs 复制代码
//指定位置前插入数据声明
void InsertFrontNode(SLNode** pphead, SLNode* pos, SLNodeDataType x);
//指定位置前插入数据实现
void InsertFrontNode(SLNode** pphead, SLNode* pos, SLNodeDataType x)
{
	assert(*pphead && pphead);
	assert(pos);
    //如果是头节点的的话,就直接头插即可
	if (*pphead == pos)
	{
		PushFront(pphead,x);
	}
	else
	{
		SLNode* new_node = GetNode(x);申请新节点
		//找到pos之前的节点
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		new_node->next = pos;//先将新的节点的下一个节点指向指定位置节点
		prev->next = new_node;//再连接指定位置之前的节点和新节点
	}
}

删除数据同理

cs 复制代码
//头删声明
void PopFront(SLNode** pphead);
//头删实现
void PopFront(SLNode** pphead)
{
	assert(*pphead && pphead);
	SLNode*new_head=(*pphead)->next;//储存头节点的下一个节点
	free(*pphead);
	*pphead = new_head;//释放后指向新的头(头节点的下一个节点)
}
cs 复制代码
//尾删声明
void PopBack(SLNode** pphead);
//尾删实现
void PopBack(SLNode** pphead)
{
	assert(*pphead && pphead);//节点不为空,不能传空
	if ((*pphead)->next == NULL)//只有一个节点
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLNode* ptail = *pphead;
		SLNode* prev = *pphead;
        //找到尾节点和为尾节点的前一个节点
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
        //将尾节点的前一个节点指向的下一个节点指向空
		prev->next = NULL;
	}
}
cs 复制代码
//指定位置删除数据声明
void DelposNode(SLNode**pphead,SLNode* pos);
//删除指定位置数据实现
void DelposNode(SLNode** pphead, SLNode* pos)
{
	assert(pos);
	if (pos == *pphead)//头删
	{
		PopFront(pphead);
	}
	else
	{
        //找到指定节点的前一个节点
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
        //将指定节点的前一个节点的下一个节点指向指定节点的下一个节点
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
cs 复制代码
//删除指定位置之后的数据声明
void PopposBackNode(SLNode*pos);
//删除指定位置之后的数据实现
void PopposBackNode(SLNode* pos)
{
	SLNode* dest = pos->next;//指向要删除的节点(pos之后)
	SLNode* next = dest->next;//指向要连接的节点(删除的节点之后)
    //将指定位置节点的下一个节点指向要删除的节点(pos之后)的下一个节点
	pos->next = dest->next; 
	free(dest);
	dest = NULL;
}
cs 复制代码
//销毁链表声明
void DestroySLNode(SLNode** pphead);
//销毁链表实现
void DestroySLNode(SLNode** pphead)
{
	SLNode* pcur = *pphead;
	while (pcur)
	{
		SLNode* Next = pcur->next;//提前存储指向pcur->next的节点
		free(pcur);
		pcur = Next;//释放后的指针指向pcur->next
	}
}

注:关于链表操作中的指针问题,对应关系如下

4.双向链表

I.基本结构

II.接口实现

cs 复制代码
typedef struct ListNode
{
	ListNodeType data;//存放的数据
	struct ListNode* prev;//指向前一个节点的指针
	struct ListNode* next;//指向下一个节点的指针
}ListNode;

和单链表一样,需要存放数据的时候,申请即可

cs 复制代码
//申请节点声明
ListNode* GetNode(ListNodeType x);
//申请节点实现
ListNode* GetNode(ListNodeType x)
{
	ListNode* tmp = (ListNode*)malloc(sizeof(ListNode));//动态申请一个节点的空间
	if (tmp == NULL)
	{
		perror("malloc:");
		exit(1);
	}
	tmp->data = x;
	tmp->next = tmp->prev=tmp;//形成闭环
	return tmp;
}
cs 复制代码
//初始化
ListNode* InitList(ListNode** ppheadd);
//初始化
ListNode* InitList(ListNode** pphead)
{
	*pphead=GetNode(-1);//哨兵位存放的值不能改变,先设定好哨兵位,作为双向链表的头节点
	return *pphead;
}
cs 复制代码
//头插
void PushFront(ListNode* phead, ListNodeType x);
//头插
void PushFront(ListNode* phead, ListNodeType x)
{
	assert(phead);
	ListNode* new_node = GetNode(x);

	//new_node    phead   phead->next
    //先处理新节点
	new_node->prev = phead;
	new_node->next = phead->next;
    //再处理哨兵位
	phead->next->prev = new_node;
	phead->next = new_node;
}
cs 复制代码
//尾插声明
void PushBack(ListNode* phead, ListNodeType x);
//尾插实现
void PushBack(ListNode* phead, ListNodeType x)
{
	assert(phead);
	ListNode* new_node = GetNode(x);
	//先让尾相连
	new_node->prev = phead->prev;
	new_node->next = phead;
    //再连接哨兵位
	phead->prev->next = new_node;
	phead->prev = new_node;
}
cs 复制代码
//指定位置后插入数据声明
void Insert(ListNode* pos, ListNodeType x);
//指定位置后插入数据实现
void Insert(ListNode* pos, ListNodeType x)
{
	assert(pos);
	ListNode* new_node = GetNode(x);

	new_node->next = pos->next;
	new_node->prev = pos;

	pos->next->prev = new_node;
	pos->next = new_node;
}
cs 复制代码
//头删声明
void PopFront(ListNode* phead);
//头删实现
void PopFront(ListNode* phead)
{
	assert(phead && phead->next != phead);
    要删除的是哨兵位的下一个节点
	ListNode* pcur = phead->next;
	phead->next = pcur->next;
	pcur->next->prev = phead;

	free(pcur);
	pcur = NULL;//记得置空
}
cs 复制代码
//尾删声明
void PopBack(ListNode* phead);
//尾删实现
void PopBack(ListNode* phead)
{
	assert(phead  &&  phead->next!=phead);//必须有节点,不能只有哨兵位
    //尾节点
	ListNode*pcur=phead->prev;
    //先让指向尾节点的指针指向正确节点,再释放
	pcur->prev->next = phead;
	phead->prev = pcur->prev;

	free(pcur);
	pcur = NULL;
}
cs 复制代码
//删除指定位置节点
void DelNode(ListNode* pos);
//删除指定位置的节点
void DelNode(ListNode* pos)
{
	assert(pos);
	ListNode* pcur = pos->next;
	pcur->prev = pos->prev;
	pos->prev->next = pcur;

	free(pos);
	pos = NULL;
}
cs 复制代码
//销毁链表声明
void Destroy(ListNode* phead);
//销毁链表实现
void Destroy(ListNode* phead)
{
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
        //提前存放要释放的节点的下一个节点
		ListNode* Next = pcur->next;
		free(pcur);
		pcur =Next;
	}
	//哨兵位也要释放
	free(phead);
	phead = NULL;
}

5.顺序表和双向链表的优缺点分析

相关推荐
丶Darling.39 分钟前
代码随想录 | Day26 | 二叉树:二叉搜索树中的插入操作&&删除二叉搜索树中的节点&&修剪二叉搜索树
开发语言·数据结构·c++·笔记·学习·算法
百里香酚兰1 小时前
【AI学习笔记】基于Unity+DeepSeek开发的一些BUG记录&解决方案
人工智能·学习·unity·大模型·deepseek
yttandb1 小时前
重生到现代之从零开始的C语言生活》—— 内存的存储
c语言·开发语言·生活
布丁不叮早起枣祈1 小时前
10.3学习
学习
结衣结衣.2 小时前
python中的函数介绍
java·c语言·开发语言·前端·笔记·python·学习
LN-ZMOI2 小时前
c++学习笔记1
c++·笔记·学习
no_play_no_games2 小时前
「3.3」虫洞 Wormholes
数据结构·c++·算法·图论
五味香2 小时前
C++学习,信号处理
android·c语言·开发语言·c++·学习·算法·信号处理
梓䈑2 小时前
【C语言】自定义类型:结构体
c语言·开发语言·windows
PYSpring3 小时前
数据结构-LRU缓存(C语言实现)
c语言·数据结构·缓存