分治法基本思想
将一个规模为 n 的问题分解为 k 个规模较小的问题,这些子问题相互独立且与原问题相同。递归
地解这些子问题,然后将各子问题的解合并得到原问题的解。
1、快速排序算法
算法思想:
(1)分解 :以中轴元素 a[p] 将 a[p:r] 划分成3段 a[p:q-1]、a[q]、a[q+1:r] 。
(2)递归求解:递归调用对 a[p:q-1]、a[q+1:r] 进行排序。
(3)合并:由于对 a[p:q-1]、a[q+1:r] 的排序是就地进行的,因此不用再做合并操作。
快速排序算法 源码:
/**
* 快速排序算法实现
*/
package quikesort;
public class QuikeSort {
public static int[] array = {49, 38, 65, 97, 76, 13, 27, 49};
public static void main(String[] args) {
QuikeSort qs = new QuikeSort();
qs.quikeSort(array, 0, array.length - 1);
for(int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
}
/**
* 分解数组,以基准(枢值)划分子数组,递归求解
* @param arr 待排序数组
* @param low 排序的起始位置
* @param high 排序的终止位置
*/
public void quikeSort(int[] arr, int low, int high) {
if(low < high) {
//基准位置
int pivotLoc = partition(arr, low, high);
//以基准划分为两个子数组,递归求解
quikeSort(arr, low, pivotLoc - 1);
quikeSort(arr, pivotLoc + 1, high);
}
}
/**
* 一趟快速排序
* @return 返回当前基准位置
*/
public int partition(int[] arr, int low, int high) {
//确定基准,以此划分数组
int pivotkey = arr[low];
//将小于基准的数交换到左边区域,将大于基准的数交换到右边区域
while(low < high) {
//从右往左,找到小于基准的数便停止,此时high指向该小于基准的数
while (low < high && arr[high] >= pivotkey) {
--high;
}
arr[low] = arr[high];
//从左往右,找到大于基准的数便停止,此时low指向该大于基准的数
while (low < high && arr[low] <= pivotkey) {
++low;
}
arr[high] = arr[low];
}
//一趟交换结束,此时low==high
//将基准放入交换的最后一个位置
arr[low] = pivotkey;
return low;
}
}
算法分析
(1)问题分析
存在的问题:
在快速排序算法中,对于数组 a[p:r],选取 a[p] 作为基准(划分中轴)可以保证算法正常结束。如果选择 a[r] 作为划分的基准,且
a[r] 又是 a[p:r] 中的最大元素时,partition 划分函数的返回值为 q=r,这会使 quikeSort 函数陷入死循环。
·
原因分析:
因为上面的代码总是先从右往左与基准比较,若 a[r] 作为基准(pivotkey = a[r];
),且 a[r] 是最大元素,则while之后high==low
直接结束本趟排序,基准重新赋值给 a[r](a[r] = pivotkey;
),导致数组无序的情况下,在 partition 函数中没有发生排序,返回的基准值任为原 a[r],导致无限循环。
·
解决办法:
由于是算法本身的限制,只能让其他方法避开此陷阱。当快速排序是先从右往左与基准比较时,避免以a[a.length-1]作为基准;当先从左往右与基准比较时,避免以a[0]作为基准。
(2)效率分析
最好时间复杂度:O(logn)
最坏时间复杂度:O(n2)
平均时间复杂度:O(logn)
空间复杂度:O(logn)
·
最好情况:每次划分所取的基准都恰好为当前序列的中值,即n/2,此时取得最好时间复杂度。
·
可知快速排序算法的性能取决于划分的对称性
。
(3)效率优化
当排序的
序列很大 且 较为有序
时,可以进行一定的效率优化。注意仅在当前情况下可提高算法效率。
根据 快速排序算法的性能取决于划分的对称性
,修改基准的选择方法,使其选到中值或者接近中值,从而提高快速排序算法的效率。
采用随机基准选择策略:
由于未排序的数组中值未知,只能用随机选择方式增大每个元素作为基准的概率,而不是一味的选取相对固定位置上的元素(如a[p]、a[r]),尤其避免了数组本身可能是相对比较有序的情况带来的划分不均。而随机选择可以期望划分是比较对称的。
2、快速排序随机基准算法
快速排序随机基准算法 源码:
/**
* 快速排序的随机基准算法
*/
package randomquikesort;
import java.util.Random;
public class RandomQuikeSort {
public static int[] array = {49, 38, 65, 97, 76, 13, 27, 49};
public static void main(String[] args) {
RandomQuikeSort qs = new RandomQuikeSort();
qs.randomQuikeSort(array, 0, array.length - 1);
for(int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
}
/**
* 分解数组,以基准(枢值)划分子数组,递归求解
* @param arr 待排序数组
* @param low 排序的起始位置
* @param high 排序的终止位置
*/
public void randomQuikeSort(int[] arr, int low, int high) {
if(low < high) {
//基准位置
int pivotLoc = randomPartition(arr, low, high);
//以基准划分为两个子数组,递归求解
randomQuikeSort(arr, low, pivotLoc - 1);
randomQuikeSort(arr, pivotLoc + 1, high);
}
}
/**
* 一趟快速排序
* @return 返回当前基准位置
*/
public int Partition(int[] arr, int low, int high) {
//确定基准,以此划分数组
int pivotkey = arr[low];
//将小于基准的数交换到左边区域,将大于基准的数交换到右边区域
while(low < high) {
//从右往左,找到小于基准的数便停止,此时high指向该小于基准的数
while (low < high && arr[high] >= pivotkey) {
--high;
}
arr[low] = arr[high];
//从左往右,找到大于基准的数便停止,此时low指向该大于基准的数
while (low < high && arr[low] <= pivotkey) {
++low;
}
arr[high] = arr[low];
}
//一趟交换结束,此时low==high
//将基准放入交换的最后一个位置
arr[low] = pivotkey;
return low;
}
/**
* 随机获取基准
* @return 返回调用Partition:返回当前基准位置
*/
public int randomPartition(int[] arr, int low, int high) {
Random ran = new Random();
//在当前数组中产生随机基准,且摒弃最后一个元素
int randomPivotkey = ran.nextInt(high - low) + low;
//将随机取得的基准交换到arr[low]以复用原Partition函数
swap(arr, low, randomPivotkey);
return Partition(arr, low, high);
}
/**
* 交换元素
*/
public void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
}
效率测试
测试符合上述理论,这里不再给出测试过程及结果。
具体测试过程及结果请转:
快速排序算法优化(快速排序随机基准算法)—— 测试过程及结果分析