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

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.顺序表和双向链表的优缺点分析

相关推荐
菜鸡中的奋斗鸡→挣扎鸡5 分钟前
滑动窗口 + 算法复习
数据结构·算法
axxy20001 小时前
leetcode之hot100---240搜索二维矩阵II(C++)
数据结构·算法
数据的世界012 小时前
.NET开发人员学习书籍推荐
学习·.net
四口鲸鱼爱吃盐2 小时前
CVPR2024 | 通过集成渐近正态分布学习实现强可迁移对抗攻击
学习
Uu_05kkq2 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法
1nullptr4 小时前
三次翻转实现数组元素的旋转
数据结构
OopspoO4 小时前
qcow2镜像大小压缩
学习·性能优化
TT哇4 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
嵌入式科普5 小时前
十一、从0开始卷出一个新项目之瑞萨RA6M5串口DTC接收不定长
c语言·stm32·cubeide·e2studio·ra6m5·dma接收不定长
A懿轩A5 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列