当前位置: 首页 > news >正文

数据结构——单向链表和双向链表的实现(C语言版)

目录

前言

1. 链表

1.1 链表的概念及结构

1.2 链表的分类

2. 单链表接口实现

2.1 数据结构设计与接口函数声明

2.2 创建结点,打印,查找

2.3 尾插,头插,尾删,头删

2.4 插入或删除

2.4.1在指定位置后

2.4.2在指定位置前

2.5 销毁链表

3. 双向带头循环链表

3.1 数据结构设计与接口函数声明

3.2 初始化,销毁,打印,动态创建结点

3.3 尾插,头插,尾删,头删

3.4 查找,插入和删除

4.链表和顺序表的区别

5. 源代码

5.1 单链表

(1)SList.h

(2)SList.c

(3)SLtest.c

5.2 双向链表

(1)Linked LIst.h

(2)Linked List.c

(3)Ltest.c

总结


前言

这篇文章关于链表的介绍,还有单向链表和双向链表的C语言实现,内容干货满满,建议边看边上手敲代码!


1. 链表

1.1 链表的概念及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

 如下图所示:

注意:

  1. 从上图可看出,链式结构逻辑上是连续的,但在物理上不一定连续的,地址存放分布不均。
  2. 现实中的节点一般都是从堆上申请出来的。
  3. 从堆上申请的空间,是按照一定策略来分配的,两次申请的空间可能连续,也可能不连续。

1.2 链表的分类

实际中链表有这三种分法,单向或者双向,带头或不带头,循环或者非循环。

  1. 单向或双向
  2. 带头或者不带头
  3. 循环或者非循环

虽然有这么多的链表结构,但是我们实际中最常用还是两种结构

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
  2.  带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

2. 单链表接口实现

在实现接口时,需要创建三个文件,分别是SList.hSList.cSLtest.c这三个文件,第一个是写入单链表数据结构设计和接口函数声明来串联三个文件,第二个是完成各个接口函数内部代码实现,第三个是来测试各个接口功能情况。

2.1 数据结构设计与接口函数声明

单链表结构体中有存储一个数据的变量,但与顺序表的不同之处,是使用指针联系着下一个结点,所以在创建个相同结构体的指针next。单链表只需要知道头结点的结构体指针就可以进行各种接口的实现,所以不用创建一个初始化接口,创建一个结构体指针就可以。

typedef int SLTDataType;typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;//动态申请一个结点
SLTNode* BuyListNode(SLTDataType x);
//单链表打印
void SListPrint(SLTNode* phead);
//单链表尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//单链表头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//单链表尾删
void SListPopBack(SLTNode** pphead);
//单链表头删
void SListPopFront(SLTNode** pphead);
//单链表查找
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//在pos位置之前去插入一个节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 在pos位置后面插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个节点
void SListEraseAfter(SLTNode** pphead, SLTNode* pos);
//单链表销毁
void SListDestory(SLTNode** pphead);

2.2 创建结点,打印,查找

 创建新结点是为了后面接口实现做准备。

SLTNode* BuyListNode(SLTDataType x)
{   //动态申请新结点SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){   //可能开辟内存失败,加上判断,增强代码的健壮性printf("malloc fail\n");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}

打印链表中的内容,用的是while循环,判断条件是cur指针不为空。

void SListPrint(SLTNode* phead)
{SLTNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}

assert是断言,判断phead是否为空结点,用while循环遍历整个链表。

SListNode* SListFind(SLTNode* phead, SLTDataType x)
{assert(phead);SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}else{cur = cur->next;}}return NULL;
}

2.3 尾插,头插,尾删,头删

因为只需要知道指向头结点的指针变量,就可以进行操作,但是第一次我们创建的头结点为空指针,尾插的时候如果传入一级指针在尾插函数内部修改头节点的值,无法影响头结点,因为尾插函数内的头结点的一份临时拷贝,即形参,改变形参的值是无法影响实参的值,所以要传入二级指针,通过指针变量的指针修改一级指针的值。故之后关于头结点的删除或者插入,都需要传二级指针,这是链表实现中较难理解的点。

void SListPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);//头结点为空时,直接赋值if (*pphead == NULL){*pphead = newnode;}else{//找到尾节点SLTNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}

我们写一个测试函数,测试一下尾插函数的功能。之后的测试函数,只写全局函数Test,不展示main函数的部分。

#include "SLinked list.h"void TestSlist1()
{SLTNode* plist = NULL;SListPushBack(&plist, 1);SListPushBack(&plist, 2);SListPushBack(&plist, 3);SListPushBack(&plist, 4);SListPrint(plist);
}int main()
{TestSlist1();return 0;
}

输出结果:

头插较为简单,只需要创建一个新结点,新结点的next指针指向头结点,再把头结点指向新结点。

void SListPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);newnode->next = *pphead;*pphead = newnode;
}

再写一个测试函数:

void TestSlist2()
{SLTNode* plist = NULL;SListPushFront(&plist, 1);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 4);SListPrint(plist);
}

输出的结果:

尾删要分情况,分为链表内有一个节点还是两个结点及以上。如果链表内只有一个结点直接释放头结点,并将其置为空指针,如果是两个节点及以上,需要找到尾结点的位置,用while循环遍历链表,新建一个tail指针,当tail的next指针为空时,便找到尾结点,然后进行释放操作。

void SListPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead != NULL);if ((*pphead)->next == NULL){//1. 一个节点free(*pphead);*pphead = NULL;}else{	//2. 两个及以上的节点SLTNode* tail = *pphead;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;}
}

在之前Test1函数上稍加改动:

void TestSlist1()
{SLTNode* plist = NULL;SListPushBack(&plist, 1);SListPushBack(&plist, 2);SListPushBack(&plist, 3);SListPushBack(&plist, 4);SListPrint(plist);SListPopBack(&plist);SListPopBack(&plist);SListPopBack(&plist);SListPrint(plist);
}

输出的结果:

但是如果你调用尾删次数超过链表存储数据个数,就会报错。所以调用尾删函数需注意。

头删函数只需在创建一个next指针,并赋值为头结点的下一个结点,释放头结点,再赋值。

void SListPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead != NULL);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}

同理,这次在Test2函数上进行改动即可。

void TestSlist2()
{SLTNode* plist = NULL;SListPushFront(&plist, 1);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 4);SListPrint(plist);SListPopFront(&plist);SListPopFront(&plist);SListPopFront(&plist);SListPopFront(&plist);SListPrint(plist);
}

输出的结果:

2.4 插入或删除

一般是在指定位置后插入或删除,这是因为单链表的结点只有下一个结点的地址,如果想要在指定位置之前插入,需要从头结点开始遍历,消耗时间。

2.4.1在指定位置后

插入操作:

// 在pos位置后面插入,这个更适合单链表
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuyListNode(x);if (newnode == NULL){return;}newnode->next = pos->next;pos->next = newnode;
}

删除操作:

void SListEraseAfter(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos->next);SLTNode* next = pos->next;pos->next = next->next;free(next);
}

写个测试函数,先通过查找函数,获得想要结点的地址,再修改。

void TestSlist3()
{SLTNode* plist = NULL;SListPushFront(&plist, 1);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 4);SListPrint(plist);SLTNode* pos = SListFind(plist, 2);if (pos){SListInsertAfter(pos, 30);}SListPrint(plist);pos = SListFind(plist, 30);if (pos){SListEraseAfter(&plist, pos);}SListPrint(plist);
}

输出的结果:

2.4.2在指定位置前

在进行插入操作之前,应该先判断该位置是否为头结点,如果是头结点,直接头插;如果不是,需要遍历链表找到该位置结点的前一个节点,再插入。

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);if (*pphead == pos){newnode->next = *pphead;*pphead = newnode;}else{// 找到pos的前一个位置SLTNode* posPrev = *pphead;while (posPrev->next != pos){posPrev = posPrev->next;}posPrev->next = newnode;newnode->next = pos;}
}

删除也是一样,需要区分该位置是否为头结点,不是的话需要先找到该位置前一个节点,再删除。

void SListErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);//删除头节点if (*pphead == pos){*pphead = pos->next;free(pos);//要不要把pos置为空指针呢pos = NULL;}else{	//找前一个节点SLTNode* posPrev = *pphead;while (posPrev->next != pos){posPrev = posPrev->next;}//开始删除posPrev->next = pos->next;free(pos);pos = NULL;}
}

再写一个测试函数,与前一个测试函数类似。

void TestSlist4()
{SLTNode* plist = NULL;SListPushFront(&plist, 1);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 4);SListPrint(plist);SLTNode* pos = SListFind(plist, 2);if (pos){SListInsert(&plist, pos, 30);}SListPrint(plist);pos = SListFind(plist, 1);if (pos){SListErase(&plist, pos);}SListPrint(plist);
}

 输出的结果:

2.5 销毁链表

销毁链表需要遍历整个链表,因为链表上的每一个结点都是动态开辟出来的。

void SListDestory(SLTNode** pphead)
{assert(pphead);SLTNode* cur = *pphead;while (cur){SLTNode* next = cur->next;free(cur); cur = next;}*pphead = NULL;
}

3. 双向带头循环链表

开始之前,需要先创建三个文件List.h ,List.c和Ltest.c这三个文件。

  • Linked List.h文件包含所有用到的头文件,还有数据结构的设计和各种接口函数声明。
  • Linked List.c文件完成所有接口函数的实现。
  • Ltest.c主要代码是测试函数,来测试接口函数功能是否达标。

3.1 数据结构设计与接口函数声明

双向链表的数据结构中,不只有指向下一个结点的指针,还有指向上一个结点的指针。

typedef int LTDataType;typedef struct ListNode
{LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;//创建返回链表的头结点
LTNode* ListInit();
//双向链表的销毁
void ListDestroy(LTNode* phead);
//双向链表打印
void ListPrint(LTNode* phead);
//双向链表尾插
void ListPushBack(LTNode* phead, LTDataType x);
//双向链表头插
void ListPopBack(LTNode* phead);
//双向链表尾删
void ListPushFront(LTNode* phead, LTDataType x);
//双向链表头删
void ListPopFront(LTNode* phead);
//双向链表查找
LTNode* ListFind(LTNode* phead, LTDataType x);
//双向链表在pos的前面进行插入
void ListInsert(LTNode* pos, LTDataType x);
//双向链表删除pos位置的结点
void ListErase(LTNode* pos);

3.2 初始化,销毁,打印,动态创建结点

初始化的时候不是都只为空,需要创建一个哨兵位结点,不存储有效数据,并且next和prev都需要指向头结点,函数返回类型是LTNode*这样子就不需要传入二级指针。

LTNode* ListInit()
{LTNode* phead = (LTNode*)malloc(sizeof(LTNode));if (phead == NULL){exit(1);}phead->next = phead;phead->prev = phead;return phead;
}

销毁操作跟单链表相同,需要逐个释放,最后头结点也得释放,但是不需要将头结点置为空指针,因为传入的是一级指针,此时销毁函数内的是形参,改变形参无法影响实参。

void ListDestroy(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);
}

打印函数使用while循环遍历链表即可。

void ListPrint(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}

动态创建一个新结点用处很大。注意next和prev都要置为空指针。

LTNode* BuyListNode(LTDataType x)
{LTNode* ptr = (LTNode*)malloc(sizeof(LTNode));if (ptr != NULL){LTNode* newnode = ptr;newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;}exit(1);
}

3.3 尾插,头插,尾删,头删

尾插不需要查找,只需要通过prev指针就能定位到。

void ListPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* tail = phead->prev;LTNode* newnode = BuyListNode(x);newnode->data = x;//改变结点连接关系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* next = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = next;next->prev = newnode;
}

尾删需要注意不能删除到哨兵位,通过断言头结点的下一个结点不能指向本身。

void ListPopBack(LTNode* phead)
{assert(phead);assert(phead->next != phead);LTNode* tail = phead->prev;LTNode* tailPrev = tail->prev;free(tail);tailPrev->next = phead;phead->prev = tailPrev;
}

头删的断言跟尾删一样,接下来就是释放并改变结点的指向。

void ListPopFront(LTNode* phead)
{assert(phead);assert(phead->next != phead);LTNode* next = phead->next;LTNode* nextNext = next->next;phead->next = nextNext;nextNext->prev = phead;free(next);
}

3.4 查找,插入和删除

查找函数跟单链表查找函数类似,遍历链表。

LTNode* ListFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x)return cur;cur = cur->next;}printf("\n");
}

插入函数因为双链表结构的复杂性,反而变得十分简单,不需要遍历链表。

//pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* posPrev = pos->prev;LTNode* newnode = BuyListNode(x);//posPrev newnode posposPrev->next = newnode;newnode->prev = posPrev;newnode->next = pos;pos->prev = newnode;
}

删除函数也是,注意可通过新创建几个变量区分pos位置的前一个结点和后一个结点,方便操作。

//删除pos位置
void ListErase(LTNode* pos)
{assert(pos);// posPrev  pos  posNextLTNode* posPrev = pos->prev;LTNode* posNext = pos->next;posPrev->next = posNext;posNext->prev = posPrev;free(pos);pos = NULL;
}

写个测试函数:

void TestList2()
{LTNode* plist = ListInit();ListPushFront(plist, 1);ListPushFront(plist, 2);ListPushFront(plist, 3);ListPushFront(plist, 4);ListPrint(plist);LTNode* pos = ListFind(plist, 3);if (pos){ListInsert(pos, 30);}ListPrint(plist);pos = ListFind(plist, 2);if (pos){ListErase(pos);}ListPrint(plist);ListDestroy(plist);plist = NULL;
}

输出的结果:

4.链表和顺序表的区别

         不同点                顺序表                  链表
存储空间上  物理上一定连续逻辑上连续,物理上不一定连续
随机访问  支持O(1)  不支持 :O(N)
任意位置插入或删除元素可能需要搬移元素,效率低O(N)只需要修改指针指向
插入动态顺序表,空间不够需要扩容没有容量的概念
应用场景 元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

5. 源代码

5.1 单链表

(1)SList.h

#pragma once#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>typedef int SLTDataType;typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;//动态申请一个结点
SLTNode* BuyListNode(SLTDataType x);
//单链表打印
void SListPrint(SLTNode* phead);
//单链表尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//单链表头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//单链表尾删
void SListPopBack(SLTNode** pphead);
//单链表头删
void SListPopFront(SLTNode** pphead);
//单链表查找
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//在pos位置之前去插入一个节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 在pos位置后面插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个节点
void SListEraseAfter(SLTNode** pphead, SLTNode* pos);
//单链表销毁
void SListDestory(SLTNode** pphead);

(2)SList.c

#include "SLinked list.h"SLTNode* BuyListNode(SLTDataType x)
{//动态申请新结点SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){//可能开辟内存失败,加上判断,增强代码的健壮性printf("malloc fail\n");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}void SListPrint(SLTNode* phead)
{SLTNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{assert(phead);SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}else{cur = cur->next;}}return NULL;
}void SListPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);if (*pphead == NULL){*pphead = newnode;}else{//找到尾节点SLTNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}void SListPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);newnode->next = *pphead;*pphead = newnode;
}void SListPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead != NULL);if ((*pphead)->next == NULL){//1. 一个节点free(*pphead);*pphead = NULL;}else{	//2. 两个及以上的节点SLTNode* tail = *pphead;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;}
}void SListPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead != NULL);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}// 在pos位置后面插入,这个更适合单链表
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuyListNode(x);if (newnode == NULL){return;}newnode->next = pos->next;pos->next = newnode;
}void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);if (*pphead == pos){newnode->next = *pphead;*pphead = newnode;}else{// 找到pos的前一个位置SLTNode* posPrev = *pphead;while (posPrev->next != pos){posPrev = posPrev->next;}posPrev->next = newnode;newnode->next = pos;}
}void SListErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);//删除头节点if (*pphead == pos){*pphead = pos->next;free(pos);//要不要把pos置为空指针呢pos = NULL;}else{	//找前一个节点SLTNode* posPrev = *pphead;while (posPrev->next != pos){posPrev = posPrev->next;}//开始删除posPrev->next = pos->next;free(pos);pos = NULL;}
}void SListEraseAfter(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos->next);SLTNode* next = pos->next;pos->next = next->next;free(next);
}

(3)SLtest.c

#include "SLinked list.h"void TestSlist1()
{SLTNode* plist = NULL;SListPushBack(&plist, 1);SListPushBack(&plist, 2);SListPushBack(&plist, 3);SListPushBack(&plist, 4);SListPrint(plist);SListPopBack(&plist);SListPopBack(&plist);SListPopBack(&plist);SListPrint(plist);
}void TestSlist2()
{SLTNode* plist = NULL;SListPushFront(&plist, 1);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 4);SListPrint(plist);SListPopFront(&plist);SListPopFront(&plist);SListPopFront(&plist);SListPopFront(&plist);SListPrint(plist);
}void TestSlist3()
{SLTNode* plist = NULL;SListPushFront(&plist, 1);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 4);SListPrint(plist);SLTNode* pos = SListFind(plist, 2);if (pos){SListInsertAfter(pos, 30);}SListPrint(plist);pos = SListFind(plist, 30);if (pos){SListEraseAfter(&plist, pos);}SListPrint(plist);
}void TestSlist4()
{SLTNode* plist = NULL;SListPushFront(&plist, 1);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 4);SListPrint(plist);SLTNode* pos = SListFind(plist, 2);if (pos){SListInsert(&plist, pos, 30);}SListPrint(plist);pos = SListFind(plist, 1);if (pos){SListErase(&plist, pos);}SListPrint(plist);
}int main()
{//TestSlist1();//TestSlist2();//TestSlist3();TestSlist4();return 0;
}

5.2 双向链表

(1)Linked LIst.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int LTDataType;typedef struct ListNode
{LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;//创建返回链表的头结点
LTNode* ListInit();
//双向链表的销毁
void ListDestroy(LTNode* phead);
//双向链表打印
void ListPrint(LTNode* phead);
//双向链表尾插
void ListPushBack(LTNode* phead, LTDataType x);
//双向链表头插
void ListPopBack(LTNode* phead);
//双向链表尾删
void ListPushFront(LTNode* phead, LTDataType x);
//双向链表头删
void ListPopFront(LTNode* phead);
//双向链表查找
LTNode* ListFind(LTNode* phead, LTDataType x);
//双向链表在pos的前面进行插入
void ListInsert(LTNode* pos, LTDataType x);
//双向链表删除pos位置的结点
void ListErase(LTNode* pos);

(2)Linked List.c

#include "Linked List.h"LTNode* ListInit()
{LTNode* phead = (LTNode*)malloc(sizeof(LTNode));if (phead == NULL){exit(1);}phead->next = phead;phead->prev = phead;return phead;
}LTNode* BuyListNode(LTDataType x)
{LTNode* ptr = (LTNode*)malloc(sizeof(LTNode));if (ptr != NULL){LTNode* newnode = ptr;newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;}exit(1);
}void ListPrint(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}void ListPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* tail = phead->prev;LTNode* newnode = BuyListNode(x);newnode->data = x;//改变结点连接关系tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;
}void ListPopBack(LTNode* phead)
{assert(phead);assert(phead->next != phead);LTNode* tail = phead->prev;LTNode* tailPrev = tail->prev;free(tail);tailPrev->next = phead;phead->prev = tailPrev;
}void ListPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyListNode(x);LTNode* next = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = next;next->prev = newnode;
}void ListPopFront(LTNode* phead)
{assert(phead);assert(phead->next != phead);LTNode* next = phead->next;LTNode* nextNext = next->next;phead->next = nextNext;nextNext->prev = phead;free(next);
}LTNode* ListFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x)return cur;cur = cur->next;}printf("\n");
}//pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* posPrev = pos->prev;LTNode* newnode = BuyListNode(x);//posPrev newnode posposPrev->next = newnode;newnode->prev = posPrev;newnode->next = pos;pos->prev = newnode;
}//删除pos位置
void ListErase(LTNode* pos)
{assert(pos);// posPrev  pos  posNextLTNode* posPrev = pos->prev;LTNode* posNext = pos->next;posPrev->next = posNext;posNext->prev = posPrev;free(pos);pos = NULL;
}void ListDestroy(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);
}

(3)Ltest.c

测试函数也可以自行封装几个,参照单链表的测试函数。

#include "Linked List.h"void TestList1()
{LTNode* plist = ListInit();ListPushBack(plist, 1);ListPushBack(plist, 2);ListPushBack(plist, 3);ListPushBack(plist, 4);ListPrint(plist);ListPopBack(plist);ListPopBack(plist);ListPrint(plist);}void TestList2()
{LTNode* plist = ListInit();ListPushFront(plist, 1);ListPushFront(plist, 2);ListPushFront(plist, 3);ListPushFront(plist, 4);ListPrint(plist);LTNode* pos = ListFind(plist, 3);if (pos){ListInsert(pos, 30);}ListPrint(plist);pos = ListFind(plist, 2);if (pos){ListErase(pos);}ListPrint(plist);ListDestroy(plist);plist = NULL;
}int main()
{//TestList1();TestList2();return 0;
}


总结

通过这篇文章,相信你已经对链表这个数据结构有了一定的了解,可以开始刷一些链表的OJ题目。如果只是看了一遍,建议上手敲敲代码,实践出真知。

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连哦,你的支持的我最大的动力!!!

相关文章:

数据结构——单向链表和双向链表的实现(C语言版)

目录 前言 1. 链表 1.1 链表的概念及结构 1.2 链表的分类 2. 单链表接口实现 2.1 数据结构设计与接口函数声明 2.2 创建结点&#xff0c;打印&#xff0c;查找 2.3 尾插&#xff0c;头插&#xff0c;尾删&#xff0c;头删 2.4 插入或删除 2.4.1在指定位置后 2.4.2在…...

TCP和UDP相关问题(重点)(4)——4.使用TCP的协议有哪些?使用UDP的协议有哪些?

4.使用TCP的协议有哪些&#xff1f;使用UDP的协议有哪些&#xff1f; 使用TCP的协议有&#xff1a;HTTP3.0之前的HTTP协议、HTTPS、FTP、SMTP、SSH... 使用UDP的协议有&#xff1a;HTTP3.0、DNS、DHCP......

Python进阶--爬取美女图片壁纸(基于回车桌面网的爬虫程序)

目录 一、前言 二、爬取下载美女图片 1、抓包分析 a、分析页面 b、明确需求 c、抓包搜寻 d、总结特点 2、编写爬虫代码 a、获取图片页网页源代码 b、提取所有图片的链接和标题 c、下载并保存这组图片 d、 爬取目录页的各种类型美女图片的链接 e、实现翻页 三、各…...

[office] excel如何计算毛重和皮重的时间间隔 excel计算毛重和皮重时间间隔方法 #笔记#学习方法

excel如何计算毛重和皮重的时间间隔 excel计算毛重和皮重时间间隔方法 在日常工作中经常会到用excel&#xff0c;有时需要计算毛重和皮重的时间间隔&#xff0c;具体的计算方式是什么&#xff0c;一起来了解一下吧 在日常工作中经常会到用excel&#xff0c;在整理编辑过磅数据…...

Pandas 对带有 Multi-column(多列名称) 的数据排序并写入 Excel 中

Pandas 从Excel 中读取带有 Multi-column的数据 正文 正文 我们使用如下方式写入数据&#xff1a; import pandas as pd import numpy as npdf pd.DataFrame(np.array([[10, 2, 0], [6, 1, 3], [8, 10, 7], [1, 3, 7]]), columns[[Number, Name, Name, ], [col 1, col 2, co…...

如何为Kafka加上账号密码(一)

Kafka认证基本概念 一直以来&#xff0c;我们公司内网的Kafka集群都是在裸奔&#xff0c;只要知道端口号&#xff0c;任何人都能连上集群操作一番。直到有个主题莫名消失&#xff0c;才引起我们的警觉&#xff0c;是时候该考虑为它添加一套认证策略了。 认证和授权就是一对孪生…...

Elasticsearch的Index Lifecycle Management(ILM)

Elasticsearch的Index Lifecycle Management&#xff08;ILM&#xff09;功能提供了一种自动化管理索引生命周期的方式。ILM使得用户可以基于特定的条件&#xff08;如索引的年龄、大小等&#xff09;来自动执行如回滚、删除等操作&#xff0c;进而优化存储和提高查询性能。ILM…...

2、学习 Nacos 注册中心

学习 Nacos 注册中心 一、使用Nacos作为注册中心1、父pom.xml文件配置SpringCloudAlibaba的dependency-management依赖2、在微服务中添加Nacos客户端依赖3、配置Nacos服务地址 二、服务的分级存储模型1、配置实例的集群属性2、权重配置 三、命名空间 一、使用Nacos作为注册中心…...

Java 如何操作 nginx 服务器上的文件?

随着Java技术的不断发展&#xff0c;越来越多的开发人员开始使用Java来操作服务器上的文件。其中&#xff0c;如何操作nginx服务器上的文件也是许多Java开发人员所关注的重点之一。本文将介绍Java操作nginx服务器上文件的基本方法。 一、使用Java的File类 Java的File类可以用…...

时序预测 | MATLAB实现基于CNN-GRU-AdaBoost卷积门控循环单元结合AdaBoost时间序列预测

时序预测 | MATLAB实现基于CNN-GRU-AdaBoost卷积门控循环单元结合AdaBoost时间序列预测 目录 时序预测 | MATLAB实现基于CNN-GRU-AdaBoost卷积门控循环单元结合AdaBoost时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 1.MATLAB实现基于CNN-GRU-AdaBo…...

中创ET4410 台式LCR数字电桥 简单开箱测评

最近买了一台LCR电桥&#xff0c;完善一下自己实验室的设备&#xff0c;选了中创ET4410&#xff0c;这款性价比高一点。 1199元在PDD买的&#xff0c;好像胜利的VC4090C也是找中创代工的。 ET4410介绍 本系列LCR数字电桥是采用自动平衡电桥原理设计的元件参数分析仪&#xf…...

格式化dingo返回内容

dingo api返回的内容中添加code 和 message &#xff0c;保持与异常返回的内容格式相一致。 失败会存在code 和 message &#xff0c;我们只需要关注成功的情况 非分页返回&#xff0c;可以创建一个父类controller&#xff0c;通过调用sucess方法来返回 class Controller ext…...

QGIS编译(跨平台编译)之四十六:minizip编译(Windows、Linux、MacOS环境下编译)

文章目录 一、minizip介绍二、minizip下载三、Linux下编译四、MacOS下编译五、Windows下编译一、minizip介绍 Minizip 是一个用于处理 ZIP 文件的开源库,它基于 zlib 库构建。zlib 是一个广泛使用的、免费的、开源的压缩库,提供数据压缩和解压缩功能。Minizip 扩展了 zlib 的…...

MySQL进阶查询篇(1)-索引的类型与创建

MySQL数据库索引是提高查询效率的重要手段之一。索引是一种特殊的数据结构&#xff0c;用于快速定位数据。通过创建索引&#xff0c;可以大大提高查询性能&#xff0c;减少数据库的IO操作。 MySQL数据库支持多种不同类型的索引&#xff0c;常用的索引类型包括&#xff1a; B-…...

【STL】list模拟实现

vector模拟实现 一、接口大框架函数声明速览二、结点类的模拟实现1、构造函数 三、迭代器类的模拟实现1、迭代器类存在的意义2、迭代器类的模板参数说明3、构造函数4、运算符的重载&#xff08;前置和后置&#xff09;&#xff08;1&#xff09;前置&#xff08;2&#xff09;后…...

常用的文件系统、存储类型小整理

最近接触到了五花八门的文件系统、存储类型&#xff0c;名词听得头大&#xff0c;趁假期整理学习一番~ 名称OSSFastDFSJuiceFSCIFSCephFSEFSNFS全称Object Storage Service (对象存储服务)Fast Distributed File System (快速分布式文件系统)Juice File System (Juice 文件系统…...

Java写标准输出进度条

学Java这么久了&#xff0c;突发奇想写一个 进度条 玩玩&#xff0c;下面展示一下成功吧&#xff01; Java代码实现如下 public class ProcessBar {public static void main(String[] args) {//进度条StringBuilder processBarnew StringBuilder();//进度条长度int total100;/…...

leetcode 算法 69.x的平方根(python版)

需求 给你一个非负整数 x &#xff0c;计算并返回 x 的 算术平方根 。 由于返回类型是整数&#xff0c;结果只保留 整数部分 &#xff0c;小数部分将被 舍去 。 注意&#xff1a;不允许使用任何内置指数函数和算符&#xff0c;例如 pow(x, 0.5) 或者 x ** 0.5 。 示例 1&#…...

【golang】24、go get 和 go mod:indrect 与 go mod tidy

文章目录 go get 会执行如下操作&#xff1a; 操作 go.mod 文件&#xff08;add、update、remove&#xff09;下载依赖到 $GOPATH/pkg/mod 中若已安装&#xff0c;则更新该包&#xff0c;到最新版本 试验前置准备&#xff1a;首先删除已下载的依赖&#xff0c;rm -rf $GOPATH…...

AI算法工程师-非leetcode题目总结

AI算法工程师-非leetcode题目总结 除了Leetcode你还需要这些实现nms旋转矩形IOU手动实现BN手动实现CONV实现CrossEntropyLoss 除了Leetcode你还需要这些 希望大家留言&#xff0c;我可以进行补充。持续更新~~~ 实现nms import numpy as np def nms(dets, threshold):x1 dets…...

HTML 语义化

目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案&#xff1a; 语义化标签&#xff1a; <header>&#xff1a;页头<nav>&#xff1a;导航<main>&#xff1a;主要内容<article>&#x…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容

基于 ​UniApp + WebSocket​实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配​微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

C# 类和继承(抽象类)

抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...

(转)什么是DockerCompose?它有什么作用?

一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用&#xff0c;而无需手动一个个创建和运行容器。 Compose文件是一个文本文件&#xff0c;通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...

laravel8+vue3.0+element-plus搭建方法

创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

JavaScript基础-API 和 Web API

在学习JavaScript的过程中&#xff0c;理解API&#xff08;应用程序接口&#xff09;和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能&#xff0c;使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...

Golang——7、包与接口详解

包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...

【C++】纯虚函数类外可以写实现吗?

1. 答案 先说答案&#xff0c;可以。 2.代码测试 .h头文件 #include <iostream> #include <string>// 抽象基类 class AbstractBase { public:AbstractBase() default;virtual ~AbstractBase() default; // 默认析构函数public:virtual int PureVirtualFunct…...

Elastic 获得 AWS 教育 ISV 合作伙伴资质,进一步增强教育解决方案产品组合

作者&#xff1a;来自 Elastic Udayasimha Theepireddy (Uday), Brian Bergholm, Marianna Jonsdottir 通过搜索 AI 和云创新推动教育领域的数字化转型。 我们非常高兴地宣布&#xff0c;Elastic 已获得 AWS 教育 ISV 合作伙伴资质。这一重要认证表明&#xff0c;Elastic 作为 …...

MySQL的pymysql操作

本章是MySQL的最后一章&#xff0c;MySQL到此完结&#xff0c;下一站Hadoop&#xff01;&#xff01;&#xff01; 这章很简单&#xff0c;完整代码在最后&#xff0c;详细讲解之前python课程里面也有&#xff0c;感兴趣的可以往前找一下 一、查询操作 我们需要打开pycharm …...