荷兰国旗问题

问题描述
拿破仑席卷欧洲大陆之后,代表自由,平等,博爱的竖色三色旗也风靡一时。荷兰国旗就是一面三色旗(只不过是横向的),自上而下为红白蓝三色。该问题本身是关于三色球排序和分类的,由荷兰科学家 Dijkstra 提出。由于问题中的三色小球有序排列后正好分为三类,Dijkstra 就想象成他母国的国旗,于是问题也就被命名为荷兰旗问题(Dutch National Flag Problem)。下面是问题的正规描述: 现有 n 个红白蓝三种不同颜色的小球,乱序排列在一起,请通过两两交换任意两个球,使得从左至右,依次是一些红球、一些白球、一些蓝球。在算法中,更多这样描述:如何通过两两交换,将一个数组中小于 N 的数放在靠左边,等于 N 的数放在靠中间,大于 N 的数放在靠右边 ;比如,数组 arr[1, 14, 5, 16, 8, 7], N=8, 则两两交换后得到[1, 7, 5, 8, 16, 14]。注意,左右两边内部不一定有序

问题分析
设计一个大(右)小(左)区间,通过不断扩展区间达到 分区(partition) 的效果。过程图示如下:

有以下几点需要注意:

  1. 左区间的初始位置为 L=-1,右区间的初始位置为 R=size;
  2. 当指针(下标为T)指向的数
    • 小于 N 时:交换 arr[T] 和 arr[L+1] ,L++ ,T++

    • 等于 N 时,T++;

    • 大于 N 时,交换 arr[T] 与 arr[R-1],R–,不可 T++ !

  3. T >= R 时,过程结束。注意,结束条件不能设置为 T==R,因为 R–,T++,可能有 T-R=1
  4. 区间内部不保证有序

代码实现

void netherLandFlag(int* arr, int size, int N)
{
	int L = -1;
	int R = size;
	int T = 0;
	while (T < R)
	{
		if (arr[T] < N)
			swap(arr[T++], arr[++L]);
		else if (arr[T] == N)
			T++;
		else
			swap(arr[T], arr[--R]);
	}
}

快速排序1.0

快速排序的核心就是递归调用荷兰国旗方法由于一旦完成 partition,中间区间(等于N的数)就不会再移动,所以就能通过不断在左右区间内部继续划分区间,直到无法划分为止,最终达到排序的效果 。在快速排序中,N 并不是上述那样人为指定的,而是固定为当前区间中最左或最右边的那个数 。如下图:

代码实现

// netherLandFlag 用于将数组划分为小于、等于和大于某个给定值 N 的部分
void partition(int* arr, int lower, int upper, int& pivotLow, int& pivotHigh) {
    int L = lower - 1;
    int R = upper + 1;
    int T = lower;
    int pivot = arr[lower]; //将该分区的首元素作为枢纽
    while (T < R) {
        if (arr[T] < pivot)
            swap(arr[T++], arr[++L]);
        else if (arr[T] == pivot)  
            T++;
        else
            swap(arr[T], arr[--R]);
    }
    pivotHigh = R - 1;
    pivotLow = L + 1;
}


// 快速排序实现
void quickSort(int* arr, int lower, int upper) {
    if (lower >= upper)
        return;
    int pivotLow, pivotHigh;
    partition(arr, lower, upper, pivotLow, pivotHigh);
    quickSort(arr, lower, pivotLow - 1);
    quickSort(arr, pivotHigh + 1, upper);
}

快速排序2.0——随机快排

阅读上文后,读者也许能敏锐地发现,区间的划分情况和 N 息息相关。我们希望,N 总是可以打到数组的中间(数值, 而非位置),这样每次划分就可以达到类似于二分的效果;而一旦数组偏有序,那么划分次数就会大大增加 ,如下:

第 K 层比较次数为 N-K 次,笼统记为 N 次( 只要与 N 线性相关,都记为 N );一共 N-1 层,笼统记为 N 层;所以时间复杂度为 O(N^2)

如果每次打到中间,就可以产生二分效果,减少划分次数,如下:
上图中比较5次更正为6次

第 K 层比较次数为 N-K 次,笼统记为 N 次;一共有 log_2N 层( log_27=2 ),笼统记为 logN ,故时间复杂度为 NlogN

那么如何使 N 刚好打到数组的数值正中间呢?遗憾的是,无法做到,只能随机取值。但好消息是,将 N 随机取值(其值必须为数组中的数)后,该算法的长期期望也可以达到 NlogN 。在最坏状况下则需要次 O(N^2) 比较,但这种状况并不常见。

代码实现

#include<iostream>
#include<cstdlib>//rand()
#include<ctime>//time()
#include<cstring>//memcpy()
#include<vector>

void partition(int* arr, int lower, int upper, int& pivotLow, int& pivotHigh) {
    int L = lower - 1;
    int R = upper + 1;
    int T = lower;
    int pivot = arr[rand() % (upper - lower + 1) + lower];//在该分区随机取pivot
    while (T < R) {
        if (arr[T] < pivot)
            swap(arr[T++], arr[++L]);
        else if (arr[T] == pivot)  
            T++;
        else
            swap(arr[T], arr[--R]);
    }
    pivotHigh = R - 1;
    pivotLow = L + 1;
}

效率比较

测试次数time=10000最大值max=1000000000最大长度size=1000000 ,快速排序 1.0 反而快于随机快排 0.6203 ms原因不详哈哈 :

image-20220920231457291
文章作者: 极简
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 后端技术分享
数据结构与算法
喜欢就支持一下吧