目录
前言
一、结构介绍
二、增删查改的实现
1.在某一位置前插入数据
2.头插
3.尾插
4.删除某一位置的节点
5.头删
6.尾删
7.查找
8.打印
9.销毁
三、完整项目代码
1.头文件(List.h)
2.函数文件(List.c)
3.主函数测试文件(test.c)
前言
在学习数据结构过程中,我们经常要用到顺序表和链表。顺序表的优点是可以随机访问,缺点是增删效率低。链表恰好弥补了顺序表的缺点,但是不能随机访问,而且代码比较麻烦。因为在进行插入删除操作时,我们必须考虑链表为空和不为空两种情况。除此之外,想要访问当前节点的上一个节点,就必须把链表再遍历一遍,降低了运行速度。那么,有没有有一种可能,链表也能非常方便地插入、删除、找前后节点呢?这就是本文要为大家介绍的,带头双向循环链表。
一、结构介绍
我们从单链表开始探索:
单链表的节点由两部分组成,即存储的数据、指向下一个节点的指针。
用代码定义如下:
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
这种结构是单向的,不能从后往前找,于是可以想到,在结构体中再加上一个指针,用来指向前一个节点,这样我们的链表就变成了双向链表。
代码定义如下:
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
struct SListNode* prev;
}SListNode;
通过指针prev即可倒回去找前面的节点。这样能在一定程度上减少麻烦。
当然,这种情况下,进行插入操作时,还需要顾及链表是否为空的情况。两种情况操作不同,会增加代码量。因此,我们可以增加一个头节点head,不存储数据,只是占一个位置,我们称之为哨兵位。这样,即使链表为空,也有一个节点,可以进行访问next指针的操作。
以下是链表创建过程:
ListNode* BuyLTNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
ListNode* ListCreate()
{
ListNode* L = BuyLTNode(-1);
L->next = NULL;
L->prev = NULL;
return L;
}
简单解释:创建一个新的节点,让其中存储-1(随便什么都行),prev和next指针置为空,这个节点是头节点,不存储实际数据,但是在插入时能简化操作。
头插方便了,但是如果要在尾部插入呢?你需要先创建一个指针,遍历链表,找到链表尾部,然后再插入,好麻烦那!别急,我们的带头双向链表继续升级,带头双向循环链表,就可以解决这个问题。
以下是链表创建过程:
ListNode* BuyLTNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
ListNode* ListCreate()
{
ListNode* L = BuyLTNode(-1);
L->next = L;
L->prev = L;
return L;
}
最初创建头节点时,头节点两个指针可以自己指向自己。
二、增删查改的实现
1.在某一位置前插入数据
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = BuyLTNode(x);
newnode->prev = pos->prev;
pos->prev->next = newnode;
newnode->next = pos;
pos->prev = newnode;
}
在这里,不管是在哪里插入,都可以用这段代码,因为任何节点之间的结构都是一样的。可以配合另一个寻找节点的函数使用,先找到想要的节点,再在前面插入。
2.头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListInsert(pHead->next, x);
}
正因这种结构良好的兼容性,插入代码可以直接用来为头插函数服务。而在有头节点的情况下,只需通过头节点找next即可找到插入位置。
3.尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListInsert(pHead, x);
}
插入代码也可以直接用来为尾插函数服务。而尾插是在链表最后插入,但是对于循环链表,链表的头即是尾,尾即是头,只需传头节点指针,头节点的前一个位置,就是链表尾。是不是巨方便?
4.删除某一位置的节点
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
这里由于要删除节点,需要断开前后联系,可以先创建临时指针表示前后的节点,方便后面操作。
不管是哪里删除,都可以用这段代码。可以配合查找函数使用。
5.头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
ListErase(pHead->next);
}
有了插入函数,我们可以直接在头删函数中使用。注意头删可不是删除头节点,头节点只是占个位置,招你惹你了?删的是头节点的下一个位置,这才是第一个元素。
6.尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead);
ListErase(pHead->prev);
}
插入函数在尾删也是可以直接用滴!想找尾?头节点的前一个就是!!!是不是巨方便?
7.查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
当然了,不管是是顺序表还是链表,要查找,他都得遍历。从头开始,找到那个存x的结点,返回它的地址就行。
8.打印
void ListPrint(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
printf("%d<==>", cur->data);
cur = cur->next;
}
printf("\n");
}
你别说,没这个函数其实也行,但是不好测试。这个函数主要是为了能在测试时把链表显示出来,让你知道,你成功辣!
9.销毁
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
ListNode* prev = cur;
cur = cur->next;
free(prev);
}
free(pHead);
}
虽然退出程序操作系统会自动回收空间,但是如果这段代码用来做其他项目的接口,可能要运行很长时间,内存泄漏可是不好的。用完要销毁。每个节点都要free掉哦。
三、完整项目代码
1.头文件(List.h)
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}ListNode;
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
2.函数文件(List.c)
#include "List.h"
ListNode* BuyLTNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
ListNode* ListCreate()
{
ListNode* L = BuyLTNode(-1);
L->next = L;
L->prev = L;
return L;
}
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
ListNode* prev = cur;
cur = cur->next;
free(prev);
}
free(pHead);
}
void ListPrint(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
printf("%d<==>", cur->data);
cur = cur->next;
}
printf("\n");
}
void ListPushBack(ListNode* pHead, LTDataType x)
{
ListInsert(pHead, x);
}
void ListPopBack(ListNode* pHead)
{
assert(pHead);
ListErase(pHead->prev);
}
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListInsert(pHead->next, x);
}
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = BuyLTNode(x);
newnode->prev = pos->prev;
pos->prev->next = newnode;
newnode->next = pos;
pos->prev = newnode;
}
void ListPopFront(ListNode* pHead)
{
assert(pHead);
ListErase(pHead->next);
}
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
3.主函数测试文件(test.c)
#include"List.h"
int main()
{
ListNode* pHead = NULL;
pHead = ListCreate();
printf("尾插1、2、3、4\n");
ListPushBack(pHead, 1);
ListPushBack(pHead, 2);
ListPushBack(pHead, 3);
ListPushBack(pHead, 4);
ListPrint(pHead);
printf("尾删一次\n");
ListPopBack(pHead);
ListPrint(pHead);
printf("头插5、6、7、8\n");
ListPushFront(pHead, 5);
ListPushFront(pHead, 6);
ListPushFront(pHead, 7);
ListPushFront(pHead, 8);
ListPrint(pHead);
printf("头删一次\n");
ListPopFront(pHead);
ListPrint(pHead);
printf("删除元素为1的节点\n");
ListErase(ListFind(pHead, 1));
ListPrint(pHead);
printf("销毁链表\n");
ListDestory(pHead);
pHead = NULL;
return 0;
}
记得最后将pHead制空,因为函数内部不能改变原指针,需要在外部制空,这样就永远找不到那份空间了。
这个测试代码可以很好地测试一下你写的函数是否可以运行起来(不包括函数的提醒报错功能,因为运行不起来不容易看结果)。那么,我们测试运行以下,结果如下:
看来我们的代码非常成功。带头双向循环链表真是太好用辣!