递归和快速排序
首先介绍D&C递归
快速排序的思想是:分而治之(divide and conquer,D&C)一种递归式问题解决思路
这里先介绍D&C的工作原理
1)找出简单的基线条件
2)确定如何缩小问题的规模,使其符合基线条件。
看一个例子。
给定一个数组 {2 4 6},把这些数组相加返回一个结果,使用循环很容易完成。
但是如果使用递归函数来完成呢。
第一步:找出基线条件。最简单的数组是什么样呢?如果数组不包含任何元素或只包含一个元素,计算总和将非常容易。
因此这就是基线条件。
第二部:每次递归都必须离空数组更进一步,缩小问题的规模。
sum(2,4,6)=2+sum{4,6} 给函数sum传递的数组更短。换言之 这缩小了问题的规模。
函数sum的工作原理类似这样
接受一个数组
如果列表为空就返回0
否则,计算表中除第一个数字意外其他数字的总和,将其与第一个数组相加,再返回结果。
sum(2,4,6)=2+sum{4,6}。sum{4,6}=4+sum(6), sum{6}=6+sum{0}. sum{0}是基线条件,返回0
public static int calSum(int[] arr,int n){ if(n>0){ return arr[n-1]+calSum(arr,n-1); }else{ return 0; } }
快速排序
使用快速排序对数组:{3,5,9,6,1}进行排序
基线条件为数组为空或只包含一个元素,在这种情况下,只需要原样返回数组——根本就不用排序。
使用D&C将数组进行分解,直到满足基线条件
下面介绍快速排序的工作原理:
1)首先从数组中选择一个元素,这个元素被称为基准值(pivot)
2)将数组分成两个子数组:小于等于基准值的元素和大于基准值的元素。
package com.sun.sort; public class QuickSort3 { // 测试 QuickSort public static void main(String[] args) { int[] arr = {7,3,2,8,1,9,5,4,6}; sort(arr,0,arr.length-1); for (Object i : arr) { System.out.print(i+" "); } } private static void sort(int[] arr, int l, int r){ if (l>=r){ return; } partition(arr, l, r); } private static void partition(int[] arr, int leftBound, int rightBound){ //取右边界当轴 int pivot=arr[rightBound]; int left=leftBound; int right=rightBound-1; while(left<right){ while (arr[left]<=pivot){ left++; } while (arr[right]>=pivot){ right--; } if (left<right){ swap(arr,left,right); } } //把轴放到该放的正确位置上去 swap(arr,left,rightBound); } private static void swap(int[] arr, int i, int j) { int t = arr[i]; arr[i] = arr[j]; arr[j] = t; } }
bug1、上面是将数组分成两个子数组,但是当我们极端测试的时候会存在bug。
比如数组最后一个位置是最大值或者最小值的时候会报数组越界错误。此时我们对代码做如下修改
while (left<right&&arr[left]<=pivot){ left++; } while (left<right&&arr[right]>=pivot){ right--; }
bug2、当数组最后一个是最大值10当轴的时候分割出现错误
arr{4,6,10}分割成-->arr{4,10,6} 最后swap的时候 6(left=1)和10(rightBound=2)交换了,
所以当left=right的时候 如果arr[left]<=arr[pivot] left也要++
这个时候就需要改成
while (left<=right&&arr[left]<=pivot){ left++; } while (left<=right&&arr[right]>=pivot){ right--; }
bug3、int[] arr ={8,1,9,5,4,6,10,6};——>4 1 5 6 8 6 10 9 其中6分裂出错 6 1 4 5 6 8 10 9
因为左边小于等于基准值,右边大于基准值 右边出现了等于6的数字
所以改成
while (left<=right&&arr[left]<=pivot){ left++; } while (left<=right&&arr[right]>pivot){ right--; }
3)递归对两个子数组进行快速排序。
package com.sun.sort; public class QuickSort3 { // 测试 QuickSort public static void main(String[] args) { int[] arr ={4,6}; sort(arr,0,arr.length-1); print(arr); } private static void sort(int[] arr, int leftBound, int rightBound){ if (leftBound>=rightBound){ return; } int mid=partition(arr, leftBound, rightBound); sort(arr,leftBound,mid-1); sort(arr,mid+1,rightBound); } private static int partition(int[] arr, int leftBound, int rightBound){ //取右边界当轴 int pivot=arr[rightBound]; int left=leftBound; int right=rightBound-1; while(left<right){ while (left<=right&&arr[left]<=pivot) left++; while (left<=right&&arr[right]>pivot) right--; if (left<right) swap(arr,left,right); } //把轴放到该放的正确位置上去 (轴一定比左边的小 不然left++) swap(arr,left,rightBound); return left; //返回轴的位置 } static void print(int[] arr){ for (int i : arr) { System.out.print(i+" "); } } private static void swap(int[] arr, int i, int j) { int t = arr[i]; arr[i] = arr[j]; arr[j] = t; } }
bug4、当剩下两个数时{4,6}会排序成{6,4},只剩下因为两个数的时候,没有进入whlie的逻辑,最后直接swap了
所以分区改成,让left=right的时候 也可以进入循环
private static int partition(int[] arr, int leftBound, int rightBound){ //取右边界当轴 int pivot=arr[rightBound]; int left=leftBound; int right=rightBound-1; while(left<=right){ while (left<=right&&arr[left]<=pivot) { left++; } while (left<=right&&arr[right]>pivot) { right--; } if (left<right) swap(arr,left,right); } //把轴放到该放的正确位置上去 (轴一定比左边的大 不然left++) swap(arr,left,rightBound); return left; //返回轴的位置 }
快速排序的平均情况和最糟情况
快速排序的性能高度依赖于你选择的基准值。
假设你总将第一个元素用作基准值,且要处理的数组是有序的,由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序。
比如数组{1,2,3,4,5,6,7,8}进行快速排序,
如果选择1作为基准值
结果是数组没有被分成两半,其中一个子数组始终为空,这导致调用栈非常长(栈长为O(n))。整个算法所需要的时间O(n)*O(n)=O(n2)
如果总是将中间的元素作为基准值,
数组被一份为二,调用栈会短很多(栈长为O(log2 n) log以2为底 n的对数) 没一层的的比较和操作是O(N)。整个算法所需要的时间为O(n)*O(logn)=O(nlog 2 n)
总结:快速排序的时候 基准值尽可能的选取中间(大小)值