C语言--带哨兵位的双向循环链表的创建及使用详解
- 1. 双向循环链表定义
- 1.1 定义
- 1.2 优点:
- 1.3 物理结构
- 2. 双向链表的创建
- 2.1 文件创建
- 2.2 节点创建
- 3. 链表操作
- 3.1 初始化
- 3.2 显示
- 3.3 尾插
- 3.4 头插
- 3.5 尾删
- 3.6 头删
- 3.7 查找
- 3.8 指定位置前插入
- 3.9 指定位置删除
- 3.10 链表销毁
- 4. 链表总内容
- 4.1 List.c 文件
- 4.2 List.h文件
- 4.3 test.c文件
- 5. 总结
1. 双向循环链表定义
1.1 定义
带哨兵位的双向循环链表是一种数据结构,它由多个节点组成,每个节点包含一个数据元素和两个指针,分别指向前一个节点和后一个节点,这样可以实现双向遍历。而且由于是循环链表,所以最后一个节点的后继节点是第一个节点,第一个节点的前驱节点是最后一个节点。与普通的双向循环链表不同的是,带哨兵位的链表在链表头部有一个哨兵节点,它不存储任何数据,仅用于简化链表操作。定义如下:
typedef int LTDataType;//定义存放数据的类型,便于不同类型数据的修改
typedef struct ListNode//定义链表结构
{
struct ListNode* next;//指向下一个节点的地址
struct ListNode* prev;//指向前一个数据的地址
LTDataType data;//存放的数据
}LN;//重新命名
1.2 优点:
- 简化边界情况处理:哨兵节点的存在简化了对链表为空或者只有一个节点的情况的处理,使得代码更加简洁和易于理解。
- 方便实现循环遍历:由于是循环链表,可以方便地实现对链表的循环遍历,并且在实现迭代器等功能时也更加方便。
- 提高代码的可读性和可维护性:哨兵节点的引入使得链表操作的边界情况处理更加统一,减少了代码中的特殊情况处理,提高了代码的可读性和可维护性。
1.3 物理结构
物理结构如下图:
- 该链表的第一个节点为哨兵位节点,不存放任何数据。phead 的next指向的节点才是第一个数据存放的位子。尾节点的next指向头节点地址,头节点的prev指向尾节点地址
- 当链表仅剩下哨兵位时,perv 和 next 均指向自己,此时数据为空,链表中总会存在一个节点。在进行后面的操作的时候不需要判空,仅需要判别next 是否和prev 指向的位置相同
2. 双向链表的创建
2.1 文件创建
为方便代码管理,将文件内容分为三个文件来共同组成单链表,如下:
List.h//存放函数的声明、链表定义
List.c//存放具体函数的定义,用于函数的实现
test.c//测试文件,用于测试函数的功能
链表的定义:
typedef int LTDataType;//定义存放数据的类型,便于不同类型数据的修改
typedef struct ListNode//定义链表结构
{
struct ListNode* next;//指向下一个节点的地址
struct ListNode* prev;//指向前一个数据的地址
LTDataType data;//存放的数据
}LN;//重新命名
2.2 节点创建
链表结构由一个个链表节点前后链接而成,每个链表节点的开辟如下:
//新链表要完成数据数据插入,然后传回插入过后的地址
LN* BuyList(LTDataType x)
{
LN* newnode = (LN*)malloc(sizeof(LN));//开辟新的节点空间
newnode->data = x;//放入数据
//前后节点指向置空
newnode->next = NULL;
newnode->prev = NULL;
return newnode;//返回新开辟空间的地址
}
3. 链表操作
3.1 初始化
带哨兵位的双向链表的初始化需要创建一个哨兵位,同时将头指针和尾指针都指向哨兵位的地址,物理图如下:
代码如下:
//初始化的本质是创建一个不放数据的节点,返回该节点地址
LN* ListInit()
{
LN* phead = BuyList(0);//创建的哨兵位节点
phead->next = phead;//头节点指向哨兵位自己
phead->prev = phead;//尾节点指向哨兵位自己
return phead;//返回创建的哨兵位的地址
}
3.2 显示
形参 phead 来接收整个链表的头地址 phead 的值,通过 phead 来访问显示链表中的数据。代码如下:
//显示操作仅对链表内部内容进行操作,仅需要哨兵位节点就可以访问所有数据
void ListPrint(LN* phead)
{
//phead指向的是哨兵位的位置,第一个数据在哨兵位之后
LN* cur = phead->next;//寻找第一个数据的位置
//循环结构,当尾指针指向哨兵位时完成一个循环
//当链表里面仅剩哨兵位时,cur指向的仍为phead,循环直接结束
while (cur != phead)
{
printf("%d-> ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
3.3 尾插
将最后一个节点tail 的next 指向新的节点newnode,新的节点newnode 的next 指向 phead,新的节点newnode 的prev 指向最后一个节点 tail 的地址,phead 的prev 指向新节点newnode 的地址.
//尾插需要链表的哨兵位节点与要插入的数据,不需要返回任何值
void ListPushBack(LN* phead, LTDataType x)
{
assert(phead);//断言,在链表哨兵位节点被误删时报错
LN* tail = phead->prev;//找尾节点
LN* newnode = BuyList(x);//新建节点
//将新节点链接到尾节点tail之后,再与哨兵位链接
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
3.4 头插
将第一个数据节点first 的prev 指向新的节点newnode,新的节点newnode 的prev 指向 phead,新的节点newnode 的next 指向第一个节点 first的地址 ,phead 的next 指向新节点newnode 的地址.
//头插需要链表的哨兵位节点与要插入的数据,不需要返回任何值
void ListPushFront(LN* phead, LTDataType x)
{
assert(phead);//断言,在链表哨兵位节点被误删时报错
LN* first = phead->next;//找到第一个数据的位置
LN* newnode = BuyList(x);//新建节点
//将新节点插入哨兵位之后,first之前
first->prev = newnode;
newnode->prev = phead;
newnode->next = first;
phead->next = newnode;
}
3.5 尾删
pead 的prev 指向倒数第二个数据pretail 的地址,prevtail 的next 指向phead 的地址,释放tail 的空间。
//尾删仅需要找到尾节点的位置,改变节点位置即可,不需要返回任何值
void ListPopBack(LN* phead)
{
assert(phead);//断言,在链表哨兵位节点被误删时报错
assert(phead->next != phead);//断言,当链表内容为空的时候报错
LN* tail = phead->prev;//寻找尾节点
LN* prevtail = tail->prev;//寻找尾节点之前的节点
//改变链接顺序,将倒数第二个节点与哨兵位链接起来
prevtail->next = phead;
phead->prev = prevtail;
//释放最后一个节点的空间
free(tail);
tail = NULL;
}
3.6 头删
pead 的next 指向第二个数据second 的地址,second 的prev 指向phead 的地址,释放first 的空间。
//头删仅需要找到第一个数据的的位置,改变节点位置即可,不需要返回任何值
void ListPopFront(LN* phead)
{
assert(phead);//断言,在链表哨兵位节点被误删时报错
assert(phead->next != phead);//断言,当链表内容为空的时候报错
LN* first = phead->next;//寻找第一个数据存放节点
LN* second = first->next;//寻找第二个数据存放节点
//改变链接顺序,将第二个节点与哨兵位链接起来
second->prev = phead;
phead->next = second;
//释放第一个节点的空间
free(first);
first = NULL;
}
3.7 查找
通过给定的数据,以遍历的方式寻找该数据,如果存在,则返回该数据的位置,不存在则返回空。
//查找需要被查找具体的数据,并且返回该数据的位置
LN* ListFind(LN* phead, LTDataType x)
{
assert(phead);//断言,在链表哨兵位节点被误删时报错
LN* cur = phead->next;//第一个数据的位置
while (cur != phead)//最坏情况为找一圈也不见要查找的数据
{
if (cur->data == x)//找到数据则返回地址
{
return cur;
}
cur=cur->next;
}
return NULL;//为找到数据则返回空
}
3.8 指定位置前插入
将指定数据节点pos 的prev 指向新的节点newnode,新的节点newnode 的prev 指向 prevpos,新的节点newnode 的next 指向pos的地址 ,prevpos 的next 指向新节点newnode 的地址.
//指定位置前插入需要指定的位置以及需要插入的数据
void ListInsert(LN* pos, LTDataType x)
{
assert(pos);//断言,在链表哨兵位节点被误删时报错
LN* prev = pos->prev;//找到指定位置前一个数据的位置
LN* newnode = BuyList(x);//定义新的节点
//将新的节点插入指定节点前面
newnode->next = pos;
pos->prev = newnode;
newnode->prev = prev;
prev->next = newnode;
}
3.9 指定位置删除
nextpos 的prev 指向prevpos 的地址,prevpos 的next 指向nextpos 的地址,释放pos 的空间。
//指定位置删除仅需指定的位置即可
void ListErase(LN* pos)
{
assert(pos);//断言,在链表哨兵位节点被误删时报错
LN* PosNext = pos->next;//找到指定位置后一个数据的位置
LN* PosPrev = pos->prev;//找到指定位置前一个数据的位置
//将指定位置前的数据与指定位置后的数据链接起来
PosNext->prev = PosPrev;
PosPrev->next = PosNext;
//释放指定位置的空间
free(pos);
pos = NULL;
}
3.10 链表销毁
通过哨兵位,将所有的链表空间一个一个的释放,最后将哨兵位的空间释放。
//链表销毁需要对整个链表进行操作
void ListDstroy(LN* phead)
{
assert(phead);//断言,在链表哨兵位节点被误删时报错
LN* cur = phead->next;//找到第一个数据的位置
while (cur != phead)
{
LN* next = cur->next;//找到下一个数据的位置
free(cur);//释放当前数据的空间
cur = next;//cur重新变为存在数据的对一个位置
}
//释放哨兵位节点空间
free(phead);
phead = NULL;
}
4. 链表总内容
4.1 List.c 文件
#define _CRT_SECURE_NO_WARNINGS
#include "List.h"
LN* BuyList(LTDataType x)
{
LN* newnode = (LN*)malloc(sizeof(LN));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
void ListPrint(LN* phead)
{
LN* cur = phead->next;
while (cur != phead)
{
printf("%d-> ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
LN* ListInit()
{
LN* phead = BuyList(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
void ListDstroy(LN* phead)
{
assert(phead);
LN* cur = phead->next;
while (cur != phead)
{
LN* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;
}
void ListPushBack(LN* phead, LTDataType x)
{
assert(phead);
/*LN* tail = phead->prev;
LN* newnode = BuyList(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;*/
ListInsert(phead->prev, x);
}
void ListPushFront(LN* phead, LTDataType x)
{
assert(phead);
/*LN* first = phead->next;
LN* newnode = BuyList(x);
first->prev = newnode;
newnode->prev = phead;
newnode->next = first;
phead->next = newnode;*/
ListInsert(phead->next, x);
}
void ListPopBack(LN* phead)
{
assert(phead);
/*assert(phead->next != phead);
LN* tail = phead->prev;
LN* prevtail = tail->prev;
prevtail->next = phead;
phead->prev = prevtail;
free(tail);
tail = NULL;*/
ListErase(phead->prev);
}
void ListPopFront(LN* phead)
{
assert(phead);
/*assert(phead->next!=phead);
LN* first = phead->next;
LN* second = first->next;
second->prev = phead;
phead->next = second;
free(first);
first = NULL;*/
ListErase(phead->next);
}
LN* ListFind(LN* phead, LTDataType x)
{
assert(phead);
LN* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur=cur->next;
}
return NULL;
}
void ListInsert(LN* pos, LTDataType x)
{
assert(pos);
LN* prev = pos->prev;
LN* newnode = BuyList(x);
newnode->next = pos;
pos->prev = newnode;
newnode->prev = prev;
prev->next = newnode;
}
void ListErase(LN* pos)
{
assert(pos);
LN* PosNext = pos->next;
LN* PosPrev = pos->prev;
PosNext->prev = PosPrev;
PosPrev->next = PosNext;
free(pos);
pos = NULL;
}
4.2 List.h文件
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LN;
LN* BuyList(LTDataType x);
LN* ListInit();
void ListDstroy(LN* phead);
void ListPushBack(LN* phead, LTDataType x);
void ListPrint(LN* phead);
void ListPushFront(LN* phead, LTDataType x);
void ListPopBack(LN* phead);
void ListPopFront(LN* phead);
LN* ListFind(LN* phead, LTDataType x);
void ListInsert(LN* pos,LTDataType x);
void ListErase(LN* pos);
4.3 test.c文件
#define _CRT_SECURE_NO_WARNINGS
#include "List.h"
void test1()
{
LN* plist = NULL;//链表头位置的创建
plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
ListPushFront(plist, 0);
ListPushFront(plist, -1);
ListPrint(plist);
ListPopFront(plist);
ListPrint(plist);
ListPopBack(plist);
ListPrint(plist);
}
void test2()
{
LN* plist = NULL;
plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
LN* pos = ListFind(plist, 3);
if (pos)
{
ListInsert(pos, 0);
}
ListPrint(plist);
ListErase(pos);
ListPrint(plist);
}
int main()
{
//test1();
test2();
return 0;
}
test1函数结果
test2函数结果
5. 总结
双向循环链表创建的难度从总体来说是小于单向链表的,只要熟悉了单向链表的创建,双向循环链表将会很快得到解决。
同时链表的学习是往后很多数据结构类型学习的基础,熟练掌握链表,对后续的学习又很大的帮助。