Leetcode_链表

寻找重复数

1 题目描述

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有一个重复的整数 ,找出 这个重复的数 。

你设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 的额外空间。

示例 1

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

示例 2

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

示例 3

输入:nums = [1,1]
输出:1

示例 4

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

提示

  • 1 <= n <= 105
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

进阶

  • 如何证明 nums 中至少存在一个重复的数字?
  • 你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?

2 解题(Java)

环形链表的思想解题

  1. 将下标n和nums[n]建立一个映射关系f(n);
  2. 从下标0出发,根据f(n)计算出一个值,以这个值为新的下标,再用这个函数计算,于是就可以产生一个类似链表一样的循环;

例如,数组[1,3,4,2,2],其映射关系n --> f(n)为:

0->1
1->3
2->4
3->2
4->2

这里 2->4 是一个循环,那么这个链表可以抽象为下图:

在这里插入图片描述
于是:

  1. 数组中有一个重复的整数:链表中存在环;
  2. 找到数组中的重复整数:找到链表的环入口;
  3. 因为已知数组中没有0,因此以0为头节点,不可能存在某个节点指向0的情况,避免了0->1->0的特殊情况。而剑指 Offer 03. 数组中重复的数字限定条件为:在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内,数组中某些数字是重复的,由于未知缺失哪个数字,因此无法选择头节点,不能采用此方法解题。

代码

class Solution {
    public int findDuplicate(int[] nums) {
        int slow = nums[0];
        int fast = nums[nums[0]];
        while(slow != fast){
            slow = nums[slow];
            fast = nums[nums[fast]];  
        }
        fast = 0;
        while(slow != fast){
            slow = nums[slow];
            fast = nums[fast];
        }
        return slow;
    }
}

3 复杂性分析

  • 时间复杂度:O(n);
  • 空间复杂度:O(1);

回文链表

1 题目描述

请判断一个链表是否为回文链表。

示例 1:

输入: 1->2
输出: false

示例 2:

输入: 1->2->2->1
输出: true

进阶:

  • 你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

2 解题(Java)

快慢指针+反转链表

class Solution {
    public static boolean isPalindrome(ListNode head) {
        if(head == null || head.next == null) {
            return true;
        }
        ListNode slow = head, fast = head;
        ListNode pre = null;
        while(fast != null && fast.next != null) {
            fast = fast.next.next;
            ListNode temp = slow.next;
            slow.next = pre;
            pre = slow;
            slow = temp;
        }
        if(fast != null) {
            slow = slow.next;
        }
        while(pre != null && slow != null) {
            if(pre.val != slow.val) {
                return false;
            }
            pre = pre.next;
            slow = slow.next;
        }
        return true;
    }
}

3 复杂性分析

  • 时间复杂度O(n)
  • 空间复杂度O(1)

排序链表

1 题目描述

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

进阶

  • 你可以在 O(nlogn) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

示例 1

在这里插入图片描述

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

示例 2

在这里插入图片描述

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

示例 3

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

提示

  • 链表中节点的数目在范围 [0, 5 * 10 ^ 4] 内
  • -10 ^ 5 <= Node.val <= 10 ^ 5

2 解题(Java)

题目解析

  1. 最适合链表的排序算法是归并排序。如果采用自顶向下的递归实现,则空间复杂度为O(log n),如果要达到O(1)的空间复杂度,则需要使用自底向上的实现方式;
  2. 首先求得链表的长度length,然后将链表拆分成子链表进行合并;
  3. 用subLength表示每次需要排序的子链表的长度,初始subLength=1;
  4. 每次将链表拆分成若干个长度为subLength的子链表(最后一个子链表的长度可以小于subLength),按照每两个子链表一组进行合并,合并后即可得到若干个长度为subLength * 2的有序子链表(最后一个子链表的长度可以小于subLength * 2);
  5. 将subLength的值加倍,重复4,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于length,整个链表排序完毕;

代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
        int length = 0;
        ListNode node = head;
        while (node != null) {
            node = node.next;
            length++;
        }
        ListNode dummyHead = new ListNode(0, head);
        for (int subLength = 1; subLength < length; subLength *= 2) {
            ListNode prev = dummyHead, curr = dummyHead.next;
            while (curr != null) {
                ListNode head1 = curr;
                for (int i=1; i<subLength && curr.next != null; i++) {
                    curr = curr.next;
                }
                ListNode head2 = curr.next;
                curr.next = null;
                curr = head2;
                for (int i = 1; i < subLength && curr != null && curr.next != null; i++) {
                    curr = curr.next;
                }
                ListNode next = null;
                if (curr != null) {
                    next = curr.next;
                    curr.next = null;
                }
                ListNode merged = merge(head1, head2);
                prev.next = merged;
                while (prev.next != null) {
                    prev = prev.next;
                }
                curr = next;
            }
        }
        return dummyHead.next;
    }

    public ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummyHead = new ListNode();
        ListNode temp = dummyHead;
        while (head1 != null && head2 != null) {
            if (head1.val <= head2.val) {
                temp.next = head1;
                head1 = head1.next;
            } else {
                temp.next = head2;
                head2 = head2.next;
            }
            temp = temp.next;
        }
        temp.next = head1 == null ? head2 : head1;
        return dummyHead.next;
    }
}

3 复杂性分析

  • 时间复杂度O(nlogn):其中 n 是链表的长度;
  • 空间复杂度O(1);

4 归并排序解法

class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null)
            return head;
        ListNode fast = head.next, slow = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode tmp = slow.next;
        slow.next = null;
        ListNode left = sortList(head);
        ListNode right = sortList(tmp);
        ListNode h = new ListNode(0);
        ListNode res = h;
        while (left != null && right != null) {
            if (left.val < right.val) {
                h.next = left;
                left = left.next;
            } else {
                h.next = right;
                right = right.next;
            }
            h = h.next;
        }
        h.next = left != null ? left : right;
        return res.next;
    }
}

环形链表 II

1 题目描述

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

进阶

  • 你是否可以使用 O(1) 空间解决此题?

示例 1

在这里插入图片描述

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2

在这里插入图片描述

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3

在这里插入图片描述

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示

  • 链表中节点的数目范围在范围 [0, 10 ^ 4] 内
  • -10 ^ 5 <= Node.val <= 10 ^ 5
  • pos 的值为 -1 或者链表中的一个有效索引

2 解题(Java)

解题思路

  1. 使用两个指针,fast 与 slow。它们起始都位于链表的头部。随后,slow 指针每次向后移动一个位置,而 fast 指针向后移动两个位置。如果链表中存在环,则 fast 指针最终将再次与 slow 指针在环中相遇;
  2. 当发现 slow 与 fast 相遇时,我们再额外使用一个指针 tmp。起始,它指向链表头部;随后,它和 slow 每次向后移动一个位置。最终,它们会在入环点相遇;

代码

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head, slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                fast = head;
                while (fast != slow) {
                    fast = fast.next;
                    slow = slow.next;
                }
                return slow;
            }
        }
        return null;
    }
}

3 复杂性分析

  • 时间复杂度O(N):其中 N 为链表中节点的数目。在最初判断快慢指针是否相遇时,slow 指针走过的距离不会超过链表的总长度;随后寻找入环点时,走过的距离也不会超过链表的总长度。因此,总的执行时间为 O(N)+O(N)=O(N);
  • 空间复杂度O(1):额外定义了 slow,fast,tmp这三个指针;

环形链表

1 题目描述

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

进阶

你能用 O(1)(即,常量)内存解决此问题吗?

示例 1

在这里插入图片描述

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2

在这里插入图片描述

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3

在这里插入图片描述

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示

  • 链表中节点的数目范围是 [0, 10 ^ 4]
  • -10 ^ 5 <= Node.val <= 10 ^ 5
  • pos 为 -1 或者链表中的一个 有效索引 。

2 解题(Java)

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast = head, slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                return true;
            }
        }
        return false;
    }
}

3 复杂性分析

  • 时间复杂度O(N):其中 N 为链表中节点的数目,slow 指针走过的距离不会超过链表的总长度(在快慢指针都进入环后,每移动一次,快慢指针的距离将减小1);
  • 空间复杂度O(1):使用了两个指针的额外空间;

删除链表的节点

1 题目描述

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。

返回删除后的链表的头节点。

示例 1:

输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.

示例 2:

输入: head = [4,5,1,9], val = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.

说明:

题目保证链表中节点的值互不相同 若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点

2 解题(Java)

2.1 Java代码

解题思路

删除值为 val 的节点可分为两步:定位节点、修改引用。

  1. 定位节点: 遍历链表,直到 cur.val == val 时跳出,即可定位目标节点;
  2. 修改引用: 设节点 cur 的前驱节点为 pre ,后继节点为 cur.next ;则执行 pre.next = cur.next ,即可实现删除 cur 节点;

算法流程

  1. 特例处理:如果head.val == val,直接返回head.next;
  2. 初始化:pre = head,cur = head.next;
  3. 定位结点:当cur为空或cur.val == val 时跳出循环:
    • 保存当前结点,即pre = cur;
    • 遍历下一结点,即cur = cur.next;
  4. 删除结点:如果cur != null,说明cur.val == val,执行pre.next = cur.next;否则就是cur == null,表示链表中不包含值为val的结点,不用做任何操作;
  5. 返回值:返回链表头结点head;

代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode deleteNode(ListNode head, int val) {
    	if(head == null) return head;
        if(head.val == val) return head.next;
        ListNode pre = head, cur = head.next;
        while(cur != null && cur.val != val) {
            pre = cur;
            cur = cur.next;
        }
        if(cur != null) pre.next = cur.next;
        return head;
    }
}

复杂性分析

  • 时间复杂度 O(N) : N 为链表长度,删除操作平均需循环 N/2 次;
  • 空间复杂度 O(1): cur, pre 占用常数大小额外空间;

两数相加

1 题目描述

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807

2 解题(Java)

2.1 思路

  1. 两链表从左到右同步遍历结点,初始化进位为0;
  2. 两结点的值与进位相加后取余,存储在新链表的结点中;两结点的值与进位相加后被10整除得进位;
  3. 如果有一个链表提前遍历结束,令其不再遍历,其结点的值始终为0,继续进行第二步的运算,直到两个链表都遍历结束;
  4. 如果进位不为0,还需新开辟一个结点,结点的值即为进位值;

2.2 代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode head = null, temp = null;
        // carry代表进位数,初始为0,表示没有进位
        int carry = 0;
        // 当l1和l2都走到链表尾部,跳出循环
        while (l1 != null || l2 != null) {
        	// 如果已经走到链表尾部,令值为0
            int num1 = l1 != null ? l1.val : 0;
            int num2 = l2 != null ? l2.val : 0;
            // sum等于两数之和加进位
            int sum = num1 + num2 + carry;
            // 注意首次开辟新结点与后面开辟新结点有所区别
            if (head == null) {
                head = temp = new ListNode(sum % 10);
            } else {
                temp.next = new ListNode(sum % 10);
                temp = temp.next;
            }
            // 获得本次运算的进位
            carry = sum / 10;
            // 如果结点没到链表尾,后移一个结点,否则不移
            if (l1 != null) l1 = l1.next;
            if (l2 != null) l2 = l2.next;
        }
        // 如果有进位,还需再开辟一个新节点
        temp.next = carry == 0 ? null : new ListNode(carry);
        return head;
    }
}

3 复杂性分析

  • 时间复杂度(O(max(M, N)):其中 M,N 为两个链表的长度,循环遍历次数为两个链表长度的大者,循环中的运算需要 O(1)的时间;
  • 空间复杂度(O(max(M, N))*:新链表的长度最大为较长链表的长度 +1;

链表中倒数第k个节点

1 题目描述

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。

示例

给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.

2 解题(Java)

2.1 解题思路

使用双指针:

  1. 初始化: 前指针 former 、后指针 latter ,双指针都指向头节点 head;
  2. 构建双指针距离: 前指针former先向前走k步(结束后,双指针 former 和 latter 间相距 k 步);
  3. 双指针共同移动: 循环中,双指针former和latter每轮都向前走一步,直至former走过链表尾节点时跳出(跳出后,latter与尾节点距离为 k−1,即latter指向倒数第k个节点);
  4. 返回值: 返回latter即可;
  5. 考虑越界问题:former在前k次移动时,每次移动前,检查是否为null,如果是,则越界,返回null;

2.2 代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */

class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode former = head, latter = head;
        // former先走k步
        for(int i = 0; i < k; i++) {
        	// 考虑越界情况
			if(former == null) return null;
            former = former.next;
		}
        while(former != null) {
            former = former.next;
            latter = latter.next;
        }
        return latter;
    }
}

3 复杂性分析

  • 时间复杂度 O(N) : N 为链表长度,former 走了 N 步, latter 走了 (N−k) 步;
  • 空间复杂度 O(1): 双指针 former , latter 使用常数大小的额外空间;

反转链表

1 题目描述

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

限制:

0 <= 节点个数 <= 5000

2 解题(Java)

2.1 解题思路

共定义3个指针:

temp暂存后继结点,cur修改引用指向,pre暂存当前结点,cur访问后继结点。

2.2 算法流程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3 代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode cur = head, pre = null;
        while(cur != null) {
            ListNode tmp = cur.next; // 暂存后继结点
            cur.next = pre;          // 修改引用指向
            pre = cur;               // 暂存当前结点
            cur = tmp;               // 访问下一结点
        }
        return pre;
    }
}

3 复杂性分析

  • 时间复杂度 O(N): 遍历链表使用线性大小时间。
  • 空间复杂度 O(1): 变量 pre 和 cur 使用常数大小额外空间。

合并两个排序的链表

1 题目描述

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

示例1:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

限制:

0 <= 链表长度 <= 1000

2 解题(Java)

2.1 解题思路

  1. 链表l1和l2是递增的,因此容易想到使用双指针l1和l2遍历两链表,根据 l1.val和 l2.val的大小关系确定节点添加顺序,两节点指针交替前进,直至遍历完毕;
  2. 引入伪头节点: 由于初始状态合并链表中无节点,因此循环第一轮时无法将节点添加到合并链表中。解决方案:初始化一个辅助节点 dummy 作为合并链表的伪头节点,将各节点添加至 dummy 之后;

2.2 算法流程

  1. 初始化:定义伪头结点dummy,定义结点cur指向dummy;
  2. 循环合并:当l1或l2为空时跳出:
    1. 当l1.val < l2.val时,cur的后继结点指定为l1,并且l1向后走一步;
    2. 当l1.val >= l2.val时,cur的后继结点指定为l2,并且l2向后走一步;
    3. cur向后走一步,即cur = cur.next;
  3. 合并剩余尾部:跳出时有两种情况:l1为空或l2为空。
    1. 如果l1==null,将l2添加到结点cur之后;
    2. 否则,将l1添加到结点cur之后;
  4. 返回值:合并链表在伪头结点dummy之后,因此返回dummy.next;

2.3 代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(0), cur = dummyHead;
        while(l1 != null && l2 != null) {
            if(l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
            }
            else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        cur.next = l1 == null ? l2 : l1;
        return dummyHead.next;
    }
}

3 复杂性分析

  • 时间复杂度 O(M+N) :M,N 分别为链表l1、l2的长度,合并操作需遍历两链表;
  • 空间复杂度 O(1): 伪头结点对象以及节点引用 dummyHead, cur 占用常数大小的额外空间;

复杂链表的复制

1 题目描述

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

示例 1:
在这里插入图片描述

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:
在这里插入图片描述

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

示例 3:
在这里插入图片描述

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

示例 4:

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

解释:给定的链表为空(空指针),因此返回 null。

提示:

  • -10000 <= Node.val <= 10000
  • Node.random 为空(null)或指向链表中的节点。
  • 节点数目不超过 1000 。

2 解题(java)

题意理解

本题的意思是复制一个链表并返回,在这里,复制的意思是指 深拷贝(Deep Copy),事实上,与此对应的还有 浅拷贝,它们的区别是:

  1. 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
  2. 但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/
class Solution {
    public Node copyRandomList(Node head) {
        if(head == null) return null;
        Node cur = head;
        Map<Node, Node> map = new HashMap<>();
        // 3. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
        while(cur != null) {
            map.put(cur, new Node(cur.val));
            cur = cur.next;
        }
        cur = head;
        // 4. 构建新链表的 next 和 random 指向
        while(cur != null) {
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
            cur = cur.next;
        }
        // 5. 返回新链表的头节点
        return map.get(head);
    }
}

3 复杂性分析

  • 时间复杂度 O(N) : 两轮遍历链表,使用 O(N)时间。
  • 空间复杂度 O(N) : 哈希表空间。

两个链表的第一个公共节点

1 题目描述

输入两个链表,找出它们的第一个公共节点。

如下面的两个链表:
在这里插入图片描述
在节点 c1 开始相交。

注意:

  • 如果两个链表没有交点,返回 null.
  • 在返回结果后,两个链表仍须保持原有的结构。
  • 可假定整个链表结构中没有循环。
  • 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

2 解题(Java)

双指针法:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode p1 = headA, p2 = headB;
        while (p1 != p2) {
            p1 = p1 != null ? p1.next : headB;
            p2 = p2 != null ? p2.next : headA;
        }
        return p1;
    }
}

3 复杂性分析

  • 时间复杂度O(N):最多遍历两链表长度之和,使用O(N)时间;
  • 空间复杂度O(1):双指针占用常数空间;

删除链表的倒数第N个节点

1 题目描述

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5.

说明:

给定的 n 保证是有效的。

2 解题(Java)

2.1 解题思路

双指针+伪头结点:

  1. 添加一个伪头结点:它的next指针指向链表的头结点,这样可以避免n越界(题目只保证n有效,但如果删除倒数第n个节点,前指针要先走n+1步,可能出现null.next的错误,也即避免了删除头结点时的特殊处理(返回dummy.next而不是返回head));
  2. 初始化: 在head前定义一个伪头结点dummy,前指针former指向头节点 head,后指针latter指向伪头结点dummy;
  3. 构建双指针距离: 前指针former先向前走n步(结束后,双指针 former 和 latter 间相距 n+1 步);
  4. 双指针共同移动: 循环中,双指针former和latter每轮都向前走一步,直至former走过链表尾节点时跳出(跳出后,latter与尾节点距离为 n,即latter指向倒数第n+1个节点);
  5. 删除倒数第n个节点:令latter.next = latter.next.next;
  6. 返回值: 返回dummy.next;

2.2 代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyHead = new ListNode(0, head);
        ListNode former = head, latter = dummyHead;
        for (int i=0; i<n; i++) {
            if (former == null) return null;
            former = former.next;
        }
        while (former != null) {
            former = former.next;
            latter = latter.next;
        }
        latter.next = latter.next.next;
        return dummyHead.next;
    }
}

3 复杂性分析

  • 时间复杂度O(N):遍历链表使用O(N)时间;
  • 空间复杂度O(1):伪头结点以及节点引用 dummy、fomer、latter占用常数大小的额外空间;

圆圈中最后剩下的数字

1 题目描述

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

示例 1:

输入: n = 5, m = 3
输出: 3

示例 2:

输入: n = 10, m = 17
输出: 2

限制:

1 <= n <= 10^5
1 <= m <= 10^6

2 解题

2.1 解法一(模拟链表)

2.1.1 解题思路
  1. 约瑟夫环的思想不多赘述,通过模拟链表每次删除一个节点,直到剩最后一个节点为止;
  2. 之所以采用ArrayList模拟链表而不是LinkedList,直观原因在于使用LinkedList会超时,ArrayList不会;
  3. 剖析原因:1 执行add操作时LinkedList时间复杂度为O(N),ArrayList为O(1),ArrayList占优;2 执行remove操作时LinkedList时间复杂度为O(N),ArrayList为O(N),而ArrayList的remove在后续移位时,是内存连续空间的拷贝,相比于LinkedList大量非连续地址访问,性能还是可以的;
2.1.2 代码
class Solution {
    public int lastRemaining(int n, int m) {
        List<Integer> list = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            list.add(i);
        }
        int index = 0;
        while (n > 1) {
            index = (index + m - 1) % n;
            list.remove(index);
            n--;
        }
        return list.get(0);
    }
}
2.1.3 复杂性分析
  • 时间复杂度O(N ^ 2):每次删除的时间复杂度为O(N),删除N-1次,所以总体时间复杂度为O(N ^ 2);
  • 空间复杂度O(N):模拟链表占用O(N)空间;

2.2 解法二(数学)

2.2.1 解题思路

例如n = 5, m = 3:

在这里插入图片描述

  1. 第一轮是 [0, 1, 2, 3, 4] ,所以是 [0, 1, 2, 3, 4] 这个数组的多个复制。这一轮 2 删除了;

  2. 第二轮开始时,从 3 开始,所以是 [3, 4, 0, 1] 这个数组的多个复制。这一轮 0 删除了;

  3. 第三轮开始时,从 1 开始,所以是 [1, 3, 4] 这个数组的多个复制。这一轮 4 删除了;

  4. 第四轮开始时,还是从 1 开始,所以是 [1, 3] 这个数组的多个复制。这一轮 1 删除了;

  5. 最后剩下的数字是 3;

在这里插入图片描述

从最后剩下的数字倒推,可以反向推出这个数字在之前每个轮次的位置,最后一轮为0,推到第一轮时,显然,这个数字即等于位置:

  1. 最后剩下的 3 的下标是 0;

  2. 第四轮反推,补上 m 个位置,然后模上当时的数组大小 2,位置是(0 + 3) % 2 = 1;

  3. 第三轮反推,补上 m 个位置,然后模上当时的数组大小 3,位置是(1 + 3) % 3 = 1;

  4. 第二轮反推,补上 m 个位置,然后模上当时的数组大小 4,位置是(1 + 3) % 4 = 0;

  5. 第一轮反推,补上 m 个位置,然后模上当时的数组大小 5,位置是(0 + 3) % 5 = 3;

  6. 所以最终剩下的数字的下标就是3。因为数组是从0开始的,所以最终的答案就是3;

总结一下反推的过程,就是 (当前index + m) % 上一轮剩余数字的个数。

2.2.2 代码
class Solution {
    public int lastRemaining(int n, int m) {
        int ans = 0;
        // 最后一轮剩下2个人,所以从2开始反推
        for (int i = 2; i <= n; i++) {
            ans = (ans + m) % i;
        }
        return ans;
    }
}
2.2.3 复杂性分析
  • 时间复杂度O(N):线性遍历2——n;
  • 空间复杂度O(1):变量i占用常数大小的额外空间;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值