Leetcode 第 82. Remove Duplicates from Sorted List II 题,题目难度 Medium。
一. 题目要求
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中没有重复出现的数字。
示例 1:
输入: 1->2->3->3->4->4->5
输出: 1->2->5
示例 2:
输入: 1->1->1->2->3
输出: 2->3
二. 解题思路 & 代码实现
和第 83.删除排序链表中的重复元素 题相比有了改进,83 题要求对于重复的元素只保留 1 个,本题要求全部去掉。
解法 1:遍历链表 & 构建结果集
为了满足要求我们需要知道哪些节点是重复的,哪些不是重复的,然后选择出不重复的节点来构建返回值。因此可以遍历链表,将重复的元素和非重复元素过滤到两个集合中,然后利用非重复元素集合构建结果链表。
实现代码如下:
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
// 无重复元素集合
ArrayList<Integer> noDuplicatesVals = new ArrayList<>();
noDuplicatesVals.add(head.val);
// 重复元素集合
ArrayList<Integer> duplicatesVals = new ArrayList<>();
ListNode next = head.next;
while (next != null) {
// 遍历链表,发现重复则将元素从无重复元素集合中移除
if(duplicatesVals.contains(next.val)){
next = next.next;
continue;
}
if (noDuplicatesVals.contains(next.val)) {
int index = noDuplicatesVals.indexOf(next.val);
noDuplicatesVals.remove(index);
duplicatesVals.add(next.val);
continue;
}else {
if (!duplicatesVals.contains(next.val)) {
noDuplicatesVals.add(next.val);
}
}
next = next.next;
}
int len = noDuplicatesVals.size();
if (len == 0) {
return null;
}
// 利用无重复元素集合构建结果链表
ListNode result = new ListNode(noDuplicatesVals.get(0));
ListNode tailNode = result;
for (int i = 1; i < len; i ++) {
ListNode node = new ListNode(noDuplicatesVals.get(i));
tailNode.next = node;
tailNode = node;
}
return result;
}
}
上面解法的运行结果为 Runtime: 6 ms, faster than 5.20%,性能不算好,究其原因是因为在遍历过程中需要频繁的对两个集合进行判断和增删操作,接下来对解法一进行优化。
解法2:遍历链表 & 修改判断逻辑
解法 1 中最主要的耗时操作是需要对 noDuplicatesVals 集合做增删操作,这一步的原因是因为我们判断重复元素的依据是:
- 构建集合,判断集合中是否已经存在元素了。
但判断元素是否重复还可以采用另外一个更为直接的逻辑:
- 每个节点的值与其前后两个节点的值不同。
因此我们可以通过判断当前节点 node 与其前后节点 pre、next 的值进行比较,从而省去了利用集合相关的操作。
实现代码如下:
public class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 确定 head 值是否重复,不重复的话加到结果集
ArrayList<Integer> vals = new ArrayList<>();
if (head.next != null) {
if (head.val != head.next.val) {
vals.add(head.val);
}
}
// 遍历链表,获取不重复的元素
int preVal = head.val;
ListNode next = head.next;
while (next != null) {
if (next.val != preVal) { // 不与前一个节点值相同
// 不与 next 节点值相同
if (next.next == null || next.next.val != next.val) {
vals.add(next.val);
}
}
preVal = next.val;
next = next.next;
}
// 根据不重复的结果集构建返回值
return this.buildListNodeByList(vals);
}
public ListNode buildListNodeByList(List<Integer> list) {
if (list.size() == 0) {
return null;
}
ListNode result = new ListNode(list.get(0));
ListNode tailNode = result;
int len = list.size();
for (int i = 1; i < len; i ++) {
ListNode node = new ListNode(list.get(i));
tailNode.next = node;
tailNode = node;
}
return result;
}
}
实地运行结果是 Runtime: 1 ms, faster than 27.30%,较解法 1 已经有了比较大的提升了,但仍有优化空间。
解法 3: 遍历链表 & 就地变换
上面两种解法返回结果的思路都是 判断元素是否重复,筛选出所有的重复元素后构建返回值,不同的只是在判断重复这件事上有所不同。
但其实如果只要了哪些节点不是重复的,直接操作节点的指针就行了,不需要额外的集合操作,这样性能可以得到进一步优化。
具体过程是:
- 找出第 1 个不重复的节点,以该节点作为返回值
- 从第 1 个不重复的节点开始遍历后续节点
- 依据是否重复的判断逻辑,将当前最后 1 个不重复节点的 next 指针指向当前的不重复节点,直到遍历结束
遍历过程如图所示
实现代码如下:
public class Solution {
public static ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 1. 找出第 1 个不重复的节点
ListNode pre=null;
ListNode next = head.next;
while(head != null) {
if ((next == null || head.val != next.val) && (pre == null || pre.val != head.val)) {
break;
}else {
pre = head;
head = head.next;
if(next != null) {
next = next.next;
}
}
}
if (head == null ) {
return null;
}
// 2. 遍历后续链表
// node 表示当前结果最后一个不重复节点
ListNode node = head;
next = node.next;
pre = node;
while (next != null) {
if(pre.val != next.val && (next.next == null || next.next.val != next.val)) {
// 发现新的不重复节点,将 node 的 next 指向该节点,并更新 node
node.next = next;
node = node.next;
}
pre = next;
next = next.next;
}
node.next = null;
return head;
}
}
实际运行结果为Runtime: 0 ms, faster than 100%,说明第三版的代码实现已经非常不错了。
三. 解题后记
严格来说题目不算难,但是第三种就地变换的解法需要搞清楚整个过程中各个节点指针的变化过程,还是挺容易搞乱的,这里主要通过两件事搞定:
- 1.明确节点的判重逻辑
- 2.画图,直观的搞懂节点的变化过程
又搞定一道题目,离干翻 LeetCode 还差 1000 多道o((⊙﹏⊙))o。
我是 AhriJ邹同学,前后端、小程序、DevOps 都搞的炸栈工程师。博客持续更新,欢迎小伙伴关注或与我私信交流,互相学习,共同进步。