前端刷题路-Day87:移动零(题号283)

357 阅读4分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战

移动零(题号283)

题目

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

说明:

  1. 必须在原数组上操作,不能拷贝额外的数组。
  2. 尽量减少操作次数。

链接

leetcode-cn.com/problems/mo…

解释

这题啊,这题是经典简我击。

虽说是简我击,但确实是需要思考的,如果是没有做过双指针的人,可以有这样的思路来一点点实现最优的效果。

首先,肯定是不能进行排序的,因为排序可能会改变其它元素的位置,这种方案可以被否定了。

接下来想到的可能是扫一遍数组,如果遇到0,就用splice将当前元素干掉,在数组末尾再push一次,这样到最后也可以实现题目想要的效果,而且splicepush都是在原数组上进行操作,并没有别的问题。

需要注意的就是需要累计0的次数,这样的结束条件可以避免无必要的操作,减少循环次数。

这种方法当然是可行的,但在性能上依旧有些问题,因为用了splicepush操作,有没有办法可以直接通过数组元素值的替换来实现需求呢?

显然是有的。

是不是可以这样做,首先从右到左遍历数组,累计遇到0的此时,这里就叫count了。

在遍历内部,如果当前的数字为0,那么需要将从当前位置开始,到数组末尾的这一段位置的所有数字位置都左右替换一次,其实也不用到数组末尾,应该是到数组末尾的长度上减去count的长度。

是不是有点不太好理解?举个例子:

[0, 1, 0, 3, 12]

从右往左走,12和3都没有问题,遇到index为2的0时,开始进行左右交换,也就是0和3交换,现在3的位置上是0了,再将其和12进行交换,此时0就在数组的末尾了,count变量也要自增一,以后如果再遇到0就不用处理到数组末尾的0了。

此时处理完成的数据应该是:

[0, 1, 3, 12, 0]

继续遍历,遇到1是没有问题的,遇到index为0时,再开始进行从当前位置开始的数据交换。

这样一直交换到数组的第一个元素,遍历就结束了,答案也就出来了。

这种做法并没有使用数组的特殊API,比方说splicepush,但重复遍历的元素较多,如果数组前面0比较多,需要处理的次数也会较多。

那有没有什么更简单的办法呢?不需要处理这么多重复的数据。

显然还是有的,这就是官方正解的双指针了。

前面两种方法都是根据0来进行处理的,让我们换个思路,处理非0数字。

先搞两个指针,左指针和右指针,都从0开始。

右指针会从左往右一次遍历,如果遇到非0数字,将这个数字和左指针的位置进行互换,互换之后,左指针自增一,右指针是每次遍历都会自增一的,所以会一直增加,而左指针只有在数字是非0的情况下才会自增一。

这样操作的结果会将所有非0的数字依据左指针自增的位置依次存放,不对0进行处理,再举个例子:

[0, 1, 0, 3, 12]

下面一步步操作:

左指针右指针数组
00[0, 1, 0, 3, 12]
11[1, 0, 0, 3, 12]
12[1, 0, 0, 3, 12]
23[1, 3, 0, 0, 12]
34[1, 3, 12, 0, 0]

这样值等右指针走到头的时候,非0数字都跑到了数组左侧,0也就自然的跑到右边了。

自己的答案(splice+push

最容易想到的方法👇:

var moveZeroes = function(nums) {
  let left = 0
  let end = nums.length
  while (left < end) {
    if (!nums[left]) {
      nums.splice(left, 1)
      nums.push(0)
      end--
    } else {
      left++
    }
  }
  return nums
};

利用end来统计0出现的次数,这样可以避免无效的遍历。

自己的答案(双while

var moveZeroes = function(nums) {
  const len = nums.length
  let right = len - 1
  let count = 0
  while (right > -1) {
    while (nums[right] === 0) {
      for (let i = right; i < len - count - 1; i++) {
        [nums[i], nums[i + 1]] = [nums[i + 1], nums[i]]        
      }
      count++
      right--
    }
    right--
  }
  return nums
};

这就是解释种说到的第二种方法,可以看到内部while种进行了数组的批量替换操作,虽然逻辑很正,但是做了很多无效操作,尤其是在数组头部出现0的情况下,性能损耗较大。

自己的答案(双指针)

var moveZeroes = function(nums) {
  const len = nums.length
  let left = 0, right = 0
  while (right < len) {
    if (nums[right]) {
      [nums[left], nums[right]] = [nums[right], nums[left]]
      left++
    }
    right++
  }
  return nums
};

双指针的做法大量减少了没必要的替换次数,而且这里也不一定用while,用for循环也是一样的,为了更好的理解双指针嘛。



PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)