数据结构C语言版 —— 链表增删改查实现(单链表+循环双向链表)

Posted 爱敲代码的三毛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构C语言版 —— 链表增删改查实现(单链表+循环双向链表)相关的知识,希望对你有一定的参考价值。

文章目录


链表

1. 链表的基本概念

链表是用一组任意的额存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。简单来说链表是一种物理结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

在数据结构中,链表的结构非常多样,以下情况结合起来有8种结构的链表。

  1. 单链表,双链表
  2. 带头,不带头
  3. 循环,非循环

单链表和双链表结构

不带头单链表和带头单链表

带头的链表有一个哨兵节点,这个节点不存储数据。它始终是在链表的第一位,头插数据都往它后后面插。

单链表和无头循环单链表

循环单链表它的最后一个元素的指针域存储着头节点的地址

带头循环双链表

双向链表它有3个域,一个存放数据元素,一个存放前一个节点的地址,一个存放后一个节点的地址。这是一个带头且循环的双向链表,它的哨兵节点存prev存放着最后一个节点的低地址,而最后一个节点的next存放的是哨兵节点的地址。

我这里主要实现无头不循环单向链表带头循环双向链表

2. 无头非循环单链表实现

无头单项非循环链表,结构比较简单,一般不会用来单独存放数据。实际中单链表更多是作为其他高阶数据结构的子结构,比如哈希表、图的邻接表等。

单链表结构

#define SLTDateType int
typedef struct SListNode

	SLTDateType data;
	struct SListNode* next;
ListNode;

我这里实现一些主要的接口

// 动态申请一个节点
ListNode* BuySListNode(SLTDateType data);
// 尾插法
void SListNodePushBack(ListNode** pList, SLTDateType data);
// 头插法
void SListNodePushFront(ListNode** pList, SLTDateType data);
// 打印链表
void SListNodePrint(ListNode* pList);
// 删除头部元素
void SListNodePopFront(ListNode** ppList);
// 删除末尾元素
void SListNodePopBack(ListNode** ppList);
// 查找元素
ListNode* SListFind(ListNode* pList, SLTDateType data);
// 在pos位置之前插入元素
void SListInsertBefore(ListNode** ppList, ListNode* pos, SLTDateType data);
// 在pos位置之后插入元素
void SListInsertAfter(ListNode** ppList, ListNode* pos, SLTDateType data);
// 删除pos位置的元素
void SListNodePopCurrent(ListNode** ppList, ListNode* pos);
// 删除pos位置之前的元素
void SListNodePopBefore(ListNode** ppList, ListNode* pos);
// 删除pos位置之后的元素
void SListNodePopAfter(ListNode** ppList, ListNode* pos);
// 销毁链表
void SListEraseAfter(ListNode* ppList);

1) 动态申请节点

链表的节点是用一个向堆区申请一个

//动态申请一个节点
ListNode* BuySListNode(SLTDateType data)

	ListNode* node = (ListNode*)(malloc(sizeof(ListNode)));
	if (node == NULL)
	
		printf("申请失败\\n");
	
	else
	
		node->data = data;
		node->next = NULL;
	
	return node;

2) 打印链表元素

//打印链表
void SListNodePrint(ListNode* pList)

	ListNode* cur = pList;
	while (cur != NULL)
	
		printf("%d->", cur->data);
		cur = cur->next;
	
	printf("NULL\\n");

这个代码的时间复杂度为 O ( N ) O(N) O(N)

3) 插入节点

头插法

通过头插法向链表头部插入一个元素,分为以下步

  • 判断是否首次插入
  • 如果不是首次插入,把申请的节点下一个节点指向头节点,再把头节点指向node节点
//头插法
void SListNodePushFront(ListNode** ppList, SLTDateType data)

	assert(ppList);
	ListNode* node = BuySListNode(data);
	//首次插入
	if (*ppList == NULL)
	
		*ppList = node;
	
	else
	
		node->next = *ppList;
		*ppList = node;
	

这个代码的时间复杂度为 O ( 1 ) O(1) O(1)

尾插法

尾插法是向链表末尾插入一个元素

  • 同样要判断是否是第一次插入
  • 如果不是第一个插入,就遍历到最后一个节点,把最后一个节点的Next指向申请的节点。
//尾插法
void SListNodePushBack(ListNode** ppList, SLTDateType data)

    assert(ppList);
	ListNode* node = BuySListNode(data);
	//如果是第一次插入
	if (*ppList == NULL)
	
		*ppList = node;
	
	else
	
		ListNode* cur = *ppList;
		while (cur->next != NULL)
		
			cur = cur->next;
		
		cur->next = node;
	

这个代码涉及到遍历整个链表,所以时间复杂度为 O ( N ) O(N) O(N)

在指定位置之前插入

在指定位置之前插入元素比较复杂,要考虑两种情况

  1. 要在头节点之前插入
  2. 如果是其它位置就需要记录它的前驱节点
//在pos位置之前插入元素
void SListInsertBefore(ListNode** ppList, ListNode* pos, SLTDateType data)

	assert(ppList && pos);
	if (*ppList == pos)
	
		//如果要插入的是头节点的位置
		//申请节点
		ListNode* node = BuySListNode(data);
		node->next = *ppList;
		*ppList = node;
	
	else
	
		//遍历到pos位置
		ListNode* cur = *ppList;
		ListNode* prev = *ppList;
		while (cur != NULL)
		
			if (cur == pos)//注意这比较的是内存地址
			
				//申请节点
				ListNode* node = BuySListNode(data);
				node->next = pos;
				prev->next = node;
				break;
			
			prev = cur;
			cur = cur->next;
		
	

这个代码的时间复杂度为 O ( N ) O(N) O(N)

在指定位置之后插入

这个比较简单直接遍历到对应位置就好,注意修改节点指向的代码顺序!

//在pos位置之后插入元素
void SListInsertAfter(ListNode** ppList, ListNode* pos, SLTDateType data)

	assert(ppList && pos);
	ListNode* cur = *ppList;
	//遍历到pos位置
	while (cur != NULL)
	
		if (cur == pos)
		
			ListNode* node = BuySListNode(data);
			node->next = cur->next;//顺序不能错
			cur->next = node;
			break;
		
		cur = cur->next;
	

这个代码的时间复杂度为 O ( N ) O(N) O(N)

4) 删除节点

删除头部节点

拿一个临时遍历记录头节点的位置,再修改后节点的指向,最后free掉要删除的节点。

//删除头部元素
void SListNodePopFront(ListNode** ppList)

    assert(ppList);
	//为NULL情况
	if (*ppList == NULL)
	
		return;
	
	else
	
		ListNode* cur = *ppList;
		*ppList = (*ppList)->next;
		free(cur);
		cur = NULL;
	

这个代码的时间复杂度为 O ( 1 ) O(1) O(1)

删除末尾节点

删除尾节点需要考虑到三种情况

  1. 链表为NULL
  2. 只有一个节点情况
  3. 多个节点情况
//删除末尾元素
void SListNodePopBack(ListNode** ppList)

	assert(ppList);

	//为NULL情况
	if (*ppList == NULL)
	
		return;
	
	else if ((*ppList)->next == NULL)
	
		//只有一个节点情况
		free(*ppList);
		*ppList = NULL;
	
	else
	
		//多个节点情况
		ListNode* cur = *ppList;
		ListNode* prev = *ppList;
		while ((cur->next) != NULL)
		
			prev = cur;
			cur = cur->next;
		
		free(cur);
		prev->next = NULL;
	

这个代码的时间复杂度为 O ( N ) O(N) O(N)

删除指定位置之前的节点

这个操作也要考虑到3种情况

  1. 如果只有一个节点,或者传递的是头节点是无法删除的
  2. 有两个节点,要删除的是头节点
  3. 其他情况

在删除的时候都需要记录要删除的前一个节点的位置!

// 删除pos位置之前的元素
void SListNodePopBefore(ListNode** ppList, ListNode* pos)

	assert(ppList && pos);
	if (*ppList == pos)
	
		//要删除的时头节点前面的元素
		return;
	

	ListNode* cur = *ppList;
	ListNode* prev = *ppList;
	while (cur != NULL)
	
		
		if (cur->next == pos)
		
			if (cur == prev)
			
				//要删除的是头节点
				*ppList = (*ppList)->next;
				free(prev);
				prev = NULL;
				cur = NULL;
				break;
			
			else
			
				//其他情况
				prev->next = cur->next;
				free(cur);
				cur = NULL;
				prev = NULL;
				break;
			
		
		else
		
			prev = cur;
			cur = cur->next;
		
		
	

这个代码的时间复杂度为 O ( N ) O(N) O(N)

删除指定位置之后的节点

直接遍历到删除节点之前两个节点进行删除

// 删除pos位置之后的元素
void SListNodePopAfter(ListNode** ppList, ListNode* pos)

	assert(ppList && pos);
	
	
	ListNode* cur = *ppList;

	while (cur->next != NULL)
	
		if (cur == pos)
		
			cur->next = cur->next->next;
			break;
		
		cur = cur->next;
	
	

这个代码的时间复杂度为 O ( N ) O(N) O(N)

删除指定位置的节点

要考虑两种情况

  1. 要删除的的是头节点
  2. 其它情况(需要记录删除节点的前驱)
// 删除pos位置的元素
void SListNodePopCurrent(ListNode** ppList, ListNode* pos)

	assert(ppList && pos);
	//如果要删除的是头节点
	if (*ppList == pos)
	
		*ppList = (*ppList)->next;
	
	else
	
		ListNode* cur = *ppList;
		ListNode* prev = *ppList;
		while (cur != NULL)
		
			if (cur == pos)
			
				prev->next = cur->next;
				break;
			
			prev = cur;
			cur = cur->next;
		
	
	

这个代码的时间复杂度为 O ( N ) O(N) O(N)

5) 查找元素

查找指定节点通过遍历就好,这个代码也可以兼顾修改节点数据。

//查找元素
ListNode* SListFind(ListNode* pList, SLTDateType data)

	if (pList == NULL)
	
		return NULL;
	
	ListNode* cur = pList;
	while (cur != NULL)
	
		if (cur->data == data)
		
			return cur;
		
		cur = cur->next;
	

	return NULL;

查找的时间复杂度为 O ( N ) O(N) O(N)

6) 销毁链表

通过双指针直接遍历链表,边遍历边free释放掉节点。

//销毁链表
void SListEraseAfter(ListNode* pList)

	assert(pList);

	ListNode* cur = pList->next;
	ListNode* curNext = NULL;
	while (cur != NULL)
	
		curNext = cur->next;
		free(cur);
		cur = curNext;
	
	free(pList);//释放头节点

这个代码的时间复杂为 O ( N ) O(N) O(N)

3. 带头循环双向链表实现

带头双向循环链表结构复杂,一般用于单独存储数据。在实际中使用链表,一般都是带头双向循环链表,虽然这个链表结构复杂,但是实现起来却是比较简单的。带头循环双向链表有以下几个特点:

  • 最后一个节点后的下一个节点指向哨兵节点
  • 哨兵节点的前一个节点指向链表的最后一个节点
  • 头插数据永远往哨兵节点后插

带头循环双向链表结构

typedef int LTDataType;

typedef struct ListNode

	LTDataType data;//数据
	struct ListNode* prev;//节点前驱
	struct ListNode* next;//节点后继
ListNode;

// 动态申请一个节点
ListNode* BuyListNode(LTDataType data);
// 初始化双向链表
ListNode* ListNodeInit();
// 打印双向链表
void ListNodePrint(ListNode* pHead);
// 销毁双向链表
void ListDestory(ListNode* pHead);

// 双向链表头插
void ListNodePushFront(ListNode* pHead, LTDataType data);
// 双向链表尾插
void ListNodePushBack(ListNode* pHead, LTDataType data);
// 双向链表指定位置之前插入
void ListPosInsertBefore(ListNode* pHead, ListNode* pos, LTDataType data);
// 双向链表指定位置之后插入
void ListPosInsertAfter(ListNode* pHead, ListNode* pos, LTDataType data);

// 双向链表删除首节点
void ListNodePopFront(ListNode* pHead);
// 双向链表删除尾节点
void ListNodePopBack(ListNode* pHead);
// 双向链表删除指定位置节点
void ListNodePopCurrent(ListNode* pHead, ListNode* pos);
// 双向链表的查找
ListNode* ListNodeFind(ListNode* pHead, LTDataType data);

1) 初始化链表

要想初始化链表必须要有申请节点,所以封装一个函数来申请节点。

// 动态申请一个节点
ListNode* BuyListNode(LTDataType data)

	ListNode* newNode = (ListNode*)(malloc(sizeof(ListNode)));
	if (newNode == NULL)
	
		printf("空间申请失败\\n!");
		exit(-1);
	
	newNode->data = data;
	newNode->prev = NULL;
	newNode->next = NULL;

	return newNode;

带头循环的双向链表初始化要先申请一个节点作为哨兵节点,这个节点不存放数据起一个标识作用,它永远位于首节点前面。初始化时先让哨兵节点的前驱和后继都指向自己。

// 初始化双向链表
ListNode* ListNodeInit(LTDataType data)

	// 申请一个头节点作为哨兵节点
	ListNode* head = BuyListNode(data);
	//让这个哨兵节点的前驱和后继都先指向自己
	head->prev = head;
	head->next = head;

	return head;

2) 插入节点

头插法

头插法只需要把新节点插入到哨兵节点后面就可以了,注意修改 节点指向顺序

// 双向链表头插
void ListNodePushFront(ListNode* pHead, LTDataType data)

	assert(pHead);
	
	ListNode* node = BuyListNode(data);
	//头插一律插到哨兵

以上是关于数据结构C语言版 —— 链表增删改查实现(单链表+循环双向链表)的主要内容,如果未能解决你的问题,请参考以下文章

C语言单链表增删改查的实现

c语言 建立一个链表,实现增删改查

C语言带头双向循环链表增删改查的实现

C语言带头双向循环链表增删改查的实现

数据结构C语言版 —— 顺序表增删改查实现

数据结构单链表SingleLinkedList,Java实现单链表增删改查