【数据结构】带头双向循环链表的实现:双链表查找 | pos之前插入 | 删除pos位置 | 提供完整代码

324 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情


0x07 双链表查找(DListFind)

💬 DList.h

DLNode* DListFind(DLNode* pHead, DLNodeDataType x);

💬 DList.c

DLNode* DListFind(DLNode* pHead, DLNodeDataType x) {
    assert(pHead); //防止pHead为空

    DLNode* cur = pHead->next;
    while(cur != pHead) {
        if (cur->data == x)
            return cur;
        cur = cur->next;
    }
    return NULL;
}

🔑 解读:很简单,和打印的思路一样。只需要创建一个 cur 从第一个有效节点(pHead->next)开始遍历链表就可以了。如果 cur->data 和传入的 x,即需要查找的值相同就返回 curcur 是带有值和地址的。如果整个链表都走完了还没有找到相同的值,就返回 NULL 。

💬 Test.c

没什么测的,懒得测了。自己测去吧。

(划掉)

0x08 双链表pos位置之前插入(DListInsert)

💬 DList.h

void DListInsert(DLNode* pos, DLNodeDataType x);

🔑 解读:根据性质,这里甚至都不需要哨兵位,我们只需要传入 pos 和待插入的值 x 即可。

💬 DList.c

void DListInsert(DLNode* pos, DLNodeDataType x) {
    assert(pos != NULL); // 防止传入的pos为空

    DLNode* new_node = CreateNewNode(x);
    DLNode* posPrev = pos->prev; //pos前驱

    //思路草图: posPrev  newnode(待插目标)   pos
    posPrev->next = new_node;
    new_node->prev = posPrev;
    new_node->next = pos;
    pos->prev = new_node;
}

🔑 解读:

① 防止 pos 传入为空。

② 首先创建新节点。因为是在 pos 位置之前插入,为了方便,我们先标出 pos 的前驱指针 posPrev,随后我们画出草图:

思路草图:posPrev newnode(待插目标) pos

posPrevnew_node 相互链接起来:

    posPrev->next = new_node;
    new_node->prev = posPrev;

new_nodepos 相互链接起来:

    new_node->next = pos;
    pos->prev = new_node;

⚡ 此时,DListInsert 就大功告成了!它就是双向链表最核心的两个接口之一,我们可以将这个万能的 "pos位置之前插入" 复用到我们刚才写的尾插和头插那里,即 DListPushBack 和 DListPushFront 中:

// 尾插(NEW)
void DListPushBack(DLNode* pHead, DLNodeDataType x) {
    assert(pHead != NULL); //防止pHead为空
    
    /*
    DLNode* tail = pHead->prev; //创建尾指针
    DLNode* new_node = CreateNewNode(x); //创建新节点

    //思路草图: pHead                 tail   new_node(尾插目标)
    tail->next = new_node;
    new_node->prev = tail;
    new_node->next = pHead;
    pHead->prev = new_node;
    */

    DListInsert(pHead, x);
}

🔑 解读:传入 pHead 进入 DListInsert 中,可以找到尾部节点,即可完成尾插。

// 头插(NEW)
void DListPushFront(DLNode* pHead, DLNodeDataType x) {
    assert(pHead != NULL); //防止pHead为空
    
    /*
    DLNode* new_node = CreateNewNode(x); //创建新节点
    DLNode* pHeadNext = pHead->next; //提出第一个节点

    //思路草图: pHead  new_node(头插目标)  First
    pHead->next = new_node;
    new_node->prev = pHead;
    new_node->next = pHeadNext;
    pHeadNext->prev = new_node;
    */

    DListInsert(pHead->next, x);
}

🔑 解读:头插,将 pHead->next 传入 DListInsert 中即可完成头插。

0x09 双链表删除pos位置(DListEarse)

💬 DList.h

void DListEarse(DLNode* pos);

💬 DList.c

void DListEarse(DLNode* pos) {
    assert(pos != NULL); //防止传入的pos为空

    DLNode* posPrev = pos->prev; //标出pos的前驱指针
    DLNode* posNext = pos->next; //标出pos的后继指针
    //思路草图: posPrev  pos(待删目标)  posNext
    posPrev->next = posNext;
    posNext->prev = posPrev;
    //释放
    free(pos);
    pos = NULL
}

🔑 解读:

① 防止 pos 传入为空。

② 既然要删除 pos 位置的节点,我们先标出 pos 的前驱指针 posPrevpos 的后继指针 posNext ,随后我们画出草图:

思路草图: posPrev pos(待删目标) posNext

之后我们开始 "缝针" ,将 posPrevposNext 相互链接起来:

posPrev->next = posNext;
posNext->prev = posPrev;

最后再将 pos 释放即可。

⚡ 此时,DListEarse 也搞定了!我们可以将这个万能的 "pos位置删除" 复用到之前写的尾删和头删那里,即 DListPopBack 和 DListPopFront 中:

// 尾删(NEW)
void DListPopBack(DLNode* pHead) {
    assert(pHead != NULL); //防止pHead为空
    assert(pHead->next != pHead); //防止链表为空
    /*
    //思路草图: pHead          tailPrev  tail(待删目标)
    DLNode* tail = pHead->prev;
    DLNode* tailPrev = tail->prev;
    //释放
    free(tail);
    //链接
    tailPrev->next = pHead;
    pHead->prev = tailPrev;
    */

    DListEarse(pHead->prev);
}

🔑 解读:尾删,将 pHead->prev 传入 DListEarse 中即可完成尾删。

// 头删(NEW)
void DListPopFront(DLNode* pHead) {
    assert(pHead != NULL); // 防止pHead为空
    assert(pHead->next != pHead); // 防止链表为空

    /*
    //思路草图:  pHead  pHeadNext(待删目标)  pHeadNextNext
    DLNode* pHeadNext = pHead->next;
    DLNode* pHeadNextNext = pHeadNext->next;
    //链接
    pHead->next = pHeadNextNext;
    pHeadNextNext->prev = pHead;
    //释放
    free(pHeadNext);
    */

    DListEarse(pHead->next);
}

🔑 解读:头删,将 pHead->next 传入 DListEarse 中即可完成头删。

四、总结

🔺 所以,双向链表严格来说只需要快速地实现 insert 和 earse 这两个接口就可以搞定了。为什么会这么简单?别问,问就是结构的优势!

如果以后让你快速实现一个双向链表,你把 "pos位置之前插入" 和 "删除pos位置" 这两个接口写好,头尾的插入和删除直接复用就可以搞定了。

五、完整代码

最后附上完整代码(包含测试用地代码)

💬 DList.h

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

typedef int DLNodeDataType;       // DLNodeDataType == int

typedef struct DoubleListNode {
    DLNodeDataType data;          // 用来存放结点的数据
    struct DoubleListNode* next;  // 指向后继节点的指针
    struct DoubleListNode* prev;  // 指向前驱节点的指针
} DLNode;  // 重命名为DLNode
       
DLNode* DListInit();
void DListPrint(DLNode* pHead);
void DListPushBack(DLNode* pHead, DLNodeDataType x);
void DListPushFront(DLNode* pHead, DLNodeDataType x);
void DListPopBack(DLNode* pHead);
void DListPopFront(DLNode* pHead);
DLNode* DListFind(DLNode* pHead, DLNodeDataType x);
void DListInsert(DLNode* pos, DLNodeDataType x);
void DListEarse(DLNode* pos);

💬 DList.c

#include "DList.h"

DLNode* DListInit() {
    //哨兵位头节点
    DLNode* pHead = (DLNode*)malloc(sizeof(DLNode));
    pHead->next = pHead;
    pHead->prev = pHead;

    return pHead;
}

DLNode* CreateNewNode(DLNodeDataType x) {
    //动态内存开辟一块 DLNode 大小的空间给 new_node
    DLNode* new_node = (DLNode*)malloc(sizeof(DLNode));
    //检查malloc
    if (new_node == NULL) {
        printf("malloc failed!\n");
        exit(-1);
    }
    //放置数据
    new_node->data = x;
    //初始化
    new_node->next = NULL;
    new_node->prev = NULL;
    //返回
    return new_node;
}

void DListPrint(DLNode* pHead) {
    assert(pHead != NULL); //防止pHead为空
    
    DLNode* cur = pHead->next; //因为pHead存的不是有效数据,所以要从pHead的下一个节点开始
    while(cur != pHead) {
        printf("%d ", cur->data); //打印
        cur = cur->next;
    }                                          
    printf("\n"); //换行
}

void DListPushBack(DLNode* pHead, DLNodeDataType x) {
    assert(pHead != NULL); //防止pHead为空

    DListInsert(pHead, x);
}

void DListPushFront(DLNode* pHead, DLNodeDataType x) {
    assert(pHead != NULL); //防止pHead为空
    DListInsert(pHead->next, x);
}

void DListPopBack(DLNode* pHead) {
    assert(pHead != NULL); //防止pHead为空
    assert(pHead->next != pHead); //防止链表为空

    DListEarse(pHead->prev);
}

void DListPopFront(DLNode* pHead) {
    assert(pHead != NULL); // 防止pHead为空
    assert(pHead->next != pHead); // 防止链表为空
    
    DListEarse(pHead->next);
}

DLNode* DListFind(DLNode* pHead, DLNodeDataType x) {
    assert(pHead != NULL); //防止pHead为空

    DLNode* cur = pHead->next;
    while(cur != pHead) {
        if (cur->data == x)
            return cur;
        cur = cur->next;
    }
    return NULL;
}

// 在pos位置之前插入
void DListInsert(DLNode* pos, DLNodeDataType x) {
    assert(pos != NULL); // 防止传入的pos为空

    DLNode* new_node = CreateNewNode(x);
    DLNode* posPrev = pos->prev; //pos前驱

    //思路草图:posPrev  newnode(待插目标)   pos
    posPrev->next = new_node;
    new_node->prev = posPrev;
    new_node->next = pos;
    pos->prev = new_node;
}

// 删除pos位置
void DListEarse(DLNode* pos) {
    assert(pos != NULL); //防止传入的pos为空

    DLNode* posPrev = pos->prev; //标出pos的前驱指针
    DLNode* posNext = pos->next; //标出pos的后继指针
    //思路草图: posPrev  pos(待删目标)  posNext
    posPrev->next = posNext;
    posNext->prev = posPrev;
    //释放
    free(pos);
    pos = NULL;
}

💬 Test.c

#include "DList.h"

void TestList1() {
    DLNode* pList = DListInit();
    DListPushBack(pList, 1);
    DListPushBack(pList, 2);
    DListPushBack(pList, 3);
    DListPushBack(pList, 4);
    DListPrint(pList);
}

void TestList2() {
    DLNode* pList = DListInit();
    DListPushFront(pList, 10);
    DListPushFront(pList, 20);
    DListPushFront(pList, 30);
    DListPushFront(pList, 40);

    DListPrint(pList);
}

void TestList3() {
    DLNode* pList = DListInit();
    DListPushBack(pList, 1);
    DListPushBack(pList, 2);
    DListPushBack(pList, 3);
    DListPushBack(pList, 4);
    DListPrint(pList);

    DListPopBack(pList);
    DListPrint(pList);
}

void TestList4() {
    DLNode* pList = DListInit();
    DListPushBack(pList, 1);
    DListPushBack(pList, 2);
    DListPushBack(pList, 3);
    DListPushBack(pList, 4);
    DListPrint(pList);

    DListPopFront(pList);
    DListPrint(pList);
}


int main() {
    TestList1();
    // TestList2();
    // TestList3();
    // TestList4();
    
    return 0;
}

参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. baike.baidu.com/.

声明:本章的 DoubleListNode 源代码参考自C++的STL库!

📌 笔者:王亦优

📃 更新: 2021.11.15

❌ 勘误: 无

📜 声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!