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

1.什么是带头双向循环链表?

要学习带头双向循环链表,需要有普通链表的基础知识-------------------->:C语言数据结构:链表

带头双向循环链表的意思是:

1.带头:有一个哨兵位的头节点,这个头节点不存储有效数据,如下图所示:

2.双向:单链表的节点中只有指向下一个节点的next指针,双向链表的节点多了一个指向上一个节点的prev指针,如下图所示:

3.循环:这个链表是循环的,我们可以把它想象成一个圈,如下图所示:

所以带头双向循环链表的逻辑图是这样的:

实际中使用的链表数据结构,都是带头双向循环链表,另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现起来简单

2.带头双向循环链表的实现

1.带头双向循环链表节点的定义

typedef int LTDataType;
typedef struct ListNode
{
    LTDataType _data;
    struct ListNode* next;
    struct ListNode* prev;
}ListNode;

2.带头双向循环链表的初始化和新增节点

ListNode* ListCreate()
{
    ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
    if(newnode == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }

    newnode->prev = newnode;
    newnode->next = newnode;
    return newnode;
}

初始化和新增节点用的都是同一个函数,除了检查在堆上是否开辟空间外,重点就是将prev和next指针指向自己,这样做方便后续的管理。初始化也是使用这个函数的好处在于不用二级指针,在外界用一级指针来接收就可以了。

3.带头双向循环链表的遍历

带头双向循环链表是循环的,但是它有一个哨兵位头节点,所以遍历从哨兵位头节点的下一个节点开始,当遍历转完一圈后又到了哨兵位头节点时,遍历就完成了。遍历方式和普通链表基本一样,只是判断结束的方式不一样而已。

// 双向链表打印
void ListPrint(ListNode* plist)
{
    assert(plist);
    ListNode* cur = plist->next;
    printf("head <->");
    while(cur != plist)
    {
        printf("%d <-> ",cur->_data);
        cur = cur->next;
    }
    printf("head\n");
}

4.带头双向循环链表的尾插

单链表中尾插需要找尾,带头双向循环链表不需要找尾,head->prev就是尾节点(单链表尾插的时间复杂度是O(n),带头双向循环链表尾插的时间复杂度是O(1))。

下图中如果我们要尾插数据3节点,影响到的有尾节点和头节点

带头双向循环链表逻辑上是很复杂的,尾插的方式有多种,看不懂图的同学可以参考我下面这种尾插的方法:

1.将数据3节点的prev指针指向尾节点:

伪代码:(数据3节点)->prev = head -> prev

2.将哨兵位头节点(head)的prev指向数据3节点:

伪代码:head -> prev = (数据3节点)

3.将数据3节点的next指向哨兵位头节点(head):

伪代码:(数据3节点)->next = head

4.将数据2节点的next指向数据3节点,尾插完成(此时的数据2节点已经不是尾节点了):

伪代码:(数据3节点)->prev->next = (数据3节点);

void ListPushBack(ListNode* plist, LTDataType x)
{
    assert(plist);
    ListNode* newnode = ListCreate();
    newnode->prev = plist->prev;
    plist->prev = newnode;
    newnode->next = plist;
    newnode->prev->next = newnode;

    newnode->_data = x;
}

5.带头双向循环链表的尾删

如果我们删除下图中的数据3尾节点,只需要将head节点的prev指向数据2节点,数据2节点的next指向head节点尾删就完成了。需要创建一个指针变量(del)来保存数据3节点,防止数据3节点丢失。

1.将head节点的prev指向数据2节:

伪代码:head->prev = del->prev

2.数据2节点的next指向head节点:

伪代码:del->prev->next = head

3.释放掉数据3节点,尾插完成:

伪代码:free(del)

尾删需要注意的点:只剩下一个哨兵位头节点(head)时,说明没有节点了,还删什么,建议用assert断言一下直接报错。

// 双向链表尾删
void ListPopBack(ListNode* plist)
{
    assert(plist);
    assert(plist->next != plist);

    ListNode* del = plist->prev;//保存尾节点

    //尾删
    del->prev->next = plist;
    plist->prev = del->prev;

    free(del);
}

6.带头双向循环链表的头插

下图中如果我们要头插(数据1前面插入)数据3节点,需要改动head节点和数据1节点

1.将数据3节点的prev指向head节点,next指向数据1节点:

伪代码:(数据3节点)->prev = head;    (数据3节点)->next = head->next;

2.将head的next指向数据3节点:

伪代码:head->next = (数据3节点)

3.将数据1节点的prev指向数据3节点,头插完成

伪代码:(数据3节点)->next->prev = (数据3节点)

// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x)
{
    assert(plist);
    ListNode* newnode = ListCreate();

    newnode->prev = plist;
    newnode->next = plist->next;
    plist->next = newnode;
    newnode->next->prev = newnode;

    newnode->_data = x;
}

7.带头双向循环链表的头删

下图中,如我们要头删(删除数据1节点),会影响到head节点和数据2节点,我们先用指针变量del保存数据1节点,方便后面的代码编写和释放。

1.将head的next指向数据2节点:

伪代码:head->next = del->next

2.将数据2节点的prev指向head:

伪代码:del->next->prev = head:

3.释放掉数据1,头删除完成。

伪代码:free(del)

// 双向链表头删
void ListPopFront(ListNode* plist)
{
    assert(plist);
    assert(plist->next != plist);

    ListNode* del = plist->next;
    plist->next = del->next;
    del->prev->prev = plist;

    free(del);
}

8.带头双向循环链表的销毁和查找

1.销毁

带头双向循环链表的销毁非常简单,循环调用头删或尾删,循环结束条件是head->next != head,前面在初始化和新增节点的那个函数里,我们让prev和next指向自己的好处就体现出来了,最后最释放掉哨兵位头节点,带头双向循环链表的销毁就完成了。

void ListDestory(ListNode* plist)
{
    assert(plist);
    while(plist->next != plist)
    {
        ListPopBack(plist);
    }
    free(plist);
}

2.查找

查找和遍历基本一样,找到了返回对应的节点,找不到就返回哨兵位头节点。

// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x)
{
    assert(plist);
    ListNode* cur = plist->next;
    while(cur != plist)
    {
        if(cur->_data == x)
        {
            return cur;
        }
        cur = cur->next;
    }
    return plist;
}

9.带头双向循环链表在pos的前面进行插入

假设pos位置是下图中的数据2节点,我们要在数据2的前面插入数据4节点。

1.将数据4节点的next指向数据2节点,prev指向数据1节点:

伪代码:(数据4节点)->next = pos;  (数据4节点)->prev = pos->prev;

2.将数据1节点的next指向数据4节点:

伪代码:(数据4节点)->prev->next = (数据4节点)

3.将数据2节点的prev指向数据4节点,插入完成:

伪代码:pos->prev = (数据4节点)

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
    assert(pos);
    ListNode* newnode = ListCreate();
    newnode->prev = pos->prev;
    newnode->next = pos;
    newnode->prev->next = newnode;
    pos->prev = newnode;

    newnode->_data = x;
}

10.带头双向循环链表删除pos位置的结点

假设pos位置是下图中的数据2节点,我们要删除数据2节点。

1.先将数据1节点的next指向数据3节点:

伪代码:pos->prev->next = pos->next;

2.将数据3节点的prev指向数据1节点:

伪代码:pos->next->prev = pos->prev;

3.释放掉数据2节点,删除完成:

伪代码:free(pos)

// 双向链表删除pos位置的结点
void ListErase(ListNode* pos)
{
    assert(pos);
    assert(pos->next != pos);
    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;
    free(pos);
}

3.扩展知识

1.关于带头双向循环链表的头查尾插:

其实头插和尾插可以复用在pos的前面进行插入(void ListInsert(ListNode* pos, LTDataType x))这个函数,为什么不提前说明和实现的原因是因为我觉得这样做不好,老老实实的画图加上伪代码可以加深大家的印象(虽然不知道有多少人能看),下面是复用在pos的前面进行插入(void ListInsert(ListNode* pos, LTDataType x))这个函数的头插和尾插:

头插:

// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x)
{
    ListInsert(plist->next,x);
}

尾插:

// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x)
{
    ListInsert(plist,x);
}

2.关于带头双向循环链表的头删尾删:

头插尾删复用的代码是删除pos位置的结点(ListErase(ListNode* pos))这个函数

头删:

void ListPopFront(ListNode* plist)
{
    ListErase(plist->next);
}

尾删:

// 双向链表尾删
void ListPopBack(ListNode* plist)
{
    ListErase(plist->prev);
}

3.链表和顺序的区别:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值