快速排序的核心思想就是在数组中找一个元素作为临界点,大于临界点就放在右边,小于临界点就放在左边,然后再递归调用该方法,对临界点左边的元素和右边的元素进行重复操作,这样就可以使得呈现从小到大的排序。
快速排序——第一版
使用数组的第一个元素作为临界值,所以arr[limit]表示第一个元素,使用指针 i 来遍历数组,并在使用一个 j 来指向小于等于临界值的元素,故让 j 指向第一个元素。
运行时(结合下面的图示一起理解会更好点):
1、 i 自增,并进行判断
- 如果 arr[i] 大于arr[limit],则不进行操作,因为这些值本来就是大于arr[limit]的;
- 若 arr[i] 小于arr[limit],则让 j 加一,并且让 arr[i]、arr[j] 交换位置,使得 j 指向的仍是小于等于arr[limit]的元素。
2、重复第一步骤直到 i > arr.length-1
3、最后让 arr[j] 与 arr[limit] 交换。这样就使得 j 左边的全是小于等于自己的,右边的全是大于自己的,然后递归调用将左边的和右边的数组段排序。
例题图示
初始状态:
不断运行,执行第一步,这里因为直到 arr[i] == 1 都是大于arr[j]的,所以只执行 i++ ,所以这里演示 arr[i] == 1 的操作图解。
执行 j++,因为从 j 到 i 的所有元素都是大于limit的,所以如果 arr[j]、arr[i]交换位置,则可以使arr[j]仍然小于等于arr[limit],而 j 右边的所有元素仍然大于arr[limit]
以此类推,你可以先演算一遍,有助于理解快速排序,第一阶段排序完之后的结果我放在下图。
最后将 j 返回,返回之后才能直到下一段要排序的为 arr[0... j-1] 和 arr[j+1...arr.length-1]。
代码实现
import java.util.Random;
/**
* @Description:快速排序版本一
* @Author: hour
* @DATE: 2021/7/16 16:31
* @PROJECT_NAME: quickSort
*/
public class QuickSort01 {
public <E extends Comparable<E>> void sort(E[] arr) {
sort(arr, 0, arr.length-1);
}
private <E extends Comparable<E>> void sort(E[] arr, int start, int end) {
/* 设置跳出条件 */
if (start >= end) { return ;}
int mid = partition(arr, start, end);
sort(arr, start, mid-1);
sort(arr, mid+1, end);
}
/**
* 快速交换的核心代码部分:
* 1、 使用i遍历数组并进行判断
* 如果arr[i]大于arr[start](第一个元素就是临界值),则继续执行;
* 若arr[i]小于arr[start],则让j++,并且交换两个的值
* 2、 循环结束,交换arr[j] 和 arr[start](即临界值)
* 3、 返回临界值所在位置下标,作为分片临界
* @return
*/
private <E extends Comparable<E>> int partition(E[] arr, int start, int end) {
int i = start + 1;
int j = start;
for (;i <= end;i++) {
if (arr[i].compareTo(arr[start]) < 0) {
swap(arr, i , ++j);
}
}
swap(arr, j, start);
return j;
}
/**
* 将arr数组中的下标为first和second的元素交换
* @param arr 待交换的数组
* @param first 第一个元素的下标
* @param second 第二个元素的下标
* @param <E> 泛型的标志
*/
private <E> void swap(E[] arr, int first, int second) {
E temp = arr[first];
arr[first] = arr[second];
arr[second] = temp;
}
}
附测试代码:
import java.util.Random;
public class QuickTest {
public static void main(String[] args) {
Integer[] arr = getRandomArrays(10);
QuickSort01 quickSort01 = new QuickSort01();
quickSort01.sort(arr);
for(int i = 0;i < arr.length-1; i++)
System.out.print(arr[i] + " ");
}
/** 获取长度为len的随机数组 */
private static Integer[] getRandomArrays(int len) {
Random random = new Random();
Integer[] arr = new Integer[len];
for (int i = 0; i < len; i++) {
arr[i] = random.nextInt(len);
}
return arr;
}
}
版一问题的指出与解决
显然,如果一个数组本来就有序(从小到大),但是如果使用版一,显然限制就很大,他一样会一层层遍历,比如说一个数组为{1,2,3,4,5,6,7,8,9},使用版一运行顺序应该如下:
- 使用limit、j 指向1,i 指向2;然后使用 i 遍历 1 右边的所有数,
- 最后limit与 j 交换,因为 j 没有执行过加一,所以就是和本身交换。
第一步和第二步相加的操作数为操作数组段的元素个数。
之后便是要对arr[j+1...arr.length-1]进行排序,也是重复上述操作,每次要进行的操作都与元素个数相同,所以整一套流程下来算法复杂度为 O(
),而这不是最难受的,更难受的是他的递归深度是O(n),而之前为什么不管是大学老师还是有些书上我们会有让我们尽量不要使用递归就是因为递归深度越大要使用的内存就越大,会相当耗费内存,且很容易爆出栈溢出,当我测试十万条有序数组时,他出现如下错误:
Exception in thread "main" java.lang.StackOverflowError
但对于这种情况可以通过修改VM Option,添加一个值 "-Xss128m",此时就不会出现栈溢出了,但结果却高达 12.607584606s
所以问题该如何解决?那么只要在数组中随机选择一个临界就可以有效避免这种麻烦,因为这样操作之后就不需要每次都轮询 n-1 个元素。
而这个实现也是相当简单。
代码
import java.util.Random;
/**
* @Description:快速排序版本一
* @Author: hour
* @DATE: 2021/7/16 16:31
* @PROJECT_NAME: quickSort
*/
public class QuickSort01 {
private Random random = new Random();
public <E extends Comparable<E>> void sort(E[] arr) {
sort(arr, 0, arr.length-1);
}
private <E extends Comparable<E>> void sort(E[] arr, int start, int end) {
/* 设置跳出条件 */
if (start >= end) { return ;}
int mid = partition(arr, start, end);
sort(arr, start, mid-1);
sort(arr, mid+1, end);
}
private <E extends Comparable<E>> int partition(E[] arr, int start, int end) {
// todo : 这里修改了,使得每次的临界值都是随机的
int mid = start + random.nextInt(end-start+1);
swap(arr, mid, start);
int j = start;
for (int i = start;i <= end;i++) {
if (arr[i].compareTo(arr[start]) < 0) {
swap(arr, i , ++j);
}
}
swap(arr, j, start);
return j;
}
private <E> void swap(E[] arr, int first, int second) {
E temp = arr[first];
arr[first] = arr[second];
arr[second] = temp;
}
}
比较结果如图 :
结果显著,差距不是一星半点。当然每次运行的结果肯定不一样,毕竟这个临界点是随机抽取的,所以他的时间复杂度仍然是O(),因为最差的情况可以使每次都抽中了最小的元素,但随着数据规模变大这种情况出现的概率可以忽略不计。
版一剩余问题
可以,有序问题基本解决,但也指出了如果每次运气都不好,抽中“最小”的了呢?
虽然我不能靠运气每次选中最小,但是我可以强迫他选择最小——元素全部相等,在这种情况下,随机选择的情况会如何?
这个问题又当如何解决,复杂度又变成了 O()。这时有人提出了双路快排,先介绍一下双路快排。
双路快排:(下面的操作如果觉得理解起来有点空洞,不妨虽然用一个例子试试,自己画个图)
- 让limit执行第一个元素,也就是作为临界点,另外使用两个指针,一个指针 i 指向start+1;另一个指针 j 指向end,两个指针从两头开始遍历
- 当 arr[i] >= arr[limit]时,跳出循环,并让 j 开始遍历直到arr[j] <= arr[limit],否则 i++ ;
- 两个遍历结束交换arr[i] 和 arr[j]的值,并且执行 i++ 、 j++ ,使得指向下一个元素继续执行遍历,重复操作2,直到 i >= j ,说明已经将所有元素遍历完成。
- 最后交换arr[limit] 和 arr[j]
- 结果仍然分布为 j 的左边为小于等于arr[limit],右边大于arr[limit]
- 最后返回 j ,然后对arr[start...j-1] 和 arr[j+1...end]进行排序
执行完这一些列操作就可以使得
代码实现
import java.util.Random;
/**
* @Description:快速排序版本一
* @Author: hour
* @DATE: 2021/7/16 16:31
* @PROJECT_NAME: quickSort
*/
public class QuickSort01 {
private Random random = new Random();
public <E extends Comparable<E>> void sort2Ways(E[] arr) {
sort2Ways(arr, 0, arr.length-1);
}
private <E extends Comparable<E>> void sort2Ways(E[] arr, int start,int end) {
if (start >= end) { return; }
int mid = partition2Ways(arr, start, end);
sort2Ways(arr, start, mid-1);
sort2Ways(arr, mid+1, end);
}
private <E extends Comparable<E>> int partition2Ways(E[] arr, int start, int end) {
int mid = start + random.nextInt(end-start);
swap(arr, mid, start);
int i = start +1;
int j = end;
while (true) {
/* 这里的 i <= j是必要的,如果第一次循环就满足该条件的情况也需要考虑 */
while (i<=j && arr[i].compareTo(arr[start]) > 0) {
i ++;
}
/* 而这个主要考虑的是运行时执行了多个i++之后的情况,所以要判断j >= i */
while (j>=i && arr[j].compareTo(arr[start]) < 0) {
j --;
}
if (i >= j) { break; }
swap(arr ,i ,j);
i ++;
j --;
}
swap(arr, i , start);
return i;
}
private <E> void swap(E[] arr, int first, int second) {
E temp = arr[first];
arr[first] = arr[second];
arr[second] = temp;
}
}
运行结果如下图:
或许有人要说了,哪有这种情况,全部元素相等,这不是扯淡吗?不实用好吧。
可是这种情况只是适用于大量元素相等的情况,这种情况就会多的多了,只要相同的数据越多,这个算法就可以有越大优势。
双路快排剩余问题
我们从上面的双路快排可以知道,我们是把数组分为两部分,大于等于临界值,小于等于临界值,但是我们知道,如果有很多元素都等于临界值,这些等于临界值的元素仍然会被排序,但其实我们都知道这些元素不需要再排序了,如果让这些元素不排序,就会节约很多资源。
那么这个问题如何解决大家应该都有想法了,分为三段,一段小于临界值、一段大于临界值
、一段等于临界值。没错,确实如此,来看看如何操作吧。
第一步——初始
数组元素如下,假设第一个元素就是抽中的临界点i = start+1,
arr[start+1...lt]值都小于arr[limit]
arr[gt...end]值都大于arr[limit]
arr[lt+1...gt-1]值都等于arr[limit],当i >= gt 的时候就结束了。
第二步——i 开始遍历数组
要让 lt 和 gt 实现上述的意义,就必须执行以下判断:
- arr[i] > 6:gt--,交换arr[i] 和 arr[gt],注意这里 i 不执行加一,因为转换过来的元素仍然可能大于临界值
- arr[i] == 6:不做任何操作,直接 i++
- arr[i] > 6:lt++,交换arr[i] 和 arr[lt],注意这里需要执行i++,比如说数组{9,4,3,5},使用三路排序,且抽中的临界就是9,执行一次lt++后,然后 i 没有改变,但会继续循环,这时又会使得lt++,这时就出现了错误,本来 lt 不应该大于 i ,上面的区间也能说明,简单想想也可以求证,就不赘述了
一轮操作完毕后,并且执行完arr[start]和arr[lt]的交换后
显然,第一次遍历的结果与我们推出的一样。而实际结果也和代码结果一样
代码实现
import java.util.Random;
/**
* @Description:快速排序版本一
* @Author: hour
* @DATE: 2021/7/16 16:31
* @PROJECT_NAME: quickSort
*/
public class QuickSort01 {
private Random random = new Random();
public <E extends Comparable<E>> void sort3Ways(E[] arr) {
sort3Ways(arr, 0, arr.length-1);
}
private <E extends Comparable<E>> void sort3Ways(E[] arr, int start,int end) {
if (start >= end) { return; }
int[] mid = partition3Ways(arr, start, end);
sort3Ways(arr, start, mid[0]);
sort3Ways(arr, mid[1], end);
}
private <E extends Comparable<E>> int[] partition3Ways(E[] arr, int start, int end) {
int mid = start + random.nextInt(end-start+1);
swap(arr, mid, start);
int i = start +1;
int gt = end + 1;
int lt = start;
while(i < gt) {
if (arr[i].compareTo(arr[start]) < 0) {
lt ++;
swap(arr ,lt ,i);
i ++;
} else if (arr[i].compareTo(arr[start]) > 0) {
gt --;
swap(arr, gt, i);
} else {
i ++;
}
}
swap(arr, lt , start);
System.out.println();
return new int[]{lt ,gt};
}
private <E> void swap(E[] arr, int first, int second) {
E temp = arr[first];
arr[first] = arr[second];
arr[second] = temp;
}
}
至此,我想总结的就这么多了,有啥问题可以说出来,初出茅庐。
对了如果需要视频资源可以向我买,老弟我也是买的,视频资源还包括很多其他算法。
详情请见https://download.csdn.net/download/qq_50679242/20334507