首页 简历|笔试面试

82.删除排序链表中的重复元素II

  • 25年9月4日 发布
  • 183.19KB 共7页
82.删除排序链表中的重复元素II82.删除排序链表中的重复元素II82.删除排序链表中的重复元素II82.删除排序链表中的重复元素II82.删除排序链表中的重复元素II

82. 删除排序链表中的重复元素 II

题意

给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的

数字 。返回 已排序的链表 。

难度

中等

示例

示例 1:

082.%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4

%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0II-20230928

104041.png

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

输出:[1,2,5]

示例 2:

082.%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4

%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0II-20230928

104052.png

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

输出:[2,3]

分析 1

注意:本题并不是把重复的节点删掉只剩一个,而是所有重复的节点全部删掉。

首先我们能想到的就是,利用一个 HashMap 来记录使用过的元素个数,出现一次增加

1,然后再重新遍历整个链表,留下个数为 1 的元素,其余的全都删除。

或者换个方法,将个数为 1 的加入到新的链表,最后再把新的链表作为结果返回。

OK,我们就来按照这样的思路试一次,链表已经定义好了,见下面注释中的代码,

LeetCode 只需要们写出题解就行了。

/**

* 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 deleteDuplicates(ListNode head) {

// 1. 若链表为空,则直接返回 null

if (head == null) {

return null;

}

// 2. 创建一个映射来存储链表中的每个值及其出现的次数

HashMap<Integer,Integer> map = new HashMap<>();

ListNode cur = head;

// 3. 遍历链表,统计每个元素出现的次数

while(cur != null) {

map.put(cur.val,map.getOrDefault(cur.val,0) + 1);

cur = cur.next;

}

// 4. 创建一个新的伪头节点以帮助后续链表的构建

ListNode grandFather = new ListNode(0,null);

ListNode temp = grandFather; // 用于最后返回结果的伪头节点的引用

cur = head; // 将 cur 重新指向原始链表的头部

// 5. 遍历原始链表,查看映射中的值,将出现一次的元素添加到新链表

while(cur != null) {

if (map.get(cur.val) == 1) {

grandFather.next = new ListNode(cur.val,null);

grandFather = grandFather.next; // 移动 grandFather 到新链表的尾部

}

cur = cur.next; // 移动原始链表的指针

}

// 6. 返回新链表的头部(跳过伪头节点)

return temp.next;

}

}

我们来模拟一下整个过程:

初始链表为: 1 -> 1 -> 2 -> 3 -> 3 -> 4

经过第一轮的 HashMap 处理后,我们知道数字 1 和 3 是重复的,所以只有 2 和 4 会被添

加到新链表中。

下面是 grandFather 在新链表构建过程中的状态变化图解:

1. 初始化:

grandFather(dummy node)

[0]

这是一个伪头节点,用于方便地开始构建新的链表。

2. 当处理到值为 2 的节点时:

grandFather(dummy node) New Node

↓ ↓

[0] -> [ 2 ]

我们创建了一个新节点并将其添加到 grandFather 的后面。

3. 更新**grandFather**的位置:

(dummy node) grandFather

↓ ↓

[0] -> [ 2 ]

现在 grandFather 已经移到了新链表的末尾。

4. 当处理到值为 4 的节点时:

(dummy node) grandFather New Node

↓ ↓ ↓

[0] -> [ 2 ] -> [ 4 ]

我们再次创建一个新节点并将其添加到 grandFather 的后面。

5. 更新**grandFather**的位置:

(dummy node) (old grandFather) grandFather

↓ ↓ ↓

[0] -> [ 2 ] -> [ 4 ]

再次更新 grandFather 的位置,使其始终指向新链表的尾部。

最终,新链表为: 2 -> 4

运行效率如下所示:

082.%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4

%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0II-20230928

105128.png

看情况不太乐观,因为我们需要遍历两次链表,而且空间占用也不理想,需要用到一个新

的链表和 HashMap。怎么办呢?

注意题目的特性——已排序的链表,也就是说,如果有重复的元素,必然是连着一起出现

的,所以我们就可以利用链表的特性,直接跳过这一片节点。

这一次,我们利用一个 grandFather 作为原先头节点的父节点,就从这个 grandFather

(cur)开始遍历,因为原先的头节点如果是重复元素,也是要被删除的。

接着遍历整个链表,如果 cur.next.val==cur.next.next.val,就说明接下来的一片节点

都是相同的元素,我们只需要记录当前的 val,然后把 cur.next 替换成 cur.next.next,

就可以完成对 cur.next 的无缝删除,也就不需要用到新的存储空间和遍历时间了。

/**

* 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 deleteDuplicates(ListNode head) {

// 判断头节点是否为空,如果为空直接返回 null。

if (head == null) {

return null;

}

// 初始化 grandFather 和 cur 为同一个虚拟节点,该虚拟节点的 next 指向 head

ListNode grandFather = new ListNode(0, head);

ListNode cur = grandFather;

// 循环遍历链表,直至链表结束或者是链表的最后一个节点

while (cur.next != null && cur.next.next != null) {

// 判断当前节点与其后续节点的值是否相等,如果相等,说明有重复

if (cur.next.val == cur.next.next.val) {

int val = cur.next.val;

// 移动 cur 的下一个指针,直至找到与 val 不同的值的节点,或者到链表末尾

while (cur.next != null && cur.next.val == val) {

cur.next = cur.next.next;

}

} else {

// 如果当前节点与其后续节点的值不相等,则直接移动 cur 指针

cur = cur.next;

}

}

// 返回删除重复之后的链表头部,由于我们初始化 grandFather 的 next 为 head,所以

这里返回 grandFather.next

return grandFather.next;

}

}

再以一个例子来进行说明:

给定链表: 1 -> 2 -> 2 -> 3 -> 4 -> 4 -> 5

• 初始化 grandFather 和 cur 指向虚拟节点。

• 当遍历到第一个 2 时,检测到它与下一个 2 是重复的,因此进入内部的 while 循环,

持续移动 cur.next,直到它指向了 3。

• 当遍历到第一个 4 时,同样检测到重复,将 cur.next 移动,直至指向 5。

• 最后,返回 grandFather.next,即结果链表:1 -> 3 -> 5。

来看这一次的题解效率,果然比上一次的好多了:

082.%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4

%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0II-20230928

110711.png

分析 2

我们还可以使用递归来解决这个问题。递归的主要思想是:

• 如果链表为空或只有一个元素,直接返回链表。

• 如果当前元素与下一个元素相同,跳过所有重复的元素。

• 如果当前元素与下一个元素不同,递归地处理后续的链表,并将当前元素与处理后的

链表连接起来。

public class Solution {

public ListNode deleteDuplicates(ListNode head) {

// 如果链表为空或只有一个元素,直接返回

if (head == null || head.next == null) {

return head;

}

// 如果当前元素与下一个元素相同

if (head.val == head.next.val) {

// 跳过所有重复的元素

while (head.next != null && head.val == head.next.val) {

head = head.next;

}

// 递归处理后续的链表

return deleteDuplicates(head.next);

} else {

// 如果当前元素与下一个元素不同

// 递归地处理后续的链表,并连接当前元素

head.next = deleteDuplicates(head.next);

return head;

}

}

}

运行效率如下所示:

082.%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4

%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0II-20230928

111408.png

总结

链表是最基础的数据结构之一,也是算法题的重中之重,抓住题目的细节(排序的链

表),才能突破瓶颈。

力扣链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/

更新: 2023-09-28 03:16:51

原文: https://www.yuque.com/itwanger/czfoq9/qvtgt6

开通会员 本次下载免费

所有资料全部免费下载! 推荐用户付费下载获取返佣积分! 积分可以兑换商品!
一键复制 下载文档 联系客服