快速排序(Quicksort),又称划分交换排序(partition-exchange sort),简称快排。它的原理和冒泡排序法一样都是用交换的方式,不过他会在数据中找到一个虚拟的中间值,把小于中间值的数据放在左边,把大于中间值的数据放在右边,再以同样的方式分别处理两边的数据,直到完成排序为止。
执行流程为:
递归:
- 先以第一个值为基准值,设置其索引为par,将这个值放入一个临时变量tmp中,防止在后面的步骤中被覆盖。再设置第一个值得索引为low,最后一个值的索引为high。先由high的位置开始,由后向前,若找到的数字大于基准值,继续向前找,直到找到小于基准值的值,将它放到索引为low的位置;
- 再由low位置从前往后查找查找,若找到的数字小于基准值,继续向后查找,直到找到大于基准值的值,将它放在此时的索引为high的位置;
- 步骤2-3都是在low<high的前提下进行,low在不断增大,high在不断减小,直到两者重合,即low=high时,将临时变量tmp中的值,即基准值放入这个位置,此时,这个位置前面的数字都必定比它小,后面的数字都必定比它大,这样,一次排序完成;
- 接下来对基准值左右两边的子序列进行同样的处理,直到某一次得到的基准值左右两边都只剩0个或一个数字,此时这组数字已经全部处于有序状态,快速排序完成;
这种快速排序算法我在另一篇博客有详细说明和演示,可点击超链接查看:
非递归:借助栈先进后出的性质
- 先进行一次快速排序,把地一个基准值放在正确的位置;
- 判断该基准值左右子序列大小,若大小为0或1,说明已经在正确的位置,不作处理,否则将左右子序列的low值和high值依次入栈;
- 在栈不为空的条件下依次取栈顶元素,一次取两个,即取一个子序列的high和low,依次确定区间,对这个区间进行找基准值操作,同样满足基准值左侧小于它,右侧大于它;
- 若栈为空,说明已经没有待排序序列,即所有元素已排序完成:
代码:
递归写法:
import java.util.Arrays;
import java.util.Random;
/***
* 快速排序(递归写法)
* 时间复杂度:O(nlog2n)
* 空间复杂度:log2n
* 稳定性:不稳定
*/
public class MyQuickSort {
public static void main(String[] args) {
int[] arr =new int[100];
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(100)+1;
}
System.out.println("原数组为:\n"+Arrays.toString(arr));
MyQuickSort(arr);
System.out.println("(递归)快速排序后的数组为:\n"+Arrays.toString(arr));
//判断,若最终结果不是有序,则打印false
for(int i = 0;i<arr.length-1;i++){
if(arr[i]>arr[i+1]){
System.out.println("false!");
}
}
}
//快速排序
public static void MyQuickSort(int[] arr){
quick(arr,0,arr.length-1);
}
public static void quick(int[] arr,int low,int high){
if(low == high){
return;
}
int par = partion(arr,low,high);
//递归左边:保证左边有两个数据以上
if(par>low+1){
quick(arr,low,par-1);
}
//递归左边:保证左边有两个数据以上
if(par<high-1){
quick(arr,par+1,high);
}
}
//找基准,基准两边已经有序
public static int partion(int[] arr,int low,int high){
int tmp = arr[low];
while(low<high){
while((low<high)&&arr[high]>=tmp){
high--;
}
if(low==high){
break;
}else{
arr[low] = arr[high];
}
while((low<high)&&arr[low]<=tmp){
low++;
}
if(low==high){
break;
}else{
arr[high] = arr[low];
}
}
arr[low] = tmp;
//或:arr[high] = tmp;
return low;
}
}
结果展示:
非递归写法:
import java.util.Arrays;
import java.util.Random;
/***
* 快速排序(非递归):
* 快速排序越往后越有序,根据直接插入排序越有序越快的特性,可以规定在小于某一区间时使用直接插入排序
* 时间复杂度:O(nlog2n)
* 空间复杂度:log2n
* 稳定性:不稳定
*/
public class MyQuickSort3 {
public static void main(String[] args) {
int[] arr =new int[100];
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(100)+1;
}
System.out.println("原数组为:\n"+Arrays.toString(arr));
MyQuickSort(arr);
System.out.println("(非递归)快速排序后的数组为:\n"+Arrays.toString(arr));
//判断,若最终结果不是有序,则打印false
for(int i = 0;i<arr.length-1;i++){
if(arr[i]>arr[i+1]){
System.out.println("false!");
}
}
}
//快速排序(非递归)
public static void MyQuickSort(int[] arr){
int[] stack = new int[arr.length*2];
int top = 0;
int low = 0;
int high = arr.length-1;
//先进行一趟快速排序
int par = partion(arr,low,high);
//1.判断当前par左右两边是否有两个以上数据
if(par>low+1){
stack[top++] = low;
stack[top++] = par-1;
}
if(par<high-1){
stack[top++] = par+1;
stack[top++] = high;
}
//以上代码执行完毕,以上数对已经全部入栈
//判断栈是否为空,不为空时取出两个数对,进行partion();
while(top>0){
//出栈
high = stack[--top];
low = stack[--top];
par = partion(arr,low,high);
if(par>low+1){
stack[top++] = low;
stack[top++] = par-1;
}
if(par<high-1){
stack[top++] = par+1;
stack[top++] = high;
}
}
}
//找基准
public static int partion(int[] arr,int low,int high){
int tmp = arr[low];
while(low<high){
while((low<high)&&arr[high]>=tmp){
high--;
}
if(low==high){
break;
}else{
arr[low] = arr[high];
}
while((low<high)&&arr[low]<=tmp){
low++;
}
if(low==high){
break;
}else{
arr[high] = arr[low];
}
}
arr[low] = tmp;
//或:arr[high] = tmp;
return low;
}
}
结果展示:
快速排序分析:
时间复杂度:
在最快及平均情况下,时间复杂度为O(nlog2n),最坏情况下,即每次挑中的中间值不是最大就是最小,这时时间复杂度为O(n2);。
空间复杂度:
在最好情况下,空间复杂度为:O(nlog2n),最坏情况下空间复杂度为:O(n)。
稳定性:
快速排序时不稳定排序法。
快速排序时平均运行时间最短的排序法。
快速排序的优化:
选取基准值的优化:
上述算法是采用以固定位置为基准值的算法,即选取第一个值或最后一个值为基准值,但是,对于快速排序分治算法,每次划分的两个子序列越趋近等长,算法效率越高,上述的这种选取基准值的方法在处理已经有序或基本有序的序列时,效率会大大降低,特别是面对完全有序序列是,时间复杂度会达到O(N2),与冒泡排序相当,所以,对于选取基准值的方式,可以进行优化:
优化方式1:随机选取基准值
在每次的待排序区间随机选取一个数做为基准值,以这个基准值来划分左右序列,但是这种方法优于第一种方式,但是也不太稳定,有可能出现多次选取的基准值刚好是待排序区间的最大值或最小值,这样的话面对有序序列时还是会出现第一种方式出现的问题,降低效率。
优化方式1:三数取中法选取基准值
选取待排序区间的第一个数据,最后一个数据和最中间的一个数据进行比较,并且保证arr[mid]>=arr[low]>=arr[high],这种方法可以在一定程度上提高处理有序序列时的效率。
代码:
public static void swap(int[] arr,int low,int high){
int tmp = arr[low];
arr[low] = arr[high];
arr[high] = tmp;
}
public static void medianOfThree(int[] arr,int low,int high){
int mid = (low+high)>>>1;
//arr[mid]<=arr[low]<=arr[high]
//确保arr[mid]<=arr[low]
if(arr[mid]>arr[low]){
swap(arr,low,mid);
}
//确保arr[mid]<=arr[high]
if(arr[mid]>arr[high]){
swap(arr,mid,high);
}
//确保arr[low]<=arr[high]
if(arr[low]>arr[high]){
swap(arr,low,high);
}
}
public static void quick(int[] arr,int low,int high){
if(low == high){
return;
}
//先三数取中,再找基准
medianOfThree(arr,low,high);
int par = partion(arr,low,high);
//递归左边:保证左边有两个数据以上
if(par>low+1){
quick(arr,low,par-1);
}
//递归左边:保证左边有两个数据以上
if(par<high-1){
quick(arr,par+1,high);
}
}
排序过程的优化:
快速排序每次都会将比基准值小的值放在基准值前面,比基准值大的值放在它后面,因此,在数据量较大的情况下,将会出现越往后越有序的情况,根据直接插入排序越有序越快的特性,可以规定在子序列大小小于某一值时使用直接插入排序对该子序列进行排序。
代码:
import java.util.Arrays;
import java.util.Random;
/***
* 快速排序(递归写法)优化:
* 快速排序越往后越有序,根据直接插入排序越有序越快的特性,可以规定在小于某一区间时使用直接插入排序
* 时间复杂度:O(nlog2n)
* 空间复杂度:log2n
* 稳定性:不稳定
*/
public class MyQuickSort2 {
public static void main(String[] args) {
int[] arr =new int[100];
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(100)+1;
}
System.out.println("原数组为:\n"+Arrays.toString(arr));
MyQuickSort(arr);
System.out.println("(递归优化)快速排序后的数组为:\n"+Arrays.toString(arr));
//判断,若最终结果不是有序,则打印false
for(int i = 0;i<arr.length-1;i++){
if(arr[i]>arr[i+1]){
System.out.println("false!");
}
}
}
//快速排序
public static void MyQuickSort(int[] arr){
quick(arr,0,arr.length-1);
}
public static void insertSort(int[] arr,int low,int high){
int tmp = 0;
for(int i = low+1;i<=high;i++){
tmp = arr[i];
int j = i-1;
for(;j>=low;j--){
if(arr[j]>tmp){
arr[j+1] = arr[j];
}else{
break;
}
}
arr[j+1] = tmp;
}
}
public static void quick(int[] arr,int low,int high){
if(low == high){
return;
}
if(high-low+1<=15){
insertSort(arr,low,high);
return;
}
int par = partion(arr,low,high);
//递归左边:保证左边有两个数据以上
if(par>low+1){
quick(arr,low,par-1);
}
//递归左边:保证左边有两个数据以上
if(par<high-1){
quick(arr,par+1,high);
}
}
//找基准,基准两边已经有序
public static int partion(int[] arr,int low,int high){
int tmp = arr[low];
while(low<high){
while((low<high)&&arr[high]>=tmp){
high--;
}
if(low==high){
break;
}else{
arr[low] = arr[high];
}
while((low<high)&&arr[low]<=tmp){
low++;
}
if(low==high){
break;
}else{
arr[high] = arr[low];
}
}
arr[low] = tmp;
//或:arr[high] = tmp;
return low;
}
}