数据结构C语言版 —— 链表增删改查实现(单链表+循环双向链表)
Posted 爱敲代码的三毛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构C语言版 —— 链表增删改查实现(单链表+循环双向链表)相关的知识,希望对你有一定的参考价值。
文章目录
链表
1. 链表的基本概念
链表是用一组任意的额存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。简单来说链表是一种物理结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
在数据结构中,链表的结构非常多样,以下情况结合起来有8种结构的链表。
- 单链表,双链表
- 带头,不带头
- 循环,非循环
单链表和双链表结构
不带头单链表和带头单链表
带头的链表有一个哨兵节点,这个节点不存储数据。它始终是在链表的第一位,头插数据都往它后后面插。
单链表和无头循环单链表
循环单链表它的最后一个元素的指针域存储着头节点的地址
带头循环双链表
双向链表它有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)
在指定位置之前插入
在指定位置之前插入元素比较复杂,要考虑两种情况
- 要在头节点之前插入
- 如果是其它位置就需要记录它的前驱节点
//在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)
删除末尾节点
删除尾节点需要考虑到三种情况
- 链表为NULL
- 只有一个节点情况
- 多个节点情况
//删除末尾元素
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种情况
- 如果只有一个节点,或者传递的是头节点是无法删除的
- 有两个节点,要删除的是头节点
- 其他情况
在删除的时候都需要记录要删除的前一个节点的位置!
// 删除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)
删除指定位置的节点
要考虑两种情况
- 要删除的的是头节点
- 其它情况(需要记录删除节点的前驱)
// 删除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语言版 —— 链表增删改查实现(单链表+循环双向链表)的主要内容,如果未能解决你的问题,请参考以下文章