数据结构初阶-双向链表

1.链表的分类

链表的结构有三种,带头与不带头,单向与双向,循环与不循环三种,之前讲的单链表全称为不带头单向不循环链表。带头的解释:单链表讲的头结点是首结点,这样的不是正确的说法。带头的代表有头结点,而头结点不存储有效数据,只占位置,相当于哨兵位。单向或双向的解释:单向的只能从首结点位置开始遍历链表,而不能从后往前遍历,只有一个指针指向下一个结点,而双向链表有两个指针分别是指向下一个结点和指向前一个结点的指针。循环与不循环的解释:尾结点的指向不同,循环的尾结点指向头结点或者首结点,而不循环的尾节点指向NULL。今天我们讲的双向链表是指双向带头循环的链表,其他还有六种链表学完这两种都会实现。

2.双向链表

2.1概念与结构

为结构体,其中包含一个指针指向下一个结点的结构体指针next,一个指针指向上一个结点的指针prev,还有所存储的数据data,由于存储的数据类型未知,所以我们typedef int LTDataType;之后如果想要改变这个存储的数据就可以直接把int改成其他的类型,所以结构为:

typedef int LTDataType;
typedef struct ListNode
{
    LTDataType data;
    struct ListNode* next;//存储下一个结点的地址
    struct ListNode* prev;//存储上一个结点的地址
}LTNode;

2.2手动实现双向链表

2.2.1申请新结点空间函数

//申请新结点
LTNode* buyNode(LTDataType x)
{
    LTNode* node = (LTNode*)malloc(sizeof(LTNode));
    node->data = x;
    //双向链表指针初始的时候要指向本身
    node->next = node->prev = node;
    return node;
}

2.2.2初始化头结点函数

//初始化
LTNode* LTInit()
{
    LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
    phead->next = phead->prev = phead;
    //初始时头结点两个指针都应该指向自己
    phead->data = 0;
    return phead;
}

2.2.3尾插函数

我们应当传递的参数是一级指针,因为单链表的时候可能头结点(首结点)会有改变,而双向链表头结点不会改变,所以我们应当注意。尾结点如何表示?不就是头结点的前一个结点为phead->prev,所以我们应当改变的是头结点的prev的指向和尾结点的next指向,并且将新插入的结点的next与prev指针都要改变,有如下代码:

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
    LTNode* newnode = buyNode(x);
    //我们应当先改的是新插入的结点,这样才会使之后改变原链表的指向不是非常麻烦
    newnode->prev = phead->prev;
    newnode->next = phead;
    newnode->prev->next = newnode;
    //也可以改为
    //phead->prev->next = newnode;
    //都表示原来的尾结点的next指针指向
    phead->prev = newnode;
}

尾插完后我们需要测试这个函数是否有问题,我们需要写一个函数来打印除尾结点之外的数据,代码如下:

//打印
void LTPrint(LTNode* phead)
{
    LTNode* pcur = phead->next;
    //结束条件就是到首结点
    while (pcur != phead)
    {
        printf("%d -> ", pcur->data);
        //如果是其他数据就可以改
        pcur = pcur->next;
        //如果要逆向打印也可以把next全部替换成prev
    }
}

测试一下有:

void test()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPrint(plist);
}
int main()
{
    test();
    return 0;
}

打印结果如果为1->2->3->4则没有问题

2.2.4头插函数

注意:头插是在第一个有效数据的前面进行插入数据,而不是头结点前插入数据,否则这样会使这个情况为尾插了。所以我们来分析,头结点的next指针指向和第一个有效数据的prev指针指向都改变了,代码如下:

/头插
void LTPushFront(LTNode* phead, LTDataType x)
{
    assert(phead);
    //不能传入空指针
    LTNode* newnode = buyNode(x);
    newnode->next = phead->next;
    newnode->prev = phead;
    phead->next->prev = newnode;
    //或者这样写
    //newnode->next->prev=newnode;
    phead->next = newnode;
}
void test()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPushFront(plist, 2);
    LTPushFront(plist, 4);
    LTPushFront(plist, 6);
    LTPushFront(plist, 8);
    LTPrint(plist);
}
int main()
{
    test();
    return 0;
}

如果结果为8->6->4->2->1->2->3->4->则没有问题

2.2.5尾删函数

我们需要判断双向链表有无数据,所以我们需要一个函数来判断,有:

//检查是否链表有数据
bool LTEmpty(LTNode* phead)
{
    assert(phead);
    return phead->next == phead;
}

尾删改变了尾结点的前面一个结点的next指向和头结点得到prev指向,所以我们应当先把尾结点存起来后再改变尾结点前一个指针指向和头结点的指针指向,最后再把尾结点的内存空间释放后置为NULL即可,代码如下:

//尾删
void LTPopBack(LTNode* phead)
{
    assert(!LTEmpty(phead));
    LTNode* del = phead->prev;
    del->prev->next = phead;
    phead->prev = del->prev;
    free(del);
    del = NULL;
}
void test()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPushFront(plist, 2);
    LTPushFront(plist, 4);
    LTPushFront(plist, 6);
    LTPushFront(plist, 8);
    LTPrint(plist);
    LTPopBack(plist);
    printf("\n");
    LTPrint(plist);
    LTPopBack(plist);
    printf("\n");
    LTPrint(plist);
    LTPopBack(plist);
    printf("\n");
    LTPrint(plist);
    LTPopBack(plist);
    printf("\n");
    LTPrint(plist);
}
int main()
{
    test();
    return 0;
}

注意:断言的那个地方要用!否则会报错,或者我们可以把那个判断是否报错的函数改为phead->next!=phead即可。如果结果为8 -> 6 -> 4 -> 2 -> 1 -> 2 -> 3 -> 4 ->
8 -> 6 -> 4 -> 2 -> 1 -> 2 -> 3 ->
8 -> 6 -> 4 -> 2 -> 1 -> 2 ->
8 -> 6 -> 4 -> 2 -> 1 ->
8 -> 6 -> 4 -> 2 ->则没有问题

2.2.6头删函数

道理差不多,代码如下:

//头删
void LTPopFront(LTNode* phead)
{
    assert(!LTEmpty(phead));
    LTNode* del = phead->next;
    del->next->prev = phead;
    phead->next = del->next;
    free(del);
    del = NULL;
}
void test1()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPushFront(plist, 2);
    LTPushFront(plist, 4);
    LTPushFront(plist, 6);
    LTPushFront(plist, 8);
    LTPrint(plist);
    LTPopFront(plist);
    printf("\n");
    LTPrint(plist);
    LTPopFront(plist);
    printf("\n");
    LTPrint(plist);
    LTPopFront(plist);
    printf("\n");
    LTPrint(plist);
}
int main()
{
    test1();
    return 0;
}

如果结果为8 -> 6 -> 4 -> 2 -> 1 -> 2 -> 3 -> 4 ->
6 -> 4 -> 2 -> 1 -> 2 -> 3 -> 4 ->
4 -> 2 -> 1 -> 2 -> 3 -> 4 ->
2 -> 1 -> 2 -> 3 -> 4 ->则代码没有问题

2.2.7链表里查找数据

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
    LTNode* pcur = phead->next;
    while (pcur != phead)
    {
        if (pcur->data == x)
        {
            return pcur;
        }
        pcur = pcur->next;
    }
    return NULL;
}
void test2()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPushFront(plist, 2);
    LTPushFront(plist, 4);
    LTPushFront(plist, 6);
    LTPushFront(plist, 8);
    LTNode* find = LTFind(plist, 2);
    if (find)
    {
        printf("找到了\n");
    }
}
int main()
{
    test2();
    return 0;
}

如果打印出找到了代码没问题

2.2.8在pos位置之后插入数据

//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
    assert(pos);
    LTNode* newnode = buyNode(x);
    newnode->next = pos->next;
    newnode->prev = pos;
    newnode->next->prev = newnode;
    pos->next = newnode;
}
void test2()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPushFront(plist, 2);
    LTPushFront(plist, 4);
    LTPushFront(plist, 6);
    LTPushFront(plist, 8);
    LTNode* find = LTFind(plist, 2);
    LTInsert(find, 3);
    LTPrint(plist);
}
int main()
{
    test2();
    return 0;
}

若结果为8 -> 6 -> 4 -> 2 -> 3 -> 1 -> 2 -> 3 -> 4 ->则代码无误,如果感兴趣的话也可以写一个在pos位置之前插入数据的函数

2.2.9把pos位置的数据删除

//把pos位置数据删除
void LTErase(LTNode* pos)
{
    assert(pos);
    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;
    free(pos);
    pos = NULL;
}
void test2()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPushFront(plist, 2);
    LTPushFront(plist, 4);
    LTPushFront(plist, 6);
    LTPushFront(plist, 8);
    LTNode* find = LTFind(plist, 2);
    LTInsert(find, 3);
    LTPrint(plist);
    printf("\n");
    LTErase(find);
    LTPrint(plist);
}
int main()
{
    test2();
    return 0;
}

如果结果为8 -> 6 -> 4 -> 2 -> 3 -> 1 -> 2 -> 3 -> 4 ->
8 -> 6 -> 4 -> 3 -> 1 -> 2 -> 3 -> 4 ->则代码无误。

2.2.10销毁链表

由于要改变头结点,所以要为二级指针,我们应当先从第一个有效数据开始删除,最后再删头结点,代码如下:

//销毁链表
void LTDesTroy(LTNode** pphead)
{
    LTNode* pcur = (*pphead)->next;
    while (pcur != *pphead)
    {
        LTNode* next = pcur->next;
        free(pcur);
        pcur = next;
        //不能置为空
    }
    free(*pphead);
    *pphead = NULL;
}
void test3()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTDesTroy(plist);
}
int main()
{
    test3();
    return 0;
}

调试发现链表最后为空了,则代码没问题。

下讲将会讲解栈的知识,将会分为两讲来讲解,下节再见,喜欢的可以一键三连,谢谢。代码如下,需要的自取:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <assert.h>
#include <ctype.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
    LTDataType data;
    struct ListNode* next;//存储下一个结点的地址
    struct ListNode* prev;//存储上一个结点的地址
}LTNode;
//申请新结点
LTNode* buyNode(LTDataType x)
{
    LTNode* node = (LTNode*)malloc(sizeof(LTNode));
    node->data = x;
    //双向链表指针初始的时候要指向本身
    node->next = node->prev = node;
    return node;
}
//初始化
LTNode* LTInit()
{
    LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
    phead->next = phead->prev = phead;
    //初始时头结点两个指针都应该指向自己
    phead->data = 0;
    return phead;
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
    LTNode* newnode = buyNode(x);
    //我们应当先改的是新插入的结点,这样才会使之后改变原链表的指向不是非常麻烦
    newnode->prev = phead->prev;
    newnode->next = phead;
    newnode->prev->next = newnode;
    //也可以改为
    //phead->prev->next = newnode;
    //都表示原来的尾结点的next指针指向
    phead->prev = newnode;
}
//打印
void LTPrint(LTNode* phead)
{
    LTNode* pcur = phead->next;
    //结束条件就是到首结点
    while (pcur != phead)
    {
        printf("%d -> ", pcur->data);
        //如果是其他数据就可以改
        pcur = pcur->next;
        //如果要逆向打印也可以把next全部替换成prev
    }
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
    assert(phead);
    //不能传入空指针
    LTNode* newnode = buyNode(x);
    newnode->next = phead->next;
    newnode->prev = phead;
    phead->next->prev = newnode;
    //或者这样写
    //newnode->next->prev=newnode;
    phead->next = newnode;
}
//检查是否链表有数据
bool LTEmpty(LTNode* phead)
{
    assert(phead);
    return phead->next == phead;
}
//尾删
void LTPopBack(LTNode* phead)
{
    assert(!LTEmpty(phead));
    LTNode* del = phead->prev;
    del->prev->next = phead;
    phead->prev = del->prev;
    free(del);
    del = NULL;
}
void test()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPushFront(plist, 2);
    LTPushFront(plist, 4);
    LTPushFront(plist, 6);
    LTPushFront(plist, 8);
    LTPrint(plist);
    LTPopBack(plist);
    printf("\n");
    LTPrint(plist);
    LTPopBack(plist);
    printf("\n");
    LTPrint(plist);
    LTPopBack(plist);
    printf("\n");
    LTPrint(plist);
    LTPopBack(plist);
    printf("\n");
    LTPrint(plist);
}
//头删
void LTPopFront(LTNode* phead)
{
    assert(!LTEmpty(phead));
    LTNode* del = phead->next;
    del->next->prev = phead;
    phead->next = del->next;
    free(del);
    del = NULL;
}
void test1()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPushFront(plist, 2);
    LTPushFront(plist, 4);
    LTPushFront(plist, 6);
    LTPushFront(plist, 8);
    LTPrint(plist);
    LTPopFront(plist);
    printf("\n");
    LTPrint(plist);
    LTPopFront(plist);
    printf("\n");
    LTPrint(plist);
    LTPopFront(plist);
    printf("\n");
    LTPrint(plist);
}
//查找
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)
{
    assert(pos);
    LTNode* newnode = buyNode(x);
    newnode->next = pos->next;
    newnode->prev = pos;
    newnode->next->prev = newnode;
    pos->next = newnode;
}
//把pos位置数据删除
void LTErase(LTNode* pos)
{
    assert(pos);
    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;
    free(pos);
    pos = NULL;
}
void test2()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPushFront(plist, 2);
    LTPushFront(plist, 4);
    LTPushFront(plist, 6);
    LTPushFront(plist, 8);
    LTNode* find = LTFind(plist, 2);
    LTInsert(find, 3);
    LTPrint(plist);
    printf("\n");
    LTErase(find);
    LTPrint(plist);
}
//销毁链表
void LTDesTroy(LTNode** pphead)
{
    LTNode* pcur = (*pphead)->next;
    while (pcur != *pphead)
    {
        LTNode* next = pcur->next;
        free(pcur);
        pcur = next;
        //不能置为空
    }
    free(*pphead);
    *pphead = NULL;
}
void test3()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTDesTroy(plist);
}
int main()
{
    test3();
    return 0;
}