如何高效对有序数组/链表去重?
对于数组来说,在尾部插入、删除元素是比较高效的,时间复杂度是 O(1)
,但是如果在中间或者开头插入、删除元素,就会涉及数据的搬移,时间复杂度为 O(N)
,效率较低。
所以对于一般处理数组的算法问题,我们要尽可能只对数组尾部的元素进行操作,以避免额外的时间复杂度。
26. 删除有序数组中的重复项
这道题由于数组已经排序,所以重复的元素一定连在一起,找出它们并不难,但如果毎找到一个重复元素就立即删除它,就是在数组中间进行删除操作,整个时间复杂度是会达到 O(N^2)
。而且题目要求我们 原地修改
,也就是说不能用辅助数组,空间复杂度得是 O(1)
。
对于数组相关的算法问题,有一个通用的技巧:要尽量避免在中间删除元素,那我就先想办法把这个元素换到最后去
。这样的话,最终待删除的元素都拖在数组尾部,一个一个 pop 掉就行了
,每次操作的时间复杂度也就降低到 O(1) 了。
双指针法:让慢指针slow走左后面,快指针fast走在前面探路,找到一个不重复的元素就告诉slow并让slow前进一步。这样当fast指针遍历完整个数组nums后,
nums[0..slow]就是不重复元素
,之后的所有元素都是重复元素
。
public int removeDuplicates(int[] nums) {
if (nums.length == 0) {
return 0;
}
int slow = 0,fast = 1;
while (fast < nums.length) {
if (nums[fast] != nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
fast++;
}
return slow + 1;
}
83. 删除排序链表中的重复元素
其实和数组是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已:
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head,fast = head.next;
while (fast != null) {
if (fast.val != slow.val) {
slow.next = fast;
slow = slow.next;
}
fast = fast.next;
}
// 断开与后面的连接
slow.next = null;
return head;
}
其它相关题目
题目链接:80. 删除有序数组中的重复项 II
public int removeDuplicates2(int[] nums) {
if (nums.length == 0) {
return 0;
}
int slow = 0,fast = 1;
int count = 1;
while (fast < nums.length) {
if (nums[slow] == nums[fast] && count < 2) {
count++;
slow++;
nums[slow] = nums[fast];
} else if (nums[slow] != nums[fast]) {
count = 1;
slow++;
nums[slow] = nums[fast];
}
fast++;
}
return slow + 1;
}
题目链接:27. 移除元素
public int removeElement(int[] nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
去重进阶
316. 去除重复字母
题目分析
题⽬的要求总结出来有三点:
- 要去重。
- 去重字符串中的字符顺序不能打乱 s 中字符出现的相对顺序。
- 在所有符合上⼀条要求的去重字符串中,字典序最⼩的作为最终结果。
⽐如说输⼊字符串 s = “babc”,去重且符合相对位置的字符串有两个,分别是 “bac” 和 “abc”,但是我们的算法得返回 “abc”,因为它的字典序更⼩。
思路分析
我们先暂时忽略要求三,用「栈」来实现一下要求一和要求二
public String removeDuplicateLetters(String s) {
Stack<Character> stack = new Stack<>();
boolean[] isUsed = new boolean[26];
for (char c : s.toCharArray()) {
if (isUsed[c - 'a']) {
continue;
}
stack.push(c);
isUsed[c - 'a'] = true;
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
return sb.reverse().toString();
}
用布尔数组inStack记录栈中元素,达到去重的目的,此时栈中的元素都是没有重复的
。
如何实现要求三呢?
如果输入 s = "bcabc"
,上面的算法会返回 "bca"
,已经符合要求一和要求二了,但是题目希望要的答案是 "abc"
。
如果想满足要求三,保证字典序,需要做些什么修改?在向栈stk中插入字符’a’的这一刻,我们的算法需要知道,字符’a’的字典序和之前的两个字符’b’和’c’相比,谁大谁小?
那么我们可以继续尝试:如果当前字符 'a'
比之前的字符字典序小,就把前面的字符 pop 出栈
,让 'a'
排在前面。
public String removeDuplicateLetters(String s) {
Stack<Character> stack = new Stack<>();
boolean[] isUsed = new boolean[26];
for (char c : s.toCharArray()) {
if (isUsed[c - 'a']) {
continue;
}
while (!stack.isEmpty() && stack.peek() > c) {
isUsed[stack.pop() - 'a'] = false;
}
stack.push(c);
isUsed[c - 'a'] = true;
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
return sb.reverse().toString();
}
这段代码就是插入了一个 while 循环,连续 pop 出比当前字符小的栈顶字符,直到栈顶元素比当前元素的字典序还小为止,有点类似于单调栈。
上面的代码还是有点小问题
对于输入 s = "bcabc"
,我们可以得出正确结果 "abc"
了。但是,如果我改一下输入,假设 s = "bcac"
,按照刚才的算法逻辑,返回的结果是 "ac"
,而正确答案应该是 "bac"
。
很容易发现,因为s中只有唯一一个’b’,即便字符’a’的字典序比字符’b’要小,字符’b’也不应该被 pop 出去。
所以在stk.peek() > c时才会 pop 元素,其实这时候应该分两种情况:
- 如果
stk.peek()
这个字符之后还会出现,那么可以把它 pop 出去,反正后面还有嘛,后面再 push 到栈里,刚好符合字典序的要求。 - 如果
stk.peek()
这个字符之后不会出现了,前面也说了栈中不会存在重复的元素,那么就不能把它 pop 出去,否则你就永远失去了这个字符。
、
回到 s = "bcac"
的例子,插入字符 'a'
的时候,发现前面的字符 'c'
的字典序比 'a'
大,且在’a’之后还存在字符’c’,那么栈顶的这个’c’就会被 pop 掉。
while 循环继续判断,发现前面的字符 'b'
的字典序还是比 'a'
大,但是在 'a'
之后再没有字符 'b'
了,所以不应该把 'b'
pop() 出去。
那么关键就在于,如何让算法知道字符’a’之后有几个’b’有几个’c’呢?统计一下就可以了
public String removeDuplicateLetters(String s) {
Stack<Character> stack = new Stack<>();
int[] count = new int[26];
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
boolean[] isUsed = new boolean[26];
for (char c : s.toCharArray()) {
count[c - 'a']--;
if (isUsed[c - 'a']) continue;
while (!stack.isEmpty() && stack.peek() > c) {
if (count[stack.peek() - 'a'] == 0) {
break;
}
isUsed[stack.pop() - 'a'] = false;
}
stack.push(c);
isUsed[c - 'a'] = true;
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
return sb.reverse().toString();
}
最终代码
public String removeDuplicateLetters(String s) {
Stack<Character> stack = new Stack<>();
int[] count = new int[26];
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
boolean[] isUsed = new boolean[26];
for (char c : s.toCharArray()) {
count[c - 'a']--;
if (isUsed[c - 'a']) continue;
while (!stack.isEmpty() && stack.peek() > c) {
if (count[stack.peek() - 'a'] == 0) {
break;
}
isUsed[stack.pop() - 'a'] = false;
}
stack.push(c);
isUsed[c - 'a'] = true;
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
return sb.reverse().toString();
}
1081. 不同字符的最小子序列
该题与 316. 去除重复字母 相同