稳定性排序概念
稳定性排序和非稳定性排序:通常把参加排序的项称为排序码或者排序项,排序序列可能含有多个相同的排序码,如果排序前后它们的相对位置保持不变化,则排序方法就是稳定的,否则就是非稳定的。
例如a和b相同,排序前a在b前面,排序后a依旧在b前面,这种排序就是稳定性排序。
快速排序的核心思想
在当前待排序的序列中,选择一个元素作为分界元素【pivot】,把小于等于该分界元素的所有元素都移动到分界元素的左边,把大于等于该分界元素的所有元素都移动到分界元素的右边,这样分界元素正好处于排序的最终位置上,并且得到了两个子序列,前一个子序列的元素都小于等于比分界元素,后一个子序列的元素都大于等于分界元素。然后分别对这两个子序列递归的进行上述过程,直到所有的元素都到达排序后它们应处的最终位置上。
基于该算法的核心思想可以看到,该排序是非稳定性排序
时间复杂度分析
最优情况:每一次分界元素最终正好定位于序列的中间,从而把当前待排序的序列分成了两个长度相等的子序列,则对长度为n的序列进行快速排序所需要的时间复杂度为O( n l o g 2 n n{log_2{n}} nlog2n)
T(n) = n + 2T(n/2) = 2n + 4T(n/4) = 4n + 8T(n/8) = … = ( l o g 2 n ) n ({log_2{n}})n (log2n)n + nT(1)
最坏情况:针对于有序数组,每一次分界元素都落在数组的一端,第一趟排序需要n-1次比较次数,第二趟排序需要n-2次比较次数;以此类推,总的比较次数为(n-1)+(n-2)+…+1=n(n-1)/2 = O( n 2 n^2 n2)
算法实现分析
重点是如何确定分解元素的最终位置,选择最左端元素为分界元素(也称为枢轴、支点或基准元素),定义两个变量i和j,j从右往左寻找小于分界元素的元素,i从左往右寻找比分界元素大的元素,找到后交换i与j对应元素的位置,先移动i再移动j,当i与j相遇时就是分界元素排序后的最终位置。
思考1:i和j谁先移动?会不会有区别?
待排序序列[3, 5, 3, 5, 1],枢纽元素选择分片最左端元素,对以下四种情况进行分析:
- i小于等于枢纽元素时前进,j大于等于枢纽元素时前进,先移动i,再移动j
- i小于等于枢纽元素时前进,j大于等于枢纽元素时前进,先移动j,再移动i
- i小于枢纽元素时前进,j大于枢纽元素时前进,先移动i,再移动j
- i小于枢纽元素时前进,j大于枢纽元素时前进,先移动j,再移动i
一:i小于等于枢纽元素时前进,j大于等于枢纽元素时前进,先移动i,再移动j
[3, 5, 3, 5, 1]
第一次循环后 i=1, j=4; 交换后[3, 1, 3, 5, 5]
第二次循环后 i=3, j=3时退出循环, i和j都指向了5,
则分界元素最终位置在索引3处[5, 1, 3, 3, 5]
显然存在5比枢纽元素3大,逻辑不对
二:i小于等于枢纽元素时前进,j大于等于枢纽元素时前进,先移动j,再移动i
[3, 5, 3, 5, 1]
第一次循环后 i=1, j=4; 交换后[3, 1, 3, 5, 5]
第二次循环后 i=1, j=1;退出循环, [1, 3, 3, 5, 5]逻辑正确
三:i小于枢纽元素时前进,j大于枢纽元素时前进,先移动i,再移动j
[3, 5, 3, 5, 1]
第一次循环后 i=1, j=4; 交换后[3, 1, 3, 5, 5]
第二次循环后 i=3, j=3时退出循环, i和j都指向了5,
则分界元素最终位置在索引3处[5, 1, 3, 3, 5]
同样逻辑不对
四:i小于枢纽元素时前进,j大于枢纽元素时前进,先移动j,再移动i
[3, 5, 3, 5, 1]
第一次循环后 i=1, j=4; 交换后[3, 1, 3, 5, 5]
第二次循环后 i=1, j=1;退出循环, [1, 3, 3, 5, 5]逻辑正确
结论:在分界元素为最左端元素时,先移动i后移动j,最终i与j指向的元素可能比分界元素大,所以需要先移动j后移动i。
思考2:i<=分界元素,j>=分界元素再前进时,快速排序是不是变成了非稳定排序?
不会
依旧选择最左端元素为分界元素,先移动j再移动i。
[3, 1, 3, 1, 5] 第一次循环,i=j=3,之后就退出循环,交换分界元素的位置发现,原先两个3的相对位置发生了改变
代码
模板代码
public class QuickSort {
public static void quickSort(int[] a) {
quickSort(a, 0, a.length - 1);
}
/**
* 对数组i-j区间进行快排
* @param a 数组
* @param l 起始索引
* @param h 结束索引
*/
private static void quickSort(int[] a, int l, int h) {
if (l >= h) {
return ;
}
int p = partition(a, l, h);
quickSort(a, l, p - 1);
quickSort(a, p + 1, h);
}
private static void swap(int[] data, int i, int j) {
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
测试代码
import java.util.Arrays;
import java.util.Random;
public class SortApp {
public static void main(String[] args) {
int[] a = buildArray();
System.out.println(Arrays.toString(a));
QuickSort.quickSort(a);
System.out.println(Arrays.toString(a));
isSorted(a);
}
public static int[] buildArray(){
int num = 10000;
int[] a = new int[num];
Random random = new Random();
for (int i = 0; i < num; i++) {
a[i] = random.nextInt(10000);
}
return a;
}
/**
* 利用冒泡排序的思想 验证数组是否有序
* @param data
*/
public static void isSorted(int[] data) {
// 两两比较如果没有发生交换那说明有序了
for (int i = 0; i < data.length - 1; i++) {
if (data[i] > data[i + 1]) {
System.out.printf("无序, 索引{%s}, 值{%s}%n", i, data[i]);
return;
}
}
System.out.println("有序");
}
}
partition版本V1
private static int partition(int[] a, int l, int h) {
// 基准点为最左端元素pv
int pivot = a[l];
// 这里的i不能等于l+1, 只有序列只有两个元素的时候不会进入循环
// 直接swap(a, l, i), 导致两个元素的序列一定会交换位置, 显然逻辑不对
// int i = l + 1;
int i = l;
int j = h;
while (i < j) {
while (i < j && a[j] > pivot){
j--;
}
// 这里要 <= 否则 < 的时候 一开始索引i对应的值就是pivot, 直接与j交换了位置, 这样逻辑就错了.
while (i < j && a[i] <= pivot){
i++;
}
// 退出上面的两个循环: ①i==j; ②i>j, 代表j找到比pivot小的值, i找到比pivot大的值
if (i < j) {
swap(a, i, j);
// System.out.println("l:"+l +" h:"+h+" pivotIndex:" + l + ", pivot:" + pivot + ", i:" + i + ", j:"+j + "\t" + Arrays.toString(a));
}
}
swap(a, l, j);
// System.out.println("l:"+l +" h:"+h+" pivotIndex:" + l + ", pivot:" + pivot + ", i:" + i + ", j:"+j + "\t" + Arrays.toString(a));
return j;
}
注意:
①如果i与j对应的元素与分界元素相同时不移动数据,那么当待排序序列元素相同时会产生死循环,因为i和j的值都不会再变化。
②如果i定义为l+1, 当待排序序列只有两个元素时一定会交换位置
partition版本V2
使用do…while,先执行j–和i++操作, 这样可以实现元素大于分界元素再移动j,元素小于分界元素时再移动i,不会出现死循环
private static int partition(int[] a, int l, int h) {
// 基准点为最左端元素pv
int pivot = a[l];
// 这里的i不能等于l+1, 只有序列只有两个元素的时候不会进入循环
// 直接swap(a, l, i), 导致两个元素的序列一定会交换位置, 显然逻辑不对
// int i = l + 1;
int i = l;
int j = h + 1;
while (i < j) {
do{
j--;
} while (i < j && a[j] > pivot);
// 先i++, 因为第一个就是基准元素, 不需要和自己比较
do{
i++;
}while (i < j && a[i] < pivot);
// 退出上面的两个循环: ①i==j; ②j+1=i, 代表j找到比pivot小的值, i找到比pivot大的值
if (i < j) {
swap(a, i, j);
// System.out.println("l:"+l +" h:"+h+" pivotIndex:" + l + ", pivot:" + pivot + ", i:" + i + ", j:"+j + "\t" + Arrays.toString(a));
}
}
// 这里必须与j交换, i最终指向的是一个比分界元素大的数, j正好指向一个比分界元素小的数,
swap(a, l, j);
// System.out.println("l:"+l +" h:"+h+" pivotIndex:" + l + ", pivot:" + pivot + ", i:" + i + ", j:"+j + "\t" + Arrays.toString(a));
return j;
}
partition版本V3【另一种思路】
以最右端元素为分界元素,记录数组区间内比分界元素小的元素个数counter,最终counter+数组起始索引就是该分界元素排序后的最终位置
private static int partition(int[] a, int l, int h) {
// 选择最右端元素为分界元素
int pivot = a[h];
// 记录l-h区间内比分界元素小的元素个数, 最终+l就是分界元素排序后最终的位置
int counter = l;
for (int i = l; i < h; i++) {
if (a[i] < pivot) {
// 把比分区元素小的元素移动到前面
swap(a, i, counter);
counter++;
}
}
// 将分区元素放置排序后的最终位置
swap(a, h, counter);
return counter;
}