数据结构与算法篇-双向循环链表

新闻资讯   2023-06-30 10:41   87   0  
👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇


作者丨写代码的牛顿
来源丨编程学习总站



01

双向循环链表结点定义和函数声明


双向循环链表结点内部有2个指针prev和next分别指向前后的结点,结点定义代码如下:


typedef struct dlist{
    int data;
    struct dlist *next;
    struct dlist *prev;
}dlist_t;


现在我们声明一些双向循环链表操作函数,代码如下:


extern dlist_t *new_dlist_node(int data); //新建一个结点
extern dlist_t *dlist_add(dlist_t *list, int data); //插入一个结点
extern dlist_t *dlist_delete(dlist_t *list, int index); //删除索引对应的结点(索引从0开始)
extern dlist_t *dlist_index_of(dlist_t *list, int index); //查找索引对应的结点
extern void dlist_destroy(dlist_t *list); //销毁双向循环链表
extern void dlist_print(dlist_t *list); //打印链表数据


可以看到很多函数都会返回一个dlist_t类型的指针,其实这是头结点。很多时候为了书写方便我们会采用typedef定义自己的数据类型,结点定义里为什么next和prev指针可以那样写,参考我上一篇文章,后面会大量这样使用。

02

双向循环链表函数实现

为了更方便释放一个结点内存,我们定义了一个文件作用域静态函数free_dlist_node。


void free_dlist_node(dlist_t *node){
    if(node == NULL){
        return;
    }

    node->next = NULL;
    node->prev = NULL;
    free(node);
    node = NULL;
}


该函数只负责释放内存的操作。

  • 新建链表结点


dlist_t *new_dlist_node(int data){
    dlist_t *node = (dlist_t *)malloc(sizeof(dlist_t));
    if(node == NULL){
        return NULL;
    }

    node->data = data;
    node->next = node;
    node->prev = node;

    return node;
}


这里我们主要注意一点就是新建立的结点next和prev指针会初始化为指向自身,事实上双向循环链表头结点必须这样初始化才能更好的利用双向循环链表特性执行插入、删除和查询等操作。

  • 插入结点


dlist_t *dlist_add(dlist_t *list, int data){
    if(list == NULL){
        return NULL;
    }

    //新建一个结点
    dlist_t *node = new_dlist_node(data);
    if(node == NULL){
        return list;
    }

    //将结点插入双向循环链表
    list->prev->next = node;  //最后的结点next指针指向node
    node->next = list;  //node的next指针指向头结点
    node->prev = list->prev;  //node的prev指针指向原尾结点
    list->prev = node;  //头结点的prev指针指向新尾结点

    return list;
}


这里list是传入的头结点,我们采用尾插法插入一个结点,最后返回头结点。这里需要注意:每次插入结点都要更新头结点的prev指针。

  • 删除指定位置的结点


dlist_t *dlist_delete(dlist_t *list, int index){
    if(list == NULL || index < 0){
        return list;
    }

    dlist_t *head = list;
    int list_index = 0;

    //删除链表头结点
    if(index == 0){
        //链表只有一个结点
        if(head == list->next){
            free_dlist_node(list);
            list = NULL;
            return NULL;
        }else{
            //链表有大于1个结点
            head = head->next; //头结点往后移一个
            head->prev = list->prev; //头结点的prev指向尾部结点
            list->prev->next = head; //尾部结点的next指针指向头结点
            free_dlist_node(list); //释放结点内存
            return head;
        }
    }

    list = list->next;
    list_index++;

    //查询目标结点,通过检查当前结点是否是头结点判断是否已经把双线循环链表遍历了一遍
    while(list_index < index && head != list){
        list = list->next;
        list_index++;
    }
    //没有找到即将删除的结点
    if(head == list){
        return head;
    }
    //找到即将删除的结点
    else{
        list->prev->next = list->next; //目标结点的上一个结点的next指针指向目标结点的下一个结点
        list->next->prev = list->prev; //目标结点的下一个结点的prev指针指向目标结点的上一个结点
        free_dlist_node(list); //释放结点内存
        return head; //返回头结点
    }
}


删除结点一定要注意:
  1. 判断被删除的结点是否是头结点

  2. 指定的位置是否超出了链表的长度。


  • 查找指定位置的结点


dlist_t *dlist_index_of(dlist_t *list, int index){
    if(list == NULL || index < 0){
        return NULL;
    }

    //如果想要获取头结点,则直接返回头结点
    if(index == 0){
        return list;
    }

    dlist_t *head = list;
    list = list->next;
    int list_index = 1;
    //遍历链表查找指定的索引,通过检查当前结点是否等于头结点判断是否已遍历完毕
    while(list_index < index && list != head){
        list_index++;
        list = list->next;
    }
    //没有找到索引对应的结点
    if(list == head){
        return NULL;
    }
    //找到了索引对应的结点
    return list;
}


查找指定位置结点要注意几个地方:
  1. 是否是头结点

  2. 是否超出了链表长度


  • 销毁链表


void dlist_destroy(dlist_t *list){
    if(list == NULL){
        return;
    }

    //如果只有一个结点
    if(list->next == list){
        free_dlist_node(list);
        return;
    }

    dlist_t *temp = list;
    list = list->next;
    while(list != temp){
        list = list->next; //遍历下一个结点
        temp->prev->next = list;
        list->prev = temp->prev;
        temp->next = NULL;
        temp->prev = NULL;
        free(temp);
        temp = list;
    }

    free_dlist_node(list);
}


  • 打印链表数据


void dlist_print(dlist_t *list){
    if(list == NULL){
        return;
    }

    dlist_t *head = list;
    printf("%d, ", list->data);
    list = list->next;
    while(list != head){
        printf("%d, ", list->data);
        list = list->next;
    }
    printf("\n");
}


最后我们写个小程序验证一下双向循环链表函数实现是否正确。


#include <stdio.h>
#include "dlist.h"

int main() {
    dlist_t *list = new_dlist_node(2);
    int i = 0;

    for(i = 0; i < 7; i++){
        list = dlist_add(list, 3 + i);
    }
    printf("打印未处理过的完整链表\n");
    dlist_print(list);

    list = dlist_delete(list, 0); //删除第一个结点
    list = dlist_delete(list, 3); //删除第四个结点
    printf("打印删除2个结点后的链表\n");
    dlist_print(list);

    dlist_t *node = dlist_index_of(list, 2);
    printf("第3个结点是:%d\n", node->data);

    printf("销毁链表\n");
    dlist_destroy(list);
    list = NULL;

    return 0;
}


编译运行输出:

打印未处理过的完整链表
2, 3, 4, 5, 6, 7, 8, 9,
打印删除2个结点后的链表
3, 4, 5, 7, 8, 9,
第3个结点是:5
销毁链表


验证完全正确

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击👆卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

文章引用微信公众号"程序员大咖",如有侵权,请联系管理员删除!

博客评论
还没有人评论,赶紧抢个沙发~
发表评论
说明:请文明发言,共建和谐网络,您的个人信息不会被公开显示。