概念

  链表是一种逻辑结构上连续,物理存储结构上非连续、非顺序的存储结构,之所以会形成这样的结构,主要是因为链表是通过指针链接次序实现的 。

  在创建链表时,申请的空间是随机的,所以物理上非连续、非顺序,但是我们可以通过链表每一个结点中的指针变量,顺序访问到下一个结点,所以逻辑上是连续的。

  链表的结构如下图所示:

 

分类

  链表的结构非常多样,由下面三种情况组合起来:

    ①单向、双向:单向是只有指向下一个结点的指针,双向是既有指向下一个结点也有指向上一个结点的指针;

    ②带头、不带头:头结点只包含指向第一个结点的指针;

    ③循环、非循环:循环是尾结点的下一个指针不指向NULL,而是指向第一个结点;

 

 

  虽然组合起来有2*2*2=8种形式的链表,但是我们在实际中最常用的就两种结构:

    ①无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希表、图的邻接表等等。另外这种结构在笔试面试中出现很多,所以要重点关注;

 

    ②带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这种链表虽然结构复杂,但是使用代码实现以后会发现复杂的结构会带来很多优势,它的实现反而简单了;

 

链表的实现

  当我们将链表创建出来后,拿给用户使用时,我们是不需要让用户明白到底是如何向里面输入数据、如何拿出数据等等的原理的,我们只需要提供一个接口,而用户只需将一个数据放到接口中,那么就完成了一个操作,这会给用户带来极大的方便;

 

  那么下面我就来实现一下各种接口,需要注意的是,接口的函数名一般是有默认形式,也就是说这个借口实现什么样的功能,那它的名字最好写的规范些,方便别人的使用;

  单向带头非循环:(带头结点的链表条理会清晰一些,所以就实现了带头结点的链表)


//这是 .h 部分的代码
#pragma once

//使用这种方式来重命名数据类型,这样可以很方便的修改后续数据的数据类型,相当于#define的作用
typedef int ListType;
//创建数据节点
typedef struct ListNode {
    ListType _date;
    //此处必须带上struct,因为此时还没被typedef呢
    struct ListNode* _next;
}ListNode;
//创建头结点
typedef struct ListHead{
    ListNode* _head;
}ListHead;

//包含所有函数的声明
//单链表初始化
void ListInit(ListHead* list);
// 动态申请一个节点
ListNode* BuyListNode(ListType val);
// 单链表打印
void ListPrint(ListHead* list);
// 单链表尾插
void ListPushBack(ListHead* list, ListType val);
// 单链表尾删
void ListPopBack(ListHead* list);
// 单链表头插
void ListPushFront(ListHead* list, ListType val);
// 单链表头删
void ListPopFront(ListHead* list);
// 单链表查找
ListNode* SListFind(ListHead* list, ListType val);
// 单链表在pos位置之后插入x
void ListInsertAfter(ListNode* pos, ListType val);
// 单链表删除pos位置之后的值
void ListEraseAfter(ListNode* pos);
//单链表的大小
int ListSize(ListHead* list);
// 单链表的销毁
void ListDestory(ListHead* list);
//这是 .c 部分的代码
#include<stdio.h>
#include<stdlib.h>
#include"List.h"

//单链表初始化
void ListInit(ListHead* list) {
    //参数合法性检验
    if (list == NULL) {
        return;
    }
    //将_head初始化为NULL
    list->_head = NULL;
}

// 动态申请一个节点
ListNode* BuyListNode(ListType val) {
    //动态开辟一个数据节点大小的空间
    ListNode* ret = (ListNode*)malloc(sizeof(ListNode));
    //未开辟成功,就直接退出
    if (ret == NULL) {
        return NULL;
    }
    //申请成功就将val赋给_date,并将_next初始化为NULL
    ret->_date = val;
    ret->_next = NULL;
    return ret;
}

// 单链表打印
void ListPrint(ListHead* list) {
    //参数合法性检验,空表就直接返回
    if (list == NULL||list->_head == NULL) {
        return;
    }
    ListNode* node = list->_head;
    //遍历,如果不是最后一个节点,就打印数据
    while (node != NULL) {
        printf("%d ", node->_date);
        node = node->_next;
    }
    printf("\n");
}

// 单链表尾插
void ListPushBack(ListHead* list, ListType val) {
    //参数合法性检验
    if (list == NULL) {
        return;
    }
    //如果是空链表,就在头指针后面插入数据
    if (list->_head == NULL) {
        list->_head = BuyListNode(val);
    }
    else {
        //遍历,找到最后一个节点
        ListNode* tail = list->_head;
        while (tail->_next != NULL) {
            tail = tail->_next;
        }
        //插入数据
        tail->_next = BuyListNode(val);
    }
}

// 单链表尾删
void ListPopBack(ListHead* list) {
    //参数合法性检验,空表时也直接返回
    if (list == NULL||list->_head == NULL) {
        return;
    }
    ListNode* tail = list->_head;
    ListNode* temp = NULL;
    //遍历,找到最后一个节点
    while (tail->_next != NULL) {
        temp = tail;
        tail = tail->_next;
    }
    //释放最后一个节点空间
    free(tail);
    //如果只有一个数据,那么就删除头指针,修改头指针指向
    if (temp == NULL) {
        list->_head == NULL;
    }
    //如果不止一个数据,那就修改倒数第二个_next指针指向
    else {
        temp->_next = NULL;
    }
}

// 单链表头插
void ListPushFront(ListHead* list, ListType val) {
    //参数合法性检验
    if (list == NULL) {
        return;
    }
    //如果是空表,就直接在头节点后面插入数据
    if (list->_head == NULL) {
        list->_head = BuyListNode(val);
    }
    //如果不是空表,先保留第一个节点,然后让头指针指向新申请的节点,然后让新申请的节点指向保留的节点
    else {
        ListNode* node = list->_head;
        list->_head = BuyListNode(val);
        (list->_head)->_next = node;
    }
}

// 单链表头删
void ListPopFront(ListHead* list) {
    //参数合法性检验,空表直接返回
    if (list == NULL || list->_head == NULL) {
        return;
    }
    //只有一个元素和有一个以上的元素,情况其实都是一样的,所以不用分情况讨论,大家可以体会一下
    ListNode* node = list->_head;
    ListNode* temp = NULL;
    temp = node->_next;
    //释放头指针
    free(node);
    //将头指针指向原来的第二个节点,若原来只有一个节点,那么原来的第二个节点为NULL,所以正好将头指针指向NULL
    list->_next = temp;
}

// 单链表查找
ListNode* SListFind(ListHead* list, ListType val) {
    //参数合法性检验,空表也直接返回
    if (list == NULL || list->_head == NULL) {
        return;
    }
    ListNode* next = list->_head;
    //使用do while语句,对获取到的当前节点进行数据判断,如果是要找的,就返回当前节点,如果不是,就指向下一个节点
    while (next != NULL) {
        if (next->_date == val) {
            return next;
        }
        //如果不是,就更新指向下一个节点
        else {
            next = next->_next;
        }
    }
    //如果没有找到就会返回NULL
    return NULL;
}

// 单链表在pos位置之后插入x,这里不能直接在pos位置插入,因为这是单向的,所以你无法获取到上一个指针
void ListInsertAfter(ListNode* pos, ListType val) {
    //保留下一个节点
    ListNode* node = pos->_next;
    //将pos指向插入的节点
    pos->_next = BuyListNode(val);
    //新插入的节点指向保留的节点
    (pos->_next)->_next = node;
}

// 单链表删除pos位置之后的值,不能直接删除,原因也和上面的一样,直接删除的话,链表就断了
void ListEraseAfter(ListNode* pos) {
    //如果pos下一个节点为空,则不需删除,直接返回
    if (pos->_next == NULL) {
        return;
    }
    //暂存pos下一个节点为node
    ListNode* node = pos->_next;
    //保留node节点的下一个指向为next(无论是否为NULL)
    ListNode* next = node->_next;
    //删除node节点
    free(node);
    //将pos指向next位置,若next为NULL,则pos为最后一个节点了
    pos->_next = next;
}

//单链表的大小
int ListSize(ListHead* list) {
    //参数合法性检验,空表大小也直接返回
    if (list == NULL || list->_head == NULL) {
        return 0;
    }
    //设置计数器
    int count = 0;
    ListNode* next = list->_head;
    //遍历,每找到一个节点,count就++
    while (next != NULL) {
        count++;
        next = next->_next;
    }
    return count;
}

// 单链表的销毁
void ListDestory(ListHead* list) {
    //参数合法性检验,空表不用销毁,直接返回
    if (list == NULL || list->_head == NULL) {
        return;
    }
    ListNode* node = list->_head;
    //遍历,若为空指针,就结束循环
    while (node) {
        //保留当前指针的下一个指向
        ListNode* next = node->_next;
        //释放掉当前指针
        free(node);
        //更新当前指针指向之前保留的下一个节点
        node = next;
    }
    //在将链表全部释放为空之后,将头指针赋为NULL,保证安全性
    list->_head = NULL;
}

int main() {
    ListHead list;
    ListInit(&list);
    ListPushBack(&list, 0);
    ListPushBack(&list, 9);
    ListPushBack(&list, 1);
    ListPushBack(&list, 4);
    ListPrint(&list);
    printf("%d\n",ListSize(&list));
    return 0;
}

  双向带头循环:


//这是 .h 部分的代码
#pragma once

//使用这种方式来重命名数据类型,这样可以很方便的修改后续数据的数据类型,相当于#define的作用
typedef int ListType;

//创建数据结点
typedef struct ListNode {
    //数据
    ListType _date;
    //指向下一个结点
    struct Listnode* _next;
    //指向上一个结点
    struct ListNode* _prev;
}ListNode;

//创建头结点
typedef struct ListHead {
    //指向第一个结点
    ListNode* _head;
}ListHead;

//包含所有函数的声明
//双向链表初始化
void ListInit(ListHead* list);
//动态申请一个节点
ListNode* BuyListNode(ListType val);
//双向链表打印
void ListPrint(ListHead* list);
//双向链表尾插
void ListPushBack(ListHead* list, ListType val);
//双向链表尾删
void ListPopBack(ListHead* list);
//双向链表头插
void ListPushFront(ListHead* list, ListType val);
//双向链表头删
void ListPopFront(ListHead* list);
//双向链表在pos位置插入x
void ListInsert(ListNode* pos, ListType val);
//双向链表删除pos位置的值
void ListErase(ListNode* pos);
//双向链表查找
ListNode* SListFind(ListHead* list, ListType val);
//双向链表的大小
int ListSize(ListHead* list);
//双向链表的销毁
void ListDestory(ListHead* list);
//这是 .c 部分的代码
#include<stdio.h>
#include<stdlib.h>
#include"list.h"

//双向链表初始化
void ListInit(ListHead* list) {
    //参数合法性检验
    if (list == NULL) {
        return;
    }
    //注意,双向链表的空表其实是有一个节点的,它的_next、_prev都指向自己,这个节点没任何实际意义,只是为了方便操作链表
    list->_head = BuyListNode(0);
    list->_head->_next = list->_head->_prev = list->_head;
}

//动态申请一个节点
ListNode* BuyListNode(ListType val) {
    //动态开辟一个节点
    ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
    //如果开辟成功再继续,如果失败返回NULL
    if (newNode) {
        //给创建出来的每一个变量赋初值
        newNode->_date = val;
        newNode->_next = NULL;
        newNode->_prev = NULL;
        return newNode;
    }
    return NULL;
}

//双向链表打印
void ListPrint(ListHead* list) {
    //参数合法性检验,没有元素就直接返回
    if (list == NULL || list->_head->_next == list->_head) {
        return;
    }
    //让循环从第一个有效结点开始
    ListNode* node = list->_head->_next;
    //当结点再次返回到头结点时,则循环结束
    while (node != list->_head) {
        printf("%d ", node->_date);
        node = node->_next;
    }
    printf("\n");
}

//双向链表尾插
void ListPushBack(ListHead* list, ListType val) {
    //参数合法性检验
    if (list == NULL) {
        return;
    }
    //修改四个指针的指向
    ListNode* tail = list->_head->_prev;
    ListNode* newNode = BuyListNode(val);
    //原链表尾节点的_next指向新创建节点
    tail->_next = newNode;
    //新创建结点的_prev指向原链表尾结点
    newNode->_prev = tail;
    //新创建结点的_next指向头结点
    newNode->_next = list->_head;
    //头结点的_prev指向新创建结点
    list->_head->_prev = newNode;

    //代码复用,我们可以直接调用在任意位置插入函数
    //ListInsert(list->_head, val);
}
//双向链表尾删
void ListPopBack(ListHead* list) {
    //参数合法性检验,空表就没得删了,直接返回
    if (list == NULL || list->_head->_next == list->_head) {
        return;
    }
    ListNode* tail = list->_head->_prev;
    ListNode* taillast = tail->_prev;
    //释放最后一个结点
    free(tail);
    //倒数第二个结点的_next指向头结点
    taillast->_next = list->_head;
    //头结点的_prev指向倒数第二个结点
    list->_head->_prev = taillast;

    //代码复用,我们可以直接调用在任意位置删除函数
    //ListErase(list->_head->_prev);
}

//双向链表头插
void ListPushFront(ListHead* list, ListType val) {
    //参数合法性检验
    if (list == NULL) {
        return;
    }
    //修改四个指针的指向
    ListNode* next = list->_head->_next;
    ListNode* node = BuyListNode(val);
    list->_head->_next = node;
    node->_prev = list->_head;
    node->_next = next;
    next->_prev = node;

    //代码复用,我们可以直接调用在任意位置插入函数
    //ListInsert(list->_head->_next, val);
}

//双向链表头删
void ListPopFront(ListHead* list) {
    //参数合法性检验,如果为空表就直接返回
    if (list == NULL || list->_head->_next == list->_head) {
        return;
    }
    ListNode* node = list->_head->_next;
    ListNode* next = node->_next;
    //释放结点
    free(node);
    //修改指针指向
    list->_head->_next = next;
    next->_prev = list->_head;

    //代码复用,我们可以直接调用在任意位置插入函数
    //ListErase(list->_head->_next);
}

//双向链表在pos位置插入x
void ListInsert(ListNode* pos, ListType val) {
    //这里就不做检查了,因为不好搞,你首先要保证插入的链表不为NULL,其次要保证链表存在pos结点
    //所以就不做检查了,默认这一切都是ok的,直接进行插入即可
    ListNode* last = pos->_prev;
    ListNode* newNode = BuyListNode(val);
    //修改四个指针的志向
    last->_next = newNode;
    newNode->_prev = last;
    newNode->_next = pos;
    pos->_prev = newNode;
}

//双向链表删除pos位置的值
void ListErase(ListNode* pos) {
    //这里就不做检查了,因为不好搞,你首先要保证链表不为NULL,其次要保证链表存在pos结点
    //所以就不做检查了,默认这一切都是ok的,直接进行插入即可
    ListNode* last = pos->_prev;
    ListNode* next = pos->_next;
    //释放结点
    free(pos);
    //修改指针指向
    last->_next = next;
    next->_prev = last;
}

//双向链表查找
ListNode* SListFind(ListHead* list, ListType val) {
    //参数合法性检验,若是空表直接返回
    if (list == NULL || list->_head->_next == list->_head) {
        return NULL;
    }
    ListNode* node = list->_head->_next;
    while (node != list->_head) {
        if (node->_date == val) {
            return node;
        }
        node = node->_next;
    }
}

//双向链表的大小
int ListSize(ListHead* list) {
    //参数合法性检验,若为空表,直接返回
    if (list == NULL || list->_head->_next == list->_head) {
        return 0;
    }
    ListNode* node = list->_head->_next;
    int size = 0;
    while (node != list->_head) {
        size++;
        node = node->_next;
    }
    return size;
}

//双向链表的销毁
void ListDestory(ListHead* list) {
    //参数合法性检验
    if (list == NULL) {
        return;
    }
    //让尾结点指向NULL
    list->_head->_prev->_next = NULL;
    ListNode* node = list->_head;
    //头指针指向NULL
    list->_head = NULL;
    //从头结点开始删除,直到NULL
    while (node) {
        ListNode* temp = node->_next;
        free(node);
        node = temp;
    }
}

int main() {
    ListHead list;
    ListInit(&list);

    ListPushBack(&list, 0);
    ListPrint(&list);
    ListPushBack(&list, 9);
    ListPrint(&list);
    ListPushBack(&list, 1);
    ListPrint(&list);
    ListPushBack(&list, 4);
    ListPrint(&list);

    ListDestory(&list);

    if (list._head == NULL) {
        printf("ok\n");
    }
    return 0;
}

链表优缺点

  优点:①双向链表在任意位置插入删除的时间复杂度都是O(1);

     ②没有增容问题,插入一个开辟一个空间;

  缺点:①空间不连续,不支持随机访问;

     ②需要额外的空间来存放指针,浪费空间;

     ③因为是一小块一小块开辟的,所以会形成内存碎片,造成空间利用率低;

     ④实现思路比顺序表复杂;(这其实不算事)

单链表相关的面试题

  单链表一般作为其他数据结构的子结构,经常会出现在面试/笔试题中,所以我罗列出了一些相关的面试/笔试题,写出了我自己的解法和思路,各位可以看看:

  1.删除链表中等于给定值val的所有节点。点击

 

  2.反转一个单链表。点击

  3.给定一个带有头结点head的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。点击

  4.输入一个链表,输出该链表中倒数第k个结点。点击

  5.将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。点击

  6.编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前。点击

  7.链表的回文结构。点击

  8.输入两个链表,找出它们的第一个公共结点。点击

  9.给定一个链表,判断链表中是否有环。点击

  10.给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回NULL。点击

  11.给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。 要求返回这个链表的深度拷贝。点击

  12.对链表进行插入排序。点击

  13.在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头 指针。点击