学习日常—快速排序及其优化
1、快速排序的基本思想:
快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
2、快速排序的三个步骤:
(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)
(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大
(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。
3、选择基准的方式
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。
最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列。
方法(1):固定位置
思想:取序列的第一个或最后一个元素作为基准
注意:基本的快速排序选取第一个或最后一个元素作为基准。但是,这是一直很不好的处理方法。
方法(2):随机选取基准
引入的原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,就引入了随机选取枢轴
思想:取待排序列中任意一个元素作为基准
方法(3):三数取中(median-of-three)
引入的原因:虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴
分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约14%的比较次数。
基本的固定位置法的实现:
private int pivot(int[] array,int start,int end){//找基准
int tem = array[start];
while(start<end){
while(start<end&&array[end]>=tem){
end--;
}
array[start] = array[end];
while(start<end&&array[start]<=tem){
start++;
}
array[end] = array[start];
}
array[end] = tem;
return start;
}
private void quick(int[] array,int low,int high){//分治思想,递归实现
if(low<high){
int mid = pivot(array,low,high);
quick(array,low,mid-1);
quick(array,mid+1,high);
}
}
public void quickSort(int[] array){
quick(array,0,array.length-1);
}
三数取中方法的实现
private int pivot(int[] array,int start,int end){//找基准
int tem = array[start];
while(start<end){
while(start<end&&array[end]>=tem){
end--;
}
array[start] = array[end];
while(start<end&&array[start]<=tem){
start++;
}
array[end] = array[start];
}
array[end] = tem;
return start;
}
private void swap(int[] array,int i,int k){//两数交换方法
int tem = array[i];
array[i] = array[k];
array[k] = tem;
}
private void medianOfThree(int[] array,int low,int high){//三数取中方法
int mid = (low+high)>>1;
if(array[low]<array[mid]){
swap(array,low,mid);
}
if(array[low]>array[high]){
swap(array,low,high);
}
if(array[low]<array[mid]){
swap(array,low,mid);
}
}
private void quick(int[] array,int low,int high){//分治思想,递归实现
if(low<high){
medianOfThree(array,low,high);
int mid = pivot(array,low,high);
quick(array,low,mid-1);
quick(array,mid+1,high);
}
}
public void quickSort(int[] array){
quick(array,0,array.length-1);
}
优化1、当待排序序列的长度分割到一定大小后,使用插入排序。
原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排
截止范围:待排序序列长度N = 10,虽然在5~20之间任一截止范围都有可能产生类似的结果,这种做法也避免了一些有害的退化情形。摘自《数据结构与算法分析》在这里插入代码片
Mark Allen Weiness 著
private void quick(int[] array,int low,int high) {//分治思想,递归实现
if (high - low + 1 < 10) {
insertSort(array,low,high);
} else if (low < high) {
int key = medianOfThree(array, low, high);
int mid = pivot(array, low, high);
quick(array, low, mid - 1);
quick(array, mid + 1, high);
}
}
优化2、在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
void QSort(int arr[],int low,int high)
{
int first = low;
int last = high;
int left = low;
int right = high;
int leftLen = 0;
int rightLen = 0;
if (high - low + 1 < 10)
{
InsertSort(arr,low,high);
return;
}
//一次分割
int key = SelectPivotMedianOfThree(arr,low,high);//使用三数取中法选择枢轴
while(low < high)
{
while(high > low && arr[high] >= key)
{
if (arr[high] == key)//处理相等元素
{
swap(arr[right],arr[high]);
right--;
rightLen++;
}
high--;
}
arr[low] = arr[high];
while(high > low && arr[low] <= key)
{
if (arr[low] == key)
{
swap(arr[left],arr[low]);
left++;
leftLen++;
}
low++;
}
arr[high] = arr[low];
}
arr[low] = key;
//一次快排结束
//把与枢轴key相同的元素移到枢轴最终位置周围
int i = low - 1;
int j = first;
while(j < left && arr[i] != key)
{
swap(arr[i],arr[j]);
i--;
j++;
}
i = low + 1;
j = last;
while(j > right && arr[i] != key)
{
swap(arr[i],arr[j]);
i++;
j--;
}
QSort(arr,first,low - 1 - leftLen);
QSort(arr,low + 1 + rightLen,last);
}