如何去除有序数组的重复元素

咱们知道对于数组来讲,在尾部插入、删除元素是比较高效的,时间复杂度是 O(1),可是若是在中间或者开头插入、删除元素,就会涉及数据的搬移,时间复杂度为 O(N),效率较低。git

因此上篇文章 O(1)时间删除/查找数组中的任意元素 就讲了一种技巧,把待删除元素交换到最后一个,而后再删除,就能够避免数据搬移。算法

PS:我认真写了 100 多篇原创,手把手刷 200 道力扣题目,所有发布在 labuladong的算法小抄,持续更新。建议收藏,按照个人文章顺序刷题,掌握各类算法套路后投再入题海就如鱼得水了。数组

有序数组/链表去重

先讲讲如何对一个有序数组去重,先看下题目:app

1cbe3f0948d6824aa7dafcd1f1380b7a.jpg

函数签名以下:ide

int removeDuplicates(int[] nums);

显然,因为数组已经排序,因此重复的元素必定连在一块儿,找出它们并不难,但若是毎找到一个重复元素就当即删除它,就是在数组中间进行删除操做,整个时间复杂度是会达到 O(N^2)。函数

PS:我认真写了 100 多篇原创,手把手刷 200 道力扣题目,所有发布在 labuladong的算法小抄,持续更新。建议收藏,按照个人文章顺序刷题,掌握各类算法套路后投再入题海就如鱼得水了。spa

简单解释一下什么是原地修改:指针

若是不是原地修改的话,咱们直接 new 一个 int[] 数组,把去重以后的元素放进这个新数组中,而后返回这个新数组便可。code

可是原地删除,不容许咱们 new 新数组,只能在原数组上操做,而后返回一个长度,这样就能够经过返回的长度和原始数组获得咱们去重后的元素有哪些了。orm

这种需求在数组相关的算法题中时很是常见的,通用解法就是咱们前文 双指针技巧 中的快慢指针技巧

咱们让慢指针 slow 走在后面,快指针 fast 走在前面探路,找到一个不重复的元素就告诉 slow 并让 slow 前进一步。这样当 fast 指针遍历完整个数组 nums 后,nums[0..slow] 就是不重复元素

int removeDuplicates(int[] nums) {
    if (nums.length == 0) {
        return 0;
    }
    int slow = 0, fast = 0;
    while (fast < nums.length) {
        if (nums[fast] != nums[slow]) {
            slow++;
            // 维护 nums[0..slow] 无重复
            nums[slow] = nums[fast];
        }
        fast++;
    }
    // 数组长度为索引 + 1
    return slow + 1;
}

看下算法执行的过程:

04f34a959ab6f013c90028c8d11241dd.gif

再简单扩展一下,若是给你一个有序链表,如何去重呢?这是力扣第 83 题,其实和数组去重是如出一辙的,惟一的区别是把数组赋值操做变成操做指针而已:

ListNode deleteDuplicates(ListNode head) {
    if (head == null) return null;
    ListNode slow = head, fast = head;
    while (fast != null) {
        if (fast.val != slow.val) {
            // nums[slow] = nums[fast];
            slow.next = fast;
            // slow++;
            slow = slow.next;
        }
        // fast++
        fast = fast.next;
    }
    // 断开与后面重复元素的链接
    slow.next = null;
    return head;
}

3bcd836dd59a37b1fe4dfb97325b5820.gif

8306e7ebbef26302521befe437ec97a2.jpg

函数签名以下:

int removeElement(int[] nums, int val);

题目要求咱们把 nums 中全部值为 val 的元素原地删除,依然须要使用 双指针技巧 中的快慢指针:

若是 fast 遇到须要去除的元素,则直接跳过,不然就告诉 slow 指针,并让 slow 前进一步。

这和前面说到的数组去重问题解法思路是彻底同样的,就不画 GIF 了,直接看代码:

int removeElement(int[] nums, int val) {
    int fast = 0, slow = 0;
    while (fast < nums.length) {
        if (nums[fast] != val) {
            nums[slow] = nums[fast];
            slow++;
        }
        fast++;
    }
    return slow;
}

注意这里和有序数组去重的解法有一个重要不一样,咱们这里是先给 nums[slow] 赋值而后再给 slow++,这样能够保证 nums[0..slow-1] 是不包含值为 val 的元素的,最后的结果数组长度就是 slow

PS:我认真写了 100 多篇原创,手把手刷 200 道力扣题目,所有发布在 labuladong的算法小抄,持续更新。建议收藏,按照个人文章顺序刷题,掌握各类算法套路后投再入题海就如鱼得水了。

移动零

这是力扣第 283 题,我来描述下题目:

给你输入一个数组 nums,请你原地修改,将数组中的全部值为 0 的元素移到数组末尾,函数签名以下:

void moveZeroes(int[] nums);

好比说给你输入 nums = [0,1,4,0,2],你的算法没有返回值,可是会把 nums 数组原地修改为 [1,4,2,0,0]

结合以前说到的几个题目,你是否有已经有了答案呢?

题目让咱们将全部 0 移到最后,其实就至关于移除 nums 中的全部 0,而后再把后面的元素都赋值为 0 便可。

因此咱们能够复用上一题的 removeElement 函数:

void moveZeroes(int[] nums) {
    // 去除 nums 中的全部 0
    // 返回去除 0 以后的数组长度
    int p = removeElement(nums, 0);
    // 将 p 以后的全部元素赋值为 0
    for (; p < nums.length; p++) {
        nums[p] = 0;
    }
}

// 见上文代码实现
int removeElement(int[] nums, int val);

至此,四道「原地修改」的算法问题就讲完了,其实核心仍是快慢指针技巧,你学会了吗?

相关文章
相关标签/搜索