【数据结构与算法】链表1:移除链表 设计链表链表反转(双指针法、递归法)

news2025/1/9 5:54:34

文章目录

    • 今日任务
    • 1.链表理论基础
        • (1)什么是链表?
        • (2)链表的类型
        • (3)链表的存储方式
        • (4)链表的定义
        • (5)链表的操作
        • (6)性能分析
    • 2.Leetcode203:移除链表元素
        • (1)题目
        • (2)思路
        • (3)代码实现
    • 3.Leetcode707:设计链表
        • (1)题目
        • (2)思路
        • (3)代码实现
    • 4.Leetcode206:反转链表
        • (1)题目
        • (2)思路
        • (3)双指针法
        • (4)递归法

今日任务

  • 链表理论基础

  • 203.移除链表元素

  • 707.设计链表

  • 206.反转链表

1.链表理论基础

(1)什么是链表?

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头节点也就是head。

(2)链表的类型

常见的链表类型有以下几种:

<1>单链表

单向链表是一种包含两部分的数据结构,即一个是数据部分(数据域),另一个是地址部分(指针域),其中包含下一个或后继节点的地址。节点中的地址部分也称为指针

image-20230217114232162

在单链表中,每一个节点除了包括自身的数值外,还包含了下一个节点的地址,在第三个节点它的地址部分包含的是NULL值,因为它不指向任何节点。此外,保存初始节点地址的指针称为头指针

由于单链表的指针域只保存了下一个节点的地址,因此在单链表中,只能向前遍历,而不能反向遍历

// 单向链表中节点的表示

struct node 
{ 
int data; 
struct node *next; 
} 

<2>双链表

前面说了单链表中的指针域只能指向节点的下一个节点。而在双链表中,每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点

这就意味着,双向链表不仅支持向前查询,还可以向后查询

image-20230217114149969

// 双向链表中节点的表示

struct node 
{ 
int data; 
struct node *next; 
struct node *prev; 
} 

<3>循环链表

循环链表,是指头节点和尾节点首位相连,以此形成一个循环结构。也可以这么认为,循环链表是单链表的变体。也就是说,循环链表没有起始节点和结束节点,我们可以朝任意方向进行遍历(向前或者向后)。

image-20230217114529563

// 循环链表中节点的表示

struct node 
{ 
int data; 
struct node *next; 
} 

乍一看,循环链表和单链表节点的表示一样,其实他们之间唯一最本质的区别就是最后一个节点不指向单链表中的任何节点,因此单链表的链接部分包含一个NULL值;相反,循环链表的最后一个节点的链接部分保存着第一个节点的地址。

(3)链表的存储方式

前面在学习数组的时候我们知道,数组在内存中是连续分布的,但是链表则是通过指针域的指针 链接在内存中的各个节点上,也就是说链表中的节点在内存中不是连续分布的,而是零散分布在内存中的某个地址上,分配机制取决于操作系统的内存管理。

image-20230217120000271

在上图中我们可以看出,该链表的起始节点为2,终止节点为7,各个节点分布在内存中的不同地址空间上,通过指针串联在一起。

(4)链表的定义

给出链表节点的定义:

// 单链表
strcut ListNode{
	int val;	//节点上存储的元素
	ListNode *next;		//指向下一个节点的指针
	ListNode(int x): val(x),next(NULL){}	// 节点的构造函数
};

下面给出使用自己定义构造函数和使用默认构造函数的区别(推荐自定义构造函数):

1、通过自己定义构造函数初始化节点:

ListNode* head = new ListNode(5);

2、使用默认构造函数初始化节点:

ListNode* head = new ListNode();
head->val = 5;

从上面不难看出,如果使用默认构造函数的话,在初始化时是不可以直接给变量赋值的。

(5)链表的操作

<1>删除节点

我们以下图为例,目的时删除D节点:

image-20230217142406253

具体操作:

C节点的next指针指向的是D节点,而我们的需求是删除D节点,那么只需要将C节点的next指针指向E节点就可以了

此时的D节点从链表中删除,但是它依然存放在内存中,需要我们手动释放这段内存。

<2>添加节点

在下图中,我们需要在C节点和D节点中添加一个F节点:

image-20230217142827964

添加F节点,只需要将C节点的next指针指向F节点,同时F节点的next指针指向D节点,这样就完成了节点的添加。

(6)性能分析

这里我们将链表和数组做一个对比,详见下图:

image-20230217143205888

  • 数组在定义的时候,长度就是固定的,想要改动数组的长度,就需要重新定义一个新的数组。
  • 链表的长度可以是不固定的,并且可以实现动态增删,适合场景:数据量不固定、增删频繁、查询需求较少

2.Leetcode203:移除链表元素

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/remove-linked-list-elements

(1)题目

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例 1:

image-20230217143757762

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

输入:head = [], val = 1
输出:[]

示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的节点数目在范围 [0, 104] 内
  • 1 <= Node.val <= 50
  • 0 <= val <= 50

(2)思路

案例1:

链表:1->4->2->4 目的:移除元素4

image-20230217144046231

其实这道题还是比较简单的,首先可以看出它是一个单链表,那么我们定义好节点的数据域和地址域,让节点1的next指针指向节点2,并且让节点2的next指针指向NULL,那么这道题就算完成了,最后的结果也就是下面这张图。

image-20230217144342989

那么此外我们还需要完成节点4的内存回收工作!

案例二:

由于考虑到在实际应用中可能存在对头节点的删除需求,所以我们这里也额外做个分析。

对于链表的操作有两种形式:

  • 1.直接使用原来的链表进行删除操作。
  • 2.设置一个虚拟头节点再进行删除操作。

<操作1>:直接使用原来的链表进行移除

移除头节点和移除其他节点的擦欧总是不一样的,因为链表的其他节点都是通过前面一个节点来移除房前节点,而头节点没有前节点。

那么对于头节点的移除,需要将头节点向后移动一位就可以了,同时记得将原头节点从内存中释放。

image-20230217145654957

对于操作一这种方法虽然可以实现,但是无疑是增加了代码的逻辑性,需要我们单独写一段逻辑处理头节点。那么这样的话不妨我们试试操作2的方法。

<操作2>:设置一个虚拟头节点再进行删除操作

如何设置虚拟头节点,首先我们需要给链表添加一个虚拟头节点作为新的头节点,同时我们移除旧的头节点,也就是下图中的元素1,并且将新的头节点的next指针指向第二个节点4

image-20230217150125673

具体实现我们详见代码。

(3)代码实现

// 操作1实现:直接使用原来的链表进行删除操作

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 删除头结点
        while (head != NULL && head->val == val) { 
        // head != NULL:这里判断头节点不为空是因为后续需要对头节点的值进行操作,如果为空就相当于操作空指针,编译会报错。
            ListNode* tmp = head;
            head = head->next;
            delete tmp;		// 此处需要对旧的头节点进行内存回收
        }

        // 删除非头结点
        ListNode* cur = head;	// 当前节点
        while (cur != NULL && cur->next!= NULL) {
        // cur->next!= NULL:这里是同样的道理,不可操作空指针
            if (cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }
        }
        return head;
    }
};

这里需要注意几点:

  • 对于可能存在节点的值为空的情况我们要避免空指针操作,否则编译会报错
  • 操作1的关键代码就是下面的这两部分

image-20230217153228463

// 操作2实现:设置一个虚拟头节点再进行删除操作

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while (cur->next != NULL) {
            if(cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }
        }
        head = dummyHead->next;
        delete dummyHead;
        return head;
    }
};

3.Leetcode707:设计链表

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/design-linked-list

(1)题目

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-> 3
linkedList.get(1);            //返回3

提示:

  • 0 <= index, val <= 1000
  • 请不要使用内置的 LinkedList 库。
  • get, addAtHead, addAtTail, addAtIndex 和 deleteAtIndex 的操作次数不超过 2000。

(2)思路

分析题目给出的要求,主要是需要完成以下功能:

  • 获取链表第index个节点的值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点面前插入一个节点
  • 删除链表的第index个节点

(3)代码实现

class MyLinkedList {
public:
    // 定义链表节点结构体
    struct LinkedNode {
        int val;
        LinkedNode* next;
        LinkedNode(int val):val(val), next(nullptr){}
    };

    // 初始化链表
    MyLinkedList() {
        _dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
        _size = 0;
    }

    // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
    int get(int index) {
        if (index > (_size - 1) || index < 0) {
            return -1;
        }
        LinkedNode* cur = _dummyHead->next;
        while(index--){ // 如果--index 就会陷入死循环
            cur = cur->next;
        }
        return cur->val;
    }

    // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
    // 这里选择插入新的头节点采用的是操作1,详情可查看第二小节中的思路
    void addAtHead(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        newNode->next = _dummyHead->next;
        _dummyHead->next = newNode;
        _size++;
    }

    // 在链表最后面添加一个节点
    void addAtTail(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(cur->next != nullptr){
            cur = cur->next;
        }
        cur->next = newNode;
        _size++;
    }

    // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果index大于链表的长度,则返回空
    // 如果index小于0,则在头部插入节点
    void addAtIndex(int index, int val) {

        if(index > _size) return;
        if(index < 0) index = 0;        
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        _size++;
    }

    // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
    void deleteAtIndex(int index) {
        if (index >= _size || index < 0) {
            return;
        }
        LinkedNode* cur = _dummyHead;
        while(index--) {
            cur = cur ->next;
        }
        LinkedNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        _size--;
    }

    // 打印链表
    void printLinkedList() {
        LinkedNode* cur = _dummyHead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }
private:
    int _size;
    LinkedNode* _dummyHead;

};

4.Leetcode206:反转链表

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/reverse-linked-list

(1)题目

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

示例 1:

image-20230217163726826

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

image-20230217163749967

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

(2)思路

链表的反转,只需要改变next指针的指向即可。

(3)双指针法

对于链表的反转问题,我们可以通过使用双指针的方式来解决这个问题。

  • cur指针,指向链表的头节点
  • pre指针,定义为cur指针的前一个节点,也就是让cur指针原本指向后一位的指针指向pre指针的地址
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* temp; // 作为一个临时节点,保存cur的下一个节点
        ListNode* cur = head;
        ListNode* pre = NULL;	// 之所以初始化为空,就是为了让cur节点指向pre节点,而我们的目标就是尾节点反转成目标的头节点,也就是NULL
        // 所以此处当pre节点和cur节点遍历到尾节点时,也就是cur指向NULL,这也就意味反转完成,因此while()的值设为cur
        while(cur) {
            temp = cur->next;  // 保存一下 cur的下一个节点,因为接下来要改变cur->next
            cur->next = pre; // 翻转操作
            // 更新pre 和 cur指针
            pre = cur;
            cur = temp;
        }
        return pre;	// 返回的是新链表的头节点pre
    }
};

(4)递归法

前面讲了双指针法,其实递归法与之逻辑都是大体一样的,不过对于递归,我们有自前向后递归、以及自后向前递归两种方法。

// 递归法:自前向后

class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }

};
// 递归法:自后向前

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 边缘条件判断
        if(head == NULL) return NULL;
        if (head->next == NULL) return head;
        
        // 递归调用,翻转第二个节点开始往后的链表
        ListNode *last = reverseList(head->next);
        // 翻转头节点与第二个节点的指向
        head->next->next = head;
        // 此时的 head 节点为尾节点,next 需要指向 NULL
        head->next = NULL;
        return last;
    }
}; 

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

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

相关文章

【设计模式】综述

设计模式概述 文章目录一、设计模式1. 基本简介2. 面向对象设计原则二、基本类别1. 创建型模式会更新每种模式的详细博客超链接&#xff0c;敬请期待呀参考博客&#x1f60a;点此到文末惊喜↩︎ 一、设计模式 1. 基本简介 定义&#xff1a;设计模式是一个针对重复发生的问题的…

使用HTTP隧道代理,请求超过频率要怎么办?

在网上&#xff0c;经常会看到有人说使用隧道代理经常遇到429错误&#xff08;请求超过频率&#xff09;&#xff0c;我们要如何解决这一问题呢&#xff1f;通常情况&#xff0c;优质的HTTP代理厂商隧道代理服务器采用的是高性能主机构建的动态IP代理服务器&#xff0c;是可以支…

IO流

标题IO流的体系结构FileReader和FileWriterFileReader读入数据的基本操作FileReader中使用read(char [] cbuf)读入数据FileWriter写出数据字节流使用FileInputStream和FileOutputStream读写文本文件使用FileInputStream和FileOutputStream读写非文本文件缓冲流缓冲流&#xff0…

智慧校园平台源码:实现互联互通的校园管理一体化

智慧校园管理平台主要以校园安全、智慧校园数据管理云平台为核心&#xff0c;实现数据统一管理&#xff0c;以智慧电子班牌为学生智慧之窗&#xff0c;以移动管理平台、家校沟通为辅。实现教师—家长一学校—学生循环的无纸化管理模式及教学服务&#xff0c;实现多领域的信息互…

【JavaSE】Lambda、Stream(659~686)

659.每天一考 1.写出获取Class实例的三种常见方式 Class clazz1 String.class; Class clazz2 person.getClass(); //sout(person); //xxx.yyy.zzz.Person... Class clazz3 Class.forName(String classPath);//体现反射的动态性2.谈谈你对Class类的理解 Class实例对应着加载…

小小bat-day1-自动文件上传

前言&#xff1a;日常服务器备份文件或者生产设备等数据文件都统一保存至文件服务器&#xff0c;进行日志分析或者将生产文件CSV、图片等转存至数仓进行数据分析&#xff0c;尤其生产的部分数据是保存在个人电脑的PC端&#xff0c;数据杂&#xff0c;获取困难&#xff0c;手动整…

day45【代码随想录】动态规划之完全平方数、单词拆分、打家劫舍、打家劫舍 II

文章目录前言一、完全平方数&#xff08;力扣279&#xff09;二、单词拆分&#xff08;力扣139&#xff09;三、打家劫舍&#xff08;力扣198&#xff09;四、打家劫舍 II前言 1、完全平方数 2、单词拆分 3、打家劫舍 4、打家劫舍 II 一、完全平方数&#xff08;力扣279&#…

2023软考报名(上半年)报名什么时候开始?-弘博创新

2023软考报名&#xff08;上半年&#xff09;报名预计在3月底-4月初开始&#xff0c;现在可以先进入备考了&#xff0c;参加学习可以到弘博创新&#xff0c;专业考前辅导多年&#xff0c;专业靠谱&#xff01; 系统集成项目管理工程师是全国计算机技术与软件专业技术资格&#…

Windows安装VMware虚拟机+配置Ubuntu的详细步骤以及解决配置过程中报错的问题(完整版)

目录 引言: 过程&#xff1a; 安装VMware虚拟机&#xff1a; 在VMware虚拟机中配置Ubuntu&#xff1a; 在VMware虚拟机中安装Ubuntu&#xff1a; VMware中启动虚拟机时报错问题的解决&#xff1a; 正式开始安装Ubuntu&#xff1a; 参考资料&#xff1a; 引言: 在学习计…

线程池源码解析项目中如何配置线程池

目录 基础回顾 线程池执行任务流程 简单使用 构造函数 execute方法 execute中出现的ctl属性 execute中出现的addWorker方法 addWorker中出现的addWorkerFailed方法 addWorker中出现的Worker类 Worker类中run方法出现的runWorker方法 runWorker中出现的getTask runWo…

Websocket详细介绍

需求背景 在某个资产平台&#xff0c;在不了解需求的情况下&#xff0c;我突然接到了一个任务&#xff0c;让我做某个页面窗口的即时通讯&#xff0c;想到了用websocket技术&#xff0c;我从来没用过&#xff0c;被迫接受了这个任务&#xff0c;我带着浓烈的兴趣&#xff0c;就…

MinIO

概述MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口&#xff0c;非常适合于存储大容量非结构化的数据&#xff0c;例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等&#xff0c;而一个对象文件可以是任意大小&#xff0c;从…

Redis学习【7】之发布_订阅命令和事务

文章目录一 发布/订阅命令1.1 消息系统1.2 subscribe1.3 psubscribe1.4 publish1.5 unsubscribe1.6 punsubscribe1.7 pubsub1.7.1 pubsub channels1.7.2 pubsub numsub1.7.3 pubsub numpat二 Redis 事务2.1 Redis 事务特性Redis 事务实现2.1.1 三个命令2.1.2 基本使用2.2. Redi…

家用洗地机哪款质量好?洗地机排行榜

伴随着人们消费水平和生活品质的提升&#xff0c;人们对家庭中的需求也随之提高&#xff0c;洗地机凭借着吸拖洗为一体的功能深受大众喜爱&#xff0c;但是市面上洗地机产品越来越多&#xff0c;清洁效果也参差不齐&#xff0c;到底哪款洗地机质量好呢&#xff0c;跟随笔者脚步…

FILE文件操作

文件指针 每个被使用的文件都在内存中开辟了一个相应的文件信息区&#xff0c;用来存放文件的相关信息&#xff08;如文件的名 字&#xff0c;文件状态及文件当前的位置等&#xff09;。这些信息是保存在一个结构体变量中的。该结构体类型是有系统 声明的&#xff0c;取名FILE…

ecaozzz

2. 图形报表ECharts 2.1 ECharts简介 ECharts缩写来自Enterprise Charts&#xff0c;商业级数据图表&#xff0c;是百度的一个开源的使用JavaScript实现的数据可视化工具&#xff0c;可以流畅的运行在 PC 和移动设备上&#xff0c;兼容当前绝大部分浏览器&#xff08;IE8/9/10/…

面试完阿里,字节,腾讯的测试岗,复盘以及面试总结

前段时间由于某些原因辞职了&#xff0c;最近一直在面试。面试这段时间&#xff0c;经历过不同业务类型的公司&#xff08;电商、酒店出行、金融、新能源、银行&#xff09;&#xff0c;也遇到了很多不同类型的面试官。 参加完三家大厂的面试聊聊我对面试的一些看法&#xff0…

AWS攻略——子网

文章目录分配子网给Public子网分配互联网网关创建互联网网关附加到VPC给Public子网创建路由表关联子网打通Public子网和互联网网关创建Public子网下的EC2进行测试配置Private子网路由给Private子网创建路由表附加在Private子网创建Private子网下的EC2进行测试创建实例在跳板机上…

Mybatis 之useGeneratedKeys注意点

一.例子 Order.javapublic class Order {private Long id;private String serial; }orderMapper.xml<?xml version"1.0" encoding"UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd…

java学习--多线程

多线程 了解多线程 ​ 多线程是指从软件或者硬件上实现多个线程并发执行的技术。 ​ 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程&#xff0c;提升性能。 并发和并行 并行&#xff1a;在同一时刻&#xff0c;有多个指令在CPU上同时执行并发&#xff1…