1. 荷兰国旗问题
在线OJ: 75. 颜色分类
荷兰国旗问题是在说, 给定你一个整数数组, 再给定你一个 K 值, 这个值在原数组中一定存在, 要求把数组中小于 K 的元素放到数组的左边, 大于 K 的元素放到数组的右边, 等于 K 的元素放到数组的中间.
时间复杂度要求O(N)
, 空间复杂度要求O(1)
.
解题思路如下:
设置三个指针less
和move
, index
; 让 index
从 0 位置开始遍历, 其中<index
位置的值都是比 K 小的数(即 less 指针确定的是左边比 K 小的位置边界), index……move
都是等于 K 的数, >move
都是大于 K 的数(即 move 指针确定的是右边比 K 大的位置边界).
设置初始值初始值 less = - 1, move = arr.length
; 此时让 index
从 0
位开始遍历数组进行判断, arr[index]
有三种情况:
- 情况 1,
arr[index] < K
, 此时将index
位置的值和less + 1
位置的值交换,然后less++,index++
; - 情况 2,
arr[index] > K
, 此时将index
位置的值和move - 1
位置的值交换, 然后move--
, index不动; - 情况 3,
arr[index] == K
, 此时没有交换操作,i++
即可;
数组遍历完毕后, 结果数组就形成了: 小于 K 的元素放到了数组的左边, 大于 K 的元素放到了数组的右边, 等于 K 的元素放到了数组的中间.
代码实现:
class Solution {
public static void sortColors(int[] arr) {
// < 区间和 > 区间一开始都没有元素
int less = -1;
int move = arr.length;
int index = 0;
// 这里就将 1 定为目标值
while (index < move) {
if (arr[index] < 1) {
// 如果遍历的元素小于基准值, 那么将该元素和 < 区间的下一个位置的元素交换
swap(arr, less + 1, index);
// 更新 < 区间的范围
less++;
index++;
} else if (arr[index] > 1) {
// 如果遍历的元素大于基准值, 那么将该元素和 > 区间的前一个位置的元素交换
swap(arr, move - 1, index);
// 更新 > 区间的范围
move--;
// 要注意index 此时不需要更新
// 因为交换后index位置是一个新的元素
// 需要再次对index位置的值进行判断
} else {
// 和目标值相等就跳过
index++;
}
}
}
private static void swap(int[] arr, int i, int j) {
if (i == j) {
return;
}
// 写成这种交换方法一定要写上面那个判断条件
// 否则, 如果 i 和 j 相等的话就会将值刷成0
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
这里再写一个更通用的代码, 以数组最后一个元素为基准值, 代码如下:
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// 这个方法以arr数组最后一个元素位基准值P
// 将小于P的元素排在左边, 大于P的排在右边, 等于P放的放在中间
public static void netherlandsFlag(int[] arr) {
int len = arr.length;
// < 区间一开始没有元素, 所以假设初始位置位-1
int less = -1;
int index = 0;
// > 区间初始包含数组的最后一个元素
int move = len - 1;
// 先遍历数组最后一个元素以前的元素
while (index < move) {
if (arr[index] < arr[len - 1]) {
swap(arr, less + 1, index);
less++;
index++;
} else if (arr[index] > arr[len - 1]) {
swap(arr, move - 1, index);
move--;
} else {
index++;
}
}
// 此时再将最后一个元素和move位置的元素交换即可
swap(arr, move, len - 1);
}
2. 快速排序
基于上述荷兰国旗算法的原型, 我们可以实现快速排序算法, 在arr[left……right]
范围上, 进行快速排序的过程如下:
- 在这个范围上, 随机选一个数记为
num
作为划分的基准值, - 在该范围使用荷兰国旗算法, 让 < num 的数在左部分, == num 的数在中间, > num 的数在右部分, 完成这部分后, 假设
== num
的数所在范围是[a,b]
, - 再对
arr[left...a-1]
进行快速排序(递归), - 对
arr[b+1...right]
进行快速排序(递归),
因为每一次荷兰国旗算法都会确定和基准数相同的数的位置且不会再变动, 所以排序能完成;
上面的第 1 步其实是为了实现随机快速排序, 如果不是随机实现, 其实也是可以完成排序的, 像 1 的实现中直接以范围内的最后一个元素作为目标值再进行划分也是可以的, 但当如果这个数组本来就是有序的(比如: 1,2,3,4,5,6,7
), 那排序的时间复杂度就会变成 O(n ^ 2)
, 也就是说这个排序目标值越靠近中间, 性能越好; 越靠近两边, 复杂度就越高.
而这里的实现, 就将最坏的这种情况可以转化成普通情况, 时间复杂度为 O(N*logN)
, 额外空间复杂度为O(logN)
.
这里和上面荷兰国旗部分的代码是稍有不同的, 这里的返回值是两个下标值, 即每次划分好后中间等于目标值的区间的开始和结尾, 有了这两个值才可以继续往下递归, 所以将这两个下标值组成一个数组返回.
随机快排递归版本代码实现:
// 荷兰国旗算法
public static int[] netherlandsFlag(int[] arr, int left, int right) {
if (left > right) {
return new int[]{
-1, -1};
}
if (left == right) {
return new int[]{
left, right};
}
int less = left - 1;
int more = right;
int index = left;
while (index < more) {
if (arr[index] == arr[right]) {
index++;
} else if (arr[index] < arr[right]) {
swap(arr, index++, ++less);
} else {
swap(arr, index, --more);
}
}
swap(arr, more, right);
return new int[]{
less + 1, more};
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void quickSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int left, int right) {
if (left >= right) {
return;
}
// 数组中随机拿到一个元素和末尾元素的值进行交换
swap(arr, left + (int) (Math.random() * (right - left + 1)), right);
int[] childIndex = netherlandsFlag(arr, left, right);
process(arr, left, childIndex[0] - 1);
process(arr, childIndex[1] + 1, right);
}
随机快排迭代版本实现:
public static class Job {
public int left;
public int right;
public Job(int left, int right) {
this.left = left;
this.right = right;
}
}
// 随机快排迭代实现
public static void quickSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
Stack<Job> stack = new Stack<>();
swap(arr, (int) (Math.random() * arr.length), arr.length - 1);
stack.push(new Job(0, arr.length - 1));
while (!stack.isEmpty()) {
Job cur = stack.pop();
swap(arr, cur.left + (int) (Math.random() * (cur.right - cur.left + 1)), cur.right);
int[] childIndex = netherlandsFlag(arr, cur.left, cur.right);
// 继续添加左区间的任务
// 要保证有左区间
if (childIndex[0] > cur.left) {
stack.push(new Job(cur.left, childIndex[0] - 1));
}
// 继续添加右区间的任务
// 要保证有右区间
if (childIndex[1] < cur.right) {
stack.push(new Job(childIndex[1] + 1, cur.right));
}
}
}