前言
C语言数据结构,链表的分类、创建双向链表结构、打印双向链表、初始化双向链表、尾插、头插、尾删、头删、查找、指定位置之后插入、删除pos节点、销毁链表等的介绍
链表的分类
- 链表可以按照带头(不带头)、单向(双向)、循环(不循环)进行分类。共 8 种。
- 带头:指的是是否包含哨兵位(一个不存储有效数据的节点)。
- 双向和循环:顾名思义。
- 如下图:
- 单向链表指的是不带头、单项、不循环的链表。
- 双向链表指的是带头、双向、循环的链表。
创建双向链表结构
- 双向链表节点除了存储自己的数据外,还存储指向上一个节点的地址和指向下一个节点的地址。
双向链表节点的创建:
// List.h
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
// 创建双向链表结构
typedef int LTNDataType;
typedef struct ListNode
{
LTNDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
- typedef int LTNDataType; 是为了方便修改每个节点数据的类型。
一、打印双向链表
后续函数都需要在双向链表的头文件(List.h)中声明,并且在双向链表的源文件(List.c)中定义。为了方便不过多说明。
在测试源文件(test.c)中进行测试,并且测试函数都要放在主函数中调用,为了方便,不过多说明。
- 打印双向链表时要求链表不能为空,同时链表不能只有哨兵位。
- 打印双向链表不打印哨兵位。
- pcur存储第一个有效节点,并遍历双向链表。当pcur->next指向phead(哨兵位)时,终止循环。
打印双向链表函数实现:
// 打印双向链表
void ListNodePrint(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
二、初始化双向链表
- 动态申请一个不存储有效数据的节点,这里用 -1 表示非有效数据。
- 并且将这个新的节点的newNode->next和newNode->prev都指向自己构成循环。
初始化双向链表函数实现:
// 申请新的节点函数
LTNode* ListBuyNode(LTNDataType* x)
{
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (NULL == newNode)
{
perror("malloc fail!");
exit(1);
}
newNode->data = x;
newNode->next = newNode->prev = newNode;
return newNode;
}
// 初始化双向链表
void ListNodeInit(LTNode** pphead)
{
*pphead = ListBuyNode(-1);
}
三、尾插
- 先创建一个新的节点。
- 通过(哨兵位)phead->prev可以直接找到双向链表的最后一个节点。
- (新的节点)newNode->prev 指向 phead->prev(原链表最后一个节点)。
- (新的节点)newNode->next 指向 phead(哨兵位)。
- (原链表最后一个节点)phead->prev->next指向新的节点(newNode)。
- (原链表的哨兵位)phead->prev指向新的节点。
尾插函数实现:
// 尾插
void ListNodePushBack(LTNode* phead, LTNDataType x)
{
assert(phead);
LTNode* newNode = ListBuyNode(x);
newNode->next = phead;
newNode->prev = phead->prev;
phead->prev->next = newNode;
phead->prev = newNode;
}
尾插函数的测试:
void ListNodeTest02()
{
LTNode* plist = NULL;
// 初始化为只含有哨兵位的双向链表
ListNodeInit(&plist);
// 测试尾插
ListNodePushBack(plist, 1);
ListNodePrint(plist);
ListNodePushBack(plist, 2);
ListNodePrint(plist);
ListNodePushBack(plist, 3);
ListNodePrint(plist);
ListNodePushBack(plist, 4);
ListNodePrint(plist);
}
效果如下:
四、头插
- 双向链表的头插指的是在双向链表的哨兵位和第一有效节点之间插入。
- 头插的思想和尾插一样,向改变新节点的指向,前后节点的指向。
头插函数实现:
// 头插
void ListNodePushFront(LTNode* phead, LTNDataType x)
{
assert(phead);
LTNode* newNode = ListBuyNode(x);
newNode->next = phead->next;
newNode->prev = phead;
phead->next->prev = newNode;
phead->next = newNode;
}
头插函数的测试:
void ListNodeTest03()
{
LTNode* plist = NULL;
// 初始化为只含有哨兵位的双向链表
ListNodeInit(&plist);
// 测试头插
ListNodePushFront(plist, 1);
ListNodePrint(plist);
ListNodePushFront(plist, 2);
ListNodePrint(plist);
ListNodePushFront(plist, 3);
ListNodePrint(plist);
ListNodePushFront(plist, 4);
ListNodePrint(plist);
}
效果如下:
五、尾删
- 创建临时变量(del)记忆最后一个节点的位置。
- 将最后一个节点(del)之前的节点的下一个节点指向哨兵位(头节点)。
- 将哨兵位(头节点)的前一个节点指向最后一个节点(del)的前一个节点。
尾删函数实现:
// 尾删
void ListNodePopBack(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->prev;
del->prev->next = phead;
del->next->prev = del->prev;
free(del);
del = NULL;
}
尾删函数的测试:
void ListNodeTest04()
{
LTNode* plist = NULL;
// 初始化为只含有哨兵位的双向链表
ListNodeInit(&plist);
// 尾插4个数字
ListNodePushBack(plist, 1);
ListNodePushBack(plist, 2);
ListNodePushBack(plist, 3);
ListNodePushBack(plist, 4);
ListNodePrint(plist);
// 测试尾删
ListNodePopBack(plist);
ListNodePrint(plist);
ListNodePopBack(plist);
ListNodePrint(plist);
ListNodePopBack(plist);
ListNodePrint(plist);
ListNodePopBack(plist);
ListNodePrint(plist);
}
效果如下:
六、头删
- 头删指的是删除第一个有效节点。
- 即删除头节点(哨兵位)的下一个节点。
- 创建临时变量del记忆头节点的下一个节点(phead->next)
- 将头节点的下一个节点指向 del 的下一个节点
- 将del的下一个节点的前一个节点指向头节点(phead)
- free释放del。
头删函数实现:
// 头删
void ListNodePopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
头删函数的测试:
void ListNodeTest05()
{
LTNode* plist = NULL;
// 初始化为只含有哨兵位的双向链表
ListNodeInit(&plist);
// 尾插4个数字
ListNodePushBack(plist, 1);
ListNodePushBack(plist, 2);
ListNodePushBack(plist, 3);
ListNodePushBack(plist, 4);
ListNodePrint(plist);
// 测试头删
ListNodePopFront(plist);
ListNodePrint(plist);
ListNodePopFront(plist);
ListNodePrint(plist);
ListNodePopFront(plist);
ListNodePrint(plist);
ListNodePopFront(plist);
ListNodePrint(plist);
}
效果如下:
七、查找
- 创建临时变量pcur记忆第一个头节点,遍历双向链表
- 若pcur的data 等于 传入的数据,则return返回pcur。
- 若循环遍历结束后没有找到,则return返回NULL。
查找函数实现:
// 查找
LTNode* ListNodeFind(LTNode* phead, LTNDataType x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
查找函数的测试:
void ListNodeTest06()
{
LTNode* plist = NULL;
// 初始化为只含有哨兵位的双向链表
ListNodeInit(&plist);
// 尾插4个数字
ListNodePushBack(plist, 1);
ListNodePushBack(plist, 2);
ListNodePushBack(plist, 3);
ListNodePushBack(plist, 4);
ListNodePrint(plist);
// 测试查找
LTNode* find = ListNodeFind(plist, 2);
if (find == NULL)
printf("2 没找到!!!\n");
else
printf("2 找到了!!!\n");
LTNode* find1 = ListNodeFind(plist, 20);
if (find1 == NULL)
printf("20 没找到!!!\n");
else
printf("20 找到了!!!\n");
}
效果如下:
八、指定位置之后插入
- 使用传入的数据创建新的节点
- 先改变新的节点的前一个节点和后一个节点的指向。
- 将pos节点的下一个节点的前一个节点指向新的节点。
- 将pos节点的下一个节点指向新的节点。
指定位置插入函数实现:
// 指定位置之后插入
void ListInsert(LTNode* pos, LTNDataType x)
{
assert(pos);
LTNode* newNode = ListBuyNode(x);
newNode->prev = pos;
newNode->next = pos->next;
pos->next->prev = newNode;
pos->next = newNode;
}
指定位置之后插入函数的测试:
void ListNodeTest07()
{
LTNode* plist = NULL;
// 初始化为只含有哨兵位的双向链表
ListNodeInit(&plist);
// 尾插4个数字
ListNodePushBack(plist, 1);
ListNodePushBack(plist, 2);
ListNodePushBack(plist, 3);
ListNodePushBack(plist, 4);
ListNodePrint(plist);
// 测试指定位置之后插入
LTNode* find = ListNodeFind(plist, 2);
ListInsert(find, 66);
ListNodePrint(plist);
}
效果如下:
九、删除pos节点
- pos节点之前的节点的下一个节点指向pos节点之后的节点。
- pos节点之后的节点的前一个节点指向pos节点之前的节点。
- free释放pos节点
- pos节点置为NULL。
删除pos节点函数实现:
// 删除pos节点
void ListErase(LTNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
删除pos节点函数的测试:
void ListNodeTest08()
{
LTNode* plist = NULL;
// 初始化为只含有哨兵位的双向链表
ListNodeInit(&plist);
// 尾插4个数字
ListNodePushBack(plist, 1);
ListNodePushBack(plist, 2);
ListNodePushBack(plist, 3);
ListNodePushBack(plist, 4);
ListNodePrint(plist);
// 测试删除pos节点
LTNode* find = ListNodeFind(plist, 2);
ListErase(find);
find = NULL;
ListNodePrint(plist);
}
效果如下:
销毁链表
- 临时变量pcur记忆第一个有效节点,遍历双向链表。释放pcur。并让pcur不断指向后一个节点。
- 当pcur的next节点指向哨兵位时,跳出循环。
- 此时pcur指向哨兵位,释放pcur。
- pcur置为NULL。
销毁链表函数实现:
// 销毁链表
void ListNodeDestroy(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(pcur);
pcur = NULL;
}
销毁链表函数的测试:
void ListNodeTest09()
{
LTNode* plist = NULL;
// 初始化为只含有哨兵位的双向链表
ListNodeInit(&plist);
// 尾插4个数字
ListNodePushBack(plist, 1);
ListNodePushBack(plist, 2);
ListNodePushBack(plist, 3);
ListNodePushBack(plist, 4);
ListNodePrint(plist);
// 测试销毁
ListNodeDestroy(plist);
plist = NULL;
}
效果如下:
总结
C语言数据结构,链表的分类、创建双向链表结构、打印双向链表、初始化双向链表、尾插、头插、尾删、头删、查找、指定位置之后插入、删除pos节点、销毁链表等的介绍