手撕面试算法题必备技巧(贰) —— 双指针(链表篇)

news2024/11/16 7:40:30

本文介绍了双指针技巧在链表、数组以及字符串中的使用,给出了大量大厂常见面试手撕题目的思路及代码,不仅适合完全不了解双指针技巧的读者,也适合老司机复习拓展。

考察过该技巧的公司有阿里巴巴、腾讯、美团、拼多多、百度等大厂。

我相信,友好的讨论交流会让彼此快速进步!文章难免有疏漏之处,十分欢迎大家在评论区中批评指正。

食用指南: ⭐️为一刷必做题,便于快速理解双指针技巧;无星题目可在刷完⭐️题目后再做,用于拓展学习双指针技巧如何与其他算法知识结合使用。

双指针技巧是什么?

双指针技巧指的就是在遍历链表、数组或者字符串的过程中,不是使用一个指针来进行访问,而是使用两个指针(甚至可以多个,合并 k 个有序链表中将会用到)进行遍历访问。

以链表为例,两个指针要么同方向访问两个链表、要么同方向访问一个链表(快慢指针)、要么相反方向遍历一个链表(左右指针),数组和字符串也是如此。

下面,先来看看双指针技巧在链表问题中的应用。我们以题目入手,从易到难,逐步感受双指针技巧的精妙之处。

几乎所有题目都提供了使用双指针技巧的思路,以及动画示意图,希望可以通过这一篇文章让你彻底弄懂双指针技巧

双指针秒杀链表问题

⭐️合并两个有序链表(力扣21)

题目描述

力扣-21-合并两个有序链表

思路(双指针)

题目中两个链表已经排好序,我们可以使用两个指针同向访问两个链表,每次比较两个指针的元素,谁小谁加到新的链表上。

合并两个有序链表

参考代码

public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    // 一条链表为空,必然返回另一条链表,即使另一条链表也为空
    if (list1 == null) {
        return list2;
    }
    if (list2 == null) {
        return list1;
    }
    // 新建一个虚拟头节点(新链表),后面连接两条链表排序后的节点
    ListNode dummy = new ListNode(-1);
    // 三个指针分别指向虚拟头节点,list1,list2
    ListNode p = dummy, p1 = list1, p2 = list2;
    // 当两条链表都不为空的时候遍历
    while (p1 != null && p2 != null) {
        // 小的节点加在新链表后面
        if (p1.val <= p2.val) {
            p.next = p1;
            // 只移动取值的指针
            p1 = p1.next;
            // 新链表指针也要移动
            p = p.next;
        } else {
            p.next = p2;
            // 只移动取值的指针
            p2 = p2.next;
            // 新链表指针也要移动
            p = p.next;
        }
    }
    // 哪条链表有剩余(剩余的值一定比前面都大),就直接连在新链表后面。
    if (p1 != null) {
        p.next = p1;
    }
    if (p2 != null) {
        p.next = p2;
    }
    // 返回时去掉虚拟头结点
    return dummy.next;
}

Tips

虚拟头节点,即代码中的 dummy 节点。使用虚拟头节点可以避免处理指针 p 为空的情况,降低代码的复杂性。

当我们需要创造一条新链表的时候,可以使用虚拟头节点简化边界情况的处理。 比如本题中两条链表要合并成一条新的有序链表,这时需要创造一条新链表就可以使用。再比如要将一条链表分解成两条链表,这时候要创建两条新链表,也可以使用。

时间复杂度

O ( n ) O(n) O(n)

最坏情况下,遍历两个链表一共 2 * N 个节点

空间复杂度

O ( 1 ) O(1) O(1)

无额外辅助空间,返回的链表是原有节点组装,是必要空间。


⭐️分隔链表(力扣86)

题目描述

力扣-86-分隔链表

思路(双指针)

在上一题中,合并两个有序链表,是将两个有序链表合二为一。而本题是将原链表一分为二,然后再合并成一条链表。具体来说,就是将原链表分成两条小链表,一条链表的元素都小于 x,另一条链表中的元素都大于等于 x,最后将两条链表再拼接到一起就可以了。两条小链表的构建过程就需要用到**「双指针」**技巧。

分割链表

参考代码

public ListNode partition(ListNode head, int x) {
    if (head == null) {
        return head;
    }
    // 存放小于 x 的链表的虚拟头节点
    ListNode dummyLess = new ListNode(-1);
    // 存放大于等于 x 的链表的虚拟头节点
    ListNode dummyMore = new ListNode(-2);
    // p 指针负责遍历原链表,pLess 负责生成小于 x 的结果链表,pMore 负责生成大于等于 x 的结果链表
    ListNode p = head, pLess = dummyLess, pMore = dummyMore;
    while (p != null) { // 原链表不为空
        if (p.val < x) {
            pLess.next = p;
            pLess = pLess.next;    
        } else {
            pMore.next = p;
            pMore = pMore.next;
        }
        // !!! 如果不断开,会成环报错,新手务必注意!
        // 断开原链表中的每个节点的 next 指针
        ListNode nxt = p.next; // 记录原链表中当前节点的下个节点
        p.next = null; // 当前节点的 next 指针设为空
        p = nxt;
    }
    // 小 x 链表连接大 x 链表
    pLess.next = dummyMore.next;
    // 返回时去掉虚拟头结点
    return dummyLess.next;
}

Tips

在循环代码块中,必须断开原链表中每个节点的 next 指针,即将原链表节点的 next 指针设为 null,然后才能向后移动指针,否则就会连接成环从而导致答案错误。

时间复杂度

O ( n ) O(n) O(n)

最坏情况下,需要完整遍历一遍原链表。

空间复杂度

O ( 1 ) O(1) O(1)

使用了常数个指针,没有额外辅助空间。


合并 K 个升序链表(力扣23)

题目描述

力扣-23-合并 K 个升序链表

思路(k个指针)

我们可以准备 k 个指针,分别放在 k 个链表头,每次取出最小的元素,加入到新的大链表中,指针后移,继续比较,每次出去的都是最小元素,就完成了题目的要求。

在 Java 中,我们可以借助优先级队列 PriorityQueue 来实现。优先级队列本质上是一种堆排序容器,根据传入的比较器确定是大根堆还是小根堆,默认是小根堆。小根堆的堆顶是容器中最小的元素,其余更大的元素在堆下方。想要了解这种结构,可以访问 Data Structure Visualizations 网站去查看小根堆的排序过程。

参考代码

public ListNode mergeKLists(ListNode[] lists) {
    // 判空
    if (lists == null || lists.length == 0) {
        return null;
    }
    // 优先级队列,小根堆
    PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
    // 将 k 个链表的头节点加入小根堆
    for (ListNode head : lists) {
        // 头节点不为空才能加入
        if (head != null)
            pq.add(head);
    }
    // 要新建一个结果链表,所以新建一个虚拟头节点,指针 p 用于构建结果链表
    ListNode dummy = new ListNode(-1), p = dummy;
    while (!pq.isEmpty()) {
        // 取出最小元素的节点
        ListNode cur = pq.poll();
        // 连接到结果链表上
        p.next = cur;
        p = p.next;
        // 如果被取出的节点后续有节点,加入到小根堆中
        if (cur.next != null) {
            pq.add(cur.next);
        }
    }
    // 返回时去掉虚拟头节点
    return dummy.next;
}

时间复杂度

O ( n l o g 2 k ) O(nlog_2k) O(nlog2k)

n 为所有链表总节点数,最坏要遍历所有节点,每次加入优先级队列排序需要 O ( l o g 2 k ) O(log_2k) O(log2k)

空间复杂度

O ( k ) O(k) O(k)

优先队列的大小不会超过 k。


⭐️删除链表的倒数第 N 个结点(力扣19)

题目描述

力扣-19-删除链表的倒数第 N 个结点

思路(双指针)

我们直接讲进阶的实现方式,也就是使用双指针技巧,一趟扫描实现这道题目。

这道题目要用到「双指针」技巧的**「快慢指针」**,假设 n = 3,我们可以先让快指针走 3 步,然后慢指针此时从链表头结点出发,快慢指针同步前进,当快指针走到 null 时,慢指针正好指向要删除的节点。

由于要删除这个节点,我们还需准备一个虚拟头节点,以及一个指向虚拟头节点的指针(前置指针),前置指针与慢指针同时同步前进,当慢指针指向要删除节点的时候,前置指针正好指向要删除节点的前一个,然后将前置指针的 next 指针指向要删除节点的下一个节点即可。

力扣-19-删除链表的倒数第 N 个结点

参考代码

public ListNode removeNthFromEnd(ListNode head, int n) {
    // 判空
    if (head == null) {
        return head;
    } 
    // 快指针先走 n 步
    ListNode fast = head;
    for (int i = 0; i < n; i++) {
        fast = fast.next;
    }
    // 给原链表添加一个虚拟头节点
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    // 准备前置指针和慢指针
    ListNode pre = dummy;
    ListNode slow = head;
    // 前置、快慢指针同步前进,当快指针为空时,慢指针就到了倒数第 n 个位置
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
        pre = pre.next;
    }
    // 删除慢指针处的节点(倒数第 n 个节点)
    pre.next = slow.next; // help GC
    // 返回时去掉表头
    return dummy.next;
}

时间复杂度

O ( n ) O(n) O(n)

最坏情况下,遍历 n 个链表节点。

空间复杂度

O ( 1 ) O(1) O(1)

常数级指针变量,无额外辅助空间使用


⭐️链表的中间结点(力扣876)

题目描述

力扣-876-链表的中间结点

思路(双指针)

这道题用到了「双指针」技巧的 「快慢指针」技巧,我们快慢指针分别指向链表头节点 head。

快指针每次走两步,慢指针每次走一步,当快指针走到链表末尾时,慢指针正好指向链表的中点。

链表的中间节点

参考代码

public ListNode middleNode(ListNode head) {
    if (head == null) {
        return head;
    }
    // 快慢指针分别指向链表头节点 head
    ListNode slow = head, fast = head;
    // 快指针走到链表末尾时停止
    while (fast != null && fast.next != null) {
        // 快指针一次走两步,慢指针一次走一步
        fast = fast.next.next;
        slow = slow.next;
    }
    // 慢指针指向链表中点
    return slow;
}

时间复杂度

O ( n ) O(n) O(n)

空间复杂度

O ( 1 ) O(1) O(1)

常数级指针变量,无额外辅助空间使用。


⭐️环形链表(力扣141)

题目描述

环形链表(力扣141)

思路(双指针)

这道题用到的也是**「快慢指针」**技巧。环形链表的环一定在链表末尾,并且末尾没有 null 了。根据这个特点,我们使用快慢指针同向访问链表,因为速度不一致,如果有环的话,快慢指针一定会相遇;如果没有环的话,快指针一定会遇到空指针。

双指针判断链表是否有环

参考代码

public boolean hasCycle(ListNode head) {
    // 判空
    if (head == null || head.next == null) {
        return false;
    }
    // 快慢双指针,初始指向链表头 head
    ListNode slow = head, fast = head;
    // 快指针走到末尾时停止
    while (fast != null && fast.next != null) {
        // 快慢指针移动
        fast = fast.next.next;
        slow = slow.next;
        // 快慢指针相遇则有环
        if (slow == fast) {
            return true;
        }
    }
    // 快指针走到末尾,则没有环
    return false;
}

时间复杂度

O ( n ) O(n) O(n)

最坏情况下遍历链表 n 个节点。

空间复杂度

O ( 1 ) O(1) O(1)

仅使用了两个指针,没有额外辅助空间。


⭐️环形链表入口节点(力扣142)

题目描述

环形链表入口节点(力扣142)

思路(双指针)

这道题也是使用「快慢指针」技巧,难点在于如何找到环的入口节点。我会先说如何解决这道题,理论推导过程我也会给出,如果觉得很难懂可以直接跳过,记住解题方法就可以了。

解法就是使用上一题(判断链表是否有环)的方法,找到相遇节点,然后慢指针留在相遇节点,快指针回到链表头节点,之后两个指针同步逐个元素地遍历链表,当两者再次相遇的节点就是环的入口节点。

快慢指针找环入口节点

为什么这样就能找到环的入口节点了呢?

假设在一个有环链表中。快指针在环中走了 n n n 圈,慢指针走了 m m m 圈,两者相遇。此时,链表头节点到环入口点距离为 x x x,环入口到相遇点距离为 y y y,相遇点到环入口点距离为 z z z

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pTyYMyjv-1683703643358)(null)]

这个过程中,快指针一共走了 x + n ( y + z ) + y x + n(y + z) + y x+n(y+z)+y 步,慢指针一共走了 x + m ( y + z ) + y x + m(y + z) + y x+m(y+z)+y 步。

快指针走的步数是慢指针的两倍,则
x + n ( y + z ) + y = 2 ( x + m ( y + z ) + y ) x + n(y + z) + y = 2(x + m(y + z) + y) x+n(y+z)+y=2(x+m(y+z)+y)
进一步推导
x + y = ( n − 2 m ) ( y + z ) x+y=(n-2m)(y+z) x+y=(n2m)(y+z)
y + z y+z y+z 就是环的大小,这说明从链表头经过环入口到达相遇地方经过的距离(x + y)等于整数倍环的大小(y + z)

那么此时,我们从头开始遍历到相遇位置(x + y),从相遇位置开始在环中遍历(y + z),会使用相同的步数,而双方最后都会经过入口到相遇位置这 y y y 个节点,说明这 y y y 个节点它们就是重叠遍历的,那它们从入口位置就相遇了,这样就找到了入口节点。

参考代码

public ListNode detectCycle(ListNode head) {
    // 判空
    if (head == null || head.next == null) {
        return null;
    }
    // 定义快慢双指针,初始化指向链表头节点 head
    ListNode fast = head, slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (slow == fast) { // 相遇跳出循环
            break;
        }
    }
    // 跳出循环有两种可能
    // 1. 快指针到末尾了,链表无环
    // 2. 链表有环,快慢指针相遇了
    // 情况 2
    if (fast != null && fast.next != null) {
        fast = head; // 快指针重新指向头结点
        // 快慢指针同步遍历链表,再次相遇的节点就是环入口节点
        while (fast != slow) { 
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }
    // 情况 1
    return null;
}

时间复杂度

O ( n ) O(n) O(n)

最坏情况下遍历链表两次

空间复杂度

O ( 1 ) O(1) O(1)

使用了常数个指针,没有额外辅助空间


⭐️相交链表(力扣160)

题目描述

相交链表(力扣160)

这道题就是要寻找到两个链表的第一个公共节点。

思路(双指针)

我们使用两个指针 p1、p2 分别在链表 1、链表 2 上同步前进,但是无法同时走到公共节点。因此,解决这道题的关键在于,如何能够让两个指针同时到达相交节点 6。

两个指针的速度是一样的,那么如何才能同时到达相同点呢?那么应该要让两个指针走同样的长度。

因此,两个指针一起遍历,如果 p1 先遍历完链表 1 的话,就从链表 2 的头节点继续遍历。同样的,如果 p2 先遍历完链表 2 的话,就从链表 1 的头节点继续遍历。也就是说,p1 和 p2 都会遍历链表 1 和链表 2。

两个指针,同样的速度,走完同样的长度(链表 1 + 链表 2),不管两条链表有无相同节点,都能够同时到达终点。

也就是说,如果有公共节点,两个指针一定会相遇,相遇节点就是第一个公共节点;如果没有公共节点,两个指针都会走到终点,此时都是 null,也算是相等了。

双指针确定两链表第一个公共节点

参考代码

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    // 判空
    if (headA == null || headB == null) {
        return null;
    }
    // 初始化 p1 和 p2 两个指针,分别指向两个链表头
    ListNode p1 = headA, p2 = headB;
    while (p1 != p2) {
        // 如果 p1 不为空,前进一步,为空则从链表 B 头节点开始
        if (p1 != null) {
            p1 = p1.next;
        } else {
            p1 = headB;
        }
        // 如果 p2 不为空,前进一步,为空则从链表 A 头节点开始
        if (p2 != null) {
            p2 = p2.next;
        } else {
            p2 = headA;
        }
    }
    return p1;
}

时间复杂度

O ( m + n ) O(m+n) O(m+n)

链表 1 和链表 2 的长度之和。

空间复杂度

O ( 1 ) O(1) O(1)

常数级指针变量,无额外辅助空间使用。


⭐️反转链表(力扣206)

题目描述

反转链表(力扣206)

思路(三指针)

反转链表一定要自己在纸上画一遍!

初始化三个指针分别是 pre(黄色)、cur(红色)、nxt(绿色)。

  • pre 指针用于指向已经反转好的链表的最后一个节点,最开始没有反转,所以为空。
  • cur 指针指向待反转链表(原链表)的第一个节点,最开始第一个节点待反转,所以指向 head。
  • nxt 指针指向待反转链表(原链表)的第二节点,用于保存链表,因为 cur 的 next 指针指向改变后,后面的链表就失效了,所以需要保存。

反转链表

参考代码

public ListNode reverseList(ListNode head) {
    if (head == null) {
        return head;
    }
    // 初始化三个指针
    ListNode pre = null, cur = head, nxt = null;
    while (cur != null) {
        nxt = cur.next; // 保存 cur 的下一个节点
        cur.next = pre; // 反转 
        // 指针后移,操作下一个未反转链表的第一个节点
        pre = cur; // 移动 pre 到 cur
        cur = nxt; // 移动 cur 到 nxt
    }
    // 返回反转后的头节点
    return pre;
}

时间复杂度

O ( n ) O(n) O(n)

最坏情况下,需要遍历整个链表。

空间复杂度

O ( 1 ) O(1) O(1)

常数级指针变量,无额外辅助空间使用。


⭐️链表内指定区间反转(力扣92)

题目描述

指定区间反转(力扣92)

思路I(双指针)

先说最简单的一种方法,我们找到要反转的链表区间,将这个区间断开,然后利用上一道题的思路反转这个区间链表,然后再重新接上就可以了。这个部分的难点就是要记得保存好一些关键节点,待反转链表的前驱节点,后继节点,待反转链表自己的头节点。

反转指定区间的链表

参考代码I

public ListNode reverseBetween (ListNode head, int m, int n) {
    if (head == null) {
        return null;
    }
    // 定义虚拟头节点,便于处理要删除链表第一个节点的情况
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode p = dummy;
    // 寻找第 m 个节点的前一个节点(待反转链表头节点的前驱节点)
    int i = 0;
    for (; i < m - 1; i++) {
        p = p.next;
    }
    // p 抵达第 m 个节点的前一个节点(待反转链表头节点的前驱节点)
    ListNode pre = p;
    // 寻找第 n 个节点
    for (; i < n; i++) {
        p = p.next;
    }
    // 保存第 n 个节点的下一个节点(待反转链表尾节点的后继节点)
    ListNode succ = p.next;
    // 切断待反转链表与后继节点的联系
    p.next = null;
    // 待反转链表的头节点
    ListNode reverseHead = pre.next;
    // 切断待反转链表头节点与前驱节点的联系
    pre.next = null;
    // 前驱节点连接反转后的链表
    pre.next = reverse(reverseHead);
    // 链表反转后,原来的头节点 reverseHead 变成了尾节点,连接后继节点
    reverseHead.next = succ;
    // 返回时去掉虚拟头节点
    return dummy.next;
}

ListNode reverse(ListNode head) {
    ListNode pre = null, nxt = null, cur = head;
    while (cur != null) {
        nxt = cur.next;
        cur.next = pre;
        pre = cur;
        cur = nxt;
    }
    return pre;
}

时间复杂度I

O ( n ) O(n) O(n)

遍历一次链表

空间复杂度I

O ( 1 ) O(1) O(1)

常数级指针变量,无额外辅助空间使用。

思路II(双指针头插迭代法)

一定要在纸上画一遍,肯定能够理解代码!

使用双指针,一个指向第 m 个节点,一个指向前驱节点(指针始终不变)。所谓头插法,以图示的 2 到 4 为例,先让 3 号节点插到 2 号节点前面,再让 4 号节点插到 3 号节点前面,这样就反转成功了。

双指针头插迭代法

参考代码II

public ListNode reverseBetween (ListNode head, int m, int n) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode pre = dummy; // 永远指向待反转区域的头结点的前驱节点(前一个节点),循环中永远不变
    // 找到第 m 个节点的前驱节点
    for (int i = 0; i < m - 1; i++) {
        pre = pre.next;
    }
    ListNode cur = pre.next; // 指向待反转区间的头结点
    ListNode nxt; // 永远指向 cur 节点的下一个节点
    for (int i = m; i < n; i++) {
        nxt = cur.next;
        // 当前节点的下一个节点要前插
        cur.next = nxt.next; // 当前节点的 next 要连接 nxt 的下一个节点 
        nxt.next = pre.next; // nxt 节点,前插到头部,连接原本的头结点(前驱节点的下一个节点)
        pre.next = nxt; // 前驱节点连向 nxt 节点
    }
    // 返回时去掉虚拟头节点
    return dummy.next;
}

时间复杂度II

O ( n ) O(n) O(n)

最坏情况下,遍历整个链表。

空间复杂度II

O ( 1 ) O(1) O(1)

常数级指针变量,无额外辅助空间使用。


K 个一组翻转链表(力扣25)

题目描述

K 个一组翻转链表(力扣25)

思路(双指针+递归)

这道题思路很简单,我们只要每轮遍历链表 k 次,分出一组,不足 k 次,不用翻转直接返回这一组的头节点即可。

分出一组后,对这组链表进行翻转,原来的头变成了尾,然后拼接在一起就可以啦。

如何翻转 k 个链表节点呢?

还记得我们之前做过的「反转链表」题目吗?题目的函数签名是 ListNode reverseList(ListNode head),这个可以理解为「反转 headnull 之间的节点」。

public ListNode reverseList(ListNode head) {
    ListNode pre = null, cur = head, succ = null;
    while (cur != null) {
        succ = cur.next;
        cur.next = pre;
        pre = cur;
        cur = succ;
    }
    return pre;
}

那么如果要翻转 headtail 之间的节点(一共 k 个节点),要如何做呢?

我们只要更改函数签名 ListNode reverseList(ListNode head, ListNode tail),将原本代码中的 null 改为 tail 即可。

// 反转 [head, tail) 的链表节点
public ListNode reverseList(ListNode head, ListNode tail) {
    ListNode pre = null, cur = head, succ = null;
    // 终止条件从 null 变成 tail
    while (cur != tail) {
        succ = cur.next;
        cur.next = pre;
        pre = cur;
        cur = succ;
    }
    // 返回反转后的头节点
    return pre;
}

完整过程如下。

K 个一组反转链表

参考代码

public ListNode reverseKGroup(ListNode head, int k) {
    if (head == null) {
        return head;
    }
    // 区间 [pHead, pTail) 有 k 个待反转的节点
    ListNode pHead = head, pTail = head;
    // 让 pTail 指向第 k 个节点
    for (int i = 0; i < k; i++) {
        // 不足 k 个节点,不需要反转,base case
        if (pTail == null) {
            return head;
        }
        pTail = pTail.next;
    }
    // 反转 k 个节点 
    ListNode newHead = reverseList(pHead, pTail);
    // 递归反转后续链表并连接起来
    pHead.next = reverseKGroup(pTail, k);
    return newHead;
}

// 反转 [head, tail) 的链表节点
public ListNode reverseList(ListNode head, ListNode tail) {
    ListNode pre = null, cur = head, succ = null;
    // 终止条件从 null 变成 tail
    while (cur != tail) {
        succ = cur.next;
        cur.next = pre;
        pre = cur;
        cur = succ;
    }
    // 返回反转后的头节点
    return pre;
}

代码中 for 循环之后的部分可能需要再解释一下,如图所示:

递归反转并连接后的样子如下:

时间复杂度

O ( n ) O(n) O(n)

遍历链表所有节点

空间复杂度

O ( n ) O(n) O(n)

递归栈最大深度为 n / k n/k n/k


⭐️奇偶链表(力扣328)

题目描述

奇偶链表(力扣328)

思路(双指针)

这道题直接使用两个指针同方向遍历链表,第一个节点索引为奇数,第二个节点索引为偶数,第三个节点索引为奇数。我们可以直接断掉节点 1 和节点 2 之间的连接,让节点 1 的 next 指向节点 3,偶数索引节点也是如此。

双指针同向遍历

参考代码

public ListNode oddEvenList(ListNode head) {
    // 如果链表为空,不用重排
    if(head == null) {
        return head;
    }
    // 奇数索引节点指针
    ListNode odd = head;
    // 偶数索引节点指针,可能为空
    ListNode even = head.next;
    // 标记偶数索引头节点,用于奇偶索引链表连接
    ListNode evenHead = even;
    while (even != null && even.next != null) {
        // 奇数索引节点的 next 连向偶数索引节点的下一个
        odd.next = even.next;
        // 奇数索引指针后移
        odd = odd.next;
        // 偶数索引节点的 next 连向奇数索引节点的下一个
        even.next = odd.next;
        // 偶数索引指针后移
        even = even.next;
    }
    // 偶数索引链表连在奇数索引链表后面
    odd.next = evenHead;
    return head;
}

时间复杂度

O ( n ) O(n) O(n)

遍历一次链表所有节点。

空间复杂度

O ( 1 ) O(1) O(1)

常数级指针变量,没有额外辅助空间。

更新日志

更新时间更新内容
2023.05.06发布文章,详解双指针技巧,详解 9 道大厂常见手撕题目:合并两个有序链表、分割链表、合并 K 个升序链表、删除链表的倒数第 N 个节点、链表的中间节点、环形链表、环形链表的入口节点、相交链表、反转链表。
2023.05.09新增题目:奇偶链表(力扣328)、指定区间反转(力扣92)。
2023.05.10新增食用指南;新增题目:K 个一组翻转链表(力扣25)

参考资料

  1. 牛客网
  2. 力扣网
  3. 算法基地 - github
  4. labuladong 的算法小抄
  5. 代码随想录
  6. draw.io

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

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

相关文章

Linux loglevel日志等级设置

Linux loglevel日志等级设置 系统支持按不同log输出方式配置不同等级输出&#xff0c;如串行口的输出等级配置为2&#xff0c;则只有0&#xff0c;1等级的Log会输出到串行口&#xff0c;以此类推&#xff1b; 系统应该支持不同等级的Log message&#xff0c;SOC分为5级以上等级…

移植三星官方的uboot到x210

1、移植前的准备工作 1.1、三星移植过的uboot源代码准备 (1)三星对于S5PV210的官方开发板为SMDKV210&#xff0c;对应移植的_uboot_smdkv210.tar.bz2 1.2、SourceInsight准备 (1)移植的时候最重要的工作就是看代码、改代码然后编译运行测试。 (2)编译代码必须在linux中&…

数字孪生技术在物流领域有何应用?

随着科技的不断进步&#xff0c;数字孪生技术在越来越多的领域得到了广泛应用。其中&#xff0c;物流领域是一个重要的应用场景。数字孪生技术可以在物流领域实现多种功能&#xff0c;如货物追踪、运输优化、风险管理等&#xff0c;从而提高物流效率、降低成本&#xff0c;更好…

(构造函数的补充1)初始化列表

tips 在引用与指针传参的时候&#xff0c;都涉及到权限的放大缩小问题&#xff0c;都需要特别去注意一下。关于引用的权限放大缩小以及判断&#xff0c;在我之前的博客里面就有写过&#xff1b;对于指针的权限放大缩小问题&#xff0c;就看星号前面是否修饰了const。他们两个的…

详解c++---优先级队列和仿函数

目录标题 什么是仿函数如何定义一个仿函数什么是优先级队列优先级队列的使用模拟实现priority_queue准备工作top函数的实现size函数的实现empty函数的实现adjustup函数的实现push函数的实现pop函数的实现adjustdown函数的实现构造函数的实现 什么是仿函数 首先仿函数就是一个类…

若依任意文件下载(CVE-2023-27025)

若依它就是一个开源项目,任何公司的各种大的项目必然需要一个后台权限管理系统,这是必然的,但是如果不想投入太多人力物力去开发,又恰好有现成且比较好用的别人已经完成的项目直接供我们来使用 。 1、使用、减少工作量 2、学习优秀的开源项目底层的编程思想,设计思路,提…

UDP通讯(服务器/客户端)

前言&#xff1a;UDP通讯实现比较简单&#xff0c;单某些情况下也会使用&#xff0c;建议先看一下说明&#xff0c;然后运行代码感受一下。 UDP服务器 传输层主要应用的协议模型有两种&#xff0c;一种是TCP协议&#xff0c;另外一种则是UDP协议。TCP协议在网络通信中占主导地…

Golang-如何判断一个 interface{} 的值是否为 nil ?

引用 起初我会下意识的回答&#xff0c;直接 v nil 进行判断不就好了吗&#xff1f; 然后翻阅了很多资料终于大致搞定里面的道道. 例子 请看下面这段代码&#xff0c;可以先猜测一下输出的结果。 package mainimport ("fmt" )func main() {var a *string nilv…

网安笔记 08 key management

Key Management —— 不考 网络加密方法 1.1 链路加密 特点&#xff1a; 两个相邻点之间数据进行加密保护 不同节点对密码机和Key不一定同中间节点上&#xff0c;先解密后加密报文报头可一起加密节点内部&#xff0c;消息以明文存在密钥分配困难保密及需求数量大 缺点&…

day36_jdbc

今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、JDBC 二、登录 三、ORM 零、 复习昨日 sql语言&#xff1a;DDL DML DQL DCL create table 表名(id int primary key auto_increment,sname varchar(2…

【SAP Abap】X-DOC:SMW0 - Excel 导入模板的上传和下载

X-DOC&#xff1a;SMW0 - Excel 导入模板的上传和下载 1、实现效果2、模板上传3、下载功能代码 做批导程序&#xff0c;离不开 Excel 导入模板&#xff0c;为了方便用户&#xff0c;一般会将模板文档整合到导入功能的界面上&#xff0c;方便用户获取模板。 1、实现效果 2、模板…

两个form表单的数据存入同一个数据库表,使用使用json操作

项目场景&#xff1a; 两个form表单的数据存入同一个数据库表 问题描述 1.两个from表单数据一起传到后端但是数据解析和xml文件的sql获取不到报错 2.数据接受到了但是提示数据类型不兼容 3.使用RequestBody注解报错Content type application/x-www-form-urlencoded;charsetUT…

深入解析linux IO Block layer

早期的 Block 框架是单队列&#xff08;single-queue&#xff09;架构&#xff0c;适用于“硬件单队列”的存储设备&#xff08;比如机械磁盘&#xff09;&#xff0c;随着存储器件技术的发展&#xff0c;支持“硬件多队列”的存储器件越来越常见&#xff08;比如 NVMe SSD&…

makefile 变量的替换,嵌套引用,命令行变量

文章目录 前言一、变量替换&#xff1a;1. 变量值的替换。2. 变量的模式替换。3. 规则中的模式替换。 二、变量的嵌套使用三、命令行变量四、override ,define 关键字总结 前言 一、变量替换&#xff1a; 1. 变量值的替换。 使用 指定的字符&#xff08;串&#xff09;替换变…

ctfshow web入门 ssrf web351-355

1.web351 尝试访问本机的flag.php payload: urlhttp://localhost/flag.php urlhttp://127.0.0.1/flag.php2.web352 必须要用http或https&#xff0c;ip没有过滤因为匹配时没加变量&#xff0c;恒为真 payload: urlhttp://127.0.0.1/flag.php urlhttp://localhost/flag.php3.…

智能美妆镜兴起,如何升级更精细、智能的化妆体验!

经常化妆的小姐姐&#xff0c;会发现化妆除了要有好皮肤、一堆化妆品之外&#xff0c;化妆镜的作用也尤其重要&#xff01;爱拍照的小姐姐们都知道&#xff0c;自拍的效果好不好&#xff0c;和背景、灯光有着很大的关系&#xff0c;其中灯光的冷调或者暖调&#xff0c;也是影响…

Linux网络——Shell编程之数组

Linux网络——Shell编程之数组 一、概念二、数组的定义三、Shell数组操作1. 获取数组的所有元素的列表2. 获取数组的所有元素下标3.取数组的元素个数4. 获取数组的某个元素的值5.删除数组某个元素6.删除数组7.数组切片8.数组字符替换9.数组追加元素 四、数组在函数的传参 一、概…

阿里云Intel(R) Xeon(R) Platinum处理器2.5 GHz主频

阿里云服务器CPU处理器Intel(R) Xeon(R) Platinum&#xff0c;2.5 GHz主频&#xff0c;3.2 GHz睿频&#xff0c;测试的云服务器ECS为通用算力型u1实例ecs.u1-c1m2.large&#xff0c;如下图&#xff1a; 阿里云服务器CPU处理器Intel(R) Xeon(R) Platinum 目前使用这款CPU处理器…

c++类和对象重要巩固练习-------日期类对象

这是对于类和对象的练习篇&#xff0c;用c来模拟完成日期计算器。 这其中需要我们完成&#xff1a; 日期 - 天数后 得到的新日期 日期 - 日期 得到相差的天数比较日期的大小等 .....具体如下头文件类中的成员函数 #pragma once #include<iostream> using namespace std…

【UE】简单实现屏幕UI定位

【UE】简单实现屏幕UI定位三维坐标方法 实现效果 屏幕空间定位 场景空间定位 一般实现兴趣点&#xff08;POI&#xff09;有两种实现方法&#xff0c;场景空间UI定位和屏幕空间UI定位。 场景空间定位&#xff1a;UI类似实例模型&#xff0c;位置和尺寸是相对于场景不变。大多…