描述
假设有一个整数序列,要求将大于 pivot 的数放在序列右边,小于 pivot 的数放在序列左边,等于 pivot 的数放在序列中间。
分析
荷兰国旗问题本质是一个序列分割问题,需要将一个序列分割成三部分。有一个很关键的地方,就是整个序列究竟存储在什么数据结构中。如果序列存储在顺序数组中,那么使用一趟快排即可。如果存储在单链表中,由于一趟快排的过程中会发生许多随机存取,在单链表上随机存取的代价十分高昂。因此对于单链表不适用快排。
数组存储序列
方案
使用一趟快排解决问题。时间复杂度为 O(N),额外空间复杂度为 O(N),不稳定。
排序前:
排序过程:
步骤
① 准备三个指针 less、more、cur。less 的初值为排序范围的左边界减 1,more 为右边界减 1,cur 为左边界。
② 判断 cur 与 pivot 的大小。如果 cur > pivot,cur 和 --more 交换。
如果 cur < pivot,cur 和 ++less 交换,并且 cur 右移一步。
如果 cur == pivot,cur 直接右移一步。
③ 重复 ②,直到 cur >= more。
代码
class DutchNationalFlagProblem {
public static void main(String[] args) {
// 原始数组
int[] a = {
3,4,9,5,7,1,0,5,2,8,5,6};
// 打印原始数组
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println();
dutchNationalFlagProblem(a, 5);
// 打印排序后的数组
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println();
}
public static void dutchNationalFlagProblem(int[] a, int pivot) {
// 一趟快排解决荷兰国旗问题
quickSort(a, 0, a.length - 1, pivot);
}
/**
* 一趟快排的例程
* @param a 待排序数组
* @param left 排序范围的左边界
* @param right 排序范围的右边界
* @param pivot
*/
public static void quickSort(int[] a, int left, int right, int pivot) {
int less = left - 1; // less 初始值为左边界减1
int more = right + 1; // more 初始值为有边界加1
int cur = left; // cur 初始值为左边界
while (cur < more) {
// cur >= more 循环停止
if (a[cur] > pivot) {
// 如果 cur > pivot,cur 和 --more 交换
swap(a, cur, --more);
} else if (a[cur] < pivot) {
// 如果 cur < prvot,cur 和 ++less 交换,且 curr 右移一步
swap(a, cur, ++less);
cur++;
} else {
// 如果 cur == pivot,cur直接右移一步
cur++;
}
}
}
// 交换数组中指定两个下标的元素
private static void swap(int[] a, int m, int n) {
int temp = a[m];
a[m] = a[n];
a[n] = temp;
}
}
out:
3 4 9 5 7 1 0 5 2 8 5 6
3 4 2 1 0 5 5 5 8 7 6 9
单链表存储序列
方案
如果实际问题对于空间复杂度和稳定性没有要求,完全可以将单链表中的元素值先存储到一个顺序数组中,再对这个数组使用一趟快排。如果实际问题要求额外空间复杂度为 O(1) 且保证算法稳定性,可以使用先拆后合的策略。将一个链表拆成三个链表 less、equal、more。less 链表按原链表先后次序存放小于 pivot 的节点。equal 链表存放等于 pivot 的节点。more 链表存放大于 pivot 的节点。最后再将三个链表接起来即可。
排序前: