本题可以用多种方法进行求解。如果将其看作排序问题,排序 0 、1、2,那么可以使用排序算法进行求解。如果将其看成原地调换数组元素位置,那么可以使用指针法进行求解。
知识点: 快速排序、快慢指针、循环不变量。
1. 快速排序
1.1 思路
快速排序采用了分治的思想,首先找出一个主元,我们一般取数组的第一个元素作为主元。然后调整数组元素的位置,使得主元左边都是小于主元的元素,主元右边都是大于主元的元素。之后再分别对主元左边的元素和主元右边的元素进行排序,直至所有元素都是有序的(至少有两个元素才可以进行排序)。
1.2 实现
我们使用递归来实现快速排序,定义快速排序函数 void quicksort(vector<int>& nums, int left, int right)
。
递归的终止条件:待排区间至少有两个元素。因为一个元素本身就是有序的。
递归函数的作用:对当前区间元素进行排序。对主元左边的元素进行排序,对主元右边的元素进行排序。
递归的参数和返回值:除了要排序的数组,还需传入区间的左右边界。
首先需要将主元放到正确的位置上,即主元左边的元素都小于主元,主元右边的元素都大于主元。并返回主元的位置索引,用于划分递归的区间。
这里定义放置主元的函数 int partition(vector<int>& nums, int left, int right)
。
当 left == right 时,说明找到了主元的位置,故把主元放到 left (right)位置上,并返回索引 left (right)。
class Solution {
public:
void sortColors(vector<int>& nums) {
quicksort(nums, 0, nums.size() - 1);
}
void quicksort(vector<int>& nums, int left, int right)
{
if(left < right) //终止条件
{
int pivot = partition(nums, left, right);
quicksort(nums, left, pivot - 1);
quicksort(nums, pivot + 1, right);
}
}
int partition(vector<int>& nums, int left, int right)
{
//将主元的值存到变量 pivot 中
int pivot = nums[left];
while(left < right)
{
// 从右边开始遍历,再次判断 left < right 防止越界
while(left < right && nums[right] >= pivot)
{
right--;
}
// 将主元右边小于主元的元素放到主元之前的空位上
nums[left] = nums[right];
// 从左边开始遍历,再次判断 left < right 防止越界
// 当 left == right 是说明循环要结束了(待排区间只有一个元素了)
// 如果不进行再次判断,当满足 nums[left] == pivot 时,即 left == right 时
// right--,继续左移会超出数组左边界
// 例如:主元为 5 ,整个数组为 [5, 6, 7, 8, 9],
// left 最终会在数组第一个元素处与 right 重合,再向左移就会超出数组左边界
while(left < right && nums[left] <= pivot)
{
left++;
}
nums[right] = nums[left];
}
nums[left] = pivot;
return left;
}
};
2. 指针法
题目要求数字 0 、1、2 分别排在一起。因此可以考虑使用指针区分数字的边界。
2.1 单指针
使用单指针需要进行两次遍历。第一次将所有的0交换到数组前端(指针ptr 前),第二次将所有的1交换到数组前端(指针ptr 前),第二次交换的起始位置是所有0之后。算法的空间复杂度为O(1)。
class Solution {
public:
void sortColors(vector<int>& nums) {
int ptr = 0;
int n = nums.size();
for(int i = 0; i < n; i++)
{
if(nums[i] == 0)
{
swap(nums[i], nums[ptr]);
ptr++;
}
}
for(int j = ptr; j < n; j++)
{
if(nums[j] == 1)
{
swap(nums[j], nums[ptr]);
ptr++;
}
}
}
};
2.2 双指针
使用双指针只需对数组进行一次遍历(循环不变量)。
循环不变量十分巧妙,具体细节可以参考力扣视频题解,我在这里简单介绍一下。
首先要给出循环不变量的定义
all in [0,p0)
all in (p0, i)
all in (p1, len - 1]
整个数组被划分为3个部分,数字0,数字1和数字2。
定义变量 p0 和 p1 用于划分界限,p0 前全是0,变量 p1 后全是1,i 是自变量用于遍历数组。在循环过程中,我们始终要保证上述变量的定义是正确的。
起始时,所有分区应该都为空,因此有
int p0 = 0;
int i = 0;
int p1 = nums.size() - 1;
由于 i 默认为开区间,因此循环终止条件为 while(i <= p1)
。因为 p1 后都为 2,无须进行循环。(p0, i) 和 (p1, len - 1] 在 i 到 p1 之间为开,因此 while 循环取等号,让 i 继续观察下一个元素。
当 nums[i] == 0
时,交换数字 0 到 [0,p0) 区间。为了维护 p0 前都为 0 的定义,p0 后移。同时变量 i 后移,去看下一个元素。
当 nums[i] == 1
时,不属于 p0 或者 p1 区间,因此 p0 和 p1 不变,自变量 i 后移,去看下一个元素。
最后是 nums[i] == 2
的部分,交换数字 2 到 (p1, len - 1] 区间。为了维护 p1 后都为 2 的定义,p1 前移。
注意:此时,无法确定交换到的元素是什么,所以 i 不变,等待下一轮的判断,即执行下一个 while 循环。
class Solution {
public:
void sortColors(vector<int>& nums) {
// all in [0,p0)
// all in [p0, i)
// all in (p1, len - 1]
int p0 = 0;
int i = 0;
int p1 = nums.size() - 1;
while(i <= p1)
{
if(nums[i] == 0)
{
swap(nums[i], nums[p0]);
p0++;
i++;
}
else if(nums[i] == 1)
{
i++;
}
else
{
swap(nums[i], nums[p1]);
p1--;
}
}
}
};