1、带头双向循环链表的结构
- 双向循环链表有一个头节点,这个头节点不存储数据,只起到标记头部的位置。
- 链表中某一节点的指针域分为两部分,一部分储存下一节点的地址,另一部分储存上一节点的地址。
- 通过指针域,链表的头节点与尾节点相连,形成循环。
带头双向循环链表因其结构优势,在实现增删查改不用进行遍历,时间复杂度为O(1)。
利用结构体将带头双向循环链表定义出来
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
- 如代码所示,结构体中有两个成员为指针域,一个成员为数据域。
- 将结构体类型与结构体中的成员数据域类型都用 typedef重新定义,便于代码维护。
2、链表的初始化
LTNode* ListInit()
{
LTNode* guard = (LTNode*)malloc(sizeof(LTNode));
if (guard == NULL)
{
perror("malloc");
exit(-1);
}
guard->next = guard;
guard->prev = guard;
return guard;
}
- exit(),是将整个程序终止,退出当前运行的程序。exit(0);表示程序正常退出,exit(其他值);表示程序异常退出。
return,是将该段函数运行中止,回到调用的函数或者回到主函数中。 - 为什么用 exit ?
当动态内存申请失败是,除了申请的内存实在过大,一般是自己的电脑硬件内存不够用了,所以直接将程序结束,去检查一下自己的电脑内存吧。 - 带头双向循环链表为空时,指针域next与指针域prev都指向自己,即自己指向自己。
3、链表的打印
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
- 从第一个存储有效的数据的节点开始打印(头节点的下一个节点),直到再次打印到头节点,链表中的所有数据打印完毕。
4、链表判空
bool ListEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
1、链表中的返回值:相等说明是空,且相等为真,所以空时返回值为真,非空时,返回值为假
5、链表申请一个节点
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc");
exit(-1);
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
- 当链表中需要插入数据时,即需要开辟一个新的节点,利用动态内存开辟函数为其开辟一块空间。
- 将新开辟空间指针域的两个部分全部置空,数据域赋值为要插入的数据。
6、链表尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
- 尾插先找到尾,尾节点就是头节点的上一个节点
- 再将新开辟的节点(即要插入的节点)与头节点还有之前的尾节点联系起来。
链表尾插并打印
void TestList1()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
}
int main()
{
TestList1();
return 0;
}
打印结果
- 数据1 2 3 4依次往后插入。
7、链表头插
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
- 链表头插首先要标记第一个有效存储数据的节点,便于新节点插入后能够找到并产生联系。
链表头插并打印
void TestList2()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPushFront(plist, 100);
ListPushFront(plist, 200);
ListPushFront(plist, 300);
ListPrint(plist);
}
int main()
{
TestList2();
return 0;
}
打印结果
- 数据 300 200 100依次在链表头上插入。
8、链表尾删
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
LTNode* tail = phead->prev;
LTNode* prev = tail->prev;
prev->next = phead;
phead->prev = prev;
free(tail);
tail = NULL;
}
- 删节点之前要先判断链表是否为空,为空即没得删,用断言函数断言。
- 头节点 phead 为空也没得删,同样用断言函数断言。
- 删尾部之前要先找到尾部,尾节点就是头节点的上一个节点
- 将尾节点的上一个节点找到并标记起来,便于后续与头节点产生联系。
链表尾删并打印
void TestList3()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPopBack(plist);
ListPopBack(plist);
ListPopBack(plist);
ListPopBack(plist);
ListPrint(plist);
}
int main()
{
TestList3();
return 0;
}
打印结果
- 调用4次尾删函数,之前插入的四个数据1 2 3 4 全被删除了。
9、链表头删
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
LTNode* first = phead->next;
LTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
first = NULL;
}
- 删除前判空,即判断是否有节点可删。
- 判断传过来的头节点 phead 是否为空,用断言函数。
- 带头双向循环链表的头删即删除第一个有效存储数据的节点。
- 将第一个有效存储数据的节点标记出来,便于后续能够找到并free掉。
- 将第二个有效存储数据的节点标记出来,便于后续能够找到并与头节点产生联系。
链表头删并打印
void TestList4()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
ListPopFront(plist);
ListPopFront(plist);
ListPrint(plist);
}
int main()
{
TestList4();
return 0;
}
打印结果
- 调用两次头删函数,数据 1 2 被删除了。
10、求链表的长度
size_t ListSize(LTNode* phead)
{
assert(phead);
size_t n = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
n++;
cur = cur->next;
}
return n;
}
- 遍历一遍链表就能求出链表长度。
- 遍历链表结束标志是再次遇到头节点。
调用求链表长度函数
void TestList5()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
size_t len = ListSize(plist);
printf("%zd\n", len);
ListPopFront(plist);
ListPopFront(plist);
size_t n = ListSize(plist);
len = ListSize(plist);
printf("%zd\n", len);
}
int main()
{
TestList5();
return 0;
}
打印结果
- 插入四个数据,链表长度是4。
- 再头删两个数据,链表长度变成2。
11、查找位置
LTNode* ListFind(LTNode* phead, LTDataType x)
{
assert(phead);
size_t n = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
- 遍历链表,找到给定的数据后,返回该数据的位置。
- 遍历完后还没找到,说明链表中没有要找的数据,返回空指针。
调用查找函数并打印
void TestList6()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
LTNode* pos = ListFind(plist,2);
printf("%p\n", pos);
}
int main()
{
TestList6();
return 0;
}
打印结果
- 查找数据 2 的位置,如代码所示,找到了。
12、在给定位置pos处插入数据
在给定pos位置插入数据,带头双向循环链表默认是在pos位置之前插入的
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyListNode(x);
newnode->next = pos;
pos->prev = newnode;
prev->next = newnode;
newnode->prev = prev;
}
- 插入数据,需要开辟一个新的节点。
- 找到给定pos位置的上一个节点并标记出来,便于后续找到并与新节点产生联系。
插入数据
void TestList7()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
LTNode* pos = ListFind(plist, 2);
if (pos != NULL)
{
ListInsert(pos, 300);
}
ListPrint(plist);
}
int main()
{
TestList7();
return 0;
}
打印结果
- 如结果所示,在2前面插入了一个300。
13、删除给定位置pos处的数据
void LiseErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
- 将pos位置的上一个节点prev与下一个节点next找到并标记出来。
- 将上一个节点prev与下一个节点next通过指针域联系起来。
- 将pos位置free掉,及时置空。
删除数据
void TestList8()
{
LTNode* plist = ListInit();
ListPushBack1(plist, 1);
ListPushBack1(plist, 2);
ListPushBack1(plist, 3);
ListPushBack1(plist, 4);
ListPrint(plist);
LTNode* pos = ListFind(plist, 2);
if (pos != NULL)
{
LiseErase(pos);
}
ListPrint(plist);
}
int main()
{
TestList8();
return 0;
}
打印结果
- 数据 2 被删除了。
14、尾插写法2
void ListPushBack1(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead, x);
}
- 在头节点前面插入就是尾插。直接调用 ListInsert(LTNode* pos, LTDataType x);函数。
15、头插写法2
void ListPushFront1(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead->next, x);
}
- 在第一个有效存储数据的节点前插入就是头插,直接调用 ListInsert(LTNode* pos, LTDataType x);函数。
16、尾删写法2
void ListPopBack1(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
LiseErase(phead->prev);
}
- 直接调用 LiseErase(LTNode* pos);函数,将尾部删除即可。
17、头删写法2
void ListPopFront1(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
LiseErase(phead->next);
}
- 直接调用 LiseErase(LTNode* pos);函数,将第一个有效存储数据的节点删除即可。
18、链表销毁
void ListDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
- 遍历链表,将链表的每一个节点都free掉。
- 最后再将头节点也free掉。
- free掉头节点后需要将头节点置空,但是因为这里是一级指针,即头节点phead本身,在函数中将其置空,不能改变---->形参是实参的临时拷贝,对形参的改变,改变不了实参。所以需要使用者在函数外部主动置空。
链表销毁展示
void TestList9()
{
LTNode* plist = ListInit();
ListPushBack1(plist, 1);
ListPushBack1(plist, 2);
ListPushBack1(plist, 3);
ListPushBack1(plist, 4);
ListPrint(plist);
ListDestory(plist);
plist = NULL;
ListPrint(plist);
}
int main()
{
TestList9();
return 0;
}
打印结果
- 我们可以看到,在链表销毁后再次打印,之前打印函数中写的断言函数起作用了,说明链表已经销毁完毕。
19、带头双向循环链表的所有代码
List.h文件:写函数的声明,函数的头文件,其他 .c文件只要包含List.h文件—>#include “List.h”,就相当于写了函数的声明与头文件。如下:
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
LTNode* ListInit();
void ListPrint(LTNode* phead);
bool ListEmpty(LTNode* phead);
void ListPushBack(LTNode* phead, LTDataType x);
void ListPushFront(LTNode* phead, LTDataType x);
void ListPopBack(LTNode* phead);
void ListPopFront(LTNode* phead);
size_t ListSize(LTNode* phead);
LTNode* ListFind(LTNode* phead, LTDataType x);
void ListInsert(LTNode* pos, LTDataType x);
void LiseErase(LTNode* pos);
void ListPushBack1(LTNode* phead, LTDataType x);
void ListPushFront1(LTNode* phead, LTDataType x);
void ListPopBack1(LTNode* phead);
void ListPopFront1(LTNode* phead);
void ListDestory(LTNode* phead);
List.c文件:用来实现链表的各种功能接口函数。如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
LTNode* ListInit()
{
LTNode* guard = (LTNode*)malloc(sizeof(LTNode));
if (guard == NULL)
{
perror("malloc");
exit(-1);
}
guard->next = guard;
guard->prev = guard;
return guard;
}
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
bool ListEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc");
exit(-1);
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
LTNode* tail = phead->prev;
LTNode* prev = tail->prev;
prev->next = phead;
phead->prev = prev;
free(tail);
tail = NULL;
}
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
LTNode* first = phead->next;
LTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
first = NULL;
}
size_t ListSize(LTNode* phead)
{
assert(phead);
size_t n = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
n++;
cur = cur->next;
}
return n;
}
LTNode* ListFind(LTNode* phead, LTDataType x)
{
assert(phead);
size_t n = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyListNode(x);
newnode->next = pos;
pos->prev = newnode;
prev->next = newnode;
newnode->prev = prev;
}
void LiseErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
void ListPushBack1(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead, x);
}
void ListPushFront1(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead->next, x);
}
void ListPopBack1(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
LiseErase(phead->prev);
}
void ListPopFront1(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
LiseErase(phead->next);
}
void ListDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
test.c文件:主函数写在这,在主函数中可以调用 List.c文件中实现的各种接口。如下:
int main()
{
return 0;
}
以上就是无头单向非循环链表链表的所有代码,在SList.c文件中实现的函数接口有以下注意:
- 函数对指定的地址要进行断言,避免传过来的是空指针。
- 形参传过来的不是头节点的地址,如果需要改变头节点本身,需要使用者在使用完函数后,在主函数中自己更改。
内容创作不易😁😁 你的关注与支持就是我的创作动力🧡💛 动动你的发财手给个一键三连吧😉 非常感谢!🌹🌹🌹
|