这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战
颜色分类(题号75)
题目
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
示例 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.length1 <= n <= 300nums[i]为0、1或2
进阶:
- 你可以不使用代码库中的排序函数来解决这道题吗?
- 你能想出一个仅使用常数空间的一趟扫描算法吗?
链接
解释
这题啊,这题是经典排序。
题目翻译一下,就是一个由0、1、2组成的乱序数组,给它原地升序排列就完事了,整啥红白蓝。
排序的话很简单,用很多种排序方法,经典的就是sort方法了,但是注意这里的进阶提示,要求不用默认的sort,并且要求空间复杂度为O(1),时间复杂度为O(n)。
由于这题的解决方案由很多种,此处就不展开分析了,详细的分析放在下面具体的答案中。
自己的答案(sort)
这就很简单了👇:
function sortColors(nums) {
return nums.sort((a, b) => a - b)
}
没啥可说的,原地排序就完事了,根据题意是原地修改,此处其实不用return,但为了打印结果方便,还是return了,问题不大。
自己的答案(Map)
根据题意,整个数组只有0、1、2三种类型的值,那是不是可以新建一个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))
在单指针方法中,其实数组是扫了两遍的,这并不符合进阶的要求,那没有可以扫一次就解决战斗的方法呢?显然是有的,那就是双指针。
这里搞两个指针p0和p1,p0指针代表0出现的位置,p1指针代表1出现的位置。
现在从头开始扫数组,数字会出现三种情况:
-
2不管它,跳过
-
1如果是
1,将当前数字和p1指针位置的数字进行交换操作,因为是从数组的第一位开始的,所以如果先出现1,那么现在的情况是1集中地出现在数组的最左侧,注意,这里和下面0的处理规则有联系 -
0如果是
0,默认做法是直接将p0指针位置上的数字和当前数字进行替换,但这里会有一个问题,如果p0指针在p1指针前,这样是不是就会有问题了?此时的
0确实被放到了它应该存在的地方,但是1的位置变成了当前循环走到的位置,如果继续循环,会跳过这个1,此时数组的顺序就出现了问题为了解决这种情况,需要进行判断,如果
p0 <= p1,需要将p1指针位置的数字和刚刚替换成0的1进行替换,并且增加1,如此才可以保证0和1的顺序是对的。
别看文字很多,其实代码很少的👇:
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
}
对上面提到的0和1的情况分别处理即可。
更好的方法(双指针(0、2))
有了0指针和1指针,自然可以有0指针和2指针,不过由于1和2的位置不同,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:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇