线性表 —— 链表

news2024/11/15 13:57:54

与数组对比

◼ 数组:
 要存储多个元素,数组(或选择链表)可能是最常用的数据结构。
 我们之前说过,几乎每一种编程语言都有默认实现数组结构

◼ 但是数组也有很多缺点:
 数组的创建通常需要申请一段连续的内存空间(一整块的内存),并且大小是固定的(大多数编程语言数组都是固定的),所以当当前数组不能满足容量需求时,需要扩容。 (一般情况下是申请一个更大的数组,比如2倍。 然后将原数组中的元素复制过去,比如 Java 的 ArrayList)
 而且在数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移。
 尽管JavaScript的Array底层可以帮我们做这些事,但背后的原理依然是这样。

概念

◼ 要存储多个元素,另外一个选择就是链表。
◼ 但不同于数组,链表中的元素在内存中不必是连续的空间。
 链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言称为指针或者链接)组成。
◼ 相对于数组,链表有一些优点:
 内存空间不是必须连续的。
✓ 可以充分利用计算机的内存,实现灵活的内存动态管理
链表不必在创建时就确定大小,并且大小可以无限的延伸下去。
 链表在插入和删除数据时,时间复杂度可以达到O(1)。
✓ 相对数组效率高很多。
◼ 相对于数组,链表有一些缺点:
 链表访问任何一个位置的元素时,都需要从头开始访问。(无法跳过第一个元素访问任何一个元素)。
 无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素。

image.png

实现

基本结构

实现链表包含两个类,一个是节点类,一个就是链表本身。

class Node<T> {
    value: T;
    next: Node<T> | null = null;
  
    constructor(value: T) {
        this.value = value;
    }
}

export class LinkedList<T> {
    head: Node<T> | null = null;
    size: number = 0;

    get length(): number {
        return this.size;
    }

    // 链表方法
}

常见操作

 append(element):向链表尾部添加一个新的项
 insert(position,element):向链表的特定位置插入一个新的项。
 get(position) :获取对应位置的元素
 indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回-1。
 update(position,element) :修改某个位置的元素
 removeAt(position):从链表的特定位置移除一项。
 remove(element):从链表中移除一项。
 isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false。
 size():返回链表包含的元素个数。与数组的length属性类似。

可以看到,链表的方法和数组的基础方法几乎是一样的。因为链表本身就是数组的替代数据结构,只是两者的底层存储结构不一样罢了。

append 添加 、traverse 遍历

追加有两种情况:

  1. 链表为空,直接追加在 head 后面
  2. 不为空,在末尾追加
export class LinkedList<T> {
    head: Node<T> | null = null;
    size: number = 0;

    get length(): number {
        return this.size;
    }

    append(value: T): void {
        const newNode = new Node(value);
        if (!this.head) {
            this.head = newNode;
        } else {
            let current = this.head;
          	// 临时变量 current 指向最后一个节点
            while (current.next) {
                current = current.next;
            }
            current.next = newNode;
        }
        this.size++;
    }

    traverse() {
        const result: T[] = [];
        let current = this.head;
        while (current) {
            result.push(current.value);
            current = current.next;
        }
        console.log(result.join(" -> "));
        return result;
    }
}

insert 插入

插入也有两种种情况:

  1. 从头插入
  2. 中间和尾部插入

注意:插入位置 position 或 index 是从 0 开始的。position 为 2,表示插入完成后,插入的新数据处于链表索引为 2 的地方。也就是插入操作是在原先 2 位置节点的前面插入。

实现:头插入不用说,中间或尾部插入,只要找到原位置上节点的前一个节点,然后把这个节点它的 next ,也就是原位置上的节点给新节点的 next;它的 next 重新指向新节点即可。

insert(value: T, position: number): boolean {
    if (position < 0 || position > this.size) return false;
    if (position === 0) {
        const newNode = new Node(value);
        newNode.next = this.head;
        this.head = newNode;
    } else {
        const newNode = new Node(value);
        let preNode = this.head;
        for (let i = 0; i < position - 1; i++) {
            preNode = preNode!.next;
        }
        newNode.next = preNode!.next;
        preNode!.next = newNode;
    }
    this.size++;
    return true;
}

removeAt 删除

根据给定索引删除元素,并返回被删除的元素。

和插入差不多,有两种情况。首位和非首位,非首位情况核心就是找到前一个节点。

removeAt(position: number): T | null {
    if (position < 0 || position >= this.size) return null;
    let deletedValue: T;
    if (position === 0) {
        deletedValue = this.head!.value;
        this.head = this.head!.next;
    } else {
        let preNode = this.head;
        for (let i = 0; i < position - 1; i++) {
            preNode = preNode!.next;
        }
        deletedValue = preNode!.next!.value;
        preNode!.next = preNode!.next?.next ?? null;
    }
  	this.size--;
    return deletedValue;
}

get 读取

根据索引获取值。

这个就更简单了,直接遍历到索引的节点即可。

get(position: number): T | null {
    if (position < 0 || position >= this.size) return null;
    let current = this.head;
    for (let i = 0; i < position; i++) {
        current = current!.next;
    }
    return current!.value;
}

封装私有方法:根据索引获取节点

可以看到上面的几个方法实现中,都有一个共同的核心逻辑,就是根据索引获取节点。因此可以将这个逻辑封装成一个私有方法。

private getNode(position: number): Node<T> | null {
    if (position < 0 || position >= this.size) return null;
    let node = this.head;
    for (let i = 0; i < position; i++) {
        node = node!.next;
    }
    return node;
}

update 更新

update(value: T, position: number): boolean {
    const node = this.getNode(position);
    if (!node) return false;
    node.value = value;
    return true;
}

indexOf 根据值获取索引

while 循环,只要节点存在就比较它的值。

indexOf(value: T): number {
    let current: Node<T> | null = this.head;
    let index = 0;
    while (current) {
        if (value === current.value) return index;
        current = current.next;
        index++;
    }
    return -1;
}

remove 根据值删除

remove(value: T): T | null {
    const index = this.indexOf(value);
    return this.removeAt(index);
}

isEmpty 判空

isEmpty(): boolean {
    return !!this.head;
}

链表完整实现

class Node<T> {
    value: T;
    next: Node<T> | null = null;
    constructor(value: T) {
        this.value = value;
    }
}

export class LinkedList<T> {
    private head: Node<T> | null = null;
    size: number = 0;

    get length(): number {
        return this.size;
    }

    private getNode(position: number): Node<T> | null {
        if (position < 0 || position >= this.size) return null;
        let node = this.head;
        for (let i = 0; i < position; i++) {
            node = node!.next;
        }
        return node;
    }

    append(value: T): void {
        const newNode = new Node(value);
        if (!this.head) {
            this.head = newNode;
        } else {
            let current = this.head;
            while (current.next) {
                current = current.next;
            }
            current.next = newNode;
        }
        this.size++;
    }

    traverse() {
        const result: T[] = [];
        let current = this.head;
        while (current) {
            result.push(current.value);
            current = current.next;
        }
        console.log(result.join(" -> "));
        return result;
    }

    insert(value: T, position: number): boolean {
        if (position < 0 || position > this.size) return false;
        if (position === 0) {
            const newNode = new Node(value);
            newNode.next = this.head;
            this.head = newNode;
        } else {
            const newNode = new Node(value);
            // let preNode = this.head;
            // for (let i = 0; i < position - 1; i++) {
            //     preNode = preNode!.next;
            // }
            const preNode = this.getNode(position - 1);
            newNode.next = preNode!.next;
            preNode!.next = newNode;
        }
        this.size++;
        return true;
    }

    removeAt(position: number): T | null {
        if (position < 0 || position >= this.size) return null;
        let deletedValue: T;
        if (position === 0) {
            deletedValue = this.head!.value;
            this.head = this.head!.next;
        } else {
            // let preNode = this.head;
            // for (let i = 0; i < position - 1; i++) {
            //     preNode = preNode!.next;
            // }
            const preNode = this.getNode(position - 1);

            deletedValue = preNode!.next!.value;
            preNode!.next = preNode!.next?.next ?? null;
        }
        this.size--;
        return deletedValue;
    }

    get(position: number): T | null {
        // if (position < 0 || position >= this.size) return null;
        // let current = this.head;
        // for (let i = 0; i < position; i++) {
        //     current = current!.next;
        // }
        const node = this.getNode(position);
        return node?.value ?? null;
    }

    update(value: T, position: number): boolean {
        const node = this.getNode(position);
        if (!node) return false;
        node.value = value;
        return true;
    }

    indexOf(value: T): number {
        let current: Node<T> | null = this.head;
        let index = 0;
        while (current) {
            if (value === current.value) return index;
            current = current.next;
            index++;
        }
        return -1;
    }

    remove(value: T): T | null {
        const index = this.indexOf(value);
        return this.removeAt(index);
    }

    isEmpty(): boolean {
        return !!this.head;
    }
}

题目

设计链表

  • https://leetcode.cn/problems/design-linked-list/description/

删除链表中的节点

  • https://leetcode.cn/problems/delete-node-in-a-linked-list/description/

这个题目的难点在于,它不给你头节点,只给你要删除的节点。说白了就是不让你拿到要删除节点的前一个节点。
显然,拿不到前一个节点,那么不可能做到真的删除该节点,所以题目也说了该节点可以不必在内存中删除,只要删除值就可以。

那么我们就可以删除本该被删除节点的下一个节点,因为目标删除节点是下一个节点的前节点,做到删除很容易。
那这样岂不是删除了不该删除的内容?
是的,所以提前将下一个节点的内容保存到目标删除节点即可,这样目标删除节点的内容就被覆盖删除了,然后内存释放的节点其实是下一个节点。

以 1234 4个节点为例,删除节点2:

  1. 把节点 3 的内容覆盖节点 2 的内容,这样节点 2 算是删除了
  2. 然后让节点 2 的 next 指向节点 4,
function deleteNode(node: ListNode | null): void {
    node.val = node.next.val;
    node.next = node.next.next;
};

反转链表

  • https://leetcode.cn/problems/reverse-linked-list/description/

非递归解法

笨笨解法:先用数组保存所有链表值,然后重新生成链表。

这其实是栈的解法:

function reverseList(head: ListNode | null): ListNode | null {
    const val = [];
    let node = head;
    while(node) {
        val.push(node.val);
        node = node.next;
    }
    let _head = new ListNode();
    let curNode = _head;
    while(val.length > 0) {
        const item = val.pop();
        const newNode = new ListNode();
        newNode.val = item;
        curNode.next = newNode;
        curNode = newNode;
    }
    _head = _head.next;
    return _head;
};

优化一下:原地修改链表。

反转链表的本质是什么?是遍历链表,然后一个一个从头插入进新链表。

function reverseList(head: ListNode | null): ListNode | null {
    if (head === null || head.next === null) return head;
    let _head: ListNode | null = null;
    let nextNode: ListNode | null;
    while (head) {
        nextNode = head.next;
        head.next = _head;
        _head = head;
        head = nextNode;
    }
    return _head;
};

原地反转,没有使用栈结构:

  1. 准备新链表头指针 _head(默认为 null) 和 指向下一个节点的变量 nextNode,以及当前处理节点变量 cur。(实际没有使用 cur 变量,直接操作头指针 head,作为 cur)。
    1. 这里假设使用了 cur 变量,因为便于理解。let  cur = head;
  2. nextNode 指向下一个节点,比如索引 1 的节点。nextNode = cur.next;
  3. 此时 0 号节点被卸下,应挂在新链表 head 上_。_cur.next = head;
    1. 为啥被卸下节点直接指向新链表头指针head?因为head 将会一直指向新链表头节点,从头插入实现反转。
    2. 另外末尾节点应指向 null,所以 _head 默认值应为 null。
  4. 让新链表头指针 head 指向被卸下的节点 cur,也就是head 重新指向新链表的头。_head = cur;
  5. 当前处理节点指针往后移动cur = nextNode;
  6. 以 cur 节点是否存在循序上述步骤。

递归解法

function reverseList(head: ListNode | null): ListNode | null {
    // 1 个节点或者空节点,直接返回该链表
    if (head === null || head.next === null) return head;
    // 开始递归,每次传入的都是下一个节点,相当于一路往后遍历
    const _head = reverseList(head?.next ?? null);
    // 此处是递归结束会执行的代码,递归结束的条件是链表只有一个节点
    // 因此此时的 head.next 指向的就是整条链表的最后一个节点。那 head 指向的就是倒数第二个节点。
    // 并且递归结束条件都是返回参数 head,最后一个递归的 reverseList 函数,它的参数 head 接收的就是最后一个节点
    // 然后它被返回了 return head,因此递归函数返回的就是反转后的头结点指针。
    // 现在问题就简化成了怎么反转两个节点。倒数第一个和最后一个。
    head.next.next = head;
    head.next = null;
    return _head;
};

递归的一些感悟:

递归是缩小问题规模的思维方式。因此我们使用递归,直接看递归结束后的状态,不要关注中间过程,会把人弄晕。
递归函数这一行代码之前之后有很大区别,递归函数之后的代码,变量的状态已经变成递归结束后的状态,具体变成什么样,要看递归函数最后的结束条件是什么。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1430757.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【EVP】Explicit Visual Prompting for Low-Level Structure Segmentations

目录 &#x1f347;&#x1f347;0.简介 &#x1f337;&#x1f337;1.研究动机 &#x1f34b;&#x1f34b;2.主要贡献 &#x1f353;&#x1f353;3.网络结构 &#x1f36d;3.1整体结构 &#x1f36d;3.2高频分量计算 &#x1f36d;3.3显示视觉提示EVP &#x1f342;&…

数学电路与电子工程2(MEE)—— 时序电路的寄存器和工作频率

1. 基本的数字逻辑存储元件&#xff1a;D锁存器和D触发器 D锁存器&#xff08;Verrou ou D latch&#xff09;&#xff0c;它是一个简单的存储设备&#xff0c;可以在使能信号&#xff08;E&#xff09;处于活动状态时存储一位数据。当E为高电平时&#xff0c;D锁存器的输出Q会…

React | Center 组件

在 Flutter 中有 Center 组件&#xff0c;效果就是让子组件整体居中&#xff0c;挺好用。 React 中虽然没有对应的组件&#xff0c;但是可以简单封装一个&#xff1a; index.less .container {display: flex;justify-content: center;align-items: center;align-content: ce…

java设计模式:策略模式

在平常的开发工作中&#xff0c;经常会用到不同的设计模式&#xff0c;合理的使用设计模式&#xff0c;可以提高开发效率&#xff0c;提高代码质量&#xff0c;提高代码的可拓展性和维护性。今天来聊聊策略模式。 策略模式是一种行为型设计模式&#xff0c;运行时可以根据需求动…

Stable diffusion使用和操作流程

Stable Diffusion是一个文本到图像的潜在扩散模型,由CompVis、Stability AI和LAION的研究人员和工程师创建。它使用来自LAION-5B数据库子集的512x512图像进行训练。使用这个模型,可以生成包括人脸在内的任何图像,因为有开源的预训练模型,所以我们也可以在自己的机器上运行它…

Windows下MySQL的界面安装

华子目录 下载MySQL安装MySQL配置MySQL配置环境变量检验MySQL是否安装成功 下载MySQL 首先我们在MySQL的官方下载MySQL https://www.mysql.com 点击download&#xff0c;开始下载 安装MySQL 下载完成后&#xff0c;我们双击msi文件 再点击next 之后我们先勾选I acc…

leetcode 1.两数之和(C++)DAY1(待补充哈希表法)

文章目录 1.题目描述示例提示 2.解答思路3.实现代码结果4.总结 1.题目描述 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&…

堪称灾难级攻击的 UDP FLOOD洪水攻击,应该如何防护?

DDOS又称为分布式拒绝服务&#xff0c;全称是Distributed Denial of Service。DDOS本是利用合理的请求造成资源过载&#xff0c;导致服务不可用&#xff0c;从而造成服务器拒绝正常流量服务。就如酒店里的房间是有固定的数量的&#xff0c;比如一个酒店有50个房间&#xff0c;当…

Java语法学习线程基础

Java语法学习线程基础 大纲 概念创建线程线程终止常用方法用户线程和守护线程线程的七大状态线程的同步互斥锁线程死锁释放锁 具体案例 1.概念 2. 创建线程 第一种&#xff1a; class Cat extends Thread {int time 0;Overridepublic void run() {while (true) {System.o…

计算机网络_1.5 计算机网络的性能指标

1.5 计算机网络的性能指标 一、总览二、常用的八个计算机网络性能指标1、速率&#xff08;1&#xff09;数据量&#xff08;2&#xff09;速率&#xff08;3&#xff09;数据量与速率中K、M、G、T的数值辨析&#xff08;4&#xff09;【练习1】计算发送数据块的所需时间 2、带宽…

C++ OpenGL绘制三维立体skybox场景obj模型AABB碰撞检测旋转动画界面

程序示例精选 C OpenGL绘制三维立体skybox场景obj模型AABB碰撞检测旋转动画界面 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《C OpenGL绘制三维立体skybox场景obj模型AABB碰撞检测旋转动…

使用Ettus USRP X440对雷达和EW系统进行原型验证

概览 无论是保障己方平台的生存能力&#xff0c;还是扰乱敌方频谱使用&#xff0c;以电磁(EM)频谱为主导都是任务成功的主要因素。电磁频谱操作(Electromagnetic Spectrum Operation, EMSO)需要使用战术系统来监测敌方的频谱活动、定位其发射器并帮助己方制定行动计划。软件无…

存算一体:架构创新,打破算力极限

1 需求背景 在全球数据量呈指数级暴涨&#xff0c;算力相对于AI运算供不应求的现状下&#xff0c;存算一体技术主要解决了高算力带来的高能耗成本矛盾问题&#xff0c;有望实现降低一个数量级的单位算力能耗&#xff0c;在功耗敏感的百亿级AIoT设备上、高能耗的数据中心、自动驾…

VSCode 安装LLDB调试器(OS X)并启动调试

插件&#xff1a;&#xff08;LLDB插件安装&#xff09; 安装这个版本不好弄错了&#xff0c;CodeLLDB&#xff08;名字&#xff09; 配置&#xff1a;&#xff08;LLDB启动调试&#xff09; {// 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。// 欲了解更…

阻塞队列(超详细易懂)

目录 一、阻塞队列 1.阻塞队列概述 2.生产者消费者模型 3.阻塞队列的作用 4.标准库中的阻塞队列类 5.例子&#xff1a;简单生产者消费者模型 二、阻塞队列模拟实现 1.实现循环队列&#xff08;可跳过&#xff09; 1.1简述环形队列 1.2代码实现 2.实现阻塞队列 2.1实…

CMake生成osg的FFMPEG插件及Windows下不生成VS工程问题解决

在Windows下&#xff0c;如何利用CMake生成osg的FFMPEG插件&#xff0c;请参考如下博文&#xff0c;同生成jpeg插件类似&#xff1a; osg第三方插件的编译方法&#xff08;以jpeg插件来讲解&#xff09;。 如下为生成FFMPEG时必要的设置&#xff1a; 注意&#xff1a; 一定要…

开发智能化企业培训平台:教育系统源码的创新方法

在传统的企业培训模式中&#xff0c;往往面临着效率低下、内容过时以及难以个性化的问题。为了解决这些挑战&#xff0c;采用智能化技术成为了企业培训领域的热门趋势。通过开发智能化企业培训平台&#xff0c;可以提高培训效果、降低成本&#xff0c;并更好地满足员工多样化的…

海量数据处理商用短链接生成器平台 - 2

第二章 短链平台项目创建git代码管理开发分层规范 第1集 短链平台实战-Maven聚合工程创建微服务项目 **简介&#xff1a;Maven聚合工程创建微服务项目实战 ** Maven聚合工程拆分 dcloud-common 公共依赖包 dcloud-app FlinkKafka实时计算 dcloud-account 账号流量包微服务 dc…

Oracle 面试题 | 10.精选Oracle高频面试题

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

【深度测试】看到技术方案后,该怎么进行分析和测试

测试左移的思想&#xff0c;讲究尽早测试&#xff0c;测试是一系列的行为&#xff0c;并不一定要等代码运行起来才能测&#xff0c;下面会分享一些经验&#xff0c;提供大家参考。 一、静态分析 1.1 分析方法调用链 目标&#xff1a;梳理结构&#xff0c;化繁为简 原理&#…