二、链表(linked-list)

2023-09-15 11:48:04

一、定义

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

二、经典例题

(一)21.合并两个有序链表

在这里插入图片描述

1.思路

根据题目描述,链表l1, l2是递增的,因此容易想到使用双指针cur1和cur2遍历两链表,根据cur1.val和cur2.val的大小关系确定节点添加顺序,两节点指针交替前进,直至遍历完毕。
同时因为两个链表都是有序的,所以,当我们遍历完一个链表,剩下的那个链表如果没到结尾,可以直接跟上。

2.复杂度分析

时间复杂度 O(M+N) : M,N别为两个链表的长度,合并操作需遍历两链表。
空间复杂度 O(1): 节点引用 dum , cur使用常数大小的额外空间。

3.注意

Dummy节点的作用是作为一个虚拟的头前节点。在不知道要返回的新链表的头结点是哪一个,它可能是原链表的第一个节点,可能在原链表的中间,也可能在最后,甚至不存在(nil)。引入Dummy节点可以涵盖所有情况,并且可以使用dummy.next返回最终需要的头结点。

4.代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* cur1 = list1;
        ListNode* cur2 = list2;
        ListNode* dummy = new ListNode(-1); // 虚拟头结点
        ListNode* cur = dummy;
        while (cur1 && cur2) {
        // 比较 p1 和 p2 两个指针
        // 将值较小的的节点接到 p 指针
            if (cur1 -> val > cur2 -> val) {
                cur -> next = cur2;
                cur2 = cur2 -> next;
            }
            else {
                cur -> next = cur1;
                cur1 = cur1 -> next;
            }
            cur = cur -> next; // p 指针不断前进
        }
        if (cur1) cur -> next = cur1;
        if (cur2) cur -> next = cur2;

        return dummy -> next;
    }
};

(二)86.分割链表

1.思路

具体来说,我们可以把原链表分成两个小链表,一个链表中的元素大小都小于 x,另一个链表中的元素都大于等于 x,最后再把这两条链表接到一起,就得到了题目想要的结果。

2.复杂度分析

时间复杂度 O(N): 其中 N为链表长度;遍历链表使用线性时间。

空间复杂度 O(1) : 假头节点使用常数大小的额外空间。

3.代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        ListNode* dummy1 = new ListNode(-1);
        ListNode* dummy2 = new ListNode(-1);
        ListNode* small = dummy1;
        ListNode* big = dummy2;
        // 新建两个链表 small,big,分别用于添加所有节点值<x、节点值>=的节点
        ListNode* cur = head;
        while (cur) {
            if (cur -> val >= x) {
                big -> next = cur;
                big = big -> next;
            }
            else {
                small -> next = cur;
                small = small -> next;
            }
            cur = cur -> next;
        }
        small -> next = dummy2 -> next; // 拼接 small 和 big
        big -> next = nullptr;
        return dummy1 -> next;
    }
};

(三)23.合并 K 个升序链表

1.思路

如何快速得到 k 个节点中的最小节点,接到结果链表上?

  • 这里我们就要用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点:

优先队列 pq 中的元素个数最多是 k,所以一次 poll 或者 add 方法的时间复杂度是 O(logk);所有的链表节点都会被加入和弹出 pq,所以算法整体的时间复杂度是 O(Nlogk),其中 k 是链表的条数,N 是这些链表的节点总数。

2.复杂度分析

时间复杂度:考虑优先队列中的元素不超过 k 个,那么插入和删除的时间代价为 O(log⁡k),这里最多有 kn 个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为 O(kn×log⁡k)。
空间复杂度:这里用了优先队列,优先队列中的元素不超过 k个,故渐进空间复杂度为 O(k)。

3.代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
// 时间复杂度 : O(n ∗ log(k))
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        if (lists.size() == 0) return nullptr;

        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy;

       priority_queue<ListNode*, vector<ListNode*>, function<bool(ListNode*, ListNode*)>> pq(
        [] (ListNode* a, ListNode* b) { return a->val > b->val; });

        for (auto head : lists) {
            if (head != nullptr) pq.push(head);
        }

        while (!pq.empty()) {
            ListNode* node = pq.top();
            pq.pop();
            p -> next = node;
            
            if (node -> next != nullptr)
                pq.push(node->next);

            p = p -> next;
        }
    return dummy -> next;
    }
};

(四)19.删除链表中的倒数第N个节点

1.思路

假设链表有 n 个节点,倒数第 k 个节点就是正数第 n - k + 1 个节点。
是的,但是算法题一般只给你一个 ListNode 头结点代表一条单链表,你不能直接得出这条链表的长度 n,而需要先遍历一遍链表算出 n 的值,然后再遍历链表计算第 n - k + 1 个节点。
也就是说,这个解法需要遍历两次链表才能得到出倒数第 k 个节点。

2.复杂度分析

3.代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
         if (head == nullptr || n == 0) return nullptr;

        ListNode* dummy = new ListNode(-1);
        dummy -> next = head;

        ListNode* x = findFromEnd(dummy, n + 1);
        x -> next = x -> next -> next;
        return dummy -> next;
    }
    ListNode* findFromEnd(ListNode* head, int k) {

    // 代码见上文

        ListNode* slow = head;
        ListNode* fast = head;

        while (k -- && fast) {
            fast = fast -> next;
        }
        while (fast) {
            slow = slow -> next;
            fast = fast -> next;
        }
        return slow;
    }
};
更多推荐

利用爬虫技术自动化采集汽车之家的车型参数数据

导语汽车之家是一个专业的汽车网站,提供了丰富的汽车信息,包括车型参数、图片、视频、评测、报价等。如果我们想要获取这些信息,我们可以通过浏览器手动访问网站,或者利用爬虫技术自动化采集数据。本文将介绍如何使用Python编写一个简单的爬虫程序,实现对汽车之家的车型参数数据的自动化采集,并使用亿牛云爬虫代理服务来提高爬虫的稳

Java AOP Framework概述

JavaAOPFramework概述1.AspectJ1.1使用AspectJ进行切面编程2.SpringAOP2.1使用SpringAOP进行切面编程2.2如何决定使用哪种动态代理2.3如何通过配置指定代理方式2.4SpringAOP和AspectJ的关系3.SpringBootAOP4.扩展4.1AspectJ织入

面向面试知识--MySQL数据库与索引

面向面试知识–MySQL数据库与索引优化难点与面试点什么是MySQL索引?索引的MySQL官方定义:索引是帮助MySQL快速获取数据的数据结构。动力节点原文:MysQL官方对于索引的定义:索引是帮助MySQL高效获取数据的数据结构。MysQL在存储数据之外,数据库系统中还维护着满足特定查找算法的数据结构,这些数据结构以

HarmonyOS Codelab 优秀样例——溪村小镇(ArkTS)

一、介绍溪村小镇是一款展示溪流背坡村园区风貌的应用,包括园区内的导航功能,小火车行车状态查看,以及各区域的风景展览介绍,主要用于展示HarmonyOS的ArkUI能力和动画效果。具体包括如下功能:打开应用时进入启动页,启动页轮播展示溪村小镇风景图,之后进入应用首页。在首页的“地图浏览”标签页,可以拖动和缩放查看地图,并

Kubernetes Ingress:灵活的集群外部网络访问的利器

《KubernetesIngress:集群外部访问的利器-打造灵活的集群网络》前提条件您已经拥有一个Kubernetes集群,并且可以访问该集群。您已经安装了kubectl命令行工具。版本选择安装前需要选择兼容你Kubernetes的版本,不能会失败ingress由两部分组成:IngressController:负责处

【踩坑日记】springboot MultipartFile上传,@Async异步执行时报错:java,io.FileNotFoundException

项目场景:springboot项目中使用MultipartFile上传文件导入时,文件内容过大会导致页面等待时间较长,所以考虑使用上传文件时用@Async异步处理数据的方式来解决页面等待问题。问题描述给处理MultipartFile文件的方法添加@Async注解后,上传文件时出现异常,找不到临时文件异常如下:(org.

RISC-V Reader 笔记(七)RV64,特权架构,未来可选扩展

RV64比起RV32,其实扩展不多。主要是添加了一系列字,双字为单位的操作。各个ISA3264比较x86:变量都存在寄存器里,不像32存在内存里,因此指令数少很多,但是因此添加了很多新操作码来操作更多的寄存器,因此指令长度变长了(添加前缀来区分),代码体积大很多。arm:有一系列和arm32类似的问题,:分支指令使用的

重庆思庄技术分享——linux du 命令

linuxdu命令inuxdu(英文全拼:diskusage)命令用于显示目录或文件的大小。du会显示指定的目录或文件所占用的磁盘空间。语法du[-abcDhHklmsSx][-L<符号连接>][-X<文件>][--block-size][--exclude=<目录或文件>][--max-depth=<目录层数>][-

Nodejs 第十六章(ffmpeg)

FFmpeg是一个开源的跨平台多媒体处理工具,可以用于处理音频、视频和多媒体流。它提供了一组强大的命令行工具和库,可以进行视频转码、视频剪辑、音频提取、音视频合并、流媒体传输等操作。FFmpeg的主要功能和特性:格式转换:FFmpeg可以将一个媒体文件从一种格式转换为另一种格式,支持几乎所有常见的音频和视频格式,包括M

Linux arm64 pte相关宏

文章目录一、pte和pfn1.1pte_pfn1.2pfn_pte二、其他宏参考资料一、pte和pfn//linux-5.4.18/arch/arm64/include/asm/pgtable.h#definepte_pfn(pte)(__pte_to_phys(pte)>>PAGE_SHIFT)#definepfn_

Zebec 生态 AMA 回顾:Nautilus 以及 $ZBC 的未来

在9月7日,Zebec创始人Sam做客社区,并进行了“NautilusChain以及$ZBC的未来”主题的AMA访谈。Sam在本次访谈中对NautilusChain生态的价值捕获、Zebec生态布局规划、可能会推出的NautilusChain治理通证NAUT进行了解读。本文将对本次AMA进行回顾与总结。主持人:社区新的

热文推荐