首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >数据结构学习笔记|链表

数据结构学习笔记|链表

原创
作者头像
有财君
修改于 2023-06-09 00:52:57
修改于 2023-06-09 00:52:57
30000
代码可运行
举报
运行总次数:0
代码可运行

1. 链表的定义

链表是一种常见的数据结构,一般的缓存管理都会选择链表来实现LRU。在常见的面试八股文中,总会提到数组和链表的区别。一般的答案主要包括几个方面:

  • 数组在内存中是连续的,链表不是连续的;
  • 数组用下标查找的时间复杂度是O(1),链表适合插入删除,时间复杂度是O(1)

在日常的工作中基本按照上面的特点选择需要的数据结构就可以了。链表是一种很有用的基础数据结构,用链表可以实现像栈、队列等数据结构。

链表又分为单链表、双向链表和循环链表。不管是那种形式,链表就是一个由指针串起来的数据结构。下图是链表在内存中的示意图,可以看到链表并不要求内存的连续,这也就决定了链表在插入和删除时完全没有内存搬迁的压力,所以无论add还是delete操作,其时间复杂度都是O(1):

如果用C语言来实现链表,需要声明一个结构体,结构体主要的两个元素分别是data、指向下一结点的指针。不过为了方便,还可以加上链表的容量和长度。

于是可以写出下面的代码:

代码语言:c
代码运行次数:0
运行
AI代码解释
复制
typedef struct node {
    linkedlist next; //下一个节点指针
    int data;        //实际保存的数字
    int size;        //链表的当前大小
    int capacity;    //链表的最大容量
} *linkedlist, node;

这样就可以构造出一个单向链表了:

这里要注意,为了统一所有结点的处理逻辑,一般都会引入一个头结点来,这个头结点的data域为空值或者0值。对于上面的定义来说,头结点还能存放size和capacity,非常方便后面的各种操作。

而尾结点的特点是next域指向null。

2. 链表的操作

链表的操作常见的无非是插入、删除和查找,这几种操作又可以变化出不同的功能接口出来。例如JDK的LinkedList就有功能非常丰富的接口供人使用。

插入结点是一个比较特殊的地方,有头插法和尾插法两种不同的逻辑。用图来表示头插法,引入头结点之后,头插法的逻辑也适用于向链表中任意位置插入的场景:

逻辑顺序是:

  1. new node的next指向原结点node的next,此时短暂的形成了两个list,上图时刻1;
  2. 原结点的next指向new node,此时两个list合二为一,插入操作完成,上图时刻2。

用代码来实现则是,这里我添加了一个capacity和size的作用就出来了,可以用来判断list是不是已经没有空间了,如果还能插入,则会在最后,给头结点的size域自加1:

代码语言:c
代码运行次数:0
运行
AI代码解释
复制
void addFirst(linkedlist list, node* n) {
    // if list is full, return
    if (checkFull(list)) {
        printf("list is full!\n");
        return;
    }
    n->next = list->next;
    list->next = n;
    list->size += 1;
}

而尾插法则是append,如图,需要借助一个尾指针p:

其逻辑顺序是:

  1. 尾指针p指向尾结点;
  2. p的next指向new node;
  3. 将指针p指向new node,即继续表示链表尾部,逻辑完成。

从图中不难看出不管是头插法还是尾插法,其操作的时间复杂度都是O(1)。但是这仅仅是操作本身的时间复杂度,在尾插法里,需要首先知道链表的尾部结点地址。而寻址的过程,时间复杂度就是O(n)了。

对于头插法来说,如果是特殊情况(每次都在头结点后插入数据,或者给定一个结点地址要求插入其后),则没有寻址的问题,时间复杂度一定是O(1)。

如果仅仅给了一个位置pos,要求插入这个pos之后,那么这时最好的情况是O(1),最差的情况是O(n)。

综上,虽然链表的插入操作本身时间复杂度并不高,但是考虑到可能存在的寻址操作,其时间复杂度也是比较可观的。

用代码来实现则能看出其时间都消耗在哪里了:

代码语言:c
代码运行次数:0
运行
AI代码解释
复制
void add(linkedlist list, node* n) {
    // if list is full, return 
    if (checkFull(list)) {
        printf("list is full!\n");
        return;
    }

    // if list has only head node, append to head node directly, then add 1 to head node's size variable, then return
    if (list->next == nullptr) {
        list->next = n;
        list->size += 1;
        return ;
    }
    // if list's size larger than 1, find list's tail node,O(n)
    linkedlist p = list;
    do {
        p = p->next;
    } while (p->next != nullptr);
    p->next = n;
    list->size += 1;
}

结点的删除操作用图来示意,也就是将指针指向变换一下即可:

其逻辑顺序是:

  1. 指针p指向要删除的结点node;
  2. node的前导结点pre的next指向p的next;
  3. 释放p的内存

如果是Java这类自动管理内存的语言,可以不用管第三步。

从这里也能看出,删除操作的时间复杂度也是O(1),但是,如果没有告知前导结点pre的地址,那么寻址的过程是不可省略的,其时间复杂度最坏情况下还是O(n)。

不管是去删除给定pos的元素还是删除给定的node,都需要寻址,代码如下:

代码语言:c
代码运行次数:0
运行
AI代码解释
复制
void remove(linkedlist list, int pos) {
    if (pos > list->size) {
        printf("pos越界!\n");
        return;
    }
    linkedlist p = list;
    int index = 0;
    while (index != pos - 1) {
        p = p->next;
        index += 1;
    }
    linkedlist temp ;
    temp = p->next;
    p->next = temp->next;
    free(temp);
    list->size -= 1;
}

void remove(linkedlist list, node* n) {
    linkedlist p = list;
    while (p->next != n) {
        p = p->next;
        if (p == nullptr) {
            printf("找不到要删除的节点\n");
            return;
        }
    }
    linkedlist temp ;
    temp = p->next;
    p->next = temp->next;
    free(temp);
    list->size -= 1;
}

3. 用双向链表解决插入和删除的O(n)问题

之前提到了删除和插入都可能被寻址搞成总体消耗O(n)的时间复杂度。而如果在给定结点删除(或者在给定结点前插入)的时候能够不要遍历就知道前导结点,那么时间整体的时间复杂度就变成O(1)了。

空间换时间,在有些场景下是划算的。

稍加修改一下结点的定义即可,相比单链表只是增加了一个prev指针指向前一个结点:

代码语言:c
代码运行次数:0
运行
AI代码解释
复制
struct duNode {
    duLinkedlist prev;
    duLinkedlist next;
    int data;
    int size;
    int capacity;
};

此时的remove方法就看着简单很多了,没有考虑越界这些情况,从核心来看,不需要额外的寻址了:

代码语言:c
代码运行次数:0
运行
AI代码解释
复制
void remove(duLinkedlist list, duNode* p) {
    duLinkedlist pre = p->prev;
    duLinkedlist n = p->next;
    pre->next = n;
    n->prev = pre;
    free(p);
    list->size -= 1;
}

不过想要得到被删除的结点p,仍然是要下一番功夫的,还是O(n)的时间复杂度,这似乎成了一个不可解的问题一直缠绕着我。在常见的缓存管理链表实现LRU的时候,不可能不对此进行优化的。最常见的一种方式是引入一个hash表,记录每个数据在链表中的位置,这样时间复杂度就变成O(1)了。

结合之前总结的单链表和双向链表,可以在下一小节讨论和实现LRU,暂时不引入hash表。

4. 利用链表实现简易的LRU算法

所谓LRU就是“最近最少使用”,是一种缓存管理的思路。具体到链表里,当一个数据被访问时,其逻辑是这样的:

  1. 遍历链表寻找该数据,如果能找到,则将其从原来的位置删除并移到链表的头部(带头结点的链表则是移动到头结点之后);
  2. 如果没有找到该数据,则分为两种情况。如果链表没有满(size < capacity),将该数据插入到链表头部;如果链表已满,则将队尾的结点删除,然后将数据插入链表头部。

这里有个问题,如果是测试人员此时访问了大量数据,那么就会导致真的热数据被逐出链表,造成后续操作耗时增加,且有需要大量的链表操作造成内存碎片。

为了解决这个问题,InnoDB则是选择每次将链表分为了新区和老区,新的数据插入到链表的3/8处,然后随着访问频次的提高,才将这个结点移动到链表的头部来。

代码语言:c
代码运行次数:0
运行
AI代码解释
复制
void lru(duLinkedlist list, int data) {
    duLinkedlist p = list;
    duNode * d = find(list, data); //寻找结点
    if (d == nullptr) { //如果结点不在LRU链表里
        auto* node = (duNode*) malloc(sizeof (duNode));
        node->data = data;
        if (list->size == list->capacity) { //如果链表已满
            removeTail(list); //删除尾结点
        }
        // 头插法插入结点
        node->next = p->next;
        p->next = node;
        node->prev = p;
        list->size += 1;
    } else {
        // 将结点移动到头部
        moveToHead(list, d);
    }
}

void moveToHead(duLinkedlist list, duNode* p) {
    duLinkedlist pre = p->prev;
    duLinkedlist head = list;

    duLinkedlist n = p->next;
    if (n == nullptr) { //p如果是尾结点
        pre->next = nullptr;
        p->next = head->next;
        head->next = p;
        p->prev = head;
        return;
    }
    pre->next = n;
    n->prev = pre;
    p->next = head->next;
    head->next = p;
    p->prev = head;
}

初步测试这个算法基本可用,虽然作为工业生产不太可能,但是足以说明算法思想和数据结构的应用。

上面代码中的moveToHead函数,因为用了双向链表,所以整个函数的时间复杂度就是O(1)了,如果是单向链表,则还需要遍历找到p的前导结点,这样时间复杂度就很可观了。而这之前又要有一个用data去find结点的过程,时间复杂度还是O(n),加在一起,那就更可观了。

5. LeetCode解题

83. 删除排序链表中的重复元素

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。

代码语言:txt
AI代码解释
复制
输入:head = [1,1,2]
输出:[1,2]

初见这个题的时候我想的比较复杂,我想如果用Java的话那我一定引入一个HashMap,如果Key存在则把链表的元素删除。因此我打算用一个bitmap来实现。不过后来我想明白了,这个题的关键是有序。

所有重复的元素一定是挨着的,那么我们只要不断地遍历当前元素和下一个元素就可以了,如果遇到一样的,直接将下一个元素删除就可以了。

标准的写法如下,但是在LeetCode提交的时候不要写free语句,会引发错误,在LeetCode里什么malloc,free之类的都可能导致错误,结果不错,只用了4ms,不过内存消耗比较大用了6.4MB:

代码语言:c
代码运行次数:0
运行
AI代码解释
复制
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* deleteDuplicates(struct ListNode* head){
    if (!head) {
        return head;
    }
    struct ListNode* L = head;
    while (L->next) {
        if (L->val == L->next->val) {
            struct ListNode* temp = L->next->next;
            L->next = temp;
            free(temp);
        } else {
            L = L->next;
        }
    }
    return head;
}

83. 删除排序链表中的重复元素-题解

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

206. 反转链表

代码语言:txt
AI代码解释
复制
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

我本来想的是再搞一个链表,用头插法。不过想想LeetCode可能会限制malloc,那就只能作罢。这道题的思路是维护三个指针:prev,cur,next,不断地移动cur指针,将cur的next指向cur的prev,然后移动prev到cur的位置,循环往复。

时间复杂度就是O(n)

代码语言:c
代码运行次数:0
运行
AI代码解释
复制
struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* L = head;
    struct ListNode* prev = NULL;
    while (L) {
        struct ListNode* next = L->next;
        L->next = prev;
        prev = L;
        L = next;
    }
    return head;
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
数据结构–链表的排序详解
1、前言 前面两篇博客,我已经把线性表的两种基本的表示形式,做了一个基本的介绍和一些对比。但是,我突然发现在链表这里我缺少一个很重要的内容,那就是对我们的链表进行排序,其实,在连接两个链表的时候,就要求我们的那两个链表是有序的。
全栈程序员站长
2022/11/07
7220
数据结构–链表的排序详解
《王道》数据结构笔记整理2022级_数据结构笔记整理
1.数据:数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机中并被程序识别和处理的符号的集合。
全栈程序员站长
2022/09/22
3.1K0
《王道》数据结构笔记整理2022级_数据结构笔记整理
数据结构学习笔记|哈希表
在实现LRU缓存管理的时候发现,由于利用了链表,导致find操作十分耗费时间。如果能有一个地方,存储了数据在LRU链表里的地址,那就完美了。
有财君
2023/06/05
3610
数据结构学习笔记|哈希表
数据结构(三)
像我们排队吃饭等叫号一样,一个接着一个,1号后面是2号,2号后面是3号,如此类推。
可爱见见
2019/09/09
4620
数据结构(三)
数据结构(二): 链表篇
链表在C语言的数据结构中的地位可不低。后面很多的数据结构,特别是树,都是基于链表发展的。 所以学好链表,后面的结构才有看的必要。
看、未来
2021/09/18
3010
数据结构:链表
工程代码 Github: Data_Structures_C_Implemention -- Link List
半纸渊
2018/08/30
1K1
数据结构:链表
数据结构-线性表
线性表是具有相同数据类型的n(n≥0)个数据元素的有限 序列,其中n为表长,当n = 0时线性表是一个空表。若用L命名线性表,则其一般表示为:
计蒙不吃鱼
2025/06/12
790
数据结构-线性表
【数据结构】单链表的增删改查
单链表需要使用的函数指针操作小技巧计算单链表的长度创建单链表单链表插入数据单链表删除数据效率分析
程序员周同学
2019/07/27
1.7K0
【数据结构】链表从实现到应用,保姆级攻略
链表是数据结构中一种非常重要的基础结构,它不同于数组,链表中的元素在物理存储上并不连续,而是通过指针(或引用)连接在一起。在Java中,链表的应用非常广泛,尤其是在需要动态添加或删除元素的场景中。
2的n次方
2024/10/15
1810
【数据结构】链表从实现到应用,保姆级攻略
Redis02-Redis的数据结构之Redis链表
上一篇文章我们学习了Redis的数据结构之简单动态字符串,这一篇我们接着来学习Redis中另外一个数据结构-链表。链表有很多种,首先,本文会首先回顾一下一些常见的链表,接着就是介绍Redis中的链表的结构。
码农飞哥
2021/08/18
4800
数据结构代码题-链表
2.在带头结点的单链表L中,删除所有值为x的结点,并释放其空间,假设值为x的结点不唯一,试编写算法以实现上述操作。
愷龍
2023/10/16
4370
数据结构代码题-链表
关于数据结构单链表
单链表作为最基础也是最常用的数据结构之一,在这门课程中占据着十分重要的地位。本文将对单链表这一章节的知识点进行总结,包括单链表的定义、基本操作、常见应用以及时间复杂度等方面。
一条晒干的咸鱼
2024/11/19
1310
关于数据结构单链表
【自考】数据结构中的线性表,期末不挂科指南,第2篇
首先假定线性表的数据元素的类型为DataType ,这个DataType 可以是自定义的,也可以是默认的int,char等类型
梦想橡皮擦
2019/12/12
5670
【自考】数据结构中的线性表,期末不挂科指南,第2篇
【数据结构】链表(C++)
链表是线性表的链式存储方式,逻辑上相邻的数据在计算机中的内存位置不必须相邻,给每一个元素 加一个指针域,指向下一个元素的位置。
半生瓜的blog
2023/05/13
5120
【数据结构】链表(C++)
【数据结构】C语言实现双链表的基本操作
经过前面几个篇章的内容分享,相信大家对顺序表和单链表的基本操作都已经熟练掌握了。今天咱们将继续分享线性表的链式存储的第二种形式——双链表。在今天的内容中,咱们将介绍双链表的创建以及一些基本操作,接下来跟我一起来看看吧!
蒙奇D索隆
2023/12/30
5560
【数据结构】C语言实现双链表的基本操作
重温数据结构(1)——数组与链表数组链表LeetCode相关题目参考
前言:终于到了疯狂学习数据结构的时候,换个好看的题图,开始吧.. 数组 什么是数组? 数组简单来说就是将所有的数据排成一排存放在系统分配的一个内存块上,通过使用特定元素的索引作为数组的下标,可以在常数时间内访问数组元素的这么一个结构; 为什么能在常数时间内访问数组元素? 为了访问一个数组元素,该元素的内存地址需要计算其距离数组基地址的偏移量。需要用一个乘法计算偏移量,再加上基地址,就可以获得某个元素的内存地址。首先计算元素数据类型的存储大小,然后将它乘以元素在数组中的索引,最后加上基地址,就可以计算出
我没有三颗心脏
2018/07/05
2.6K0
数据结构学习笔记|栈和队列
栈是一种先进后出的数据结构,其操作更是被限制在了pop和push里,而且仅仅是针对栈顶操作,所以时间复杂度是O(1)。想象栈其实和现实中叠放的盘子一样。
有财君
2023/06/05
2120
数据结构学习笔记|栈和队列
数据结构——链表
链式存储结构 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻有关术语 结点:数据元素的存储映像。由数据域和指针域两部分组成 - 数据域:存储元素数值数据 - 指针域:存储直接后继结点的存储位置 链表:n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构 - 单链表 - 结点只有一个指针域的链表,称为单链表或线性链表 - 双链表 - 有两个指针域的链表,称为双链表 - 循环链表 - 首尾相接的链表称为循环链表 头指针
ruochen
2021/06/28
7320
数据结构——链表
数据结构实验之链表
1、线性表: 线性表是最基本的一种数据结构。线性表是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。
LucianaiB
2025/05/28
880
数据结构实验之链表
数据结构笔记(一):数组、链表
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
free赖权华
2020/05/21
4590
相关推荐
数据结构–链表的排序详解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档