线性表文档之循环双链表

Posted 二木成林

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线性表文档之循环双链表相关的知识,希望对你有一定的参考价值。

循环双链表

定义

概念

循环双链表就是在双链表的基础上,见链表的尾结点和链表的第一个结点连接起来,形成一个循环。

如果是带头结点的循环双链表,则将链表的尾结点的 next 指针指向链表的头结点,将链表头结点的 prior 指针指向链表的尾结点。

如果是不带头结点的循环双链表,则将链表的尾结点的 next 指针指向链表的开始结点,将链表开始结点的 prior 指针指向链表的尾结点。

结构体

循环双链表结点跟双链表结点一样,都是由一个数据域和两个指针域组成,其中数据域存储当前结点的数据值,而其中一个指针域存储当前结点的直接前驱的地址,另一个结点存储当前结点的直接后继的地址。如图所示:

循环双链表结点的结构体如下:

/**
 * 循环双链表的节点
 */
typedef struct CDLNode 
    /**
     * 节点的数据域
     */
    int data;
    /**
     * 节点的指针域,指向前驱节点
     */
    struct CDLNode *prior;
    /**
     * 节点的指针域,指向后继节点
     */
    struct CDLNode *next;
 CDLNode;

特点

双链表有的特点,循环双链表也有:

  • 链式存储线性表时,不需要使用地址连续的存储元素,即不要求逻辑上相邻的元素在物理位置上也相邻。
  • 链表由于是通过『链』建立起的数据元素之间的逻辑关系,插入和删除操作不需要移动元素,只需要修改链结点指针域的指向。
  • 因为链表的元素是离散地分布在存储空间中,所以链表不能随机存取,如果要找到某个数据元素,最坏情况下需要遍历整个链表。
  • 链表存储数据不需要大量连续存储空间,但链表结点除了存储数据值之外,还附加有指针域,就存在浪费存储空间的缺点。
  • 由于有两个指针域,所以比单链表更占用存储空间。
  • 双链表可以双向读取,即从任一结点开始既可以到表尾,也可以到表头。

循环双链表独有的特点:

  • 最后一个结点的 next 指针域指向链表的第一个结点,第一个结点的 prior 指针指向链表的最后一个结点。因此可以从任一结点的前驱或后继开始,找到链表的所有结点。

基本操作

注:如无特殊说明,下面关于链表的所有操作都是基于带头结点的链表。完整代码请参考:

概述

注:下面都是 C 语言代码,所以如果要对单链表进行删除或新增操作,链表参数都是双指针。如果要使用 C++ 的引用则改成 *&。一般如果是考研建议使用 & 引用,避免双指针。

循环双链表的常见操作如下:

  • void init(CDLNode **list):初始化循环双链表。其中 list 是未初始化的循环双链表。
  • CDLNode *createByHead(CDLNode **list, int nums[], int n):通过头插法创建循环双链表。其中 list 是未初始化的循环双链表;nums 是待插入到链表的数据数组;n 是数组长度。返回创建成功的循环双链表。
  • CDLNode *createByTail(CDLNode **list, int nums[], int n):通过尾插法创建循环双链表。其中 list 是未初始化的循环双链表;nums 是待插入到链表的数据数组;n 是数组长度。返回创建成功的循环双链表。
  • int insert(CDLNode **list, int i, int ele) :在循环双链表第 i 个位置插入值为 ele 的新节点。其中 list 是循环双链表;i 是指定结点序号,范围在 [1, length 之间;ele 是新节点的数据值。如果插入成功则返回 1,否则返回 0 表示插入失败。
  • void insertFirst(CDLNode **list, int ele):在循环双链表的头部插入新节点。其中 list 是循环双链表;ele 是新节点的数据值。
  • void insertLast(CDLNode **list, int ele):在循环双链表的尾部插入新节点。其中 list 是循环双链表;ele 是新节点的数据值。
  • int removeByNum(CDLNode **list, int i, int *ele):删除循环双链表第 i 个结点,并用 ele 保存被删结点的数据值。其中 list 是循环双链表;i 是待删结点的序号,范围在 [1, length] 之内;ele 用来保存被删结点的数据值。如果删除成功则返回 1,否则返回 0 表示删除失败。
  • int removeByEle(CDLNode **list, int ele):删除循环双链表中第一个等于指定值 ele 的结点。其中 list 是循环双链表;ele 是指定值。如果删除成功则返回 1,否则返回 0 表示删除失败。
  • int removeFirst(CDLNode **list, int *ele) :删除循环双链表第一个结点。其中 list 是循环双链表;ele 用来保存被删结点的数据值。如果删除成功则返回 1,否则返回 0 表示删除失败。
  • int removeLast(CDLNode **list, int *ele):删除循环双链表最后一个结点。其中 list 是循环双链表;ele 用来保存被删结点的数据值。如果删除成功则返回 1,否则返回 0 表示删除失败。
  • CDLNode *getPrior(CDLNode *list, int ele):得到循环双链表中第一个等于指定值 ele 结点的前驱结点。其中 list 是循环双链表;ele 是指定值。如果查找成功则返回第一个值为 ele 的结点的前驱结点。
  • CDLNode *getNext(CDLNode *list, int ele):得到循环双链表中第一个等于指定值 ele 结点的后继结点。其中 list 是循环双链表;ele 是指定值。如果查找成功则返回第一个值为 ele 的结点的后继结点。
  • CDLNode *findByNum(CDLNode *list, int i):寻找循环单链表中第 i 个结点。其中 list 是循环双链表;i 是结点序号,从 1 开始。如果该结点存在则返回,否则返回 NULL
  • CDLNode *findByEle(CDLNode *list, int ele):寻找循环单链表中第一个值等于 ele 的结点。其中 list 是循环双链表;ele 是指定值。如果该结点存在则返回,否则返回 NULL
  • int size(CDLNode *list):获取循环双链表的长度,即结点个数。其中 list 是循环双链表。返回循环双链表的长度。
  • int isEmpty(CDLNode *list):判断循环双链表是否为空。其中 list 是循环双链表。如果循环双链表为空则返回 1,否则返回 0。
  • void clear(CDLNode **list):清空循环双链表。其中 list 是循环双链表。
  • void print(CDLNode *list):打印循环双链表所有结点。其中 list 是循环双链表。

init

初始化循环双链表。如果是带头结点的循环双链表,则将头结点的 priornext 指针指向头结点本身;如果是不带头结点的循环双链表,则将头指针指向 NULL,不区分什么 priornext 指针指向。

实现代码如下:

/**
 * 循环双链表的初始化函数
 * @param list 带初始化的循环双链表
 */
void init(CDLNode **list) 
    // 1.初始化循环双链表,即创建头结点。头结点的前驱节点指针和后继节点指针都指向自身,表示循环
    // 1.1 为头结点分配空间
    *list = (CDLNode *) malloc(sizeof(CDLNode));
    // 1.2 将头结点的 prior 指针指向自身
    (*list)->prior = *list;
    // 1.3 将头结点的 next 指针指向自身
    (*list)->next = *list;

如果是使用 C++ 中的引用,则代码如下:

/**
 * 初始化单链表
 * @param list 单链表
 */
void init(CDLNode *&list) 
    // 给双链表头结点分配空间
    list = (CDLNode *) malloc(sizeof(CDLNode));
    // 将双链表的指针域都指向 null,不需要理会数据域
    list->prior = list;
    list->next = list;

如果是不带头结点的初始化代码如下:

/**
 * 初始化不带头节点的单链表
 * @param list 待初始化的单链表
 */
void init(CDLNode *&list) 
    // 将头指针直接置为 NULL,表示空单链表
    list = NULL;

createByHead

通过头插法创建循环双链表。所谓的头插法就是每次插入一个新元素都是插入在开始结点的位置,无论双链表是否带有头节点。

nums=[1, 2, 3, 4, 5]; n=5 为例如图所示:

实现步骤:

  • 初始化循环双链表。
  • 循环遍历数组 nums,根据数组中的每个元素创建新结点,为其分配数据域和指针域(初始创建时指针域都指向 NULL),然后将新结点插入到链表头结点和原开始结点之间。

实现代码如下:

/**
 * 通过头插法创建循环双链表
 * @param list 循环双链表
 * @param nums 待插入到双链表中的数据数组
 * @param n 数组长度
 * @return 创建成功的循环双链表
 */
CDLNode *createByHead(CDLNode **list, int nums[], int n) 
    // 1. 初始化循环双链表,即创建头结点,也可以直接调用 init 方法进行初始化
    // 1.1 为头结点分配空间
    *list = (CDLNode *) malloc(sizeof(CDLNode));
    // 1.2 将头结点的 prior 指针指向自身
    (*list)->prior = *list;// 注意,将头结点的 prior 和 next 指针指向自身
    // 1.3 将头结点的 next 指针指向自身
    (*list)->next = *list;

    /// 2.循环输入数组 nums,将每个元素插入到链表中
    for (int i = 0; i < n; i++) 
        // 2.1 创建新节点
        // 2.1.1 给新节点分配空间
        CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
        // 2.1.2 给新节点的数据域指定内容
        newNode->data = nums[i];
        // 2.1.3 将新节点的 prior 指针指向 null
        newNode->prior = NULL;
        // 2.1.4 将新节点的 next 指针指向 null
        newNode->next = NULL;

        // 2.2 将新节点插入到链表的头部,即头结点的后面
        // 2.2.1 修改新节点的 next 指针,指向原链表的第一个节点
        newNode->next = (*list)->next;
        // 2.2.2 修改新节点的 prior 指针,指向原链表的头结点
        newNode->prior = *list;
        // 2.2.3 将原链表的第一个节点的 prior 指针指向新节点
        (*list)->next->prior = newNode;
        // 2.2.4 将头节点的 next 指针指向新节点
        (*list)->next = newNode;
    

    // 3.返回创建成功的循环双链表
    return *list;

createByTail

通过尾插法创建循环双链表。

nums=[1, 2, 3, 4, 5]; n=5 为例如图所示:

实现步骤:

  • 初始化循环双链表。
  • 声明一个变量 tailNode 记录循环双链表的尾结点。
  • 循环遍历数组 nums 中的每个元素,根据元素值创建新结点,为其分配存储空间,并指定数据域和指针域(初始创建时新结点的两个指针域 priornext 都指向 NULL)。
  • 然后将新结点插入到链表的尾部称为新的尾结点,并且将新尾结点的 next 指针域指向链表的头结点,将头结点的 prior 指针域指向链表的尾结点。

实现代码如下:

/**
 * 通过尾插法创建循环双链表
 * @param list 循环双链表
 * @param nums 待插入到双链表中的数据数组
 * @param n 数组长度
 * @return 创建成功的循环双链表
 */
CDLNode *createByTail(CDLNode **list, int nums[], int n) 
    // 1. 初始化循环双链表,即创建头结点,也可以直接调用 init 方法进行初始化
    // 1.1 为头结点分配空间
    *list = (CDLNode *) malloc(sizeof(CDLNode));
    // 1.2 将头结点的 prior 指针指向自身
    (*list)->prior = *list;// 注意,将头结点的 prior 和 next 指针指向自身
    // 1.3 将头结点的 next 指针指向自身
    (*list)->next = *list;

    // 保存链表的尾节点,其实可以利用循环双链表的特性,直接使用 list.prior 来获得链表的尾节点
    CDLNode *tailNode = *list;

    // 2.循环输入数组 nums,将每个元素插入到链表中
    for (int i = 0; i < n; i++) 
        // 2.1 创建新节点
        // 2.1.1 给新节点分配空间
        CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
        // 2.1.2 给新节点的数据域指定内容
        newNode->data = nums[i];
        // 2.1.3 将新节点的 prior 指针指向 null
        newNode->prior = NULL;
        // 2.1.4 将新节点的 next 指针指向 null
        newNode->next = NULL;

        // 2.2 将新节点插入到链表的尾部
        // 2.2.1 将链表原尾节点的 next 指针指向新节点
        tailNode->next = newNode;
        // 2.2.2 将新节点的 prior 指针指向链表原尾节点
        newNode->prior = tailNode;
        // 2.2.3 将新节点的 next 指针指向链表的头结点
        newNode->next = *list;
        // 2.2.4 注意,将头结点的 prior 指针指向新节点(即新的尾节点),这样才能循环起来
        (*list)->prior = newNode;
        // 2.2.5 更新记录链表尾节点的变量 tailNode
        tailNode = newNode;
    

    // 3.返回创建成功的链表
    return *list;

insert

在循环双链表的第 i 个位置插入值为 ele 的新结点。以 list=[1, 2, 3, 4, 5]; i=3; ele=66 为例如图所示:

实现步骤:

  • 参数校验,结点序号 i 必须在范围 [1, length] 之间,否则返回 0 表示插入失败。
  • 遍历循环双链表,找到链表中第 i 个结点,并用变量 node 进行记录。
  • 创建新结点 newNode,分配存储空间,并为其指定数据域和指针域(初始时 priornext 指针都指向 NULL),然后将新结点 newNode 插入到第 i-1 个结点和第 i 个结点之间。注意,双链表结点的链接需要处理 priornext 两个指针域。
  • 最后返回 1 表示插入成功。

实现代码如下:

/**
 * 在第 i 个位置插入新节点
 * @param list 循环双链表
 * @param i 待插入新节点的序号位置,从 1 开始
 * @param ele 新节点的数据值
 * @return 如果插入成功则返回 1,否则返回 0
 */
int insert(CDLNode **list, int i, int ele) 
    // 0.参数校验
    if (i < 1 || i > size(*list)) 
        return 0;
    

    // 变量,记录链表的每一个节点,从第一个节点开始
    CDLNode *node = (*list)->next;
    // 变量,计数器,记录已经迭代的节点个数
    int count = 0;

    // 1.循环遍历链表,寻找第 i 个节点
    while (node != *list) 
        // 1.1 计数器加 1,表示已经迭代一个节点
        count++;
        // 1.2 找到第 i 个节点
        if (count == i) 
            // 1.2.1 创建新节点
            // 1.2.1.1 为新节点分配空间
            CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
            // 1.2.1.2 为新节点指定数据域
            newNode->data = ele;
            // 1.2.1.3 将新节点的 prior 指针指向 null
            newNode->prior = NULL;
            // 1.2.1.4 将新节点的 next 指针指向 null
            newNode->next = NULL;

            // 1.2.2 将新节点插入到链表中
            // 1.2.2.1 将第 i-1 个节点的 next 指针指向新节点
            node->prior->next = newNode;
            // 1.2.2.2 将新节点的 prior 指针指向第 i-1 个节点(即第 i 个节点的前驱节点)
            newNode->prior = node->prior;
            // 1.2.2.3 将新节点的 next 指针指向第 i 个节点
            newNode->next = node;
            // 1.2.2.4 将第 i 个节点的 prior 指针指向新节点,此时完成了新节点与第 i 个节点的链接
            node->prior = newNode;

            return 1;
        
        // 1.3 继续链表的下一个节点
        node = node->next;
    
    return 0;

insertFirst

在循环双链表的头部插入新结点(如果是带头结点的循环双链表,则插入到头结点的后面,原开始结点的前面;如果说是不带头结点的循环双链表,则插入到原开始结点的前面,成为新的开始结点)。以 list=[1, 2, 3, 4, 5]; ele=66 为例如图所示:

实现步骤:

  • 声明变量 firstNode 记录原循环双链表的开始结点。
  • 创建新结点 newNode ,为其分配空间,并指定数据域和指针域(priornext 都指向 NULL)。
  • 将新结点 newNode 插入到头结点和原链表开始结点 firstNode 之间。

实现代码如下:

/**
 * 插入新节点在链表中第一个节点的位置
 * @param list 循环双链表
 * @param ele 新节点的数据值
 */
void insertFirst(CDLNode **list, int ele) 
    // 变量,记录链表的第一个节点
    CDLNode *firstNode = (*list)->next;

    // 1.创建新节点
    // 1.1 为新节点分配空间
    CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
    // 1.2 为新节点的数据域指定内容
    newNode->data = ele;
    // 1.3 将新节点的 prior 指针指向 null
    newNode->prior = NULL;
    // 1.4 将新节点的 next 指针指向 null
    newNode->next = NULL;

    // 2.将新节点插入到链表的头部,即头结点的后面
    // 2.1 将新节点的 next 指针指向原链表的第一个节点
    newNode->next = firstNode;
    // 2.2 将原链表的第一个节点的 prior 指针指向新节点,此时完成了新节点与原链表第一个节点的链接
    firstNode->prior = newNode;
    // 2.3 将头结点的 next 指针指向新节点,即让新节点成为链表的第一个节点
    (*list)->next = newNode;
    // 2.4 将新节点的 prior 指针指向链表头结点,此时完成了新节点与链表头结点的链接
    newNode->prior = *list;

insertLast

在循环双链表的尾部插入新结点。可以通过 head->prior 直接获取到循环双链表的尾结点。以 list=[1, 2, 3, 4, 5]; ele=66 为例如图所示:

实现步骤:

  • 创建新结点 newNode,为其分配存储空间,并为其指定数据域和两个指针域。
  • 通过 head->next 获取链表的尾结点 lastNode,然后将新结点 newNode 插入到尾结点 lastNode 和头结点 head 之间,形成循环链接。

实现代码如下:

/**
 * 插入新节点在链表中最后一个节点的位置
 * @param list 循环双链表
 * @param ele 新节点的数据值
 */
void insertLast(CDLNode **list, int ele) 
    // 1.创建新节点
    // 1.1 为新节点分配空间
    CDLNode *newNode = (CDLNode *) malloc(sizeof(CDLNode));
    // 1.2 为新节点的数据域指定内容
    newNode->data = ele;
    // 1.3 将新节点的 prior 指针指向 null
    newNode->prior = NULL;
    // 1.4 将新节点的 next 指针指向 null
    newNode->next = NULL;

    // 2.把新节点插入到链表的尾部
    // 2.1 变量,记录循环双链表的尾节点,这里就是通过 list.prior 获得的链表尾节点
    CDLNode *lastNode = (*list)->prior;
    // 2.2 将链表尾节点的 next 指针指向新节点
    lastNode->next = newNode;
    // 2.3 将新节点的 prior 指针指向原尾节点,此时完成了原链表尾节点与新节点的链接(链接成功后,新节点就成为了链表的尾节点)
    newNode->prior = lastNode;
    // 2.4 将链表头结点的 prior 指针指向新节点,为了循环
    (*list)->prior = newNode;
    // 2.5 将新节点的 next 指针指向头结点,为了循环
    newNode->next = *list;

removeByNum

删除循环双链表中第 i 个结点,并用 ele 保存被删结点的数据值。以 list=[1, 2, 3, 4, 5]; i=3 为例如图所示:

实现步骤:

  • 参数校验,首先如果循环双链表为空则无法删除,其次序号 i 应该在 [1, length] 范围内,否则返回 0 表示删除失败。
  • 扫描循环双链表,找到第 i 个结点 node,并通过其前驱指针 prior 和后继指针 next 找到第 i-1 个结点 preNode 和第 i+1 个结点 nextNode
  • 然后删除结点 node,用 ele 保存被删结点数据值,并释放结点存储空间。

实现代码如下:

/**
 * 删除第 i 个位置的节点
 * @param list 循环双链表
 * @param i 待删除节点的序号,从 1 开始
 * @param ele 如果删除成功则返回被删除的节点,否则返回 null
 */
int removeByNum(CDLNode **list, int i, int *ele) 
    // 0.参数校验
    // 0.1 判断链表是否为空
    if ((*list)->next == *list) 
        return 0;
    
    // 0.2 判断序号是否在范围内
    if (i < 1 || i > size(*list)) 
        return 0;
    

    // 变量,记录链表中每个节点,初始为链表第一个节点
    CDLNode *node = (*list)->next;
    // 变量,记录已经迭代的节点个数
    int count = 0;

    // 1.循环遍历双链表,注意循环结束的条件
    while (node != *list) 
        // 1.1 计数器加1
        count++;
        // 1.2 找到第 i 个节点
        if (count == i) 
            // 1.2.1 局部变量,记录第 i-1 个节点
            CDLNode *preNode = node->prior;
            // 1.2.2 局部变量,记录第 i+1 个节点
            CDLNode *nextNode = node->next;
            // 1.2.3 将第 i-1 个节点的 next 指针指向第 i+1 个节点
            preNode->next = nextNode;
            // 1.2.4 将第 i+1 个节点的 prior 指针指向第 i-1 个节点
            nextNode->prior = preNode;
            // 1.2.5 保存被删除节点的数据值
            *ele = node->data;
            // 1.2.6 释放节点空间
            free(node);
            // 1.2.7 返回,结束程序
            return 1;
        
        // 1.3 继续链表的下一个节点
        node = node->next;
    

    return 0;

removeByEle

删除循环双链表中第一个值为 ele 的结点。

实现步骤:

  • 参数校验,如果循环双链表为空表,则无法删除。
  • 声明变量 node 来记录循环双链表结点,直到遇到数据域值为 ele 的结点。
  • 再声明局部变量 preNodenextNode 分别记录 node 结点的前驱结点和后继结点。
  • 删除结点 node,并释放结点存储空间。

实现代码如下:

/**
 * 删除值为 ele 的节点
 * @param list 循环双链表
 * @param ele 指定值
 * @return 如果删除成功则返回 1,否则返回 0
 */
int removeByEle(CDLNode **list, int ele) 
    // 0.参数校验
    if ((*list)->next == *list) 
        return 0;
    

    // 变量,记录链表中的每一个节点,从链表的第一个节点开始
    CDLNode *node = (*list)->next;

    // 1.循环遍历链表所有节点
    while (node != *list) 
        // 1.1 找到值等于 ele 的节点
        if (node->data == ele) 
            // 1.1.1 局部变量,记录第 i-1 个节点
            CDLNode *preNode = node->prior;
            // 1.1.2 局部变量,记录第 i+1 个节点
            CDLNode *nextNode = node->next;
            // 1.1.3 将第 i-1 个节点的 next 指针指向第 i+1 个节点
            preNode->next = nextNode;
            // 1.1.4 将第 i+1 个节点的 prior 指针指向第 i-1 个节点
            nextNode->prior = preNode;
            // 1.1.5 释放节点空间
            free(node);
            // 1.1.6 结束程序
            return 1;
        
        // 1.2 继续链表的下一个节点
        node = node->next;
    

    return 0;

removeFirst

删除循环双链表的开始结点。

实现步骤:

  • 参数校验,如果循环双链表为空表,则无法删除。
  • 声明变量 firstNode 记录循环双链表的开始结点。
  • 删除结点 firstNode,即让 firstNode->next 成为链表新的开始结点。即修改头结点的 next 指针指向 firstNode->next,将 firstNode->next 结点的 prior 指针指向头结点。
  • ele 保存被删结点 firstNode 的值,并释放结点空间。

实现代码如下:

/**
 * 删除链表的第一个节点
 * @param list 循环双链表
 * @param ele 保存被删除节点的数据域值
 */
int removeFirst(CDLNode **list, int *ele) 
    // 0.参数校验
    if ((*list)->next == *list) 
        return 0;
    

    // 1.删除链表第一个节点
    数据结构--线性表的链式存储之循环双链表

(王道408考研数据结构)第二章线性表-第三节2:双链表的定义及其操作(插入和删除)

数据结构与算法之PHP实现链表类(单链表/双链表/循环链表)

《数据结构》复习之线性表(顺序表和链表)

(王道408考研数据结构)第二章线性表-第三节3:循环单链表和循环双链表

线性链表之顺序表