顺序表和链表(2)

简介: 【10月更文挑战第23天】

【10月更文挑战第23天】
• 头部、中间位置的插入删除,时间复杂度变成O(1)

• 减少或者避免增容带来的性能消耗

• 避免空间浪费,要几个空间就给几个空间

链表是否能解决这些问题呢?

链表也是一个统称,链表是线性表的一种

逻辑结构:线性

物理结构不一定是线性的

链表的结构与概念

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

image.png

与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/结点”

我们的理解就是链表是由一个个节点组成的

节点之间的地址不一定是连续的

节点有两个部分组成:数据+指向下一个节点的指针

单链表 :SList single单身的

//节点的结构
typedef int LTDataType;//数据类型不一定是int类型
//方便我们后面一键替换
struct ListNode
{
    LTDataType  data;
    struct ListNode* next;//下个节点的指针,指向的类型是struct ListNode
};

在链表中没有增容的概念,直接插入新的数据,申请一个节点大小的空间就可以了,用malloc就行了

如果链表为空的话,pilist指向的是第一个结点,因为链表为空,那么plist指向的就是NULL

单链表的实现

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void SLTPrint(SLTNode* phead)
{
    SLTNode* pcur = phead;//pcur指向的是第一个节点的地址
    while (pcur)//最后的时候pcur为NULL,我们就跳出了循环
    {
        printf("%d->", pcur->data);
        pcur = pcur->next;//next指针保存的是下个节点的地址
    }
    printf("NULL\n");
}

//申请新节点
SLTNode* SLTBuyNode(SLTDataType x)
{
    SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));//申请一个新节点
    //一个节点的大小就是结构体的大小
    if (node == NULL)
    {
        perror("malloc fail!");
        exit(1);
    }
    node->data = x;
    node->next = NULL;//该节点指向的下一个指针是NULL

    return node;
}

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
    //不能传空值
    assert(pphead);
    //pphead---->&plist
    //对pphead进行解引用得到的就是plist
    SLTNode* newnode = SLTBuyNode(x);
     if (*pphead == NULL)
    {
        *pphead = newnode;
    }
    else
    {
        //尾节点-->新节点
    //phead指向的是第一个节点,我们创建一个指针pcur,pcur指向的是phead
    //我们通过pcur来寻找尾节点

    //找尾节点
        SLTNode* pcur = *pphead;
        while (pcur->next != NULL)
            //最后一个节点的next是空指针,我们对于这个循环的
        {
            pcur = pcur->next;//下一个节点的指针
        }
        //pcur的下个节点我们不让他指向的是空指针,我们让他指向的是新的节点
        pcur->next = newnode;
    }



}

//头部增加数据

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);

    //申请一个新节点
    SLTNode* newnode = SLTBuyNode(x);
    newnode->next = *pphead;//让新的节点的指针指向之前的头节点
    *pphead = newnode;//原先是*pphead指向的头节点,现在头节点发生变化,我们让头节点变化为我们刚刚创建的新节点


}


//尾部删除数据
void SLTPopBack(SLTNode** pphead)
{
    //链表为空:不能进行删除
    assert(pphead && *pphead);//pphead就是说传上来的数据不能为空,*pphead就是说明链表不能为空
     //处理只有一个节点的情况:要删除的就是头节点,当前指针的next为NULL
    if ((*pphead)->next == NULL)//就说明只有一个节点
        //因为箭头的优先级高,所以我们要将*pphead进行括起来
    {
        free(*pphead);//我们直接将这个节点释放
        *pphead = NULL;
    }
    else//不止一个节点
    {
        SLTNode* ptail = *pphead;//头节点   *pphead就是plist
        SLTNode* prev = NULL;//前节点为空

        //找尾节点
        while (ptail->next != NULL)
        {
            prev = ptail;//prev指向ptail的前一个节点,在经历下面的代码之后
            ptail = ptail->next;
            //我们让ptail指向下一个节点的之前,我们将现在的prev赋值为现在的ptail
            //等ptail指向下一个节点之后,那么prev就是一直指向的是前一个节点了

        }
        //假设我们有4个节点,此时的prev指向的是第三个节点,ptail指向的是最后一个节点
        //我们现在为了删除尾节点,我们将prev指向的下一个指针变为空指针
        prev->next = NULL;
        free(ptail);//我们直接将ptail这个指针进行释放,因为这个指针指向的节点就是我们要删除的尾节点
        ptail = NULL;
    }



}

//头部删除数据
void SLTPopFront(SLTNode** pphead)
{
    assert(pphead && *pphead);//参数不能传空,链表不能为空

    SLTNode* next = (*pphead)->next;//将下一个节点存储起来
    free(*pphead);//直接将第一个节点进行释放

    *pphead = next;//因为我们之前将下一个节点存储起来了
    //现在我们将头节点删除了,那么新的头节点就是之前的下一个节点
    //那么我们将头节点的地址赋值上之前存储的下一个节点的地址

    //我们还能先改头结点,再进行释放
    /*
    SLTNode*del=*pphead;
    *pphead=(*pphead)->next;//将头结点改变了
    * 
    free(del);//直接将头结点释放掉
    dal=NULL;    



    */
}


//查抄
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//返回的是一个节点的指针
{
    assert(phead);
    SLTNode* pcur = phead;//重新定义一个指针
    while (pcur!=NULL)
    {
        if (pcur->data == x)//找到了我们要找的节点了
        {
            return pcur;
        }

        pcur = pcur->next;
    }
    //跳出循环就说明没有我们要找的节点
    return NULL;
}


//在指定位置之前插⼊数据(传二级指针,就说明我们要将链表中数据进行改变)
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
    assert(pphead);//链表是可以为空的,因为我们是插入数据不是删除数据
    assert(pos);

    if (pos == *pphead)
    {
        SLTPushFront(pphead, x);
    }
    else
    {
        //申请一个新的节点
        SLTNode* newnode = SLTBuyNode(x);

        //找prev:pos的前一个节点
        SLTNode* prev = *pphead;//初始定义为第一个节点
        while (prev->next != pos)//prev的下个节点不是pos我们就一直进行循环
            //直到我们的下个节点是pos,那么我们的prev就是pos的前一个节点了
            //我们的目的就达到了
        {
            prev = prev->next;
        }
        //此时已经是循环之外了,那么我们的prev已经是pos的前一个节点了 
        //我们在之前已经找到了prev和pos的位置,并且我们已经创建好了新的节点
        //现在我们将新节点放到两个节点中间,就实现了指定位置插入了
        newnode->next = pos;
        prev->next = newnode;

    }
}


//在指定位置之后插⼊数据
//我们就不用找pos前面的指针了,
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
    assert(pos);
    //申请一个新的节点
    SLTNode* newnode = SLTBuyNode(x);
    newnode->next = pos->next;
    pos->next = newnode;
}


//删除pos结点    pos前一个节点和后一个节点受到影响
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
    assert(pphead && *pphead);
    assert(pos);
    //头删特殊处理
    if (pos == *pphead)
    {
        SLTPopFront(pphead);
    }
    else
    {
        SLTNode* prev = *pphead;//创建一个指针指向头结点
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        prev->next = pos->next;//pos前的节点指向pos的下一个节点
        free(pos);
        pos = NULL;
    }
}

//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
    //我们需要知道三个节点,pos和pos后面要删除的节点以及删除节点后面的节点
    assert(pos&&pos->next);//pos和pos的下个节点都不能为空

    //pos  pos->next  pos->next->nxet
    SLTNode* del = pos->next;//将pos下个节点指针保存下来
    //我们需要让pos后面结点指向的要删除的节点后面的节点
    //我们要让pos节点后面是要删除的节点后面那个节点
    pos->next = pos->next->next;
    free(del);
    del = NULL;
}


//销毁链表
void SListDestroy(SLTNode** pphead)//传址
{
    //遍历链表找到每个节点
    //我们不能直接将第一个节点释放,我们要先将第二个节点进行保存
    //如果提前将第一个节点删除了我们就找不到后面的节点了
    assert(pphead && *pphead);//不能传空,不能传空链表

    //创建一个指针遍历链表
    SLTNode* pcur = *pphead;//先指向第一个节点
    while (pcur != NULL)
    {
        SLTNode* next = pcur->next;//将pcur指向的下个节点进行保存
        free(pcur);
        pcur = next;

    }
    //跳出循环的时候,pcur已经指向的是NULL
    *pphead = NULL;

}
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>


//定义链表(结点)的结构
typedef int SLTDataType;
typedef struct SListNode
{
    SLTDataType data;
    struct SListNode* next;
}SLTNode;//表示的是单链表节点

//链表的打印
void SLTPrint(SLTNode* phead);    //将第一个节点传过去,我们命名为头节点

//尾部增加数据
void SLTPushBack(SLTNode* *pphead, SLTDataType x);

//头部增加数据
void SLTPushFront(SLTNode** pphead, SLTDataType x);

//尾部删除数据
void SLTPopBack(SLTNode** pphead);

//头部删除数据
void SLTPopFront(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//返回的是一个节点的指针

//在指定位置之前插⼊数据(传二级指针,就说明我们要将链表中数据进行改变)(时间复杂度是O(N))
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插⼊数据(时间复杂度是O(1))
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);


//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);

//销毁链表
void SListDestroy(SLTNode** pphead);
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"

//创建一个链表,并打印链表
void creatSList()
{
    //链表是由一个个节点组成的   SLTNode*是结构体指针
    SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));//申请一个节点大小的空间就行了

    node1->data = 1;//对节点内的数据进行初始化

    SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));//申请一个节点大小的空间就行了

    node2->data = 2;

    SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));//申请一个节点大小的空间就行了

    node3->data = 3;

    SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));//申请一个节点大小的空间就行了

    node4->data = 4;
    /*
    我们申请了四个节点,每个节点里面都有一个date和下一个节点的指针
    */

    node1->next = node2;
    node2->next = node3;
    node3->next = node4;
    node4->next = NULL;//最后一节点的指针指向的是NULL


    //打印链表
    //将第一个节点的地址作为参数传递过去


    SLTPrint(node1);
}

void SListTest01()
{
    SLTNode* plist = NULL;
    //SLTPushBack(&plist, 1);//因为phead是一级指针,所以我们在接受的时候我们要用二级指针进行接收
    //SLTPrint(plist);
    //SLTPushBack(&plist, 2);
    //SLTPushBack(&plist, 3);
    //SLTPrint(plist);
    //为什么打印出来的都是NULL
    //phead发生改变,但是plist没有发生改变,形参改变,但是实参没有改变,那么我们应该进行传址操作
    /*
    第一个节点:*plist
    指向第一个节点的指针:plist
    指向第一个节点的指针的地址:&plist----->pphead*/
    SLTPushFront(&plist, 1);
    SLTPushFront(&plist, 2);
    SLTPushFront(&plist, 3);
    SLTPushFront(&plist, 4);
    SLTPrint(plist);//期望结果:4->3->2->1->NULL

    //尾删
    /*SLTPopBack(&plist);
    SLTPrint(plist);*/
    /*打印出来的结果就是
    4->3->2->1->NULL
    4->3->2->NULL

    */
    /*SLTPopBack(&plist);
    SLTPrint(plist);
    SLTPopBack(&plist);
    SLTPrint(plist);
    SLTPopBack(&plist);
    SLTPrint(plist);*/

    /*
    当链表还只剩下一个节点的时候我们要进行处理了,因为剩一个节点的时候我们没有前置节点了

    */
    //进行处理之后我们得到的结果就是这样的
    /*
    4->3->2->1->NULL
    4->3->2->NULL
    4->3->NULL
    4->NULL
    NULL
    */
    //如果我们进行第五次的删除的操作的话,就会报错了Assertion failed: pphead && *pphead, file
    /*SLTPopFront(&plist);
    SLTPrint(plist);*/
    //查找节点
    SLTNode* find=SLTFind(plist,2 );
    /*if (find == NULL)
    {
        printf("没有找到\n");
    }
    else
    {`
        printf("找到了\n");
    }*/

    //SLTInsert(&plist, find, 11);//在3之前插入11
    ////期望结果就是4->11->3->2->1->NULL
    //SLTPrint(plist);

    /*我们将要查找的数字改为4,我们在4之前插入数据,但是出现了报错
    * 那为什么会报错呢?
    因为此时的pos是头节点,前面是没有节点的 ,但是我们一开始是将prev定义为第一个节点的,
    我们一直满足循环的条件,一直进行循环,那么代码中的while就成了一个死循环
    所以我们要对着这种情况进行特殊处理的
    我们直接调用头插函数

    */
    /*SLTInsertAfter(find, 11);
    SLTPrint(plist);*/

    //SLTErase(&plist, find);
    //SLTPrint(plist);//尾删没问题,但是头删有问题
    //对于头删,pos是头节点,那么一开始的prev指向的是头结点,一直满足循环
    //就一直循环下去了
    //所以我们要进行特殊处理下

    /*SLTEraseAfter(find);
    SLTPrint(plist);*/

    SListDestroy(&plist);
    SLTPrint(plist);
}

int main()
{
    SListTest01();
    //creatSList();
    return 0;
}

链表头插的时间复杂度是O(1)

链表头删的时间复杂度是O(1)

链表的分类

image.png

image.png

虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:单链表和双向带头循环链表

  1. ⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。

  2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。

双向链表我们能从头遍历到尾,我们也能从尾遍历到头

在带头链表中,除了头结点,其他节点都存储有效的数据

头节点的作用是占位子的,叫做“哨兵位”

不带头的链表,从第一个节点开始就是存储的就是有效的数据

带环链表尾节点next不为空

那么单链表的全称是:不带头单向不循环链表

双向链表:带头双向循环链表

双向链表结构相较于单链表来说结构要复杂一些,但是接口的实现要比单链表简单很多
双向链表:带头双向循环链表

双向链表结构相较于单链表来说结构要复杂一些,但是接口的实现要比单链表简单很多

双向链表的节点结构:数据+指向后一个节点的指针+指向前一个节点的指针

尾节点的next指针指向哨兵节点


struct ListNode
{
    int data;
    struct ListNode* next;//下个节点的指针
    struct ListNode* prev;//指向前个节点的指针

};

双向链表为空的情况:只有一个自循环的哨兵位

第一个节点:第一个有效的节点

哨兵位:头节点

新插入的数据的尾节点要指向哨兵位,prev指针要指向前一个节点

我们还要改变新节点之前的节点的next指针的指向,要指向新的节点

我们的哨兵位的prev指针还要指向新的尾节点

那么我们在插入新节点的时候,受到影响的节点有之前的尾节点和哨兵位以及新节点

在存在新的节点的情况下,我们的哨兵位的perv指针指向的是尾节点

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

LTNode* LTBuyNode(LTDataType x)//创建节点
{
    LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
    if (newnode == NULL)
    {
        perror("malloc fail!");
        exit(1);//直接退出
    }
    newnode->data = x;
    newnode->next = newnode->prev = newnode;//让这两个指针都指向当前节点的自己,实现循环的效果

    return newnode;
}
////初始化
//void LTInit(LTNode** pphead)
//{
//    //创建一个头结点(哨兵位)
//    *pphead = LTBuyNode(-1);
//}

LTNode* LTInit1()//通过返回值的方法进行初始化,我们要返回一个哨兵位节点的指针
{
    LTNode* phead = LTBuyNode(-1);//创建一个指向哨兵位节点的指针
    return phead;
}


//销毁    链表的销毁是整个都销毁的
void LTDesTory(LTNode** pphead)
{
    //我们需要遍历这个链表,一个个删除
    assert(pphead && *pphead);//哨兵位不能为空,参数也不能传一个空链表
    LTNode* pcur = (*pphead)->next;//创建一个指针让这个指针指向哨兵位的下个节点,就是第一个有效非节点
    //我们从哨兵位的下个节点进行销毁操作
    while (pcur != (*pphead))//循环操作一直进行到下个节点不是哨兵位
    {
        LTNode* Next = pcur->next;//创建一个指针指向pcur的下个节点,进行保存
        free(pcur);
        pcur = Next;//让pcur这个指针走到Next指向的节点
    }
    //遇到哨兵位跳出循环了
    //销毁哨兵位节点
    free(*pphead);
    *pphead = NULL;
    pcur = NULL;
}


//打印链表
void LTPrint(LTNode* phead)
{
    LTNode* pcur = phead->next;//定义一个指针指向哨兵位的next指向的第一个有效的节点
    //那么我们就从第一个有效的节点开始打印了
    while (pcur != phead)//直到pcur走到哨兵位的位置我们就跳出循环了
    {
        printf("%d ->", pcur->data);//打印当前节点保存的数据
        pcur = pcur->next;//在链表中进行遍历
    }
    printf("\n");
}


//尾插数据
void LTPushBack(LTNode* phead, LTDataType x)
{
    //断言一下,传的参数一定要是有效的参数
    assert(phead);
    LTNode* newnode = LTBuyNode(x);//创建新的节点

    //phead(哨兵位)    phead->prev(之前的尾节点)    newnode(新插入的节点)
    //我们先进行newnode的指针修改,prev指向上个尾节点,next指向哨兵位
    newnode->next = phead;
    newnode->prev = phead->prev;//之前的尾节点的表示就是phead->prev
    //我们再对原先的尾节点进行指针的修改,原先的next指针指向的是哨兵为,那么现在就指向的是新节点了
    phead->prev->next = newnode;
    //接下来我们对哨兵位进行修改
    //哨兵位的prev原本指向的是之前的尾节点,那么我们现在让Prev指新节点
    phead->prev = newnode;

    //与单链表相比的话,我们不用判断链表是不是空的,我们也不用从头开始遍历来找尾节点
}


//头插数据    哨兵位之后    头插到第一个有效数据的前面
void LTPushFront(LTNode* phead, LTDataType x)
{
    assert(phead);

    //创建新节点
    LTNode* newnode = LTBuyNode(x);
    //我们先修改插入数据的指针指向,因为newnode的指针指向不会影响原链表
    newnode->next = phead->next;//指向原先的第一个节点
    newnode->prev = phead;//prev指向哨兵位
    //我们头插的节点的next指向的是之前的第一个有效的节点
    //prev指向的是哨兵位
    //我们还要改变之前的第一个有效节点的Prev指针的指向,让指针指向位头插的新数据
    phead->next->prev=newnode;//原先的第一个有效节点的prev指针指向新插入的数据
    //还有就是哨兵位的next指针要指向新插入的节点了
    phead->next = newnode;
}

//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
    assert(phead);
    return phead->next == phead;//如果哨兵位的next指针指向的是自己,那么就说明这个链表是空链表
}//如果链表为空的话,那么就返回的是true
//尾删数据
void LTPopBack(LTNode* phead)
{
    assert(phead);//哨兵位不能为空
    assert(!LTEmpty(phead));//判断链表不为空,那么我们就能进行删除节点的操作了

    //我们删除节点的话,我们是将哨兵位prev指向的尾节点删掉
    //我们将尾节点删掉的话,收到影响的节点有哨兵位和尾删节点的前一个节点
    /*
    如果将尾节点删掉的话,我们的删掉节点前一个节点的next指针需要指向head'
    并且head的prev指针要指向我们的删除节点的前一个节点

    我们要创建一个指针指向删除的节点,以便寻找到删除节点的前一个节点

    那么我们通过哨兵位就能找到要删除的节点以及他之前的节点
    */
    //del是要删除的节点
    //del(phead->prev)  prev(del->prev)    phead
    LTNode* del = phead->prev;//创建一个指针指向我们要删除的节点
    LTNode* prev = del->prev;//删除节点的前一个节点

    //我们先修改prev节点的指针
    prev->next = phead;//prev指向了哨兵位
    phead->prev = prev;//哨兵位的prev指向了prev,那么现在prev指向的节点就是新的尾节点了

    //因为我们之前创建了指针指向我们要删除的节点,那么我们通过这个指针将节点删除
    free(del);
    del = NULL;
} 

//头删数据     删除第一个有效节点
void LTPopFront(LTNode* phead)
{
    assert(phead);//哨兵位不能为空
    assert(!LTEmpty(phead));//判断链表不为空,那么我们就能进行删除节点的操作了

    /*删除第一个节点受到影响的节点有哨兵位以及第二个有效节点
     删除第一个节点之后我们哨兵位的next指针要指向第二个节点
     第二个节点的prev指针要指向我们的哨兵位*/


    LTNode* del = phead->next;//创建一个指针指向我们要删除的节点,就是现在的第一个节点
    //我们先改第二个指针的prev指针的指向
    del->next->prev = phead;//第二个节点的prev指针要指向哨兵位
    phead->next = del->next;//哨兵位的下个节点是第二个节点

    free(del);
    del = NULL;
}

//查找数据
LTNode* LTFind(LTNode* phead, LTDataType x)//返回值是指向节点的指针
{
    //我们需要进行链表的遍历,我们找一圈就行了,再次遇到哨兵位我们直接跳出循环
    LTNode* pcur = phead->next;//我们从第一个有效的节点进行遍历
    while (pcur != phead)//如果下个节点是哨兵位我们直接跳出循环
    {
        if (pcur->data == x)
        {
            return pcur;//找到了我们直接返回这个节点的地址
        }
        pcur = pcur->next;//没有找到我们继续往后找
    }
    return NULL;//找不到了,直接返回空
}

//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x)
{
    /*
    我们先将要插入的节点的next和prev指针进行解决
    我们先将newnode 的next和prev处理好
    我们是在pos这个位置的后面进行插入节点,那么就会影响到pos和pos->next这两个节点
    我们要将pos的next指针进行改变,指向了新的节点以及pos原先后面的节点的prev指向的位置要变为新的节点
    */
    //创建一个节点
    LTNode* newnode = LTBuyNode(x);
    //pos newnode   pos->next
    //先处理newnode
    newnode->next = pos->next;
    newnode->prev = pos;
    //处理pos后面节点的prev指针的指向
    pos->next->prev = newnode;
    //改变pos的next指针的指向
    pos->next = newnode;
}


//删除指定位置的节点
void LTIErase(LTNode* pos)
{
    assert(pos);//传过来的位置不为空
    /*删除指定位置的节点
    pos前面的节点pos->prev
    pos后面的节点pos->next
    那么删除pos就出影响到这两个节点了
    pos前面指针的节点的next指针就会指向了Pos后面的节点了
    pos后面的节点的prev指针就会指向了pos前面的节点了*/
    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;

    free(pos);
    pos = NULL;
}


void LTDesTory2(LTNode* phead)
{
    assert(phead);
    LTNode* pcur = phead->next;//定义一个指针指向哨兵位的下个节点,就是第一个有效节点
    while(pcur != phead)//遇到哨兵位就跳出
    {
        LTNode* Next = pcur->next;//创建一个指针指向当前位置的下个节点
        free(pcur);
        pcur = Next;
    }
    free(phead);
    phead = NULL;
    pcur = NULL;
    //我们已经将链表的空间销毁了,但是plist仍然指向原先的那片空间
    //所以我们需要进行手动将plist置为NULL
}
#pragma once
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>


//定义双向链表节点的结构
typedef int LTDataType;//链表数据类型
typedef struct ListNode
{
    LTDataType data;
    struct ListNode* next;
    struct ListNode* prev;
}LTNode;

//为了保持接口的一致性,优化接口都为一级指针

//初始化
//void LTInit(LTNode**pphead);//我们拿着一个空瓶子让老板给我们装饮料
LTNode* LTInit1();//直接让老板给我们一个装好了的饮料
//通过返回值的方法初始化


//销毁    链表的销毁是整个都销毁的
void LTDesTory(LTNode** pphead);
void LTDesTory2(LTNode* phead);//传一级我们需要手动将plist置为NULL

//打印链表
void LTPrint(LTNode* phead);

//尾插数据
//第一个参数传一级还是二级,,要看pphead指向的节点会不会发生改变
//如果发生改变,那么pphead的改变要影响实参,传二级
//如果不发生改变,pphead不会影响实参,传一级
//我们通过传递的一级指针来找到头结点,就可以找到之后的节点了

//那么我们在插入新节点的时候,受到影响的节点有之前的尾节点和哨兵位以及新节点
void LTPushBack(LTNode* phead, LTDataType x);

//头插数据
void LTPushFront(LTNode* phead, LTDataType x);

//尾删数据
void LTPopBack(LTNode* phead);

//头删数据
void LTPopFront(LTNode* phead);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//查找数据
LTNode* LTFind(LTNode* phead, LTDataType x);

//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x);

//删除指定位置的节点
void LTIErase(LTNode* pos);
#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

void ListTest01()
{
    //创建双向链表变量
    /*LTNode* plist = NULL;
    LTInit(&plist);*///这个里面的哨兵位是自循环的

    //初始化操作优化
    LTNode* plist = LTInit1();//调用这个函数直接给我们一个指向哨兵位的指针
    //这种初始化操作之后我们就不用在外面创建指针传过去了


    //尾插
    LTPushBack(plist,1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPrint(plist);
    //头插
    /*LTPushFront(plist, 1);
    LTPrint(plist);
    LTPushFront(plist, 2);
    LTPrint(plist);
    LTPushFront(plist, 3);
    LTPrint(plist);
    LTPushFront(plist, 4);
    LTPrint(plist);*/

    //尾删
    /*LTPopBack(plist);
    LTPrint(plist);
    LTPopBack(plist);
    LTPrint(plist);
    LTPopBack(plist);
    LTPrint(plist);*/

    //头删
    /*LTPopFront(plist);
    LTPrint(plist);
    LTPopFront(plist);
    LTPrint(plist);*/

    //查找数据
    LTNode*pos=LTFind(plist, 1);
    /*if (pos == NULL)
    {
        printf("没有找到\n"); 
    }
    else
    {
        printf("找到了\n");

    }*/

    //删除指定位置之后的节点
    /*LTInsert(pos, 11);
    LTPrint(plist);*/

    //删除pos位置上的节点
    LTIErase(pos);
    LTPrint(plist);

    //销毁操作
    LTDesTory(&plist);
}
int main()
{
    ListTest01();
    return 0;
相关文章
|
7月前
|
存储
数据结构链表详解(不仅顺序表可以,我链表也可以)
数据结构链表详解(不仅顺序表可以,我链表也可以)
44 0
|
1月前
|
存储 算法 数据管理
顺序表和链表(1)
【10月更文挑战第22天】
|
2月前
|
存储 安全 Java
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
26 3
|
7月前
|
存储 缓存
【编织时空四:探究顺序表与链表的数据之旅】(上)
【编织时空四:探究顺序表与链表的数据之旅】
【编织时空四:探究顺序表与链表的数据之旅】(上)
|
6月前
|
存储 缓存 算法
数据结构和算法学习记录——总结顺序表和链表(双向带头循环链表)的优缺点、CPU高速缓存命中率
数据结构和算法学习记录——总结顺序表和链表(双向带头循环链表)的优缺点、CPU高速缓存命中率
53 0
|
7月前
|
存储 缓存
[数据结构]——顺序表和链表
[数据结构]——顺序表和链表(下):https://developer.aliyun.com/article/1515507
|
4月前
|
存储 算法
【初阶数据结构篇】顺序表和链表算法题
此题可以先找到中间节点,然后把后半部分逆置,最近前后两部分一一比对,如果节点的值全部相同,则即为回文。
32 0
|
4月前
|
存储 缓存
【数据结构】——顺序表与链表
【数据结构】——顺序表与链表
|
6月前
|
存储 索引
顺序表和链表
通过以上示例,我们可以看到顺序表和链表在实际应用中如何操作。顺序表适合于需要频繁读取数据的场景,而链表则更适用于频繁修改数据的情况。在选择使用哪种数据结构时,应考虑到实际应用的需求和上下文环境。
35 2
|
6月前
|
存储
2.顺序表_链表(附练习)
2.顺序表_链表(附练习)