C语言中数据结构——带头双向循环链表

博主主页:@ᰔᩚ. 一怀明月ꦿ 

❤️‍专栏系列:线性代数,C初学者入门训练,题解C,C的使用文章,「初学」C++,数据结构

座右铭:“不要等到什么都没有了,才下定决心去做”

大家觉不错的话,就恳求大家点点关注,点点小爱心,指点指点

目录

带头双向循环链表

概念

链表结构体的定义 

链表为空的判断

链表节点的创建

链表的初始化

链表的打印

链表的尾插

链表的头插

链表的尾删

链表的头删

链表的查找

链表中在pos之前插入

删除pos的值

链表的销毁

链表为什么使用的是一级指针

(1)单链表(非头单向不循环连链表)使用二级指针

(2)带头双向循环连链表使用一级指针

狡猾的面试官

链表的源码

main函数

         test.h文件

test.c文件


带头双向循环链表

概念

1.无头单向非循环的链表:结构简单,一般不会单独用来存储数据。实际中更多是作为其他数据结构的子结构,例如哈希桶、图的邻接表等等。这种结构在笔试面试中出现的比较多。(之前提到的单链表是一种无头单向非循环的链表。)

C语言中数据结构——带头双向循环链表_第1张图片 

2.带头双向循环链表:结构复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构最燃复杂,但是使用代码实现以后会发现结构会带来很多优势,实现起来反而简单。

C语言中数据结构——带头双向循环链表_第2张图片

链表结构体的定义 

typedef int LTDatatype;
typedef struct ListNode
{
    struct ListNode* next;//保存下个节点的地址
    struct ListNode* prev;//保存上个节点的地址
    LTDatatype data;//存储的数据
}ListNode;

链表为空的判断

bool ListNodeEmpty(ListNode* pphead)
{
    assert(pphead);
    return pphead->next==pphead;//哨兵位的下个节点的地址和哨兵位的地址相等则链表为空,这里返回true
}

链表节点的创建

动态创建一个节点,将节点的prev指针置空,将节点next指针置空,值赋值为x(x是传进来的参数),然后返回这个节点的地址。

ListNode* BuyListNode(LTDatatype x)
{
    ListNode* newnode=(ListNode*)malloc(sizeof(ListNode));
    if(newnode==NULL)
    {
        perror("malloc fail!");
        return NULL;
    }
    newnode->next=NULL;
    newnode->prev=NULL;
    newnode->data=x;
    return newnode;
}

链表的初始化

链表的初始化是创建一个哨兵位,哨兵位的数据值为-1。且初始化时,是将哨兵位的prev指针指向自己,哨兵位的next指向自己。返回这个哨兵位的地址

ListNode* ListNodeInit(ListNode* pphead)
{
    pphead=BuyListNode(-1);
    pphead->next=pphead;
    pphead->prev=pphead;
    return pphead;
}

链表的打印

创建一个结构体指针cur,让cur指向哨兵位的下一个节点(链表的头节点),然后遍历整个链表并打印每个节点的数据值,当cur的值等于哨兵位的指针的值,说明遍历完成,结束打印。

void ListNodePrint(ListNode* pphead)
{
    assert(pphead);
    ListNode* cur=pphead->next;//从头节点开始打印
    while(cur!=pphead)//当cur的值等与哨兵位的值(这里的值是存放的节点的地址)相等纠结束打印,
    {
        printf("%d ",cur->data);
        cur=cur->next;
    }
    printf("\n");
    printf("print has done\n");
}

链表的尾插

这里不管是头节点还是中间节点,都能完成尾插,但是单向不循环链表,得分头节点和中间节点,体现出双向循环链表在尾删的便利

void ListNodePushBack(ListNode* pphead,LTDatatype x)
{
    assert(pphead);
    ListNode* tail=pphead->prev;
    ListNode* newnode=BuyListNode(x);
    tail->next=newnode;
    newnode->prev=tail;
    newnode->next=pphead;
    pphead->prev=newnode;
}

链表的头插

void ListNodePushFront(ListNode* pphead,LTDatatype x)
{
    //这里不管是头节点还是中间节点,都能完成头插,但是单向不循环链表,得分头节点和中间节点
    assert(pphead);
    ListNode* newnode=BuyListNode(x);
    newnode->next=pphead->next;//a
    pphead->next->prev=newnode;//b
    //这里的a b一定在c d的前面,不然pphead->next被改后,没有找到原来的头节点。
    //这里其实用慢指针去赋值,就可以不在意顺序了
    pphead->next=newnode;//c
    newnode->prev=pphead;//d
}

链表的尾删

尾删的时候,要注意不要把哨兵位删除了,所以需要判断链表是否为空,如果为空不能删除

void ListNodePopBack(ListNode* pphead)
{
    assert(pphead);
    assert(!ListNodeEmpty(pphead));
    ListNode* tail=pphead->prev;
    ListNode* tailPrev=tail->prev;
    free(tail);
    tail=NULL;
    pphead->prev=tailPrev;
    tailPrev->next=pphead;
}

链表的头删

void ListNodePopFront(ListNode* pphead)
{
    assert(pphead);
    assert(!ListNodeEmpty(pphead));

    ListNode* tail=pphead->next;
    ListNode* tailNext=tail->next;
    free(tail);
    tail=NULL;

    pphead->next=tailNext;
    tailNext->prev=pphead;
}

链表的查找

根据给出的值,然后去遍历整个链表,如果链表中有与给出的值相匹配的节点,就返回这个节点的地址,如果没有返回空指针。

ListNode* ListNodeFind(ListNode* pphead,LTDatatype x)
{
    assert(pphead);
    ListNode* cur=pphead->next;
    while(cur!=pphead)
    {
        if(cur->data==x)
        {
            return cur;
        }
        cur=cur->next;
    }
    return NULL;
}

链表中在pos之前插入

void ListNodeInsert(ListNode* pos,LTDatatype x)
{
    assert(pos);
    ListNode* newnode=BuyListNode(x);
    //这种方法的可读性高
    ListNode* prev=pos->prev;
    prev->next=newnode;
    newnode->prev=prev;
    newnode->next=pos;
    pos->prev=newnode;
    //少定义一个指针的方法,但一定要记住先修改pos的右边
//    pos->prev->next=newnode;
//    newnode->prev=pos->prev;
//    newnode->next=pos;
//    pos->prev=newnode;
}

删除pos的值

void ListNodeErease(ListNode* pos)
{
    assert(pos);
    ListNode* posPrev=pos->prev;
    ListNode* posNext=pos->next;
    free(pos);
    posPrev->next=posNext;
    posNext->prev=posPrev;
}

链表的销毁

利用快慢指针的方式,做到free掉每个节点

void ListNodeDestroy(ListNode* pphead)
{
    assert(pphead);
    ListNode* cur=pphead->next;
    ListNode* prev=cur->next;
    while(cur!=pphead)
    {
        free(cur);
        cur=prev;
        prev=prev->next;
    }
    free(pphead);
}

链表为什么使用的是一级指针

(1)单链表(非头单向不循环连链表)使用二级指针

我们在实现单链表(非头单向不循环连链表)使用的是二级指针。其实我们也可以使用一级指针,但前提是链表不能对头节点进行操作。因为我们在主函数创建的结构体指针plist,如果没有对plist指向的链表的头节点进行操作,在调用删除或插入等函数时,就可以通过形参指针去修改链表。如果对plist指向的链表的头节点进行操作,在调用删除或插入函数时,就不能保证操作正确性了。传给函数的是plist值,函数的形参指针无法对plist进行操作,要想要改变plist的值,只能传plist的地址并且函数形参指针必须是二级指针。

(2)带头双向循环连链表使用一级指针

我们的带头双向循环连链表之所以使用一级指针,因为我们在进行删除或插入等操作时,plist始终指向哨兵位节点,在一系列操作中,哨兵位的位置没有改变。所以链表只需传plist的值且函数形参指针使用一级指针。

狡猾的面试官

如果在面试时,面试官让你在10分钟以内写完一个链表,链表的功能包括:链表的打印,链表的头删,链表的尾删,链表的头插,链表的尾插,链表的任意位置删除,链表任意位置插入,链表销毁。对于我而言,就算我准备的有多么的充足,在面试这种紧张环境下,我根本不可能完成这种要求,也就是我将得不到这家公司的offer。但是我们冷静分析一下,链表面试没有规定是哪一种,链表的种类一共有8之多,我们应该首选带头双向循环链表,别看它结构复杂,但是实现的功能的逻辑简单(可以减少很多空指针的情况),我们选了带头双向循环链表之后,其实链表的任意位置删除和链表的头删,链表的尾删其实功能类似,链表的任意位置删除和链表的头插,链表的尾插实现的功能类似。所以我们只需要复用代码就行。

链表的尾插:

void ListNodePushBack(ListNode* pphead,LTDatatype x)
{
    //复用ListNodeInsert
    ListNodeInsert(pphead->prev, x);
}

链表的头插:

void ListNodePushFront(ListNode* pphead,LTDatatype x)
{  
    //复用ListNodeInsert
    ListNodeInsert(pphead->next, x);
}

链表的尾删:

void ListNodePopBack(ListNode* pphead)
{    
    //复用ListNodeErease
    ListNodeErease(pphead->prev);
}

链表的头删:

void ListNodePopFront(ListNode* pphead)
{
//    复用ListNodeErease
    ListNodeErease(pphead->next);
}

这样我们就可以在10分钟之内识破面试官的诡计

链表的源码

main函数

#include "test.h"
void test1(void)
{
    ListNode* plist=NULL;
    plist=ListNodeInit(plist);
    ListNodePushBack(plist, 1);
    ListNodePushBack(plist, 2);
    ListNodePushBack(plist, 3);
    ListNodePushBack(plist, 4);
    ListNodePrint(plist);
}
void test2(void)
{
    ListNode* plist=NULL;
    plist=ListNodeInit(plist);
    ListNodePushFront(plist, 1);
    ListNodePushFront(plist, 2);
    ListNodePushFront(plist, 3);
    ListNodePushFront(plist, 4);
    ListNodePrint(plist);
}
void test3(void)
{
    ListNode* plist=NULL;
    plist=ListNodeInit(plist);
    ListNodePushFront(plist, 1);
    ListNodePushFront(plist, 2);
    ListNodePushFront(plist, 3);
    ListNodePushFront(plist, 4);
    ListNodePrint(plist);
    ListNodePopBack(plist);//尾删
    ListNodePrint(plist);
    ListNodePopFront(plist);//头删
    ListNodePrint(plist);
    ListNodePopBack(plist);//尾删
    ListNodePrint(plist);
    ListNodePopFront(plist);//头删
    ListNodePrint(plist);
    //ListNodePopFront(plist);//头删
}
void test4(void)
{
    ListNode* plist=NULL;
    plist=ListNodeInit(plist);
    ListNodePushFront(plist, 1);
    ListNodePushFront(plist, 2);
    ListNodePushFront(plist, 3);
    ListNodePushFront(plist, 4);
    ListNodePrint(plist);
    ListNode* pos=ListNodeFind(plist, 3);
    ListNodeInsert(pos, 100);
    ListNodePrint(plist);
}
void test5(void)
{
    ListNode* plist=NULL;
    plist=ListNodeInit(plist);
    ListNodePushFront(plist, 1);
    ListNodePushFront(plist, 2);
    ListNodePushFront(plist, 3);
    ListNodePushFront(plist, 4);
    ListNodePrint(plist);
    ListNode* pos=ListNodeFind(plist, 3);
    ListNodeErease(pos);
    ListNodePrint(plist);
}
void test6(void)
{
    ListNode* plist=NULL;
    plist=ListNodeInit(plist);
    ListNodePushFront(plist, 1);
    ListNodePushFront(plist, 2);
    ListNodePushFront(plist, 3);
    ListNodePushFront(plist, 4);
    ListNodePrint(plist);
    ListNodeDestroy(plist);
}
int main()
{
    //test1();//头插
    //test2();//尾插
    //test3();//头删尾删
    //test4();//任意位置的插入
    test5();//任意位置的删除
    //test6();//销毁链表
    return 0;
}

test.h文件

#ifndef test_h
#define test_h
#include 
#include
#include
#include
#endif /* test_h */

typedef int LTDatatype;
typedef struct ListNode
{
    struct ListNode* next;
    struct ListNode* prev;
    LTDatatype data;
}ListNode;

//链表为空的判断
bool ListNodeEmpty(ListNode* pphead);
//链表的初始化
ListNode* ListNodeInit(ListNode* pphead);
//链表的打印
void ListNodePrint(ListNode* pphead);
//链表的尾插
void ListNodePushBack(ListNode* pphead,LTDatatype x);
//链表的头插
void ListNodePushFront(ListNode* pphead,LTDatatype x);
//链表的尾删
void ListNodePopBack(ListNode* pphead);
//链表的头删
void ListNodePopFront(ListNode* pphead);
//链表的查找
ListNode* ListNodeFind(ListNode* pphead,LTDatatype x);
//在pos之前插入
void ListNodeInsert(ListNode* pos,LTDatatype x);
//删除pos的值
void ListNodeErease(ListNode* pos);
//链表的销毁
void ListNodeDestroy(ListNode* pphead);

test.c文件

#include "test.h"
bool ListNodeEmpty(ListNode* pphead)
{
    assert(pphead);
    return pphead->next==pphead;
}
ListNode* BuyListNode(LTDatatype x)
{
    ListNode* newnode=(ListNode*)malloc(sizeof(ListNode));
    if(newnode==NULL)
    {
        perror("malloc fail!");
        return NULL;
    }
    newnode->next=NULL;
    newnode->prev=NULL;
    newnode->data=x;
    return newnode;
}
ListNode* ListNodeInit(ListNode* pphead)
{
    pphead=BuyListNode(-1);
    pphead->next=pphead;
    pphead->prev=pphead;
    return pphead;
}
void ListNodePrint(ListNode* pphead)
{
    assert(pphead);
    ListNode* cur=pphead->next;//从头节点开始打印
    while(cur!=pphead)//当cur的值等与哨兵位的值(这里的值是存放的节点的地址)相等纠结束打印,
    {
        printf("%d ",cur->data);
        cur=cur->next;
    }
    printf("\n");
    printf("print has done\n");
}
void ListNodePushBack(ListNode* pphead,LTDatatype x)
{
//    //这里不管是头节点还是中间节点,都能完成尾插,但是单向不循环链表,得分头节点和中间节点
//    assert(pphead);
//    ListNode* tail=pphead->prev;
//    ListNode* newnode=BuyListNode(x);
//    tail->next=newnode;
//    newnode->prev=tail;
//    newnode->next=pphead;
//    pphead->prev=newnode;
    
    //复用ListNodeInsert
    ListNodeInsert(pphead->prev, x);
}
void ListNodePushFront(ListNode* pphead,LTDatatype x)
{
//    //这里不管是头节点还是中间节点,都能完成头插,但是单向不循环链表,得分头节点和中间节点
//    assert(pphead);
//    ListNode* newnode=BuyListNode(x);
//    newnode->next=pphead->next;//a
//    pphead->next->prev=newnode;//b
//    //这里的a b一定在c d的前面,不然pphead->next被改后,没有找到原来的头节点。
//    //这里其实用慢指针去赋值,就可以不在意顺序了
//    pphead->next=newnode;//c
//    newnode->prev=pphead;//d
    
    
    //复用ListNodeInsert
    ListNodeInsert(pphead->next, x);
}
void ListNodePopBack(ListNode* pphead)
{
//    assert(pphead);
//    assert(!ListNodeEmpty(pphead));
//    ListNode* tail=pphead->prev;
//    ListNode* tailPrev=tail->prev;
//    free(tail);
//    tail=NULL;
//    pphead->prev=tailPrev;
//    tailPrev->next=pphead;
    
    
    //复用ListNodeErease
    ListNodeErease(pphead->prev);
}
void ListNodePopFront(ListNode* pphead)
{
//    assert(pphead);
//    assert(!ListNodeEmpty(pphead));
//
//    ListNode* tail=pphead->next;
//    ListNode* tailNext=tail->next;
//    free(tail);
//    tail=NULL;
//
//    pphead->next=tailNext;
//    tailNext->prev=pphead;
//
//    复用ListNodeErease
    ListNodeErease(pphead->next);
}
ListNode* ListNodeFind(ListNode* pphead,LTDatatype x)
{
    assert(pphead);
    ListNode* cur=pphead->next;
    while(cur!=pphead)
    {
        if(cur->data==x)
        {
            return cur;
        }
        cur=cur->next;
    }
    return NULL;
}
void ListNodeInsert(ListNode* pos,LTDatatype x)
{
    assert(pos);
    ListNode* newnode=BuyListNode(x);
    //这种方法的可读性高
    ListNode* prev=pos->prev;
    prev->next=newnode;
    newnode->prev=prev;
    newnode->next=pos;
    pos->prev=newnode;
    //少定义一个指针的方法,但一定要记住先修改pos的右边
//    pos->prev->next=newnode;
//    newnode->prev=pos->prev;
//    newnode->next=pos;
//    pos->prev=newnode;
}
//ListNodeErease有一个缺陷,就是可能删除了哨兵位
void ListNodeErease(ListNode* pos)
{
    assert(pos);
    ListNode* posPrev=pos->prev;
    ListNode* posNext=pos->next;
    free(pos);
    posPrev->next=posNext;
    posNext->prev=posPrev;
}
void ListNodeDestroy(ListNode* pphead)
{
    assert(pphead);
    ListNode* cur=pphead->next;
    ListNode* prev=cur->next;
    while(cur!=pphead)
    {
        free(cur);
        cur=prev;
        prev=prev->next;
    }
    free(pphead);
}

  如果大家还有不懂或者建议都可以发在评论区,我们共同探讨,共同学习,共同进步。谢谢大家!

你可能感兴趣的:(数据结构,链表,数据结构,c语言,带头双向循环链表)