【dawn·算法】颜色分类问题

简要说明
(1) 题目来源:LeetCode(75. 颜色分类)
(2) 代码仅供参考,尚可优化。如有改进建议,欢迎评论分享。

题目简介

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

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

输入输出样例:

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

热身:数组整理(奇数和偶数)

  对于本题需要作出以下有用的假设:我们希望在O(n)时间复杂度内解决这个问题。事实上,一个简单的排序就可以解决这个问题,但除去一些良好组织的排序外都以O(nlogn)为下界,对我们来说仍然不太理想。
  另一个有意义的假设(本文也基于此假设)是,我们希望在一遍扫描内解决这一问题。即便有上一个假设,也有取巧的办法:分别数0、1(和2)的个数,然后还原它(计数排序)。但这实则上有两遍“扫描”:第一次扫描获取个数,第二次扫描填充结果。
  因此,如果直接对这个问题有思路的话就可以跳过本部分。不然,我们可以从这道题目入手,来为颜色分类问题构思:

  • 给定一个整型正数数组,由若干奇数和偶数组成。要求使用一遍扫描整理数组,使得所有奇数在所有偶数的左侧。例如,原数组为[5, 2, 4, 7, 9],那么结果就应是[5, 7, 9, 2, 4]。为简化这一问题,不追求奇数和偶数在原数组中的顺序。

  读者可以在此稍作停留,自行思考本题。接下去是本题的思路。
  本题的简要思路就是交换奇数和偶数,我们可以只由奇数或偶数发起交换操作。如果我们主动交换偶数,那就需要放在尾部;如果我们主动交换奇数,那就需要放在头部。接下去我们进一步思考后者的思路。
  其实最头疼的问题就是如何找到“头部”,因为“头部”可能已经有整理好(或本身就在)的奇数,它们不需要被挪动。因此自然而然地,我们就引入一个“指针”,去指向整理完的奇数位置。

// 测试样例
int array[] = {5,6,7,2,4,8,7}; 
int length = 7;

void swap(int *array, int i, int j) {
	int temp = array[i];
	array[i] = array[j];
	array[j] = temp;
}

void organize(int *array, int length) {
	int p = 0;
	for (int i=0; i<length; ++i) {
		if (array[i] & 0x1) {
			// n是奇数
			swap(array, p++, i);
		}
	}
}

int main() {
	organize(array, length);
	for (int i=0; i<length; ++i)
		cout << array[i] << ' ';
}

  可以看到,organize()函数内的p就是用于这一个“指针”的作用,它负责记录整理完的奇数的尾部。在我们的测试样例中,第一个数就是奇数,实际上就是自己与自己交换。之后遇到偶数就先置之不理,直到遇到下一个奇数,再和第二个位置交换,以此类推。
  如果我们主动整理偶数,也就是更换organize()内的if的逻辑(即!array[i] & 0x1),那么指针应该从尾部依次前挪。读者可类似地写出代码。
  更有趣的问题是,如果我们要求保证偶数的顺序,应该怎么做?这个问题留给读者思考。显然,并不要求偶数顺序的话,上述代码就足够了。

回到本题:思路分析与代码

注:仅代表个人思路

  和上面的数组整理问题进行比较的话,实际上这道题就多出了一个颜色:奇数偶数的组织问题就是对两个颜色进行整理,它需要使用一个指针去标记整理的头部(主动整理奇数的话)。

  如果仅要求O(n)而不限制扫描次数的话,上述整理的思路便是两次整理,先把0整理到最前面,对剩下部分再针对1和2进行整理。

  回到本题。本题只有三个颜色,因此我们可以分开整理:把0整理到最前面,把2整理到最后面,这就需要一头一尾两个指针。事情解决了一半,但仍然有许多的代码细节的问题。先贴上代码。

    void swap(vector<int>& v, int i, int j) {
        if (i==j)  return;  //这行无关紧要
        int temp = v[i];
        v[i] = v[j];
        v[j] = temp;
    }

    void sortColors(vector<int>& nums) {
        int size = nums.size();
        int number_red, number_blue;
        number_red = number_blue = 0;
        for (int i=0; i<size-number_blue;) {
            switch (nums[i]) {
                case 0:
                    swap(nums, number_red++, i++);
                    break;
                case 1:
                    ++i;
                    break;
                case 2:
                    swap(nums, size-1-number_blue, i);
                    number_blue++;
                    break;
            }
        }
    }

  在这里,number_red和number_blue就负责我们上面两个指针的概念,分别表示对应颜色的数量。在一次扫描中:

  • 如果遇到0,需要把这个表项放到最前面,这就需要将nums[i]和nums[number_red]进行交换。
      我们考虑新的nums[i](即原先的nums[number_red])会是什么,它可能是0(当且仅当0在头时的第一次扫描会出现这个情况),或者是1和2。由于number_red ≤ i,不可能是2(已经会被放到尾部),因此只可能是1。我们不对1进行操作,于是下一次的扫描对象是nums[i+1],即需要自增i。
  • 如果遇到1,我们不动,扫描下一项,即需要自增i。
  • 如果遇到2,需要把这个表项放到最后面,这就需要将nums[i]和nums[size-1-number_blue]进行交换。
      我们考虑新的nums[i](即原先的nums[number_red])会是什么。因为新的nums[i]我们从未处理过,因此它完全有可能是0、1、2,这就需要再次处理。因此在这种情况下,i不需要自增

  最后我们考虑边界情况。如果我们依然像之前的热身问题一样一直整理到尾部,就会出现问题:此时number_blue > 0,也就是我们现在扫描的对象已经是整理好的2了,那么再发生上述交换的话就可能把1(或0)换回尾部,我们之前的操作就变成徒劳了!因此扫描停止的情况是扫描到整理好的尾部2就停止,对应代码中的i < size - number_blue

  在这里我们留下另一个问题:如果出现4种(甚至更多)颜色,我们应当如何整理。事实上,当颜色数趋近于无限时,这就变成了排序问题,因此我们只考虑颜色数在比较少的情况。(对于4种颜色的情况,有时间再添这个坑)

补充部分

  • 事实上并不是对这个问题的补充……之所以会在做颜色分类本题时联想到本博客中的热身题,除了它们思路一致之外,还有就是这道题给我留下了一些印象——据说是之前某高校保研的算法上机题,要求并没有那么高——只是O(n)解决。本问题实际上就是对这个问题的延伸应用了。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值