链表详解——单链表 和 双向循环链表 的实现

本文将主要介绍链表的概念和结构,并实现最常用的两种链表:无头单向非循环链表 和 带头双向循环链表。

目录

一、链表的概念及其结构

二、链表的分类

1. 单向或者双向

2. 带头或者不带头

3. 循环或者非循环 

三、链表的实现

1.无头单向非循环链表:

1.动态节点的申请

2.单链表打印

3.单链表的头插

4.单链表的尾插

5.单链表的头删

6.单链表的尾删

7.单链表的查找

8.在指定位置插入节点

9.在指定位置删除节点

10.在指定位置之后插入节点

11.在指定位置之后删除节点

12.单链表的销毁

2.带头双向循环链表(图片演示):

1.初始化

2.指定位置插入数据(头插、尾插)

3.指定位置删除数据(头删、尾删)

4.打印链表

5.销毁链表


一、链表的概念及其结构

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

 注意:

1.从上图可看出,链式结构在逻辑上是连续的,但在物理上不一定连续。

2.现实中的结点一般都是从堆上申请出来的。

3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续也可能不连续。

二、链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1. 单向或者双向

2. 带头或者不带头

3. 循环或者非循环 

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 

无头单向非循环链表:

带头双向循环链表: 

 

1. 无头单向非循环链表:

结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

2. 带头双向循环链表:

结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

三、链表的实现

1.无头单向非循环链表:

链表节点定义:

typedef int SLTDateType;//假设int为该链表的数据类型
typedef struct SListNode
{
	SLTDateType data;//存放链表数据
	struct SListNode* next;//指向下一个节点的指针
}SListNode;//重命名简化

需要实现的接口:

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* phead);
// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pphead, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pphead);
// 单链表头删
void SListPopFront(SListNode** pphead);
// 单链表查找
SListNode* SListFind(SListNode* phead, SLTDateType x);
// 单链表在pos位置之前插入x
void SListInsert(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之前的值
void SListErase(SListNode* pos);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 单链表销毁
void SListDestory(SListNode* phead);

我们分模块来实现上述接口:

① SeqList.h  文件:存放头文件的包含、顺序表结构体和函数的声明。

② SeqList.c  文件:存放函数的定义,来实现单链表的各个功能。

③ test.c  文件: 用于单链表的测试。

1.动态节点的申请

SListNode* BuySListNode(SLTDateType x)//申请一个新的节点
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));

	assert(newnode);//判断是否申请成功
	newnode->data = x;//存入新节点的数据
	newnode->next = NULL;//下一个节点为空
    return newnode;
}

2.单链表打印

打印单链表,定义一个指针cur,遍历链表并记录当前位置,当cur->next 为 NULL 时,结束遍历。

void SListPrint(SListNode* phead)
{
	SListNode* cur = phead;  //从头节点开始
	while (cur != NULL)
	{
		printf("%d -> ", cur->data);//打印链表数据
		cur = cur->next;
	}
	printf("NULL\n");
}

 

3.单链表的头插

创建一个新的节点,让新节点的指针指向原头节点,并将新的节点修改为头节点即可。

注意:要修改传入的参数指针plist,需要参数的地址。或者返回新的头指针。

void SListPushFront(SListNode** pphead, SLTDateType x)
{
	assert(pphead);
	SListNode* newnode = BuySListNode(x);//申请一个新的节点
	newnode->next = *pphead;//将新节点的指针指向原来的头节点
	*pphead = newnode;//将新节点设置为头节点
}

4.单链表的尾插

创建一个新的节点,找到链表中的尾节点,让尾节点的指针指向新节点,将新节点的指针置空。

void SListPushBack(SListNode** pphead, SLTDateType x)
{
	assert(pphead);
	SListNode* newnode = BuySListNode(x);//申请一个新节点
	if (*pphead == NULL)//判断原链表是否为空
	{
		*pphead = newnode;//原链表为空,新链表为头节点
	}
	else
	{
		SListNode* tail = *pphead;
		while (tail->next != NULL)//遍历链表找尾节点
		{
			tail = tail->next;
		}
		tail->next = newnode;//新节点在申请时,指针已经置空
	}
}

5.单链表的头删

让头指针指向下一个节点,并释放掉原来的头节点。

void SListPopFront(SListNode** pphead)
{
	assert(pphead);
	assert(*pphead);//确保原链表和指针参数不为空
	SListNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

6.单链表的尾删

遍历链表,找到链表最后节点的前一个节点,释放最后一个节点,并将最后一个节点的前一个接的的指针置空。

void SListPopBack(SListNode** pphead)
{
	assert(pphead);
	assert(*pphead);//确保原链表和指针参数不为空
	if ((*pphead)->next == NULL)//如果链表只有一个节点
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SListNode* tail = *pphead;
		while (tail->next->next != NULL)//寻找最后节点的前一个节点
			//如果链表只有一个节点,会造成越界访问
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

7.单链表的查找

遍历链表,查找目标数据,如果找到了返回该节点地址,没找到则返回空指针。

SListNode* SListFind(SListNode* pphead, SLTDateType x)
{
	SListNode* cur = pphead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

8.在指定位置插入节点

首先找到指定位置之前的节点,创建一个新的节点,让新的节点的指针指向指定的节点,让指定的节点之前的节点指向新节点即可。

void SListInsert(SListNode** pphead, SListNode* pos, SLTDateType x)
{
	assert(pphead);
	assert(pos);

	if (*pphead == pos)//当置指定位置为头节点时,此时为头插
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SListNode* prev = *pphead;
		while (prev->next != pos)//寻找指定位置的前一个节点
		{
			prev = prev->next;
		}
		SListNode* newnode = BuySListNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

9.在指定位置删除节点

首先找到指定位置之前的节点,让指定节点前一个节点的指针指向指定节点后一个节点,再释放掉指定节点即可。

void SListErase(SListNode** pphead, SListNode* pos)
{
	assert(pphead);
	assert(pos);

	if (pos == *pphead)//pos为头节点时,为头删
	{
		*pphead = (*pphead)->next;
		free(pos);
	}
	else
	{
		SListNode* prev = *pphead;
		while (prev->next != pos)//寻找指定位置的前一个节点
		{
			prev = prev->next;
		}
		prev->next = pos->next;//让指定位置的前一个节点的指针指向指定位置的后一个节点
		free(pos);
	}
}

10.在指定位置之后插入节点

创建一个新的节点,让新节点的指针 指向 指定节点 指向的节点,再让指定节点 指向的新创建的节点即可。修改指定位置之后的节点,不需要考虑是否会改变头指针的情况,因此不需要使用二级指针。

void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);

	SListNode* newnode = BuySListNode(x);
	newnode = pos->next;
	pos->next = newnode;
}

11.在指定位置之后删除节点

记录指定节点的地址,让指定节点之前的节点指向指定位置之后的节点,再结束指定节点。

void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);//避免指定位置之后为空指针
	SListNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
}

12.单链表的销毁

遍历链表,逐个节点释放。

void SListDestory(SListNode* phead)
{
	assert(phead);
	SListNode* cur = phead;
	while (cur) 
	{
		SListNode* next = cur->next;
		free(cur);
		cur = next;
	}
}

2.带头双向循环链表(图片演示):

链表节点定义:

这里的头节点为哨兵位,仅作为链表的头节点,不存储有效的数据。

typedef int LTDataType;//假设链表数据类型
typedef	struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;

需要实现的接口:

//初始化链表
LTNode* ListInit();
//尾插
void ListPushBack(LTNode* phead, LTDataType x);
//头插
void ListPushFront(LTNode* phead, LTDataType x);
//头删
void ListPopFront(LTNode* phead);
//尾删
void ListPopBack(LTNode* phead);
//打印
void ListPrint(LTNode* phead);
//在指定位置插入数据
void ListInsert(LTNode* pos, LTDataType x);
//在指定位置删除数据
void ListErase(LTNode* pos);
//查找
LTNode* ListFind(LTNode* plist, LTDataType x);
//销毁
void ListDestory(LTNode* phead);

1.初始化

初始化创建一个不存储数据的哨兵位(如下图):

LTNode* BuyListNode(LTDataType x)//创建一个新节点
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)//判断节点是否开辟成功
	{
		perror("BuyListNode::malloc");
		exit(-1);
	}
	node->next = NULL;
	node->prev = NULL;
	node->data = x;
	return node;
}

LTNode* ListInit()
{
	LTNode* phead = BuyListNode(-1);//创建一个哨兵位头节点
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

2.指定位置插入数据(头插、尾插)

 

 

void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	
	LTNode* newnode = BuyListNode(x);//创建一个新节点
	LTNode* prev = pos->prev;

	prev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
	newnode->prev = prev;
}

 由于存在哨兵位,头插如下图:

即 在指定位置head->next ,插入一个数据

 

因此直接对指定位置插入函数进行复用即可。

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	ListInsert(phead->next, x);
}

尾插:

循环链表首尾相连,在head之前插入即可。

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	ListInsert(phead, x);
}

3.指定位置删除数据(头删、尾删)

改变指针,释放pos即可。

 

void ListErase(LTNode* pos)
{
	assert(pos);
	assert(pos->next != pos);//除哨兵位外,链表至少有一个节点

	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	prev->next = next;
	next->prev = prev;
	free(pos);

}

头删:

即删除head->next 的节点 

 

void ListPopFront(LTNode* phead)
{
	ListErase(phead->next);
}

尾删:

即删除head->prev 的节点 

 

void ListPopBack(LTNode* phead)
{
	ListErase(phead->prev);
}

4.打印链表

定义指针cur遍历链表,直到回到头节点。

void ListPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

5.销毁链表

遍历链表,逐个删除,最后释放头节点。

void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode*next = cur->next;
		ListErase(cur);
		cur = next;
	}
	free(phead);
}

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值