零、总言
C语言简单实现带头双向循环链表的各个流程,代码解释在注释中。
文章目录
一、整体模式:
List.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>//布尔
//双向、带头、循环链表:
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
struct List
{
LTNode* phead;
int size;
};
//链表初始化
void ListInitform(LTNode** pphead);
//链表初始化改进版
LTNode* ListInit(void);
//链表打印
void ListPrint(LTNode* plist);
//链表尾插
void ListPushBack(LTNode* phead, LTDataType x);
//链表头插
void ListPushFront(LTNode* phead, LTDataType x);
//链表头删
void ListPopBack(LTNode* phead);
//链表尾删
void ListPopFront(LTNode* phead);
//在pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x);
//删除pos位置处的结点
void ListErase(LTNode* pos);
//计算链表长度
int ListSize(LTNode* phead);
//链表销毁
void ListDestory(LTNode* phead);
Text.c
#include"List.h"
void TestList1()
{
//测试链表初始化
//LTNode* plist = NULL;
//ListInit(&plist);//注意实参传递
//测试链表初始化改进版
LTNode* plist = ListInit();
//测试尾插
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPushBack(plist, 5);
ListPrint(plist);
//测试头删
ListPopFront(plist);
ListPrint(plist);//1
ListPopFront(plist);
ListPrint(plist);//2
ListPopFront(plist);
ListPrint(plist);//3
ListPopFront(plist);
ListPrint(plist);//4
ListPopFront(plist);
ListPrint(plist);//5
ListPopFront(plist);
ListPrint(plist);//哨兵位
}
void TestList2()
{
LTNode* plist = ListInit();
//从链表为空(只有哨兵位)开始测试头插
ListPushFront(plist, 1);
ListPushFront(plist, 2);
ListPushFront(plist, 3);
ListPushFront(plist, 4);
ListPushFront(plist, 5);
ListPrint(plist);
//测试尾删
ListPopBack(plist);
ListPrint(plist);//1
ListPopBack(plist);
ListPrint(plist);//2
ListPopBack(plist);
ListPrint(plist);//3
ListPopBack(plist);
ListPrint(plist);//4
ListPopBack(plist);
ListPrint(plist);//5
ListPopBack(plist);
ListPrint(plist);//测试哨兵位
}
void TestList3()
{
LTNode* plist = ListInit();
//测试pos作用于头插、尾插
ListPushFront(plist, 1);
ListPrint(plist);//1
ListPushFront(plist, 2);
ListPrint(plist);//2 1
ListPushFront(plist, 3);
ListPrint(plist);//3 2 1
ListPushBack(plist, 1);
ListPrint(plist);//3 2 1 1
ListPushBack(plist, 2);
ListPrint(plist);//3 2 1 1 2
ListPushBack(plist, 3);
ListPrint(plist);//3 2 1 1 2 3
//测试pos作用于头删、尾删
ListPopFront(plist);
ListPrint(plist);//2 1 1 2 3
ListPopFront(plist);
ListPrint(plist);//1 1 2 3
ListPopBack(plist);
ListPrint(plist);//1 1 2
ListPopBack(plist);
ListPrint(plist);//1 1
}
int main(void)
{
//TestList1();
//TestList2();
TestList3();
return 0;
}
List.c
#include"List.h"
//辅助函数:动态开辟一个内存空间,新结点,可用于创建哨兵为的头、尾插、头插。
LTNode* BuyListNode(LTDataType x)
{
//动态开辟:注意malloc的使用细节
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)//开辟失败
{
perror("malloc:fail");
exit(-1);
}
//开辟成功时:
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
//链表初始化:双向链表、带头且循环
//参照之前的单链表,其若不带头也不循环,初始时链表为空,则指向NULL即可,
//但对于此双向链表,其带头、循环,因此但链表中无数据时,初始时要有一个哨兵位的头,且其前驱指针和后续指针都指向自己本身。
//细节:根据TestList1知,要改变实参本身(即链表指针),则此处链表初始函数化中,形参需要使用对应类型的二级指针。
void ListInitform(LTNode** pphead)
{
*pphead = BuyListNode(-1);//初始时,链表中只有哨兵位的头结点
(*pphead)->next = *pphead;//处理后续指针指向,使其指向自己
(*pphead)->prev = *pphead;//处理前驱指针指向,使其指向自己
}
//链表初始化改进版
//由于上述链表初始化需要使用到二级指针,此处改进版是在只使用一级指针的情况下:相应的TestList中接收方法需要变动
LTNode* ListInit(void)
{
LTNode *phead = BuyListNode(-1);//在初始化函数中创建一个新指针,表示哨兵位的头结点(命名变量的含义在此体现)
phead->next = phead;
phead->prev = phead;
return phead;//返回该指针地址
}
//链表打印
//思考:双链表的打印,由于其循环、带哨兵位的原因,需要确定何时停止,以及保证不打印出哨兵位的数据。
//故一个方法是用于遍历的指针初始时就指向哨兵位头结点后的首位有效结点,当遍历指针迭代更新,其指向哨兵位的头结点时,循环结束。
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;//指向哨兵位后一结点:即不打印哨兵位数据
while (cur != phead)//判断的条件:由于链表循环,最终会回到哨兵位
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
//链表尾插
//思考:在先前写过的单链表中,两链表尾插需要二级指针,此处此类型的双链表中,链表尾插是否需要二级指针?
//此处双链表中,已经含有哨兵位的头,尾插只需要变动结点内部成员,不需要变动结点的地址。(即next、prev指针是结构体成员,因此函数传参时使用一级指针即可)
//细节:注意结点间指向关系的变动
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
//pos位置前插入函数作用于尾插:
ListInsert(phead, x);//此处要注意phead->prev不是尾插
//不借助Insert函数的写法:
//新增结点
LTNode* newnode = BuyListNode(x);
//改变结点中指针指向关系
LTNode* tail = phead->prev;//找到原先链表的尾结点
//改变tail:
tail->next = newnode;
//改变newnod:
newnode->prev = tail;
newnode->next = phead;
//改变phead:
phead->prev = newnode;
}
//链表头插
//思考:带哨兵位的头插,需要理解此时头插的含义,哨兵位始终占据理论上的头结点不动,此处头插指的是在哨兵位后链表第二个结点前头插。
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//pos位置前插入函数作用于头插:
ListInsert(phead->next, x);
//不借助Insert函数的写法:
//新增结点
LTNode* newnode = BuyListNode(x);
//改变结点中指针指向关系
LTNode* second = phead->next;//找到原链表中哨兵位后一个结点
//改变second:
second->prev = newnode;
//改变newnode:
newnode->next = second;
newnode->prev = phead;
//改变phead:
phead->next = newnode;
//若不使用second指针,改变结点指向关系时有顺序要求,以下为示范:
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;//此处出现phead->next,故需要放置在phead->next被修改之前
newnode->prev = phead;
phead->next->prev = newnode;//此处为了寻找second结点,故修改需要放置在phead->next被修改之前
phead->next = newnode;
}
//辅助删除函数:用于判断链表是否为空(只剩带哨兵位的头结点时)
bool ListEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
//若判等为真,返回ture,表示链表只剩哨兵位头;
//若判等为假,返回false,表示链表中还有有效结点
}
//链表尾删
//思考:这种双向带头循环链表,就体现了尾删的好处,找尾时不必遍历。此处创建了一个tailprev,和上述头插中second一样,能够方便后续结点关系指向的修改。
//此方法对只有一个有效结点时仍旧适用,当链表为空时(只有哨兵位的头结点),需要额外处理。
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));//其它写法:assert(phead->next!=phead);
//借助Ereas函数的写法:(上述两句断言仍旧需要)
ListErase(phead->prev);
//不借助Erase函数的写法:
//尾删,找尾结点,找尾删后的新尾结点
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
//改变关系
phead->prev = tail->prev;
tailprev->next = phead;
//释放待删除结点
free(tail);
}
//链表头删
//思考:和头插一样要找准有效头结点以及删除后剩余结点间的指向关系
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
//借助Ereas函数的写法:(上述两句断言仍旧需要)
ListErase(phead->next);
//不借助Erase函数的写法:
//头删,有效头结点,新的有效头结点
LTNode* second = phead->next;
LTNode* secondnext = second->next;
//改变指向关系
phead->next = secondnext;
secondnext->prev = phead;
//释放结点
free(second);
}
//pos位置前插入
//思考:带头双向链表,在pos位置前插入,prev指针的存在,不用遍历找pos位置
//关于pos位置如何得到:例如,使用ListFind函数找到链表中对应需要的结点,再在该结点前插入,或对该结点删除。
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
//确定指针
LTNode* posprev = pos->prev;
LTNode* newnode = BuyListNode(x);
//修改关系
//对newnode:
newnode->next = pos;
newnode->prev = posprev;
//对pos:
pos->prev = newnode;
//对posprev:
posprev->next = newnode;
}
//pos位置处删除
void ListErase(LTNode* pos)
{
assert(pos);
//确定所需指针:
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
//修改关系:
posprev->next = posnext;
posnext->prev = posprev;
//删除结点:
free(pos);
}
//求链表长度
//思路:遍历一遍计数,时间复杂度O(N)
int ListSize(LTNode* phead)
{
assert(phead);
LTNode* cur = phead;
int size = 0;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
//为使该函数和其它函数一样保持一致时间复杂度(即不遍历链表),
//一种方法是适用哨兵位的头结点中的data用来统计链表的长度,但此方法有bug,
//结构体中data类型为LTDataType,若为char类型,则存储结点有限,会溢出。(此方法在确定链表长度时可以选择性使用)
//实际要解决此问题,则使用结构体关联模式比较方便,或者可使用一个静态变量来完成
//链表销毁
void ListDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
ListErase(cur);
cur = next;
}
free(phead);
}
二、分块模式(附图):
初始化
//辅助函数:动态开辟一个内存空间,新结点,可用于创建哨兵为的头、尾插、头插。
LTNode* BuyListNode(LTDataType x)
{
//动态开辟:注意malloc的使用细节
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)//开辟失败
{
perror("malloc:fail");
exit(-1);
}
//开辟成功时:
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
//链表初始化:双向链表、带头且循环
//参照之前的单链表,其若不带头也不循环,初始时链表为空,则指向NULL即可,
//但对于此双向链表,其带头、循环,因此但链表中无数据时,初始时要有一个哨兵位的头,且其前驱指针和后续指针都指向自己本身。
//细节:根据TestList1知,要改变实参本身(即链表指针),则此处链表初始函数化中,形参需要使用对应类型的二级指针。
void ListInitform(LTNode** pphead)
{
*pphead = BuyListNode(-1);//初始时,链表中只有哨兵位的头结点
(*pphead)->next = *pphead;//处理后续指针指向,使其指向自己
(*pphead)->prev = *pphead;//处理前驱指针指向,使其指向自己
}
//链表初始化改进版
//由于上述链表初始化需要使用到二级指针,此处改进版是在只使用一级指针的情况下:相应的TestList中接收方法需要变动
LTNode* ListInit(void)
{
LTNode *phead = BuyListNode(-1);//在初始化函数中创建一个新指针
phead->next = phead;
phead->prev = phead;
return phead;//返回该指针地址
}
尾插
//链表尾插
//思考:在先前写过的单链表中,两链表尾插需要二级指针,此处此类型的双链表中,链表尾插是否需要二级指针?
//此处双链表中,已经含有哨兵位的头,尾插只需要变动结点内部成员,不需要变动结点的地址。(即next、prev指针是结构体成员,因此函数传参时使用一级指针即可)
//细节:注意结点间指向关系的变动
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
//pos位置前插入函数作用于尾插:
ListInsert(phead, x);//此处要注意phead->prev不是尾插
//不借助Insert函数的写法:
//新增结点
LTNode* newnode = BuyListNode(x);
//改变结点中指针指向关系
LTNode* tail = phead->prev;//找到原先链表的尾结点
//改变tail:
tail->next = newnode;
//改变newnod:
newnode->prev = tail;
newnode->next = phead;
//改变phead:
phead->prev = newnode;
}
头插
//链表头插
//思考:带哨兵位的头插,需要理解此时头插的含义,哨兵位始终占据理论上的头结点不动,此处头插指的是在哨兵位后链表第二个结点前头插。
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//pos位置前插入函数作用于头插:
ListInsert(phead->next, x);
//不借助Insert函数的写法:
//新增结点
LTNode* newnode = BuyListNode(x);
//改变结点中指针指向关系
LTNode* second = phead->next;//找到原链表中哨兵位后一个结点
//改变second:
second->prev = newnode;
//改变newnode:
newnode->next = second;
newnode->prev = phead;
//改变phead:
phead->next = newnode;
//若不使用second指针,改变结点指向关系时有顺序要求,以下为示范:
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;//此处出现phead->next,故需要放置在phead->next被修改之前
newnode->prev = phead;
phead->next->prev = newnode;//此处为了寻找second结点,故修改需要放置在phead->next被修改之前
phead->next = newnode;
}
头插、尾插测试结果
尾删
//辅助删除函数
bool ListEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
//若判等为真,返回ture,表示链表只剩哨兵位头;
//若判等为假,返回false,表示链表中还有有效结点
}
//链表尾删
//思考:这种双向带头循环链表,就体现了尾删的好处,找尾时不必遍历。此处创建了一个tailprev,和上述头插中second一样,能够方便后续结点关系指向的修改。
//此方法对只有一个有效结点时仍旧适用,当链表为空时(只有哨兵位的头结点),需要额外处理。
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));//其它写法:assert(phead->next!=phead);
//借助Ereas函数的写法:(上述两句断言仍旧需要)
ListErase(phead->prev);
//不借助Erase函数的写法:
//尾删,找尾结点,找尾删后的新尾结点
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
//改变关系
phead->prev = tail->prev;
tailprev->next = phead;
//释放待删除结点
free(tail);
}
尾删测试结果
头删
//辅助删除函数
bool ListEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
//若判等为真,返回ture,表示链表只剩哨兵位头;
//若判等为假,返回false,表示链表中还有有效结点
}
//链表头删
//思考:和头插一样要找准有效头结点以及删除后剩余结点间的指向关系
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
//借助Ereas函数的写法:(上述两句断言仍旧需要)
ListErase(phead->next);
//不借助Erase函数的写法:
//头删,有效头结点,新的有效头结点
LTNode* second = phead->next;
LTNode* secondnext = second->next;
//改变指向关系
phead->next = secondnext;
secondnext->prev = phead;
//释放结点
free(second);
}
头删测试结果
pos位置前插入
//pos位置前插入
//思考:带头双向链表,在pos位置前插入,prev指针的存在,不用遍历找pos位置
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
//确定指针
LTNode* posprev = pos->prev;
LTNode* newnode = BuyListNode(x);
//修改关系
//对newnode:
newnode->next = pos;
newnode->prev = posprev;
//对pos:
pos->prev = newnode;
//对posprev:
posprev->next = newnode;
}
pos位置处删除
//pos位置处删除
void ListErase(LTNode* pos)
{
assert(pos);
//确定所需指针:
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
//修改关系:
posprev->next = posnext;
posnext->prev = posprev;
//删除结点:
free(pos);
}
pos任意位置测试结果