数据结构:链表的双指针技巧

文章目录

      • 一、链表相交问题
      • 二、单链表判环问题
      • 三、回文链表
      • 四、重排链表结点

初学双指针的同学,请先弄懂删除链表的倒数第 N 个结点。
并且在学习这一节时,不要将思维固化,认为只能这样做,这里的做法只是技巧。

一、链表相交问题

LeetCode:160.相交链表
  题目要求时间复杂度为O(L1+L2),空间复杂度为O(1),实际上并不是说只能遍历一次,单个链表遍历常数次,时间复杂度仍为O(L)。我们可以采用如下方法解决:

  1. 计算两个链表的长度:首先遍历两个链表,计算它们各自的长度(lenAlenB),同时检查链表的末尾节点是否相同。如果末尾节点不同,则两个链表不相交,直接返回NULL。这一步也确保了,如果两个链表相交,它们的末尾节点必定是相同的。(对于存在相交的链表,它们的的末尾结点一定是一样的!

  2. 调整起点:为了同步遍历两个链表找到交点,需要从两个链表相同的“距离”开始遍历。由于两个链表可能长度不同,我们先计算长度差abs(lenA-lenB)。然后让较长的链表的指针先前进这个长度差的步数。这样做的目的是让两个链表的指针能够在同一起跑线上“开始比赛”。

  3. 同步前进直到找到交点:之后,同时前进两个链表的指针,直到它们相遇。由于步骤2已经确保了两个指针距离链表末尾的距离相同,所以它们会在交点相遇。如果两个链表有交点,那么这个交点是两个指针第一次相遇的地方。

这段代码的关键在于通过计算长度差并同步前进两个指针来找到可能的交点。这种方法不需要修改链表结构,也不需要额外的存储空间,效率较高。在最坏的情况下,时间复杂度是O(L1+L2),其中L1和L2分别是两个链表的长度。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        //if(!headA||!headB) return NULL;
        int lenA=0,lenB=0;
        ListNode * travelA=headA,* travelB=headB;
        while(travelA->next||travelB->next){
            if(travelA->next) {travelA=travelA->next;++lenA;}
            if(travelB->next) {travelB=travelB->next;++lenB;}
        }
        if(travelA!=travelB) return NULL;
        if(lenB>lenA) swap(headA,headB);
        lenA=abs(lenA-lenB);//复用
        for(int i=0;i<lenA;++i) headA=headA->next;
        while(headA!=headB){
            headA=headA->next;
            headB=headB->next;
        }
        return headA;
    }
};


防止大脑已经不会暴力,暴力解法:
①对于L1,从头遍历L1的每一个结点,判断该结点是否在L2中,如果在,则是相交结点。(从头遍历,第一个在的是相交结点),如果每次都遍历一次L2,则时间复杂度O(L1*L2)
②无脑的哈希优化,将L2的结点存入一个哈希表(集合),每次判断时只平均需要O(1)的时间,所以时间复杂度为O(L1+L2),空间复杂度O(L2)。

哈希优化:

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        //if(!headA||!headB) return NULL;
        unordered_set<ListNode * > Set;
        while(headA){
            Set.insert(headA);
            headA=headA->next;
        }
        while(headB){
            if(Set.count(headB)) return headB;
            headB=headB->next;
        }
        return NULL;
    }
};

二、单链表判环问题

LeetCode:141.环型链表
  题目要求使用空间复杂度为O(1),因此不能用哈希解决,用哈希只需要遍历的过程中将结点存入哈希表,每次遍历先判断该结点是否在哈希表中,如果不在则存入哈希表,如果在则找到环。但是哈希表的空间复杂度为O(L),因此我们只能用其他方法。
  这里使用双指针,一个慢指针slow,一个快指针fastslow一次走一步,fast一次走两步。类似于跑步比赛,从同一个长道出发,跑向运动场的跑道上跑步,由于快指针跑的速度是慢指针的两倍,快指针总会多跑一圈然后超过慢指针。
  因此如果只需要判断是否存在环只需要这样做,无环的话fast一定先跑到底:

class Solution {
public:
    bool hasCycle(ListNode *head) {
        if(!head||!head->next) return false;
        ListNode * slow=head;
        ListNode * fast=head;
        while(fast!=nullptr && fast->next!=nullptr){
            slow=slow->next;
            fast=fast->next->next;
            if(fast==slow) return true;
        }
        return false;
    }
};

不过,我们知道在行走时,还有信息我们没有用到,我们是否能利用这些信息找到环的入口呢?信息:

  • 慢指针走的步数:step_slow,快指针走的步数:step_fast,则有step_fast=step_slow*2
  • 假设环外结点数为a,环中结点数为b,设x是fast和slow相遇时离换入口的距离,则step_fast=a+nb+x,step_slow=a+cb+x,(0<=x<b)。
  • 联立可得①a+nb+x=2a+2x+2cb②nb=a+x+2cb③(n-2c)b=a+x。
  • 由③可知a+x>0,且a+x是圈中结点数的倍数,换句话说,由于现在走到的位置是x,则在fastslow相遇的位置,再走a步可以走到环的入口(因为再走这么多步刚好是环的倍数步),换句话说,如果现在有一个指针p从环外起点出发,每次走一步,与fast或slow一同行走,走完环外结点个数步(a步)之后会在环的入口处相遇!

寻找环的入口

ListNode *detectCycle(ListNode *head) {
    ListNode *slow = head, *fast = head;
    // 第一次遍历,找到快慢指针相遇点
    while (fast != nullptr && fast->next != nullptr) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) { // 发现环
            // 将其中一个指针(这里选择slow)移回头节点
            slow = head;
            // 两个指针以相同速度移动,再次相遇点即为环入口
            while (slow != fast) {
                slow = slow->next;
                fast = fast->next;
            }
            return slow; // 返回环入口
        }
    }
    return nullptr; // 无环
}

三、回文链表

LeetCode:234.回文链表
  回文链表,我们可以这样做,我们单独存一个原链表的反置之后的链表,然后反置的 和 原链表 从首位置开始齐步向前就行。(当然逆序的话,使用栈也是可以的!栈就相当于你存进去的东西想逆着拿出来,换句话说,你只需要逆序来查看某个东西的元素,存入栈是很好的选择!)不过时间复杂度需要的是O(1),我们需要一点点技巧。但是单链表怎么也不可能反着遍历呀!怎么办呢?!
在这里插入图片描述
将中点之后的链表部分翻转吧朋友:找到链表中点,然后将后面的链表翻转,然后就可以首尾齐进了。翻转链表只需要O(1)的时间复杂度,三个指针~,不过如果原题说不允许修改原结构,那就再翻转过了即可(虽然输入和输出只是一个接口,但是这确实只能说很离谱)。

反转链表的后半部分

  1. 找到中点:使用快慢指针找到链表的中间节点。
  2. 反转后半部分:从中间节点开始反转链表的后半部分。
  3. 比较前后半部分:将前半部分和反转后的后半部分进行比较,如果每个节点的值都相同,则链表是回文。
  4. 恢复链表(可选):如果需要保持原链表的结构,可以再次反转后半部分以恢复原链表。

这种方法的空间复杂度为O(1),但需要改变链表结构(如果不恢复的话)。

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if (head == nullptr) {
            return true;
        }

        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode* firstHalfEnd = endOfFirstHalf(head);
        ListNode* secondHalfStart = reverseList(firstHalfEnd->next);

        // 判断是否回文
        ListNode* p1 = head;
        ListNode* p2 = secondHalfStart;
        bool result = true;
        while (result && p2 != nullptr) {
            if (p1->val != p2->val) {
                result = false;
            }
            p1 = p1->next;
            p2 = p2->next;
        }        

        // 还原链表并返回结果
        firstHalfEnd->next = reverseList(secondHalfStart);
        return result;
    }

    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* curr = head;
        while (curr != nullptr) {
            ListNode* nextTemp = curr->next;
            curr->next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }

    ListNode* endOfFirstHalf(ListNode* head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while (fast->next != nullptr && fast->next->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
};

四、重排链表结点

在这里插入图片描述
这个和回文链表是一样的,将中点之后的链表部分翻转,然后就可以一个一个选了。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【免费获取】【下片神器】IDM非主流网站视频免费下载神器IDM+m3u8并解决idm下载失败问题 idm下载器超长免费试用

当你浏览一个网站&#xff0c;看到一个喜欢的视频&#xff0c;不知道如何下载的时候&#xff0c;本教程或许可以帮你点小忙。大部分的主流网站都有专门的下载工具&#xff0c;本篇教程主要针对的是一些非主流的小网站。 我们的下载方法就是利用IDM&#xff08;Internet Downlo…

npm卸载不掉的解决方案

不管怎么重装重启都报错 真服了&#xff0c;npm卸载不掉绝对是有缓存存在&#xff0c;用where npm查到d盘 实际上根本不在这个地方&#xff0c;这个是我安装的6.14.12版本的npm的地方&#xff0c;我说我怎么怎么重装怎么导包都不行呢&#xff0c;偷偷隐藏在这个目录里面&#…

Unity 学习日记 13.地形系统

下载源码 UnityPackage 1.地形对象Terrain 目录 1.地形对象Terrain 2.设置地形纹理 3.拔高地形地貌 4. 绘制树和草 5.为地形加入水 6.加入角色并跑步 7.加入水声 右键创建3D地形&#xff1a; 依次对应下面的按钮 || 2.设置地形纹理 下载资源包 下载资源包后&#x…

【Ubuntu】文件和目录的增、删、改、查操作

这里写目录标题 (一)文件和目录类命令的使用1、目录与文件的增加&#xff08;1&#xff09;目录的增加 :&#xff08;2&#xff09;文件的增加 2、目录与文件的删除&#xff08;1&#xff09;目录和文件的删除 3、目录与文件的修改&#xff08;1&#xff09;mv命令 4、目录与文…

【跟着CHATGPT学习硬件外设 | 01】SPI

文章目录 &#x1f680; 概念揭秘关键精华&#x1f31f; 秒懂案例生活类比实战演练 &#x1f50d; 原理与工作流程探秘步骤1&#xff1a;初始化SPI接口步骤2&#xff1a;主设备启动通信步骤3&#xff1a;主设备发送数据步骤4&#xff1a;从设备接收数据步骤5&#xff1a;从设备…

Zookeeper(九)客户端的启动流程

目录 一 ZooKeeper会话的创建与连接1.1 会话的创建1.1.1 ClientWatchManager1.1.2 ConnectStringParser1.1.3 HostProvider1.1.4 ClientCnxn 1.2 会话的连接1.2.1 SendThread1.2.2 eventThread 二 ZooKeeper会话的响应2.1 接受服务端响应 三 ClientCnxn 详解3.1 Packet3.2 队列…

一文彻底搞懂并发容器

文章目录 1. 什么是并发容器2. 并发容器的分类 1. 什么是并发容器 并发容器是一种用于多线程环境的数据结构&#xff0c;它们能够有效地处理并发访问和修改的问题。在多线程应用程序中&#xff0c;多个线程可能会同时访问和修改共享的数据结构&#xff0c;这可能会导致数据不一…

一文教你如何轻松领取阿里云优惠券

随着云计算技术的飞速发展&#xff0c;越来越多的企业和个人选择使用阿里云作为他们的云服务提供商。为了吸引更多的用户上云&#xff0c;阿里云推出了各种优惠券和促销活动。本文将教大家如何轻松领取阿里云优惠券&#xff0c;以便在购买阿里云产品和服务时享受更多优惠。 一、…

激发数据潜力:企业数据中台的策略性构建与优化_光点科技

在信息时代&#xff0c;数据是企业价值链中不可或缺的一环。构建一个策略性的企业数据中台不仅能够整合分散的数据资源&#xff0c;还能提高决策效率和业务敏捷性。本文聚焦于如何策略性地构建和优化数据中台&#xff0c;以便企业能够最大化地利用数据资源&#xff0c;推动企业…

Sora是否能颠覆视频制作行业?一文带你了解

一个月前OpenAI宣布了一款名为Sora的新生成式人工智能系统&#xff0c;该系统可以根据文本提示生成短视频。虽然Sora尚未向公众开放&#xff0c;但迄今为止发布的高质量样本已经引起了兴奋和担忧的反应。 OpenAI发布的样本视频&#xff08;该公司称这些视频是由Sora直接制作&am…

反应式编程(三)什么是粘包、拆包?如何解决?

目录 一、粘包、拆包介绍1.1 什么是 TCP 协议&#xff1f;1.2 什么是粘包、拆包&#xff1f;1.3 粘包、拆包的四种情况1.4 粘包、拆包的原因1&#xff09;TCP协议中的滑动窗口机制2&#xff09;传输层的 MSS 与链路层的 MTU3&#xff09;TCP协议中的 Nagle 算法4&#xff09;应…

【智能算法】晶体结构算法(CryStAl)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2021年&#xff0c;S Talatahari等人受到晶体自然结构启发&#xff0c;提出了晶体构造算法&#xff08;Crystal Structure Algorithm , CryStAl&#xff09;。 2.算法原理 2.1算法思想 CryStAl受…

Python学习笔记-简单案例实现多进程与多线程

Python 的多进程与多线程是并发编程的两种重要方式&#xff0c;用于提高程序的执行效率。它们各自有不同的特点和适用场景。 多进程&#xff08;Multiprocessing&#xff09; 概念&#xff1a; 多进程是指操作系统中同时运行多个程序实例&#xff0c;每个实例称为一个进程。…

Jmeter 分布式压测,你的系统能否承受高负载?

‍你可以使用 JMeter 来模拟高并发秒杀场景下的压力测试。这里有一个例子&#xff0c;它模拟了同时有 5000 个用户&#xff0c;循环 10 次的情况‍。 请求默认配置 token 配置 秒杀接口 ​结果分析 ​但是&#xff0c;实际企业中&#xff0c;这种压测方式根本不满足实际需求。下…

java入门学习Day03

本篇文章主要有java中的变量、命名方法、数据类型。 一、java中的变量 数据类型 变量名 数据值&#xff1b;int money 50&#xff1b; public class varibledemo {public static void main(String[] args) {int money 50;//变量的输出System.out.println(money);money 6…

ctfshow-web入门-xxe

什么是xxe&#xff1f; XXE&#xff0c;全称XML External Entity Injection&#xff0c;即XML外部实体注入。这是一种针对应用程序解析XML输入类型的攻击。当包含对外部实体的引用的XML输入被弱配置的XML解析器处理时&#xff0c;就会发生这种攻击。这种攻击通过构造恶意内容&…

bugku-web-alert

这里可以看到flag在页面弹窗内 有两种弹窗 利用Python和bp各自尝试 得到的结果 这里得到一串不知道是什么的加密代码 经过尝试大量解码器后得知&#xff0c;这时unicode编码 进行解码

Linux中的文件操作

共识原理 在讲文件操作之前&#xff0c; 我们先形成一个共识 1 文件 内容 属性 2 文件分为打开的文件 和 没打开的文件 3 打开的文件是谁打开的&#xff1f; 进程&#xff01;&#xff01; – 研究文件操作本质是研究进程和文件的关系&#xff01; 4 没打开的文件&#xff1…

基于ssm的留学生交流互动论坛网站(java项目+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的留学生交流互动论坛网站。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 留学生交流互动论坛网站…

RUST工程构建工具CARGO及代码编写工具RUSTROVER使用

1.使用cargo创建rust工程 cargo new hello_rust 生成的内容如下: 使用cargo build进行编译工程 编译成功会生成一个target目录 进入target目录运行生成程序 也可直接使用cargo run直接编译并运行 如果要测试工程执行cargo test 如果要为工程创建文档执行cargo doc 也可发布工程…