【数据结构】带头双向循环链表

带头双向循环链表其实就是 链表的pro-max版本。注意下一些细节就很好懂了。
这里我们先创建两个.c文件,test.c,用来测试代码,LIstNode.c用来实现链表的功能。ListNode.h用来放一些头文件和功能函数名。
【数据结构】带头双向循环链表_第1张图片

head是一个单纯的头结点,它没啥具体含义,也不参与链表的功能实现。

  1. 链表的结点实现

1.1结构体结点

(有时候因为输入法的原因,结点容易变成节点,各位理解一下)

结点说的这么高级,其实不就是一个结构体吗。

#include
#include
#include
#include
using namespace std;

typedef int LTDataType;  //
typedef struct ListNode
{
    struct ListNode* next;  //后继指针
    struct ListNode* prev;  //前驱指针
    LTDataType data;
}ListNode;
using namespace std是C++中的命名区间,主要是不写他就不能用cout进行打印,各位也可以用printf打印。

注意:

  1. 这里之所以typedef int 是为了更便捷的改变存储类型,如果以后存的是double类,直接把int改成double就可以了。

  1. 这里的两个指针是struct ListNode* 而不是ListNode是因为重命名从最后的ListNode开始生效。

  1. 因为是双向链表所以用到两个指针。

1.2为啥next,prev用指针?为啥用的是结构体指针?

我们为啥要用这俩玩意?为的是链接前后的节点?咋链接?用→吗?这不扯淡吗。我们是通过存储前面或者后面结点的地址来实现串联起来。能存储地址的只有指针。
存储整形地址用整形指针,存储字符地址用字符指针,那存储结构体不用结构体指针用啥?

下面有更详细关于指针介绍,不要错过啊!

2.链表功能函数的实现

2.1初始化函数ListInit()

【数据结构】带头双向循环链表_第2张图片

其实在初始化之前要先创建一个哨兵结点:

//创建节点函数
ListNode* BuyListNode(LTDataType x)
{
    ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));  //创建一个新节点
    if (newnode == NULL)   //创建新节点失败
    {
        printf("malloc fail");
        exit(-1);
    }
    newnode->next = newnode->prev = NULL;
    newnode->data = x;
    return newnode;
}

再进行初始化:

ListNode* ListInit()
{
    ListNode* phead = BuyListNode(-1);  //哨兵位结点,不存储有效数据
    phead->data = 0;
    phead->next = phead->prev = phead;   //初始化是phead而不是NULL
    return phead;
}

这里还有一个很简单但是不容易注意的细节:初始化中两个指针指向的不是NULL而是哨兵位本身。

2.2打印功能ListPrint

void ListPrint(ListNode* phead)
{
    assert(phead);
    ListNode* cur = phead->next;
    while (cur != phead)
    {
        cout << cur->data << "->" << " ";
        cur = cur->next;
    }
    cout << endl;
}

这个cout也是实现打印功能的,不想用可以用printf.

注意:不要把哨兵结点打印进去,它没有实质作用。

刚开始我是while(phead!=phead->next)这样行吗?
这样写的话打印必须是phead->next->next;
phead->next=phead->next->next,但是这样看有点别扭啊。

2.3头插函数ListPushBack

【数据结构】带头双向循环链表_第3张图片

这个最需要注意的一个细节就是一定要保存phead->next,要不然链接phead->tmp以后,就找不到front的地址了。

这个思路很简单的。tmp链接完哨兵结点后在链接d1就可以啦。

//头插
void ListPushFront(ListNode* phead, LTDataType x)
{
    assert(phead);
    ListNode* tmp = BuyListNode(x);   //定义一个要插入的节点
    ListNode* pre = phead->next;   //定义phead原本后面节点位置
    phead->next = tmp;
    tmp->prev = phead;
    //链接tmp和后面的节点
    tmp->next = pre;
    pre->prev = tmp;
}

2.4尾插函数ListPushFront

这个和头插的思路一毛一样

【数据结构】带头双向循环链表_第4张图片

唯一的改变就是从记录phead的后节点到记录前结点。

//尾插实现
void ListPushBack(ListNode* phead, LTDataType x)
{
    assert(phead);
    ListNode* newnode = BuyListNode(x);//申请一个结点,数据域赋值为x
    ListNode* tail = phead->prev;//记录头结点的前一个结点的位置
    //建立新结点与头结点之间的双向关系
    newnode->next = phead;
    phead->prev = newnode;
    //建立新结点与tail结点之间的双向关系
    tail->next = newnode;
    newnode->prev = tail;
}

2.5查找函数ListFind

//查找元素
ListNode* ListFind(ListNode* phead, LTDataType x)
{
    assert(phead);
    ListNode* cur = phead->next;//从头结点的后一个结点开始查找
    while (cur != phead)//当cur指向头结点时,说明链表已遍历完毕
    {
        if (cur->data == x)
        {
            return cur;//返回目标结点的地址
        }
        cur = cur->next;
    }
    return NULL;//没有找到目标结点
}
【数据结构】带头双向循环链表_第5张图片

2.6头删和尾删

【数据结构】带头双向循环链表_第6张图片

只需要把倒数第二个结点和哨兵结点链接就行。

//尾删
void ListPopback(ListNode* phead)
{
    assert(phead);
    assert(phead->next);
    ListNode* tail = phead->prev;
    ListNode* newtail = tail->prev;
    phead->prev = newtail;
    newtail->next = phead;

    free(tail);
}
【数据结构】带头双向循环链表_第7张图片
头删思路虽然不是完全一样,但是可以说一毛一样:
//头删
void ListPopFront(ListNode* phead)
{
    assert(phead);
    assert(phead->next);
    ListNode* pre = phead->next;
    ListNode* newpre = pre->next;
    phead->next = newpre;
    newpre->prev = phead;
    free(pre);
}

3.其他功能实现

3.1任意位置插入实现

就比如要在d2位置插入一个新节点:

【数据结构】带头双向循环链表_第8张图片
//pos位置插入
void ListInsert(ListNode* pos, LTDataType x)
{
    assert(pos);
    ListNode* pre = pos->prev;
    ListNode* newnode = BuyListNode(x);   //开辟一个要插入的新结点

    pre->next = newnode;
    newnode->prev = pre;
    //链接新结点与后面的结点
    newnode->next = pos;
    pos->prev = newnode;

}

3.2任意位置删除

这个记录pos位置后面那个结点位置,然后链接pos的前后结点,再释放掉pos结点就好了。

//删除指定位置结点
void ListErase(ListNode* pos)
{
    assert(pos);

    ListNode* before = pos->prev;//记录pos指向结点的前一个结点
    ListNode* after = pos->next;//记录pos指向结点的后一个结点
    //建立before结点与after结点之间的双向关系
    before->next = after;
    after->prev = before;
    free(pos);//释放pos指向的结点
}

3.3获取链表中元素个数

这个不就是遍历吗。

//获取链表中的元素个数
int ListSize(ListNode* phead)
{
    assert(phead);

    int count = 0;//记录元素个数
    ListNode* cur = phead->next;//从头结点的后一个结点开始遍历
    while (cur != phead)//当cur指向头结点时,遍历完毕,头结点不计入总元素个数
    {
        count++;
        cur = cur->next;
    }
    return count;//返回元素个数
}

3.4销毁链表

//销毁链表
void ListDestroy(ListNode* phead)
{
    assert(phead);

    ListNode* cur = phead->next;//从头结点后一个结点开始释放空间
    ListNode* next = cur->next;//记录cur的后一个结点位置
    while (cur != phead)
    {
        free(cur);
        cur = next;
        next = next->next;
    }
    free(phead);//释放头结点
}

4.思路拓展

4.1为啥链表用二级指针,但是循环链表用不到二级指针呢?

改变结构体的内容用到结构体指针,改变结构体指针用到结构体指针的指针。

在这里面plist相当于实参,phead是形参,形参的改变不会影响到实参,对吧。

但是在链表中,在空链表中进行头插。

【数据结构】带头双向循环链表_第9张图片
【数据结构】带头双向循环链表_第10张图片
phead中已经改变了,但是plist中却没有任何反应。plist本来指向的是NULL,但是现在想让他指向这个新插入的结点偷,所以就改变了plist的指向,所以就用二级指针去改变一级指针。
但是对于带头循环链表来说,plist始终指向的是哨兵结点,而插入和删除操作都是在哨兵节点后进行的,所以各项操作都没改变plist的指向。(注意,plist一直指向链表的头结点)plist一直指向head没有改变。
【数据结构】带头双向循环链表_第11张图片
【数据结构】带头双向循环链表_第12张图片

这里的plist和phead的地址一样,无论如何插入,plist都未改变。

4.2为啥顺序表中的指针是整形,但是链表中的是结构体指针

(我们就规定顺序表中存的是整形)

顺序表开辟的是一整块连续的空间,它存的地址是有规律的,所以就不需要向链表一样需要一个专门的结构体指针来专门存放下一个结点地址。,而是直接通过下标进行访问。
其次顺序表中存的是整形,保存整形变量的地址就要用到整形指针
【数据结构】带头双向循环链表_第13张图片

你可能感兴趣的:(链表,数据结构)