前端刷题路-Day74:颜色分类(题号75)

691 阅读3分钟

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

颜色分类(题号75)

题目

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 012 分别表示红色、白色和蓝色。

示例 1:

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

示例 2:

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

示例 3:

输入:nums = [0]
输出:[0]

示例 4:

输入:nums = [1]
输出:[1]

提示:

  • n == nums.length
  • 1 <= n <= 300
  • nums[i]012

进阶:

  • 你可以不使用代码库中的排序函数来解决这道题吗?
  • 你能想出一个仅使用常数空间的一趟扫描算法吗?

链接

leetcode-cn.com/problems/so…

解释

这题啊,这题是经典排序。

题目翻译一下,就是一个由012组成的乱序数组,给它原地升序排列就完事了,整啥红白蓝。

排序的话很简单,用很多种排序方法,经典的就是sort方法了,但是注意这里的进阶提示,要求不用默认的sort,并且要求空间复杂度为O(1),时间复杂度为O(n)

由于这题的解决方案由很多种,此处就不展开分析了,详细的分析放在下面具体的答案中。

自己的答案(sort

这就很简单了👇:

function sortColors(nums) {
  return nums.sort((a, b) => a - b)
}

没啥可说的,原地排序就完事了,根据题意是原地修改,此处其实不用return,但为了打印结果方便,还是return了,问题不大。

自己的答案(Map

根据题意,整个数组只有012三种类型的值,那是不是可以新建一个Map对象,来存储数组中每种类型出现的次数,之后根据Map去修改数组。

这种方案显然是可行的,因为笔者第一时间就想到了👇:

function sortColors(nums) {
  const map = new Map()
  for (const v of nums) {
    if (!map.has(v)) map.set(v, 0)
    map.set(v, map.get(v) + 1)
  }
  const valArr = [...map].sort((a, b) => a[0] - b[0])
  let i = 0
  for (const item of valArr) {
    let count = item[1]
    const color = item[0]
    while (count > 0) {
      nums[i] = color
      i++
      count--
    }
  }
  return nums
}

在👆的代码中,首先扫了一遍源数组,拿到每个数字出现的频率,并且存在Map中。

之后对Map的值进行排序,因为有可能是乱序存储的,而这里我们需要升序排列,所以得对结果进行排序。

举个栗子,如果数组是酱婶的:[2,0,2,1,1,0],那么valArr就是:[[0, 2], [1, 2], [2, 2]]

这也就代表最后的数组应该是什么样子的。

接下来使用i作为替换的位置,不断更新当前位置,在第二个for循环内部,对每个数字进行while循环,然后就完事了。

如此操作完之后nums就是排序之后的结果了。

更好的方法(单指针)

这就是官方的解答了,利用一个指针,循环两次。

指针从数组的第一位开始,先循环一遍,遇到0就把当前位置的数字和指针所在的位置交换,如此循环一次之后,数组中的0就会全部被移动到数组左侧。

接下来开始第二次循环,从指针所在的位置开始,遇到1就把当前位置的数字和指针所在的位置进行交换,如果走完之后,数组中的1就会出现在0后面。

这样扫两次之后数组就完成排序了,代码👇:

function sortColors(nums) {
  const len = nums.length
  let ptr = 0
  for (let i = 0; i < len; i++) {
    if (nums[i] === 0) {
      [nums[i], nums[ptr]] = [nums[ptr], nums[i]]
      ptr++
    }    
  }
  for (let i = ptr; i < len; i++) {
    if (nums[i] === 1) {
      [nums[i], nums[ptr]] = [nums[ptr], nums[i]]
      ptr++
    }    
  }
  return nums
}

更好的方法(双指针(0、1))

在单指针方法中,其实数组是扫了两遍的,这并不符合进阶的要求,那没有可以扫一次就解决战斗的方法呢?显然是有的,那就是双指针。

这里搞两个指针p0p1p0指针代表0出现的位置,p1指针代表1出现的位置。

现在从头开始扫数组,数字会出现三种情况:

  • 2

    不管它,跳过

  • 1

    如果是1,将当前数字和p1指针位置的数字进行交换操作,因为是从数组的第一位开始的,所以如果先出现1,那么现在的情况是1集中地出现在数组的最左侧,注意,这里和下面0的处理规则有联系

  • 0

    如果是0,默认做法是直接将p0指针位置上的数字和当前数字进行替换,但这里会有一个问题,如果p0指针在p1指针前,这样是不是就会有问题了?

    此时的0确实被放到了它应该存在的地方,但是1的位置变成了当前循环走到的位置,如果继续循环,会跳过这个1,此时数组的顺序就出现了问题

    为了解决这种情况,需要进行判断,如果p0 <= p1,需要将p1指针位置的数字和刚刚替换成01进行替换,并且增加1,如此才可以保证01的顺序是对的。

别看文字很多,其实代码很少的👇:

function sortColors(nums) {
  const len = nums.length
  let p0 = 0, p1 = 0
  for (let i = 0; i < len; i++) {
    if (nums[i] === 1) {
      [nums[i], nums[p1]] = [nums[p1], nums[i]]
      p1++
    } else if (nums[i] === 0) {
      [nums[i], nums[p0]] = [nums[p0], nums[i]]
      if (p0 < p1) {
        [nums[i], nums[p1]] = [nums[p1], nums[i]]
      }
      p0++
      p1++
    }
  }
  return nums
}

对上面提到的01的情况分别处理即可。

更好的方法(双指针(0、2))

有了0指针和1指针,自然可以有0指针和2指针,不过由于12的位置不同,2指针需要从数组末尾开始,于是可以声明这样的两个指针:

const len = nums.length
let p0 = 0, p1 = len -1

接下来开始循环,整体逻辑是这样的:

  • 0

    跟👆两个指针方法一样,直接和p0指针位置的数字互换即可。

  • 1

    由于没有p1指针,不管,跳过

  • 2

    按理说遇到2应该简单的和p2指针位置的数字进行替换即可,但这里可能会有一个问题,如果p2开始的位置就是2呢?或者说p2指针当前所在位置的数字就是2呢?这种情况下换不换其实没有区别,问题也就出现了。

    为了保证p2指针右边的数字都是2,且当前位置的数字也不是2,每次循环的时候都需要进行一个轮询操作,更新p2指针的位置,条件就是当前位置数字是2,并且i要小于等于p2,因为根据整体逻辑,两个指针是同时开始移动的,如果i跑到了p2后面,那么循环就可以结束了,这是因为我们保证了p2右侧的数字都是2

逻辑部分就解释到这里,👇看看代码:

function sortColors(nums) {
  const len = nums.length
  let p0 = 0, p2 = len - 1
  for (let i = 0; i <= p2; i++) {
    while (i <= p2 && nums[i] === 2) {
      [nums[i], nums[p2]] = [nums[p2], nums[i]]
      p2--
    }
    if (nums[i] === 0) {
      [nums[i], nums[p0]] = [nums[p0], nums[i]]
      p0++
    }
  }
  return nums
}

总的来说,这三种指针方法的逻辑其实比较类似,只是p1指针和p2需要根据逻辑的不同增加不同的判断,p0指针的逻辑一直都是一样的。

笔者提供的Map方法,虽然在某种程度上扫了两次数组,但可以在数字种类更多的情况下使用,指针类型的方法就局限在题目下使用。



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

这里是按照日期分类的👇

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

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

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