线性表文档之单链表

Posted 二木成林

tags:

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

单链表

定义

概念

线性表的链式存储称为单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存储一个指向其后继元素的指针。

结构体

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

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

typedef struct LNode 
    int data;// 存放结点数据域,默认是 int 类型,可以修改为其他类型
    struct LNode* next;// 存放结点指针域,指向后继节点的指针
 LNode;// 定义单链表结点类型

特点

单链表的特点如下:

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

顺序表和单链表的区别:

顺序表单链表
是否逻辑相邻
是否物理相邻
存储空间是否连续连续离散
是否存在空间浪费如果确定要存储的数据多少,那么使用顺序表不存在明显的空间浪费;如果不确定要存储的数据多少,那么使用顺序表可能有较大的空间浪费。如果确定要存储的数据多少,那么使用单链表相比顺序表存在空间浪费,因为还需要存储指针域;如果不确定要存储的数据多少,那么使用单链表更划算。
访问随机元素的时间复杂度支持下标访问元素,时间复杂度为 O(1)必须从头开始遍历整个单链表直到找到某元素为止,访问随机元素的平均时间复杂度为 O(n)
随机位置插入和删除元素的时间复杂度由于顺序表的元素是连续存储的,如果要在特定位置插入或删除元素时要将它之后的元素全部后移或前移一个元素的位置,时间开销较大。平均时间复杂度为 O(n)单链表在插入或删除元素时,只需要改变它的前驱节点和插入或删除元素的指针指向即可。时间复杂度为 O(1)
使用建议如果查询操作比较频繁则使用顺序表比较好。如果插入或删除操作比较频繁时则使用单链表较好。

基本操作

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

概述

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

单链表的常见基本操作如下:

  • void init(LNode **list):初始化单链表。其中 list 表示未初始化的单链表。
  • LNode *createByHead(LNode **list, int nums[], int n):通过头插法创建单链表。其中 list 是未初始化的单链表;nums 表示待插入到单链表中的数据数组;n 表示数组 nums 数组长度。返回创建成功的单链表。
  • LNode *createByTail(LNode **list, int nums[], int n):通过尾插法创建单链表。其中 list 是未初始化的单链表;nums 表示待插入到单链表中的数据数组;n 表示数组 nums 数组长度。返回创建成功的单链表。
  • LNode *findByNum(LNode *list, int i):查找单链表中第 i 个结点。其中 list 是单链表;i 是结点序号,从 1 开始,支持的范围是 [1, length]。如果查找成功则返回第 i 个位置的结点,否则返回 NULL
  • LNode *findByEle(LNode *list, int ele):查找单链表中等于指定值 ele 的结点。其中 list 是单链表;ele 是指定值。如果查找成功则返回等于该值的第一个结点,否则返回 NULL
  • int insert(LNode **list, int i, int ele):在单链表指定 i 位置插入新元素 ele。其中 list 是已经初始化的单链表;i 是链表节点序号,从 1 开始;ele 是待插入新节点的数据值。如果插入成功则返回 1,否则返回 0。
  • int remove(LNode **list, int i, int *ele):删除单链表中指定 i 位置的元素,并且用 ele 来存储被删除元素。其中 list 是已经初始化的单链表;i 是链表节点序号,从 1 开始;ele 是用来存放被删除节点的数据值。如果删除成功则返回 1,否则返回 0。
  • int removeEle(LNode **list, int ele):删除单链表中第一个数据值为 ele 的元素。其中 list 是已经初始化的单链表;ele 是待删除元素的数据值。如果删除成功则返回 1,否则返回 0。
  • void removeAllEle(LNode **list, int ele):删除单链表中所有数据值为 ele 的元素。其中 list 是已经初始化的单链表;ele 是待删除元素的数据值。
  • int size(LNode *list):计算单链表的长度。其中 list 是已经初始化的单链表。返回单链表中元素个数。
  • int isEmpty(LNode *list):判断单链表是否为空。其中 list 是已经初始化的单链表。如果单链表为空则返回 1,否则返回 0。
  • void clear(LNode **list):清空单链表。其中 list 是待清空的单链表。
  • void print(LNode *list):打印单链表所有节点。其中 list 是待打印的单链表。

init

初始化单链表。如果是带头结点的单链表,初始化是创建头结点并将头结点的 next 指针指向 NULL;如果是不带头结点的单链表,初始化是将头指针指向 NULL

实现代码如下:

/**
 * 初始化单链表
 * @param list 待初始化的单链表
 */
void init(LNode **list) 
    // 创建头结点,分配空间
    *list = (LNode *) malloc(sizeof(LNode));
    // 同时将头节点的 next 指针指向 NULL,因为空链表没有任何节点
    (*list)->next = NULL;

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

/**
 * 初始化单链表
 * @param list 待初始化的单链表
 */
void init(LNode *&list) 
    // 创建头结点,分配空间
    list = (LNode *) malloc(sizeof(LNode));
    // 同时将头节点的 next 指针指向 NULL,因为空链表没有任何节点
    list->next = NULL;

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

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

createByHead

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

实现步骤:

  • 对链表进行初始化。注意,是带头结点的单链表。
  • 循环遍历数组中的每个元素,然后根据数组元素创建单链表结点,创建新节点时将数组元素值赋给结点数据域,将新节点的指针域指向 NULL
  • 将创建的新节点的 next 指针域指向单链表的第一个节点,然后将单链表的头结点的 next 指针指向新节点。

注:

  • 为什么不直接新节点的指针域?是因为保持创建新节点的完整性,便于知道做了哪些操作。
  • 在进行单链表插入操作时,先处理新节点的 next 指针域,再处理前驱节点的 next 指针域指向新节点。即先后再前

实现代码如下:

/**
 * 通过头插法创建单链表
 * @param list 单链表
 * @param nums 数据数组
 * @param n 数组长度
 * @return 创建成功的单链表
 */
LNode *createByHead(LNode **list, int nums[], int n) 
    // 1.初始化单链表
    // 创建链表必须要先初始化链表,也可以选择直接调用 init() 函数
    *list = (LNode *) malloc(sizeof(LNode));
    (*list)->next = NULL;

    // 2.通过循环将数组中所有值通过头插法插入到单链表中
    for (int i = 0; i < n; i++) 
        // 2.1 创建新节点,并指定数据域和指针域
        // 2.1.1 创建新结点,分配空间
        LNode *newNode = (LNode *) malloc(sizeof(LNode));
        // 2.1.2 给新节点的数据域指定值
        newNode->data = nums[i];
        // 2.1.3 给新节点的指针域指定为 null
        newNode->next = NULL;

        // 2.2 将新节点插入到单链表的头部,但是在头结点的后面
        // 2.2.1 获取到单链表的第一个节点,即头结点的后继节点
        LNode *firstNode = (*list)->next;// 单链表的第一个节点
        // 2.2.2 将头结点的 next 指针指向新节点
        newNode->next = firstNode;
        // 2.2.3 将新节点的 next 指针指向原单链表的第一个节点,此时新节点成为了单链表头结点的后继节点
        (*list)->next = newNode;
    
    return *list;

createByTail

通过尾插法批量插入元素然后创建一个非空单链表。所谓的尾插法就是每次将新节点插入到链表的尾部。

[1, 2, 3, 4, 5] 为例使用尾插法创建单链表步骤如图:

实现步骤:

  • 初始化单链表。
  • 设置一个变量来记录单链表的尾结点 tailNode,初始为单链表的头结点。
  • 循环遍历数组中的每个元素,然后根据数组元素创建单链表结点,创建新节点时将数组元素值赋给结点数据域,将新节点的指针域指向 NULL
  • 将尾结点 tailNodenext 指针指向新结点,然后将新节点记录为新的尾节点。

实现代码如下:

/**
 * 通过尾插法创建单链表
 * @param list 单链表
 * @param nums 创建单链表时插入的数据数组
 * @param n 数组长度
 * @return 创建好的单链表
 */
LNode *createByTail(LNode **list, int nums[], int n) 
    // 1.初始化单链表
    // 创建链表必须要先初始化链表,也可以选择直接调用 init() 函数
    *list = (LNode *) malloc(sizeof(LNode));
    (*list)->next = NULL;

    // 尾插法,必须知道链表的尾节点(即链表的最后一个节点),初始时,单链表的头结点就是尾节点
    // 因为在单链表中插入节点我们必须知道前驱节点,而头插法中的前驱节点一直是头节点,但尾插法中要在单链表的末尾插入新节点,所以前驱节点一直都是链表的最后一个节点,而链表的最后一个节点由于链表插入新节点会一直变化
    LNode *node = (*list);

    // 2.循环数组,将所有数依次插入到链表的尾部
    for (int i = 0; i < n; i++) 
        // 2.1 创建新节点,并指定数据域和指针域
        // 2.1.1 创建新节点,为其分配空间
        LNode *newNode = (LNode *) malloc(sizeof(LNode));
        // 2.1.2 为新节点指定数据域
        newNode->data = nums[i];
        // 2.1.3 为新节点指定指针域,新节点的指针域初始时设置为 null
        newNode->next = NULL;

        // 2.2 将新节点插入到单链表的尾部
        // 2.2.1 将链表原尾节点的 next 指针指向新节点
        node->next = newNode;
        // 2.2.2 将新节点置为新的尾节点
        node = newNode;
    
    return *list;

findByNum

查找单链表中第 i 个结点。以 list=[1, 2, 3, 4, 5]; i=3 为例如图:

实现步骤:

  • 参数校验,i 的范围必须在 [1, length],否则就是非法参数,返回 NULL。
  • 从头结点 node 开始,每次前进一步,前进 i 次,循环结束后 node 刚好指向第 i 个结点。

实现代码如下:

/**
 * 发现单链表中第 i 个结点
 * @param list 单链表
 * @param i 指定序号位置,从 1 开始
 * @return 如果第 i 个结点存在则返回,否则返回 NULL
 */
LNode *findByNum(LNode *list, int i) 
    // 0.参数校验,序号 i 必须在 [1, length] 范围内
    if (i < 1 || i > size(list)) 
        return NULL;
    

    // 1.从头到尾扫描单链表,找到第 i 个结点
    LNode *node = list;// 注意,从头结点开始
    while (i > 0) 
        node = node->next;
        i--;
    
    return node;

findByEle

查找单链表中结点值等于 ele 的结点。以 list=[11, 22, 33, 44, 55]; ele=33 为例如图所示:

实现步骤:

  • 从头到尾扫描单链表所有结点,比较结点值,然后返回单链表中第一个结点值等于 ele 的结点。如果单链表存在等于该值的结点,则返回 NULL

实现代码如下:

/**
 * 查找单链表中等于指定值 ele 的结点
 * @param list 单链表
 * @param ele 指定值
 * @return 如果单链表中有等于值 ele 的结点则返回该结点,否则返回 NULL
 */
LNode *findByEle(LNode *list, int ele) 
    // 变量,记录链表结点,初始为链表第一个结点
    LNode *node = list->next;
    // 从头到尾扫描单链表所有结点,判断结点值
    while (node != NULL) 
        // 如果当前结点的值等于 ele,则返回
        if (node->data == ele) 
            return node;
        
        // 否则继续下一个结点的判断
        node = node->next;
    
    // 如果链表中不存在值等于 ele 的结点,则直接返回 NULL
    return NULL;

insert

在顺序表 list 的第 i1<=i<=length)个位置插入新元素 ele。若 index 的输入不合法,则返回 0,表示插入失败。否则,找到第 i-1 个结点 iPreNode,然后创建新节点 newNode,将新结点的 next 指针域指向原 iPreNode->next,再将 iPreNode 结点的 next 指针域指向新节点 newNode。如果插入成功则返回 1。

list=[1, 2, 3, 4, 5]; i=3; ele=66 为例,步骤如下:

实现步骤:

  • 参数校验,判断序号 i 的合法性。如果超出 [1, length] 范围则返回 0 表示插入失败。
  • 通过循环找到第 i 个结点的前驱结点 iPreNode
  • 将新结点插入到链表中。即创建新结点 newNode,然后将新结点 newNodenext 指针域指向原第 i 个结点(即 iPreNode->next 结点),再将第 i-1 个结点(即 iPreNode 结点)的 next 指针域指向新结点 newNode
  • 插入成功后,返回 1。

实现代码如下:

/**
 * 在单链表的第 i 个结点(从 1 开始)前插入一个结点
 * @param list 单链表
 * @param i 节点序号,从 1 开始
 * @param ele 待插入的数据
 * @return 如果插入成功则返回 1,如果插入失败则返回 0
 */
int insert(LNode **list, int i, int ele) 
    // 0.校验参数
    if (i < 1 || i > size(*list) + 1) // 当 i=1 并且单链表为空时也能有效插入
        return 0;
    

    // 1.计算第 i 个节点的前驱节点。注意,第一个节点的前驱节点是头结点
    // 1.1 声明三个变量
    LNode *iPreNode = *list;// 用来保存第 i 个节点的前驱节点,初始时链表第一个节点的前驱节点是头结点
    LNode *node = (*list)->next;// 用来保存链表中的每一个节点为了遍历循环,初始时链表的第一个节点
    int count = 1;// 计数器,记录遍历次数,初始为 1 而不能是 0,因为 i 表示节点的序号(序号从 1 开始的)
    // 1.2 找到第 i 个节点的前驱节点,循环 i-1 次
    while (count < i) 
        // 1.2.1 计数器加 1,表示已经遍历 1 次
        count++;
        // 1.2.2 保存当前节点为前驱节点
        iPreNode = node;
        // 1.2.3 继续下一个节点
        node = node->next;
    

    // 2.将新节点插入到链表中
    // 2.1 创建新节点
    // 2.1.1 为新节点分配空间
    LNode *newNode = (LNode *) malloc(sizeof(LNode));
    // 2.1.2 为新节点指定数据域
    newNode->data = ele;
    // 2.1.3 为新节点指定指针域,初始时都指向 null
    newNode->next = NULL;
    // 2.2 将新节点连接到链表中
    // 2.2.1 将新节点的 next 指针指向第 i 个节点的前驱节点的后继节点(实际上就是第 i 个节点)上
    newNode->next = iPreNode->next;
    // 2.2.2 将第 i 个节点的 next 指针指向新节点
    iPreNode->next = newNode;
    return 1;

remove

删除单链表指定位置 i 的结点,并且将被删结点的值保存到 ele 中返回。

list=[1, 2, 3, 4, 5]; i=3 为例,步骤如下:

实现步骤:

  • 参数校验,指定位置 i 必须是合法的参数,范围在 [1, length] 之间,包含边界。
  • 找到第 i 个结点的前驱结点 iPreNode(即第 i-1 个结点),并根据 iPreNode 得到第 i 个结点 iNode(即 iPreNode->next 结点)。
  • 然后将 iPreNode 结点的 next 指针指向 iNode 结点的后继结点,这样就断开了第 i 个结点在链表中的连接,最后返回 iNode 结点的数据值给 ele并调用 free 函数释放空间,完成删除返回 1。

实现代码如下:

/**
 * 删除单链表中第 i 个结点
 * @param list 单链表
 * @param i 节点序号,从 1 开始
 * @param ele 被删除节点的数据
 * @return 如果删除成功则返回 1,如果删除失败则返回 0
 */
int remove(LNode **list, int i, int *ele) 
    // 0.校验参数
    if (i < 1 || i > size(*list)) 
        return 0;
    

    // 1.计算第 i 个节点的前驱节点。注意,第一个节点的前驱节点是头结点
    // 1.1 声明三个变量
    LNode *iPreNode = *list;// 用来保存第 i 个节点的前驱节点,初始时链表第一个节点的前驱节点是头结点
    LNode *node = (*list)->next;// 用来保存链表中的每一个节点为了遍历循环,初始时链表的第一个节点
    int count = 1;// 计数器,记录遍历次数,初始为 1 而不能是 0,因为 i 表示节点的序号(序号从 1 开始的)
    // 1.2 找到第 i 个节点的前驱节点,循环 i-1 次
    while (count < i) 
        // 1.2.1 计数器加 1,表示已经遍历 1 次
        count++;
        // 1.2.2 保存当前节点为前驱节点
        iPreNode = node;
        // 1.2.3 继续下一个节点
        node = node->next;
    

    // 2.删除第 i 个节点
    // 2.1 得到第 i 个节点
    LNode *iNode = iPreNode->next;
    // 2.2 保存被删除节点的数据
    *ele = iNode->data;
    // 2.3 删除第 i 个节点,即将第 i 个节点的前驱节点的 next 指针指向第 i 个节点的后继节点
    iPreNode->next = iNode->next;
    // 2.4 释放被删除节点的空间
    free(iNode);

    return 1;

removeEle

删除单链表中第一个值等于 ele 的结点。

list=[1, 2, 3, 4, 5]; ele=4 为例,步骤如下:

实现步骤:

  • 设定两个变量 nodepre,其中 node 记录链表中的每一个结点(从单链表的第一个结点开始)直到遇到值等于 ele 的结点;而 pre 则记录 node 结点的前驱结点,便于进行删除 node 结点的操作。初始时 node 指向单链表的第一个结点,pre 指向链表的头结点。
  • 从第一个结点开始扫描单链表,如果发现正在遍历的结点 node 的数据域等于 ele,则删除 node 结点(即 pre->next=node->next),删除 node 结点之后就释放它的空间,然后返回 1 表示删除成功。

实现代码如下:

/**
 * 删除单链表中指定值的结点。
 * 注意,只会删除找到的第一个节点,如果有多个节点的值都等于指定值则只会删除第一个。
 * @param list 单链表
 * @param ele 指定值
 * @return 如果删除成功则返回 1,否则返回 0
 */
int removeEle(LNode **list, int ele) 
    // 链表的第一个节点
    LNode *node = (*list)->next;
    // 保存前驱节点,链表第一个节点的前驱节点是头结点
    LNode *pre = *list;
    // 循环单链表
    while (node != NULL) 
        // 发现待删除的节点
        if (node->data == ele) 
            // 删除节点,即将被删除节点的前驱节点的 next 指针指向被删除节点的后后继节点
            pre->next = node->next;
            // 释放被删除节点的空间
            free(node);
            return 1;
        
        pre = node;
        node = node->next;
    
    return 0;

removeAllEle

删除单链表中所有值为 ele 的结点,而非 removeEle 中只删除第一次出现值为 ele 的结点。以 list=[1, 2, 2, 3, 2, 4, 2]; ele=2 为例,步骤如下:

实现步骤:

  • 声明两个变量 nodepre,其中 node 记录链表中的每个结点,而 pre 记录 node 结点的前驱结点。初始时 node 指向单链表的第一个结点,pre 指向单链表的头结点。
  • 从单链表的第一个结点开始扫描单链表中的每一个结点 node。如果结点 node 的数据域等于 ele,则删除 node 结点(pre->next=node->next),并且 node 指向它的后继结点(node=node->next);如果结点 node 的数据域不等于 ele,则更新 prenode 变量的值,都指向它们的后继结点(pre=pre->next; node=node->next;)。
  • 直到扫描完成,删除单链表中所有值等于 ele 的结点。

实现代码如下:

/**
 * 删除单链表中结点值等于 ele 的所有结点
 * @param list 单链表
 * @param ele 待删除节点的值
 */
void removeAllEle(LNode **list, int ele) 
    // 链表的第一个节点
    LNode *node = (*list)->next;
    // 保存前驱节点,初始化链表第一个节点的前驱节点就是链表的头结点
    LNode *pre = *list;
    // 循环链表
    while (node != NULL) 
        // 如果查找到要删除的节点
        if (node->data == ele) 
            // 保存被删除的节点
            LNode *temp = node;
            // 删除节点,但这里不能用 pre->next = node->next; 来删除节点,其实也是可以的,将代码顺序改为:pre->next=node->next;node=node->next;,但需要考虑如何释放被删除节点的空间
            node = node->next;
            pre->next = node;
            // 释放被删除节点的空间
            free(temp);
         else 
            // 如果没有找到则继续判断链表的下一个节点,但注意更新 node 和 pre
            node = node->next;
            pre = pre->next;
        
    

size

计算单链表的长度,即单链表中的结点个数。

实现步骤:

  • 从单链表的第一个结点开始扫描,统计结点个数。注意,不把单链表头结点计算在内。

实现代码如下:

/**
 * 计算单链表的长度,即节点个数
 * @param list 单链表
 * @return 链表节点个数
 */
int size(LNode *list) 
    // 计数器,记录链表的节点个数
    int count = 0;
    // 链表的第一个节点
    LNode *node = list->next;
    // 循环遍历链表
    while (node != NULL) 
        // 计数器加1
        count++;
        // 继续链表的下一个节点
        node = node->next;
    
    // 返回链表节点个数
    return count;

isEmpty

判断单链表是否为空,即单链表结点个数是否为零。

实现步骤:

  • 如果是带头结点的单链表判空条件则是 head->next==NULL;如果是不带头结点的单链表判空条件是 head==NULL

实现代码如下:

/**
 * 判断单链表是否为空
 * @param list 单链表
 * @return 如果链表为空则返回 1,否则返回 0
 */
int isEmpty(LNode *list) 
    // 只需要判断链表的第一个节点是否存在即可
    return list->next == NULL;

clear

以上是关于线性表文档之单链表的主要内容,如果未能解决你的问题,请参考以下文章

数据结构第四篇——线性表的链式存储之双向链表

线性表文档之单链表

线性表文档之循环单链表

Python线性表——单链表

单链表浅析

用单链表实现栈 Push,Pop时间为O