快速排序和归并排序,两种算法都是分治法的经典表现,递归形式写起来也水到渠成。
一、快速排序
快速排序的递归程序主要分三部分:驱动程序、递归主程序、中枢点(pivot)选取。前两部分用来实现快排的中心思想,把中枢点放到某个位置,使得该位置左侧元素均不大于中枢元素,右侧元素均不小于右侧元素。算法属于就地排序,通过交换元素在数组中的位置来进行。
中枢点的选取,主要会在原始数组不完全随机的情况下,影响算法的最坏时间复杂度。最流行的办法是选取当前排序子数组的第一个元素作为中枢点,这在原始数组随机的情况下当然无所谓。但很多情况下,待排序数组是部分有序甚至“基本有序”的,这种选取办法就会导致最坏时间复杂度O(n2)的产生。
降低最坏时间复杂度情形发生概率的常用办法:三点均值法,取子数组首元素、尾元素和中间元素大小排中间的元素作为中枢点。以下的实现程序就是采取的这种方案:
public void quickSort(int[] nums) {
// 快速排序驱动程序
quickSort(nums, 0, nums.length - 1);
}
public void quickSort(int[] nums, int head, int tail) {
if(head < tail) {
int pivot = choosePivot(nums, head, tail);
swap(nums, pivot, tail);
int left = head;
int right = tail;
while(left < right) {
while(left < right && nums[left] <= nums[tail]) {
left++;
}
while(left < right && nums[right] >= nums[tail]) {
right--;
}
if(left < right) {
swap(nums, left, right);
left++;
}
}
swap(nums, left, tail);
quickSort(nums, head, left - 1);
quickSort(nums, left + 1, tail);
}
}
public int choosePivot(int[] nums, int head, int tail) {
// 快速排序-取第一个元素为中枢点
// return head;
// 快速排序-三数平均中枢点
int mid = (head + tail) / 2;
if(nums[mid] >= nums[head]) {
if(nums[mid] <= nums[tail]) {
return mid;
} else {
if(nums[head] <= nums[tail]) {
return tail;
} else {
return head;
}
}
} else {
if(nums[mid] >= nums[tail]) {
return mid;
} else {
if(nums[head] <= nums[tail]) {
return head;
} else {
return tail;
}
}
}
}
算法时间复杂度:最好情形O(nlogn)、最坏O(n2)、平均O(nlogn),最坏发生在每次选取的中枢均为边界点(最大或最小)时;空间复杂度:算法为就地排序,但递归过程中栈会消耗O(logn)的存储空间,因此空间复杂度为O(logn);稳定性:假设数组为[6, 6, 3, 5],取最末为中枢,则左边一个6会在排序过程中与3交换,因此不稳定。
二、归并排序
归并排序的依据是,把两个分别有序的子数组进行合并,让合并数组有序的时间复杂度是O(n)。在此基础上递归,直到子数组大小为1时自动有序即可。
代码实现还是比较容易的,唯一需要注意的地方是,在驱动程序里创建唯一的缓存数组,供各个递归子程序在缓存数组不同位置上缓存数据,避免递归地创建新数组。否则递归程序中同时存在好几个数组,会增大空间的消耗。
public void mergeSort(int[] nums) {
// 归并排序驱动程序
int length = nums.length;
mergeSort(nums, new int[length], 0, length - 1);
}
public void mergeSort(int[] nums, int[] tmp, int head, int tail) {
if(head < tail) {
int h1 = head;
int t1 = (tail + head) / 2;
int h2 = t1 + 1;
int t2 = tail;
mergeSort(nums, tmp, h1, t1);
mergeSort(nums, tmp, h2, t2);
int cur = head;
while(h1 <= t1 && h2 <= t2) {
if(nums[h1] <= nums[h2]) {
tmp[cur++] = nums[h1++];
} else {
tmp[cur++] = nums[h2++];
}
}
while(h1 <= t1) {
tmp[cur++] = nums[h1++];
}
while(h2 <= t2) {
tmp[cur++] = nums[h2++];
}
for(int i = head; i <= tail; i++) {
nums[i] = tmp[i];
}
}
}
算法时间复杂度:快排要每次“恰好”二分需要看运气,归并则每次都恰好二分,因此最好、最坏、平均时间复杂度均为O(nlogn);空间复杂度:统一使用的缓存数组大小为n,因此为O(n);稳定性:显然稳定。