线性表文档之单链表

单链表

定义

概念

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

线性表文档之单链表_第1张图片

结构体

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

线性表文档之单链表_第2张图片

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

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

特点

单链表的特点如下:

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

顺序表和单链表的区别:

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

基本操作

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

  • LinkedList.c

  • LinkedList.java

  • LinkedListTest.java

概述

注:下面都是 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

线性表文档之单链表_第3张图片

实现代码如下:

/**
 * 初始化单链表
 * @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

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

线性表文档之单链表_第4张图片

实现步骤:

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

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

线性表文档之单链表_第5张图片

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

线性表文档之单链表_第6张图片

实现步骤:

  • 初始化单链表。
  • 设置一个变量来记录单链表的尾结点 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 为例如图:

线性表文档之单链表_第7张图片

实现步骤:

  • 参数校验,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 为例如图所示:

线性表文档之单链表_第8张图片

实现步骤:

  • 从头到尾扫描单链表所有结点,比较结点值,然后返回单链表中第一个结点值等于 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。

线性表文档之单链表_第9张图片

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

线性表文档之单链表_第10张图片

实现步骤:

  • 参数校验,判断序号 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 中返回。

线性表文档之单链表_第11张图片

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

线性表文档之单链表_第12张图片

实现步骤:

  • 参数校验,指定位置 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 的结点。

线性表文档之单链表_第13张图片

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

线性表文档之单链表_第14张图片

实现步骤:

  • 设定两个变量 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 为例,步骤如下:

线性表文档之单链表_第15张图片

实现步骤:

  • 声明两个变量 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

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

线性表文档之单链表_第16张图片

实现步骤:

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

实现代码如下:

/**
 * 计算单链表的长度,即节点个数
 * @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

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

线性表文档之单链表_第17张图片

实现步骤:

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

实现代码如下:

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

clear

清空单链表所有结点。

实现步骤:

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

实现代码如下:

/**
 * 清空单链表
 * @param list 单链表
 */
void clear(LNode **list) {
    // 链表的第一个节点
    LNode *node = (*list)->next;
    // 循环遍历单链表所有节点
    while (node != NULL) {
        // 保存当前节点的下一个节点
        LNode *temp = node->next;
        // 释放当前节点的空间
        free(node);
        // 继续链表的下一个节点
        node = temp;
    }
    // 最重要的是,将链表的头结点的 next 指针指向 null
    (*list)->next = NULL;
}

print

打印单链表所有结点。

线性表文档之单链表_第18张图片

实现步骤:

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

实现代码如下:

/**
 * 打印链表的所有节点
 * @param list 单链表
 */
void print(LNode *list) {
    printf("[");
    // 链表的第一个节点
    LNode *node = list->next;
    // 循环单链表所有节点,打印值
    while (node != NULL) {
        printf("%d", node->data);
        if (node->next != NULL) {
            printf(", ");
        }
        node = node->next;
    }
    printf("]\n");
}

注意事项

关于链表结点

链表结点的数据类型是结构体类型,我们在创建单链表或者为单链表新增结点时,结点不是直接赋值使用的,而是需要先分配一片空间。例如:

// 创建新节点
LNode* newNode = (LNode*)malloc(sizeof(LNode));

结点是内存中一片由用户分配的存储空间,只有一个地址来表示它的存在,没有显式的名称。因此,如果我们要使用这片空间,那么我们会在创建链表结点空间时,同时定义一个指针,来存储这片空间的地址,并且常用这个指针的名称来作为结点的名称。有了这个指针,我们就可以访问或者修改这片空间的内容了。

如上代码用户分配了一片 LNode 类型的空间,也就是构造了一个 LNode 类型的结点,这时候定义了一个名字为 newNode 的指针来指向这个结点,同时也把 newNode 当作这个结点的名称。这里 newNode 有两个作用:一个表示结点,一个表示指向这个结点的指针。newNode 既是结点名又是指针名。

带头结点和不带头结点的单链表

在单链表每个结点中包含数据域和指针域,其中指针域用以指向其后继结点。为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。因此在链表使用过程中可以区分带头结点的单链表和不带头结点的单链表。

带头结点的单链表:在带头结点的单链表中,头指针 head 指向头结点,头结点的数据域中不包含任何信息(也可以包含一些链表的如长度相关的信息),从头结点的后继结点(即单链表的第一个结点,也称为开始结点)开始存储数据信息。头指针 head 始终不等于 NULL,而 head->next==NULL 表示带头结点的单链表为空。

不带头结点的单链表:在不带头结点的单链表中,头指针 head 直接指向开始结点(即单链表的第一个结点),头指针 head 可以为 NULL,而当 head==NULL 时表示不带头结点的单链表为空。

线性表文档之单链表_第19张图片

注:带头结点和不带头结点的单链表最明显的区别是:带头结点的单链表中有一个结点(头节点)不存储信息(或者仅存储一些描述链表属性的信息,如表长),只是作为标志存在;而不带头结点的单链表的所有结点都存储信息。

头结点和头指针的区别:

  • 无论是带头结点还是不带头结点的单链表,头指针都指向链表的第一个结点(如果是带头结点的单链表则第一个结点是头节点;如果是不带头结点的单链表则第一个节点是开始节点),即图中的 head 指针。
  • 头结点是带头结点单链表中的第一个节点,只是作为链表存在的标志,数据域不存储信息,或者只存储一些链表属性信息。

单链表判空条件

带头结点的单链表为空的条件是:head->next==NULL

不带头结点的单链表为空的条件是:head==NULL

线性表文档之单链表_第20张图片

引入头结点的好处

引入头结点后有如下两个优点:

  • 由于第一个数据节点的地址被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置的操作(如新增节点或删除节点)一致,无须进行特殊处理。如下是带头结点单链表和不带头结点单链表插入新节点的代码:
/**
 * 在带头结点的单链表的指定位置插入新节点
 * @param list 带头结点的单链表
 * @param i 指定位置,序号,从 1 开始
 * @param ele 新节点的值
 */
void insertWithHead(LNode **list, int i, int ele) {
    // 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;
}

/**
 * 在不带头结点的单链表的指定位置插入新节点
 * @param list 不带头结点的单链表
 * @param i 定位置,序号,从 1 开始
 * @param ele 新节点的值
 */
void insertWithoutHead(LNode **list, int i, int ele) {
    // 0.校验参数

    // 1.创建新节点
    LNode *newNode = (LNode *) malloc(sizeof(LNode));
    newNode->data = ele;
    newNode->next = NULL;

    // 2.首先判断是否是空表
    // 2.1 如果是空表则将新节点作为链表的第一个结点
    if (*list == NULL) {
        *list = newNode;
    }
    // 2.2 如果不是空表
    else {
        // 2.2.1 继续判断插入位置是否是第一个位置,特殊处理
        if (i == 1) {
            // 2.2.1.1 那么将新节点的 next 指针指向原链表第一个结点
            newNode->next = *list;
            // 2.2.1.2 然后将新节点作为链表的第一个结点,即将让头指针指向新节点
            *list = newNode;
        }
        // 2.2.2 如果插入位置不是第一个,则找到第 i-1 个节点,将新节点插入到它的后面
        else {
            // 2.2.2.1 找到第 i 个节点的前驱节点
            LNode *iPreNode = *list;// 表示第一个节点
            LNode *node = (*list)->next;// 从第二个节点开始
            int count = 2;// 计数器,记录找到第几个节点了。注意,是从 2 开始的,因为第一个节点已经处理了
            while (count < i) {
                count++;
                iPreNode = node;
                node = node->next;
            }
            // 2.2.2.2 将新节点插入到 iPreNode 节点的后面
            newNode->next = iPreNode->next;
            iPreNode->next = newNode;
        }
    }
}
  • 无论单链表是否为空,其头指针 head 都指向头结点(空单链表头结点的指针域为 NULL;非空单链表头结点的指针域指向开始节点),因此空表和非空表的处理也得到了统一。如下是带头结点单链表和不带头结点单链表在链表尾部追加新节点的代码(完整代码请参考:附录一):
/**
 * 向带头结点的单链表的尾部追加新节点
 * @param head 带头结点的单链表,head 是头指针指向头结点
 * @param ele 值
 */
void appendWithHead(LNode **head, int ele) {
    // 创建新节点并赋予数据域和指针域
    LNode *newNode = (LNode *) malloc(sizeof(LNode));
    newNode->data = ele;
    newNode->next = NULL;

    // 找到链表的尾节点
    LNode *node = *head;
    while (node->next != NULL) {
        node = node->next;
    }

    // 无论带头结点的单链表是空表还是非空表,都是在 node 节点的后面附加新节点
    // 将新节点插入到链表的尾部
    node->next = newNode;
}

/**
 * 向不带头结点的单链表的尾部追加新节点
 * @param head 不带头结点的单链表,head 是头指针指向开始节点
 * @param ele 值
 */
void appendWithoutHead(LNode **head, int ele) {
    // 创建新节点并赋予数据域和指针域
    LNode *newNode = (LNode *) malloc(sizeof(LNode));
    newNode->data = ele;
    newNode->next = NULL;
    // 判断是否是空表,如果是空表则将新节点作为链表
    if ((*head) == NULL) {
        // 将新节点作为链表的第一个节点
        *head = newNode;
    }
    // 如果不是空表则找到原链表的尾节点然后插入到其后
    else {
        // 找到链表的尾节点
        LNode *node = *head;
        while (node->next != NULL) {
            node = node->next;
        }
        // 将新节点插入到链表的尾部
        node->next = newNode;
    }
}

练习题

以下是一些单链表的练习题:

  • Example001-创建不重复字母字符的单链表
  • Example002-删除递增非空单链表中的重复值域节点
  • Example003-删除单链表最小值节点
  • Example006-比较两个有序链表是否相等
  • Example007-原地逆置单链表
  • Example009-求两个有序递增链表的差集
  • Example011-分解链表中的奇数节点和偶数节点
  • Example012-逆序打印单链表
  • Example015-求单链表倒数第 k 个节点
  • Example018-删除单链表中所有值为 x 的节点
  • Example019-删除单链表中所有介于给定两个值之间的元素的元素
  • Example020-将一个单链表拆分成两个链表,一个顺序一个倒序
  • Example032-将一个带头结点的单链表 A 分解成两个单链表 A 和 B,其中 A 表只包含原表中序号为奇数的元素,B 表中只包含原表中序号为偶数的元素
  • Example033-将两个按元素值递增次序排列的单链表归并为一个按元素值递减次序排列的单链表
  • Example034-从有序递增元素组成的单链表 A 和 B 中的公共元素产生单链表 C,要求不破坏 A 和 B 的节点
  • Example035-求由递增元素组成的单链表 A 和 B 的交集并且把结果存放于链表 A 中
  • Example036-判断单链表 B 是否是单链表 A 的连续子序列
  • Example040-删除单链表中数据域绝对值相等节点,仅保留第一次出现的节点而删除其余绝对值相等的节点
  • Example041-重排链表节点,由 L=(a1, a2, a3, ..., a(n-2), a(n-1), an) 排成 L'=(a1, an, a2, a(n-1), a3, a(n-2), ...)
  • Example042-设计一个递归算法,删除不带头结点的单链表 L 中所有值为 x 的结点
  • Example043-给定两个单链表,编写算法找出两个链表的公共节点
  • Example044-按递增次序输出单链表中各节点的数据元素,并释放节点所占的存储空间并要求不使用辅助空间
  • Example045-有一个带头结点的单链表 L,设计一个算法使其元素递增有序
  • Example047-找出由 str1 和 str2 所指向两个链表共同后缀的起始位置
  • Example048-判断一个单链表是否有环,如果有则找出环的入口点并返回,否则返回 NULL

附录

附录一

带头结点的单链表和不带头结点的单链表在尾部追加新元素,对于空表和非空表的处理,完整代码如下:

#include 
#include 

/**
 * 单链表节点
 */
typedef struct LNode {
    /**
     * 单链表节点的数据域
     */
    int data;
    /**
     * 单链表节点的的指针域,指向当前节点的后继节点
     */
    struct LNode *next;
} LNode;

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

/**
 * 初始化不带头结点的单链表
 * @param list 待初始化的单链表
 */
void initWithoutHead(LNode **list) {
    // 直接赋为 NULL
    *list = NULL;
}


/**
 * 打印带头结点的单链表的所有节点
 * @param list 带头结点的单链表
 */
void printWithHead(LNode *list) {
    printf("[");
    // 链表的第一个节点
    LNode *node = list->next;
    // 循环单链表所有节点,打印值
    while (node != NULL) {
        printf("%d", node->data);
        if (node->next != NULL) {
            printf(", ");
        }
        node = node->next;
    }
    printf("]\n");
}

/**
 * 打印不带头结点的单链表的所有结点
 * @param list 不带头结点的单链表
 */
void printWithoutHead(LNode *list) {
    printf("[");
    // 不是空表才能打印链表所有节点
    if (list != NULL) {
        // 循环单链表所有节点,打印值
        while (list != NULL) {
            printf("%d", list->data);
            if (list->next != NULL) {
                printf(", ");
            }
            list = list->next;
        }
    }
    printf("]\n");
}

/**
 * 向带头结点的单链表的尾部追加新节点
 * @param head 带头结点的单链表,head 是头指针指向头结点
 * @param ele 值
 */
void appendWithHead(LNode **head, int ele) {
    // 创建新节点并赋予数据域和指针域
    LNode *newNode = (LNode *) malloc(sizeof(LNode));
    newNode->data = ele;
    newNode->next = NULL;

    // 找到链表的尾节点
    LNode *node = *head;
    while (node->next != NULL) {
        node = node->next;
    }

    // 无论带头结点的单链表是空表还是非空表,都是在 node 节点的后面附加新节点
    // 将新节点插入到链表的尾部
    node->next = newNode;
}

/**
 * 向不带头结点的单链表的尾部追加新节点
 * @param head 不带头结点的单链表,head 是头指针指向开始节点
 * @param ele 值
 */
void appendWithoutHead(LNode **head, int ele) {
    // 创建新节点并赋予数据域和指针域
    LNode *newNode = (LNode *) malloc(sizeof(LNode));
    newNode->data = ele;
    newNode->next = NULL;
    // 判断是否是空表,如果是空表则将新节点作为链表
    if ((*head) == NULL) {
        // 将新节点作为链表的第一个节点
        *head = newNode;
    }
    // 如果不是空表则找到原链表的尾节点然后插入到其后
    else {
        // 找到链表的尾节点
        LNode *node = *head;
        while (node->next != NULL) {
            node = node->next;
        }
        // 将新节点插入到链表的尾部
        node->next = newNode;
    }
}

int main() {
    // 创建带头结点的单链表
    LNode *A;
    initWithHead(&A);// 初始化带头结点的单链表
    appendWithHead(&A, 3);// 为带头结点的空单链表追加元素 
    appendWithHead(&A, 33);// 为带头结点的非空单链表追加元素
    appendWithHead(&A, 333);
    appendWithHead(&A, 3333);
    appendWithHead(&A, 33333);
    printWithHead(A);

    // 创建不带头结点的单链表
    LNode *B;
    initWithoutHead(&B);// 初始化不带头结点的单链表
    appendWithoutHead(&B, 4);// 为不带头结点的空单链表追加元素
    appendWithoutHead(&B, 44);// 为不带头结点的非空单链表追加元素
    appendWithoutHead(&B, 444);
    appendWithoutHead(&B, 4444);
    appendWithoutHead(&B, 44444);
    printWithoutHead(B);
}

执行结果如下:

[3, 33, 333, 3333, 33333]
[4, 44, 444, 4444, 44444]

附录二

带头结点的单链表和不带头结点的单链表在指定位置插入新元素,完整代码如下:

#include 
#include 

/**
 * 单链表节点
 */
typedef struct LNode {
    /**
     * 单链表节点的数据域
     */
    int data;
    /**
     * 单链表节点的的指针域,指向当前节点的后继节点
     */
    struct LNode *next;
} LNode;

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

/**
 * 初始化不带头结点的单链表
 * @param list 待初始化的单链表
 */
void initWithoutHead(LNode **list) {
    // 直接赋为 NULL
    *list = NULL;
}


/**
 * 打印带头结点的单链表的所有节点
 * @param list 带头结点的单链表
 */
void printWithHead(LNode *list) {
    printf("[");
    // 链表的第一个节点
    LNode *node = list->next;
    // 循环单链表所有节点,打印值
    while (node != NULL) {
        printf("%d", node->data);
        if (node->next != NULL) {
            printf(", ");
        }
        node = node->next;
    }
    printf("]\n");
}

/**
 * 打印不带头结点的单链表的所有结点
 * @param list 不带头结点的单链表
 */
void printWithoutHead(LNode *list) {
    printf("[");
    // 不是空表才能打印链表所有节点
    if (list != NULL) {
        // 循环单链表所有节点,打印值
        while (list != NULL) {
            printf("%d", list->data);
            if (list->next != NULL) {
                printf(", ");
            }
            list = list->next;
        }
    }
    printf("]\n");
}

/**
 * 在带头结点的单链表的指定位置插入新节点
 * @param list 带头结点的单链表
 * @param i 指定位置,序号,从 1 开始
 * @param ele 新节点的值
 */
void insertWithHead(LNode **list, int i, int ele) {
    // 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;
}

/**
 * 在不带头结点的单链表的指定位置插入新节点
 * @param list 不带头结点的单链表
 * @param i 定位置,序号,从 1 开始
 * @param ele 新节点的值
 */
void insertWithoutHead(LNode **list, int i, int ele) {
    // 0.校验参数

    // 1.创建新节点
    LNode *newNode = (LNode *) malloc(sizeof(LNode));
    newNode->data = ele;
    newNode->next = NULL;

    // 2.首先判断是否是空表
    // 2.1 如果是空表则将新节点作为链表的第一个结点
    if (*list == NULL) {
        *list = newNode;
    }
    // 2.2 如果不是空表
    else {
        // 2.2.1 继续判断插入位置是否是第一个位置,特殊处理
        if (i == 1) {
            // 2.2.1.1 那么将新节点的 next 指针指向原链表第一个结点
            newNode->next = *list;
            // 2.2.1.2 然后将新节点作为链表的第一个结点,即将让头指针指向新节点
            *list = newNode;
        }
        // 2.2.2 如果插入位置不是第一个,则找到第 i-1 个节点,将新节点插入到它的后面
        else {
            // 2.2.2.1 找到第 i 个节点的前驱节点
            LNode *iPreNode = *list;// 表示第一个节点
            LNode *node = (*list)->next;// 从第二个节点开始
            int count = 2;// 计数器,记录找到第几个节点了。注意,是从 2 开始的,因为第一个节点已经处理了
            while (count < i) {
                count++;
                iPreNode = node;
                node = node->next;
            }
            // 2.2.2.2 将新节点插入到 iPreNode 节点的后面
            newNode->next = iPreNode->next;
            iPreNode->next = newNode;
        }
    }
}

int main() {
    // 创建带头结点的单链表
    LNode *A;
    initWithHead(&A);// 初始化带头结点的单链表
    printWithHead(A);
    insertWithHead(&A, 1, 11);
    printWithHead(A);
    insertWithHead(&A, 1, 22);
    printWithHead(A);
    insertWithHead(&A, 2, 33);
    printWithHead(A);
    insertWithHead(&A, 2, 44);
    printWithHead(A);

    printf("\n");

    // 创建不带头结点的单链表
    LNode *B;
    initWithoutHead(&B);// 初始化不带头结点的单链表
    printWithoutHead(B);
    insertWithoutHead(&B, 1, 111);
    printWithoutHead(B);
    insertWithoutHead(&B, 1, 222);
    printWithoutHead(B);
    insertWithoutHead(&B, 2, 333);
    printWithoutHead(B);
    insertWithoutHead(&B, 2, 444);
    printWithoutHead(B);
}

执行结果如下:

[]
[11]
[22, 11]
[22, 33, 11]
[22, 44, 33, 11]

[]
[111]
[222, 111]
[222, 333, 111]
[222, 444, 333, 111]

你可能感兴趣的:(数据结构,数据结构,线性表,单链表)