【每日力扣1】删除排序数组中的重复项

前言

开启CSDN博主之旅的第一篇~

今日开始在LeetCode学习算法,计划每天学习一题,并且有时间则在CSDN中分享当日所学算法,一者可以通过“知识输出”的方式大幅加深对所学知识的印象,二来也可以在这里记录小白算法学习之旅的点点滴滴(日后可以拿来考古哈哈哈),第三也能和大家一起分享学习心得(本人萌新哈哈哈,算法上现在还是纯纯的小白,希望大家多多指教)~

一、题目

给你一个有序数组 nums ,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组 并在使用 O(1) 额外空间的条件下完成。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

示例 1:

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

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

解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

示例 2:

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

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

解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

提示:

  • 0 <= nums.length <= 3 * 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 已按升序排列

二、思路

题目关键为“去重”,笔者联想到邓俊辉在清华大学版《数据结构》介绍向量Vector和列表List数据结构中讲解过其中的去重方法Deduplicate()。其中对于无序向量(列表)和有序向量(列表)的去重分开作了介绍。

对于无序向量,下面是给出的去重方法

template <typename T> int Vector<T>::deduplicate() { //删除无序向量中重复元素(高效版)
   int oldSize = _size; //记录原规模
   Rank i = 1; //从_elem[1]开始
   while (i < _size) //自前向后逐一考查各元素_elem[i]
      if (find(_elem[i], 0, i) < 0) //在前缀[0,i)中寻找与之雷同者(至多一个)
         i++; //若无雷同则继续考查其后继
      else
         remove(i); //否则删除当前元素
   return oldSize - _size; //被删除元素总数
}

因为是无序的,只能对于每个元素_elem[i],逐一向前检查前面所有元素是否相同。若相同则需删除,这里remove()方法时间复杂度为O(N),整个方法的时间复杂度为O(N²)。

对于有序向量,现在各个元素排好序了,邓公首先给出了第一种方法

template <typename T> int Vector<T>::uniquify() { //有序向量重复元素剔除算法(低效版)
   int oldSize = _size; int i = 1; //当前比对元素的秩,起始于首元素
   while ( i < _size ) //从前向后,逐一比对各对相邻元素
      _elem[i - 1] == _elem[i] ? remove ( i ) : i++; //若雷同,则删除后者;否则,转至后一元素
   return oldSize - _size; //向量规模变化量,即被删除元素总数
}

这种方法就是利用了有序的性质,逐一将每个元素与前一元素比对,相同则删,不同则继续往下。但是时间复杂度相比于无序时并没有改进,仍为O(N²)。

接下来便给出了第二种高效方法,时间复杂度改进为O(N)。

template <typename T> int Vector<T>::uniquify() { //有序向量重复元素剔除算法(高效版)
   Rank i = 0, j = 0; //各对互异“相邻”元素的秩
   while ( ++j < _size ) //逐一扫描,直至末元素
      if ( _elem[i] != _elem[j] ) //跳过雷同者
         _elem[++i] = _elem[j]; //发现不同元素时,向前移至紧邻于前者右侧
   _size = ++i; shrink(); //直接截除尾部多余元素
   return j - i; //向量规模变化量,即被删除元素总数
}

这里用了双索引i和j,巧妙避开了remove()方法的调用(它会带来O(N)的时间复杂度)。

对于这道题目,由于本身已是排好序的数组,于是笔者用邓公介绍的有序数组的第一种去重方法的思想来求解该题。

三、我的方案

class Solution
 {
     public:
     int removeDuplicates(vector<int>& nums)
     {
        int n = nums.size();
        if(n == 0) return 0; 
        for(int i = 1; i<n;i++)
        {
            if(nums[i] == nums[i-1])
             {
                 for(int j = i;j<n-1;j++) nums[j] = nums[j+1];
                 n--; i--; 
             }
        } 
        return n;
    }
};

虽然通过测试用例,但是基于前文复杂度的分析,该解法时间复杂度为O(N²),超出时间限制。

官方解法(如下所示)则类似于邓公介绍的有序向量去重的第二种方法。

四、官方解法

双指针法

这道题目的要求是:对给定的有序数组 nums 删除重复元素,在删除重复元素之后,每个元素只出现一次,并返回新的长度,上述操作必须通过原地修改数组的方法,使用O(1) 的空间复杂度完成。

由于给定的数组 nums 是有序的,因此对于任意 i<j,如果nums[i]=nums[j],则对任意 i≤k≤j,必有 nums[i]=nums[k]=nums[j],即相等的元素在数组中的下标一定是连续的。利用数组有序的特点,可以通过双指针的方法删除重复元素。

如果数组 nums 的长度为 0,则数组不包含任何元素,因此返回 0。

当数组nums 的长度大于 0 时,数组中至少包含一个元素,在删除重复元素之后也至少剩下一个元素,因此 nums[0] 保持原状即可,从下标 1 开始删除重复元素。

定义两个指针fast 和slow 分别为快指针和慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置,初始时两个指针都指向下标 1。

假设数组 nums 的长度为 n。将快指针 fast 依次遍历从 1到 n−1 的每个位置,对于每个位置,如果nums[fast]≠nums[fast−1],说明 nums[fast] 和之前的元素都不同,因此将nums[fast] 的值复制到 nums[slow],然后将slow 的值加 1,即指向下一个位置。

遍历结束之后,从nums[0] 到nums[slow−1] 的每个元素都不相同且包含原数组中的每个不同的元素,因此新的长度即为 slow,返回 slow 即可。

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int n = nums.size();
        if (n == 0) {
            return 0;
        }
        int fast = 1, slow = 1;
        while (fast < n) {
            if (nums[fast] != nums[fast - 1]) {
                nums[slow] = nums[fast];
                ++slow;
            }
            ++fast;
        }
        return slow;
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/solution/shan-chu-pai-xu-shu-zu-zhong-de-zhong-fu-tudo/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组的长度。快指针和慢指针最多各移动 n 次。
  • 空间复杂度:O(1)。只需要使用常数的额外空间。

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/solution/shan-chu-pai-xu-shu-zu-zhong-de-zhong-fu-tudo/
来源:力扣(LeetCode)

五、学习心得

双指针法:使用两个索引i和j访问数组,可以避免耗费时间的remove()等方法的调用,从而降低时间损耗。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值