快速排序
快速排序是对冒泡排序的改进,冒泡排序的缺陷就是一轮走完虽然也做了大量的交换,但是剩下的数据之间没有关系下一次还要再重新找一个最大的。而快速排序对它的改进是,一轮走完不会找到最大的值,而是采用了分治思想将数分为左小右大的两部分。然后这两部分再递归的进行这种轮,最终慢慢趋于有序。
基本思想
老韩思想
选取一个基准值,然后将所有的数据分为左右两部分,左边小于等于基准值,右边大于基准值。注意点,基准值是取出来做一个参照的,遍历的时候还是遍历整个数据集的,而不会跳过这个挑出来的基准值。额,感觉还是看看代码然后debug吧。
算法导论思想
这个就比较好理解了,接收参数quickSort(arr,p,r),arr就是需要排序的那个数组,arr = [p,.....,r],p和r就是这个数组的开始和结束索引。arr[r]也就是最后一个元素作为基准。然后设一个i等于p-1,这i就是指向小的那部分的最后一个的下标,由于一开始还没有所以指向p-1,就是表示溢出。开始循环for j = p to r - 1,这个意思就是循环p到r-1,要将它们处理分到小部分还是大部分,取出的那个下标用j来保存。每个j对应的数据取出来之后,如果它的值小于等于基准,那么就放入到小部分,大于基准就放入大部分。
需要知道以下,k是任意一个下标
- p<=k<=i表示小于等于基准那部分
- i
大于基准的部分 - j
- k == r是基准,基准是不需要处理(分部分)的
最终,j == r的时候分好部分了,最后将基准和大部分的第一个值进行交换就能够完成一轮处理,然后可以进行递归。进行快排的元素小于等于1的时候就完成了排序。
代码实现
这里有两种实现思想
尚硅谷老韩
package _6_sort;
import java.util.Arrays;
import java.util.Date;
public class QuickSort {
public static void quickSort(int[] data, int left, int right) {
//取第一个数作为基准,基准随便选一个都可以的
int pivot = data[left], l = left, r = right;
int temp;
while (l < r) {
//找到左边一个大于等于基准的
// 如果没有这个等于,可能基准就是最大值的话,
// 然后找不到了(会一直找下去),还需要加一个限制不让下标溢出
while (data[l] < pivot) l++;
//找到右边一个小于等于基准的
while (data[r] > pivot) r--;
if (r <= l)
//已经分好了,就两种情况,一:两边都找到了基准(说明基准左边都是小于等于基准的,右边都是大于等于基准的)
// ,二:已经分好了
break;
//交换
temp = data[r];
data[r] = data[l];
data[l] = temp;
//注意:虽然两个都是符合的,而且不需要下次再试的了,但是如果两个同时移位的话,会出现问题,
//就是比如说现在基准是4,l指向2,r指向了4,他们中间有个3,如果两个同时进行移位的话就出现
//l、r同时指向一个不是基准的值,这样下面进行处理的时候就不能又两个同时移动了,也就是出现了两种情况
//就又得if-else进行判断然后分别处理了,还得判断这个不是基准的值到底小于基准还是大于基准(麻烦)
if (data[l] == pivot) r--;
if (data[r] == pivot) l++;
//为什么data[l] == pivot却要移动r,也就是为什么移动不是pivot那个值呢?
//这也是为了想要等于跳出这个循环必须由上面的if来完成,保障等于的时候一定指向pivot
//如果此时现在基准是4,l指向一个2,r指向一个4然后他们两个是挨着的,那移动了r的话
//就直接l==r出去了,然后此时data[r]等于2,经过下面的处理必然又出错了
}
//如果此时r==l(此时这个位置肯定等于基准值,这个位置不需要再排序了,值是处于中间位置的),
// 那么需要进行移位,不然递归的时候会出现两个部分重合的情况
if (r == l) {
r--;
l++;
}
if (left < r) {//如果这一部分小于等于1的话就不用再递归了,直接是有序的
quickSort(data, left, r);
}
if (right > l) {
quickSort(data, l, right);
}
}
//测试代码
public static void main(String[] args) {
// int[] arr = RandomProducer.produceRandom(-500, 100000000, 8000000);
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 8, 9, 1, 7, 2, 3, 5, 4, 8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
// System.out.println("排序前");
// System.out.println(Arrays.toString(arr));
Date from = new Date();
quickSort(arr, 0, arr.length - 1);
// ShiftShellSort(arr);
// System.out.println("排序后");
System.out.println(Arrays.toString(arr));
System.out.println("运行了" + (new Date().getTime() - from.getTime()) + "毫秒");
}
}
算法导论第三版实现方式
package _6_sort.AlgorithmBooks;
import _6_sort.RandomProducer;
import java.util.Arrays;
import java.util.Date;
/**
* author waigo
* create 2021-01-28 11:31
*/
public class QuickSort {
//partition(A,p,r),这个方法用来分开那个区域
//quickSort(A,p,r),处理区域[p,...,r] E A
public static int partition(int[] A, int p, int r) {
int x = A[r];
int i = p - 1;
for (int j = p; j < r; j++) {
if (A[j] <= x) {
i++;
exchange(A, i, j);
}
}
exchange(A, i + 1, r);
return i + 1;
}
public static void exchange(int[] A, int aIndex, int bIndex) {
if(aIndex!=bIndex){
int temp = A[aIndex];
A[aIndex] = A[bIndex];
A[bIndex] = temp;
}
}
//A = [p,....,r]
public static void quickSort(int[] A, int p, int r) {
if (p < r) {
int q = partition(A, p, r);
quickSort(A, p, q - 1);
quickSort(A, q + 1, r);
}
}
//测试代码
public static void main(String[] args) {
int[] arr = RandomProducer.produceRandom(-500, 100000000, 8000000);
// int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
// System.out.println("排序前");
// System.out.println(Arrays.toString(arr));
Date from = new Date();
quickSort(arr, 0, arr.length - 1);
// ShiftShellSort(arr);
// System.out.println("排序后");
// System.out.println(Arrays.toString(arr));
System.out.println("运行了" + (new Date().getTime() - from.getTime()) + "毫秒");
}
}
可以看出,算法导论这种实现方式的好处是不需要考虑那么多条件,只要循环不变量设置好,后面就只要处理判断分边的问题了。经过测试,虽然实现的方式不同,但是由于算法的思想内核是相同的,所以这两种方式的速度不相伯仲。而快排比起希尔来说真的是又上了一个档次,我的电脑测试800,0000条数据的排序才用了1200ms左右。而移位式希尔排序800,0000条数据需要差不多2500ms。
不愧是快速排序,还真是名不虚传。不过我们这个一次只能处理完一个就是那个pivot,而和pivot相同的还得走下面的流程再次递归,所以可以改良。
上面讲的就是1.0版的快排,核心是"01partition"的操作,下面来介绍一个新玩意儿,荷兰国旗问题,就是将一个数组分为三块,左边小于pivot,中间等于pivot,右边大于pivot,这就是升级版的partition.
思想:还和partition一样整一个指针smallTop指向最小区的头,不过现在需要一个指针指向最大区的头bigLow,我们这里的pivot就取最后一个值,所以最大区的头一开始指向最后一个位置,若是pivot从外面传进来就可以指向r+1(r指的是处理区域的最右边)位置,要灵活一点。然后一个指针j从p位置(处理区域开始位置)开始遍历,若是小于pivot则进入小于区(j指针后移),等于pivot就直接后移,大于的时候需要注意,由于从大于区换下来的元素是还没看过的,所以要停在原地继续看这个新换来的。这里的循环结束条件和partition可不同,不能再设置为j
show you my code.
/**
* @param arr 待处理的数组
* @param p 处理区域[p...r]
* @param r
* @return 下标0:小于x的top 下标1:大于x的low
* 经过荷兰国旗处理,[p...smallTop] 小于x,[smallTop+1...bigLow-1]等于x,[bigLow...r]大于0
*/
public static int[] netherlangsFlag(int[] arr, int p, int r) {
int smallTop = p - 1, bigLow = r;
int j = p, pivot = arr[r];
while (j < bigLow) {
if (arr[j] < pivot) swap(arr, ++smallTop, j);
else if (arr[j] > pivot) swap(arr, --bigLow, j--);//这个bigLow位置的元素还没看过,所以j需要留在原地再看一遍
j++;//等于的时候直接移动j不用进行交换操作
}
swap(arr, bigLow++, r);
return new int[]{smallTop, bigLow};
}
利用荷兰国旗操作升级快排2.0
/**
* quickSort2.0版本,使用荷兰国旗操作进行优化,一次将等于x的全部搞定
* @param arr
*/
public static void quickSort2(int[] arr) {
process2(arr, 0, arr.length - 1);
}
public static void process2(int[] arr, int p, int r) {
if (p < r) {
int[] flag = netherlangsFlag(arr, p, r);
int smallTop = flag[0], bigLow = flag[1];
process2(arr, p, smallTop);
process2(arr, bigLow, r);
}
}
升级点,一次解决了等于pivot的一堆数据。这就是最经典的快排。
不过上面两个算法其实是O(N2)的算法,注意算法的时间复杂度是按最差情况来算的,假设一个算法直接就是有序的然后进行快排,那么每次处理完成的都是最后一个,然后剩下全部进入下一轮处理,这个时间复杂度就是O(N2),那么升级点就是让它不要每次都只是处理最后一个,而是随机挑一个位置做pivot,这好处就是下一轮两边可能数量是比较均衡的,这样就减少了递归的次数。
额,你可能会说了,既然算法按照最差情况来估时间复杂度,那最差情况能不能就是每次随机找到的都是最后一个。这里还得解释一下,这个最差情况说的是同一批数据进行处理时流程是固定的,就是像1.0和2.0版本,不管你怎么试都会是最后一个做pivot。但是3.0版本这个随机pivot是随机生成的,你无法保证会出现在哪里,所以最差情况就不能说它全生成在r位置。经过研究,随机快排也就是快排3.0,时间复杂度为O(N*logN),空间复杂度为O(logN)
2.0-->3.0,show you my code.
其实其他和2.0都一样,就加一个随机pivot选择
public static void process3(int[] arr, int p, int r) {
if (p < r) {
//(int) (Math.random() * (r - p + 1)) --> [0,r-p] + p -->[p,r]
int pivotIdx = (int) (Math.random() * (r - p + 1)) + p;//[p,r],+p别忘了,偏移量
swap(arr, pivotIdx, r);//x放在r位置
int[] flags = netherlangsFlag(arr, p, r);
process3(arr, p, flags[0]);
process3(arr, flags[1], r);
}
}