快速排序介绍
-
它的基本思想是: 选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;
-
其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,
-
整个排序过程可以递归进行,以此达到整个数据变成有序序列。
-
注意:开始一定要从基准数的对面开始检索,要不然i==j时会把比基准数大的值放在了基准数位置上
-
(会把数组中的一个数当做基准数,一般会把数组中最左边的树当做基准数。然后从两边进行检索。先从右边检索比基准数小的。再从左边检索比基准数大的。如果检索到了,就停下,然后交换这两个元素。然后再继续检索。左右检索相遇,就停止检索,把基准数和相遇位置的元素交换。)
快速排序时间复杂度和稳定性
快速排序稳定性
- 快速排序是不稳定的算法,它不满足稳定算法的定义。
- 算法稳定性 – 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
快速排序时间复杂度
-
快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)。
-
这句话很好理解: 假设被排序的数列中有N个数。遍历一次的时间复杂度是O(N),需要遍历多少次呢? 至少lg(N+1)次,最多N次。
-
为什么最少是lg(N+1)次? 快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度,
-
而根据完全二叉树的定义,它的深度至少是lg(N+1)。因此,快速排序的遍历次数最少是lg(N+1)次。
-
为什么最多是N次? 这个应该非常简单,还是将快速排序看作一棵二叉树,它的深度最大是N。因此,快读排序的遍历次数最多是N次。
-
问题一:
如果已是有序或基本有序,为何用快速排序会慢很多?
因为基本有序作为一个二叉树,最大深度是N;每次分块又是N, 就是NN=N2
无序可以作为完全二叉树,至少lg(N+1),每分块又是N,就是 NlgN=lg(N+1)
public class QuickSort {
public static void main(String[] args) {
int[] a = {40, 30, 10, 60, 50, 20};
System.out.printf("quickSortLeft-排序之前:%s", Arrays.toString(a));
System.out.println();
quickSortLeft(a, 0, a.length - 1);
System.out.printf("quickSortLeft-排序之后:%s", Arrays.toString(a));
System.out.println();
// 测试方式不准确
int[] arr = new int[10000];
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt();
}
int[] arr1 = Arrays.stream(arr).toArray();
int[] arr2 = Arrays.stream(arr).toArray();
test(arr1);
test3(arr2);
// test1();
// test2();
}
// 测试随机顺序 耗时
private static void test(int[] arr) {
long start = System.currentTimeMillis();
quickSortLeft(arr, 0, arr.length - 1);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
// 测试从低到高 顺序时耗时
private static void test1() {
int[] arr = new int[10000];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
long start = System.currentTimeMillis();
quickSortLeft(arr, 0, arr.length - 1);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
// 测试从高到低 顺序时耗时
private static void test2() {
int[] arr = new int[10000];
for (int i = arr.length-1; i >=0; i--) {
arr[i] = i;
}
long start = System.currentTimeMillis();
quickSortLeft(arr, 0, arr.length - 1);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
// 测试从高到低 顺序时耗时
private static void test3(int[] arr) {
long start = System.currentTimeMillis();
quickSort(arr, 0, arr.length - 1);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
/**
* 【基准数从左】
*
* @param a
* @param left
* @param right
*/
private static void quickSortLeft(int[] a, int left, int right) {
// 左右相遇直接返回
if (left > right) {
return;
}
// 保存基准数
int base = a[left];//------------------------------>和【基准数从右】就这换一下right
int i = left;
int j = right;
while (i != j) {
// 从右往左检索
while (a[j] >= base && i < j) {//------------------------------>(1) 和【基准数从右】就(1)(2)这换一下顺序
j--;
}
// 从左往右检索
while (a[i] <= base && i < j) {//------------------------------>(2)
i++;
}
// 代码走到这里,都暂停了交换
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
// 如果i和j相遇,停止检索,交换基准数和相遇位置数
a[left] = a[i];//------------------------------>和【基准数从右】就这换一下right a[right] = a[i];
a[i] = base;
// 基准数归位,左边比他小,右边比他大
quickSortLeft(a, left, i - 1);
quickSortLeft(a, i + 1, right);
}
/**
* 优化版本
* @param arr
* @param low
* @param high
*/
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找寻基准数据的正确索引
int index = getIndex(arr, low, high);
// 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
//quickSort(arr, 0, index - 1); 之前的版本,这种姿势有很大的性能问题
quickSort(arr, low, index - 1);
quickSort(arr, index + 1, high);
}
}
private static int getIndex(int[] arr, int low, int high) {
// 基准数据
int base = arr[low];
while (low < high) {
// 当队尾的元素大于等于基准数据时,向前挪动high指针
while (low < high && arr[high] >= base) {
high--;
}
// 如果队尾元素小于base了,需要将其赋值给low
arr[low] = arr[high];
// 当队首元素小于等于base时,向前挪动low指针
while (low < high && arr[low] <= base) {
low++;
}
// 当队首元素大于base时,需要将其赋值给high
arr[high] = arr[low];
}
// 跳出循环时low和high相等,此时的low或high就是base的正确索引位置
// 由原理部分可以很清楚的知道low位置的值并不是base,所以需要将base赋值给arr[low]
arr[low] = base;
return low; // 返回base的正确位置
}
}