线性表文档之双链表

Posted 二木成林

tags:

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

双链表

定义

概念

单链表到达链表的尾结点,那么无法再倒序找到链表的第一个结点,同时如果我们找到单链表中的某个结点,只能获取到它的后继结点,而无法获取到它的前驱结点,如果要记录它的前驱结点,必须专门准备一个变量来进行记录。而双链表能够解决这些问题。

双链表即双向链表,也是链表的一种。双链表中的每个数据结点除了数据域之外,还有两个指针域,分别指向当前结点的直接前驱和直接后继。即从双链表的任一结点开始,都可以快速访问它的前驱结点和后继结点。

结构体

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

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

/**
 * 双链表节点结构体
 */
typedef struct DLNode 
    /**
     * 双链表节点数据域
     */
    int data;
    /**
     * 双链表节点的前驱节点,数据域
     */
    struct DLNode *prior;
    /**
     * 双链表节点的后继节点,数据域
     */
    struct DLNode *next;
 DLNode;

特点

单链表有的特点,双链表也有:

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

双链表独有的特点:

  • 由于有两个指针域,所以比单链表更占用存储空间。
  • 双链表可以双向读取,即从任一结点开始既可以到表尾,也可以到表头。

基本操作

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

概述

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

双链表的常见操作如下:

  • void init(DLNode **list):初始化双链表。其中 list 表示未初始化的双链表。
  • DLNode *createByTail(DLNode **list, int nums[], int n):通过尾插法创建双链表。其中 list 表示未初始化的双链表;nums 是待插入到双链表中的数据数组;n 表示数组长度。返回创建成功的双链表。
  • DLNode *createByHead(DLNode **list, int nums[], int n):通过头插法创建双链表。其中 list 表示未初始化的双链表;nums 是待插入到双链表中的数据数组;n 表示数组长度。返回创建成功的双链表。
  • DLNode *findByEle(DLNode *list, int ele):查找双链表中等于指定值 ele 的结点。其中 list 表示双链表;ele 表示待查找的指定值。如果查找成功则返回该结点,否则返回 NULL
  • DLNode *findByNum(DLNode *list, int i):查找双链表中第 i 个结点。其中 list 表示双链表;i 表示双链表中的结点序号,从 1 开始。如果查找成功则返回该结点,否则返回 NULL
  • int insert(DLNode **list, int i, int ele) :在双链表中第 i 个位置插入值为 ele 的新结点。其中 list 表示双链表;i 表示序号,从 1 开始;ele 表示新结点的数据值。如果插入成功则返回 1,否则返回 0。
  • void append(DLNode **list, int ele):在双链表的尾部插入值为 ele 的新结点。其中 list 表示双链表;ele 表示新结点的数据值。
  • int removeByNum(DLNode **list, int i, int *ele):删除双链表中第 i 个结点,并用 ele 保存被删结点的数据值。其中 list 表示双链表;i 表示待删结点的序号,从 1 开始;ele 用来存放被删结点的数据值。如果删除成功则返回 1,否则返回 0。
  • int isEmpty(DLNode *list):判断双链表是否为空。其中 list 表示待判断的双链表。如果双链表为空则返回 1,否则返回 0。
  • int size(DLNode *list):获取双链表的长度,即结点个数。其中 list 表示双链表。返回双链表的长度,如果为空则返回 0。
  • void clear(DLNode **list):清空双链表。其中 list 表示待清空的双链表。
  • void print(DLNode *list):打印双链表所有结点。其中 list 表示待打印的双链表。

init

初始化双链表。即如果是带头结点的双链表,首先为双链表的头结点分配存储空间,然后将头结点的 priornext 指针指向 NULL。如果是不带头结点的双链表,则直接将头指针指向 NULL

实现代码如下:

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

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

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

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

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

createByTail

通过尾插法批量插入元素然后创建一个非空双链表。所谓的尾插法就是每次插入一个新元素都是插入在双链表最后一个结点的位置,无论双链表是否带有头结点。

list=[1, 2, 3, 4, 5]; n=5 为例使用尾插法创建双链表:

实现步骤:

  • 初始化双链表,即为双链表头结点分配空间,并且将头结点的 priornext 指向 NULL
  • 声明一个指针变量 tailNode,用来记录双链表的尾结点,初始为双链表的第一个结点。
  • 循环数组中所有元素,根据当前数组元素的值创建新结点,并且将其指定为新结点数据域的值,然后将新结点的指针域 priornext 都指向 NULL
  • 然后将新结点 newNode 插入到尾结点 tailNode 的后面,插入完成后,更新变量 tailNode=newNode

实现代码如下:

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

    // 使用尾插法,最重要的是知道链表的尾节点,初始时为链表的头结点
    DLNode *tailNode = *list;

    // 2.循环数组中元素,然后插入到链表的尾部
    for (int i = 0; i < n; i++) 
        // 2.1 创建新节点
        // 2.1.1 为新节点分配空间
        DLNode *newNode = (DLNode *) malloc(sizeof(DLNode));
        // 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 不要忘记更新尾节点,让新节点成为新的尾节点,才能进行下一次插入
        tailNode = newNode;
    
    return *list;

createByHead

通过头插法批量插入元素然后创建一个非空双链表。所谓的头插法就是每次插入一个新元素都是插入在第一个结点的位置,无论双链表是否带有头节点。

nums=[1, 2, 3, 4, 5]; n=5 为例使用头插法创建双链表:

实现步骤:

  • 初始化双链表,即为双链表头结点分配空间,并且将头结点的 priornext 指向 NULL
  • 循环数组中所有元素,根据当前数组元素的值创建新结点,并且将其指定为新结点数据域的值,然后将新结点的指针域 priornext 都指向 NULL
  • 注意,这里判断了双链表是否为空的情况,如果双链表为空则新结点就是链表的第一个结点,无序连接后继结点;如果双链表不为空,则将新结点插入到头结点后面,并且将新结点与头结点和原双链表第一个结点建立连接。

注:

  • 创建新结点之后,为其指定了数据域和指针域,两个指针域初始都指向 NULL,是为了表示创建的完整性。
  • 将新结点与头结点和原双链表第一个结点建立连接有四步:
    • 第一步,将新结点的 next 指针指向原双链表第一个结点,即 newNode->next=head->next;。其中 head->next 表示双链表的第一个结点。
    • 第二步,将原链表第一个结点的 prior 指针指向新结点,即 head->next->prior=newNode。此时完成了新结点与原链表第一个结点的连接。
    • 第三步,将头结点的 next 指针指向新结点,即 head->next=newNode
    • 第四步,将新结点的 prior 指针指向头结点,即 newNode->prior=head。此时完成了新结点与头结点的连接。

实现代码如下:

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

    // 2.循环数组中元素,然后插入到链表的头部
    for (int i = 0; i < n; i++) 
        // 2.1 创建新节点
        // 2.1.1 为新节点分配空间
        DLNode *newNode = (DLNode *) malloc(sizeof(DLNode));
        // 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 需要考虑链表为空的情况,当链表为空时新节点只能插入到头结点的后面
        if ((*list)->next == NULL) 
            // 2.2.1.1 链表为空,只需要将头结点的 next 指针指向新节点
            (*list)->next = newNode;
            // 2.2.1.2 然后将新节点的 prior 指针指向头结点,即完成了头结点和新节点的连接
            newNode->prior = *list;
        
            // 2.2.2 如果链表不为空,则直接在头结点后面插入新节点即可,注意新节点和头结点和原链表第一个节点的连接
        else 
            // 2.2.2.1 保存原链表的第一个节点,必定不为 null
            DLNode *firstNode = (*list)->next;
            // 2.2.2.2 将原链表的第一个节点的 prior 指针指向新节点
            firstNode->prior = newNode;
            // 2.2.2.3 将新节点的 next 指针指向原链表的第一个节点,已经完成了新节点与原链表第一个节点的连接
            newNode->next = firstNode;
            // 2.2.2.4 将头结点的 next 指针指向新节点
            (*list)->next = newNode;
            // 2.2.2.5 将新节点的 prior 指针指向头结点,已经完成了新节点与头结点的连接
            newNode->prior = *list;
        
    
    return *list;

findByEle

查找双链表中等于指定值 ele 的结点。

实现步骤:

  • 从双链表的第一个结点开始,扫描整个双链表,判断正在扫描的结点的数据域值是否等于 ele,如果找到该结点则返回;否则返回 NULL

实现代码如下:

/**
 * 通过节点数据检索双链表中的节点
 * @param list 双链表
 * @param ele 节点数据值
 * @return 查找到的节点
 */
DLNode *findByEle(DLNode *list, int ele) 
    // 链表的第一个节点
    DLNode *node = list->next;
    // 1.循环链表所有节点,查找能够匹配值的节点
    while (node != NULL) 
        // 1.1 如果正在迭代的节点的数据域值等于指定值,表示找到该节点,则返回
        if (node->data == ele) 
            return node;
        
        // 1.2 如果不等于则继续双链表的下一个节点
        node = node->next;
    
    return NULL;

findByNum

查找双链表中第 i 个结点,如果不存在第 i 个结点,则返回 NULL

实现步骤:

  • 声明一个计数器变量 count 用来记录已经遍历的结点个数。
  • 从头到尾扫描双链表,在遍历过程中首先计数器 count 加一,然后用 counti 进行比较,如果相等则返回该结点,否则继续下一个结点。直至到链表尾部。

实现代码如下:

/**
 * 通过节点序号检索双链表中的节点
 * @param list 双链表
 * @param i 节点序号,从 1 开始
 * @return 查找到的节点
 */
DLNode *findByNum(DLNode *list, int i) 
    // 链表的第一个节点
    DLNode *node = list->next;
    // 计数器,记录当前遍历到第几个节点
    int count = 0;
    // 循环遍历链表
    while (node != NULL) 
        // 计数器加1,记录已经遍历的节点个数
        count++;
        // 查找到第 i 个节点,然后返回该节点
        if (count == i) 
            return node;
        
        // 继续链表的下一个节点
        node = node->next;
    
    return NULL;

insert

在双链表的第 i 个位置插入值为 ele 的新结点。如果插入成功则返回 1,否则返回 0 表示插入失败。

实现步骤:

  • 参数校验,序号 i 的位置必须在 [1, length] 范围内,否则就是参数不合法则返回 0 表示插入失败。
  • 找到第 i 个结点 node,这里通过 findByNum 函数来完成。注意判断找到的第 i 个结点是否存在,如果为 NULL 则直接返回 0 表示第 i 个结点不存在,因此总会插入失败。
  • 然后创建新结点,然后通过第 i 个结点的前驱指针 prior 找到它的前驱结点 temp,然后将新结点插入到 tempnode 之间。

注:在双链表中插入结点要进行四步的连接。可参考 createByHead 的图。

实现代码如下:

/**
 * 向双链表中的指定序号位置插入节点
 * @param list 双链表
 * @param i 指定序号,从 1 开始
 * @param ele 节点数据值
 * @return 如果插入成功则返回 1,否则返回 0
 */
int insert(DLNode **list, int i, int ele) 
    // 0.参数校验
    if (i < 1 || i > size(*list)) 
        return 0;
    

    // 1.首先查找第 i 个节点,直接调用我们已经写好的函数即可。在双链表中插入不需要找第 i-1 个节点,因为可以通过第 i 个节点的 prior 指针得到它的前驱节点
    DLNode *node = findByNum(*list, i);
    if (node == NULL) 
        return 0;
    

    // 2.将新节点插入到链表指定位置
    // 2.1 创建新节点
    // 2.1.1 为新节点分配空间
    DLNode *newNode = (DLNode *) malloc(sizeof(DLNode));
    // 2.1.2 指定新节点的数据域
    newNode->data = ele;
    // 2.1.3 将新节点的 prior 指针指向 null
    newNode->prior = NULL;
    // 2.1.4 将新节点的 next 指针指向 null
    newNode->next = NULL;

    // 2.2 将新节点插入到链表中
    // 2.2.1 保存第 i 个节点的前驱节点,即第 i-1 个节点
    DLNode *temp = node->prior;
    // 2.2.2 将第 i 个节点的 prior 指针指向新节点
    node->prior = newNode;
    // 2.2.3 将新界的的 next 指针指向第 i 个节点,此时完成了新节点与第 i 个节点的连接
    newNode->next = node;
    // 2.2.4 将第 i-1 个节点的 next 指针指向新节点
    temp->next = newNode;
    // 2.2.5 将新节点的 prior 指针指向第 i-1 个节点,此时完成了新节点与第 i-1 个节点的连接
    newNode->prior = temp;
    return 1;

append

在双链表的尾部追加值为 ele 的新结点。

实现步骤:

  • 第一步,找到双链表的尾结点 lastNode
  • 第二步,创建新结点 newNode 并且将新结点 newNode 插入到 尾结点 lastNode 的后面。

注:完成新结点 newNode 与尾结点 lastNode 的链接只需要两步:

  • 将新结点 newNodeprior 指针指向尾结点 lastNode,即 newNode->prior=lastNode
  • 将尾结点 lastNodenext 指针指向新结点 newNode,即 lastNode->next=newNode

实现代码如下:

/**
 * 向双链表的尾部附加一个新节点
 * @param list 双链表
 * @param ele 新节点的数据值
 */
void append(DLNode **list, int ele) 
    // 1.获取到链表的最后一个节点,使用下面的方式能有效处理空链表的情况
    // 1.1 保存链表的最后一个节点,初始时为链表的头结点
    DLNode *node = (*list)->next;
    // 1.2 链表的第一个节点,用于遍历循环
    DLNode *lastNode = *list;
    // 1.3 循环遍历链表每一个节点
    while (node != NULL) 
        // 1.3.1 保存正在迭代的节点
        lastNode = node;
        // 1.3.2 继续链表的下一个节点
        node = node->next;
    

    // 2.将新节点插入到链表的尾部
    // 2.1 创建新节点
    // 2.1.1 为新节点分配空间
    DLNode *newNode = (DLNode *) malloc(sizeof(DLNode));
    // 2.1.2 指定新节点的数据域
    newNode->data = ele;
    // 2.1.3 将新节点的 prior 指针指向 null
    newNode->prior = NULL;
    // 2.1.4 将新节点的 next 指针指向 null
    newNode->next = NULL;
    // 2.2 将新节点附加到链表的尾部
    // 2.2.1 将新节点的 prior 指针指向原链表最后一个节点
    newNode->prior = lastNode;
    // 2.2.2 将原链表最后一个节点的 next 指针指向新节点,此时完成了新节点和原链表最后一个节点的连接
    lastNode->next = newNode;

removeByNum

删除双链表中第 i 个结点,并用 ele 保存被删结点的数据值。

list=[1, 2, 3, 4, 5]; i=5 为例(即删除双链表最后一个结点的情况):

list=[1, 2, 3, 4, 5]; i=3 为例(即删除双链表非尾结点的情况):

实现步骤:

  • 参数校验,序号 i 必须在 [1, length] 范围内。否则判定参数不合法则返回 0 表示删除失败。
  • 找到第 i 个结点 node,这里通过 findByNum 函数完成。注意,判断第 i 个结点必须存在,否则无法删除,即返回 0 表示删除失败。
  • 如果待删结点 node 是双链表的最后一个结点(即如果 node->next==NULL 则表示是最后一个结点),则特殊处理,将 node 前驱结点的 next 指针指向 NULL
  • 如果待删结点 node 不是双链表的最后一个结点(即 node->next!=NULL),则需要将 node 结点的前驱结点(即 node->prior)与其后继结点(即 node->next)链接起来。
  • 最后用 ele 保存被删结点 node 的数据值,然后释放 node 结点的存储空间,返回 1 表示删除成功。

实现代码如下:

/**
 * 删除双链表中指定序号的节点
 * @param list 双链表
 * @param i 指定序号,从 1 开始
 * @param ele 保存被删除节点的数据值
 * @return 如果删除成功则返回 1,否则返回 0
 */
int removeByNum(DLNode **list, int i, int *ele) 
    // 0.参数校验
    if (i < 1 || i > size(*list)) 
        return 0;
    

    // 1.找到第 i 个节点,利用我们已经写好的函数即可。也不需要找到第 i-1 个节点,因为我们通过第 i 个节点的 prior 指针获得
    DLNode *node = findByNum(*list, i);
    if (node == NULL) 
        return 0;
    

    // 2.移除第 i 个节点
    // 2.1 需要判断待删除的节点是否还有后继节点,如果没有后继节点则表示该节点是链表的最后一个节点则需要特殊处理
    if (node->next == NULL) 
        // 2.1.1 如果被删除节点是最后一个节点,则只需要将第 i-1 个节点的 next 指针指向 null
        node->prior->next = NULL;
    
    // 2.2 如果被删除节点不是链表的最后一个节点,则需要通过被删除节点的前驱节点和后继节点进行删除
    else 
        // 将第 i+1 个节点的 prior 指针指向第 i-1 个节点
        node->next->prior = node->prior;
        // 将第 i-1 个节点的 next 指针指向第 i+1 个节点,完成第 i 个节点的删除
        node->prior->next = node->next;
    
    // 2.3 保存被删除节点的数据
    *ele = node->data;
    // 2.4 释放空间
    free(node);
    return 1;

isEmpty

判断双链表是否为空。如果为空则返回 1,否则返回 0。

实现步骤:

  • 如果是不带头结点的双链表,则判断链表头指针 head 是否等于 NULL,如果等于 NULL 则是空链表,否则不是;如果是带头结点的双链表,则判断 head->next 是否等于 NULL,即判断链表的开始结点是否存在,如果不为 NULL 则表示不是空链表。

实现代码如下:

/**
 * 双链表是否为空链表
 * @param list 双链表
 * @return 如果链表为空则返回 1,否则返回 0
 */
int isEmpty(DLNode *list) 
    return list->next == NULL;

size

统计双链表的长度,即链表中的结点个数(不包括头结点)。

实现步骤:

  • 声明计数器变量 count 用来记录双链表中结点个数。
  • 从头到尾扫描单链表,遍历每个结点,记录结点个数。注意,不包括头结点。

实现代码如下:

/**
 * 获取双链表中的节点个数
 * @param list 双链表
 * @return 节点个数
 */
int size(DLNode *list) 
    // 计数器,记录链表的节点个数
    int count = 0;
    // 链表的第一个节点
    DLNode *node = list->next;
    // 循环双链表,统计节点个数
    while (node != NULL) 
        count++;
        node = node->next;
    
    return count;

clear

清空双链表所有结点。

实现步骤:

  • 从头到尾扫描双链表所有结点,然后调用 free 函数释放结点空间。

实现代码如下:

/**
 * 清空双链表
 * @param list 双链表
 */
void clear(DLNode **list) 
    // 链表的第一个节点
    DLNode *node = (*list)->next;
    // 循环遍历链表所有节点,回收空间
    while (node != NULL) 
        // 临时保存当前节点的后继节点
        DLNode *temp = node->next;
        // 释放当前节点的空间
        free(temp);
        // 继续链表的下一个节点
        node = temp;
    
    // 将头节点的 next 指针指向 null
    (*list)->next = NULL;

print

打印双链表所有结点。

实现步骤:

  • 从头到尾扫描双链表,打印所有结点的数据域值。

实现代码如下:

/**
 * 打印双链表中所有节点数据
 * @param list 双链表
 */
void print(DLNode *list) 
    // 链表的第一个节点
    DLNode *node 以上是关于线性表文档之双链表的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

数据结构--线性表的链式存储之循环双链表

单链表浅析

双链表算法之遍历