排序的基本知识
排序是很重要的,一般排序都是针对数组的排序,可以简单想象一排贴好了标号的箱子放在一起,顺序是打乱的因此需要排序。
排序的有快慢之分,常见的基于比较的方式进行排序的算法一般有六种。
- 冒泡排序(bubble sort)
- 选择排序(selection sort)
- 插入排序(insertion sort)
- 归并排序(merge sort)
- 堆排序(heap sort)
- 快速排序(quick sort)
前三种属于比较慢的排序的方法,时间复杂度在\(O(n^2)\)级别。后三种会快一些。但是也各有优缺点,比如归并排序需要额外开辟一段空间用来存放数组元素,也就是\(O(n)\)的空间复杂度。
快速排序的三种实现
这里主要说说快速排序,通常有三种实现方法:
- 顺序法
- 填充法
- 交换法
下面的代码用java语言实现
可以用下面的测试代码,也可以参考文章底部的整体的代码。
public class Test {
public static void main(String[] args) {
int[] nums = {7,8,4,9,3,2,6,5,0,1,9};
QuickSort quickSort = new QuickSort();
quickSort.quick_sort(nums, 0, nums.length-1);
System.out.println(Arrays.toString(nums));
}
}
递归基本框架
所有的快速排序几乎都有着相同的递归框架,先看下代码
public void quick_sort(int[] array, int start, int end) {
if(start < end){
int mid = partition(array, start, end);
quick_sort(array, start, mid-1);
quick_sort(array, mid+1, end);
}
}
代码有如下特点
- 因为快速排序是原地排序(in-place sort),所以不需要返回值,函数结束后输入数组就排序完成
- 传入quick_sort函数的参数有数组array,起始下标start和终止下标end。这样方便对子数组进行操作。
- 代码使用了分治(divide and conquer)的思想,并用递归来完成
单看递归的框架,思路其实很简单。
- 利用partition函数在数组中找到一个mid下标,通过移动元素,使得mid左边的元素都比mid小,右边的都比mid大。
- 递归地,对mid左边和右边的元素进行快速排序。
- 不断进行下去,区间会越来越小,函数如果start==end说明区间只有一个元素,也就不用排序,这就是终止条件。
快速排序的副产品就是快速选择算法,因为partition函数实际上返回的mid值就是array[mid]在已经排序的array里面所处的顺位。
因此真正的难点就落在了怎么样对数组进行划分了,首先介绍顺序法
顺序法
public int partition(int[] array, int start, int end) {
int firstHigh = start; // > pivot
int pivot = array[end];
for(int j = start; j < end; j++) {
if(array[j] <= pivot) {
swap(array, firstHigh, j);
firstHigh++;
}
}
swap(array, firstHigh, end);
return firstHigh;
}
2-3行是一些指针的定义,这里选取最后一个元素作为主元(pivot),
因此任务也就变成了:调整数组使得pivot左侧的元素都比pivot小,右侧的都比pivot大。
| 小于或者等于pivot | pivot | 大于pivot |
|---|---|---|
array[start...mid-1] |
array[mid] |
array[mid+1...end] |
4-9行是循环体,代表着如何将给定范围的数组变成表格中的样子,不断循环最终做到上面表格中的样子,其中firstHigh变量也大于pivot。
| 小于或者等于pivot | firstHigh | 大于pivot | unexplored |
|---|---|---|---|
array[start...firstHigh-1] |
array[firstHigh] |
array[firstHigh...j-1] |
array[j...end] |
- 初开始循环
j=start,整个数组都是unexplored的状态,firstHigh也等于start。 - 逐个遍历数组元素与pivot比较,
- 如果
array[j] > pivot,可知array[j]处于pivot右边的高区,容易知道j始终大于等于firstHigh,此时不需要做额外操作,j向右移动一位,高区多一个元素。 - 如果
array[j] <= pivot,在左边,array[j]应该移动到左边的低区,只需与firstHigh交换即可,然后firstHigh向右移动一位,低区多一个元素 - j==end循环结束,除了pivot所有的元素都被遍历,小于等于pivot都在
firstHigh左边,大于都在firstHigh右边。 - 此时需要考虑pivot的具体位置,pivot还在最后一位没有动,注意到
firstHigh左侧都比pivot小,因此array[end]与array[firstHigh]交换即可,也就是10行 - 返回的下标就是firstHigh,这就是pivot的位置
这个方法算是比较常见的吧,但也有点不好写。
填充法
public int insertPartition(int[] array, int start, int end) {
int pivot = array[start];
int low = start;
int high = end;
while (low < high) {
while (low < high && array[high] >= pivot) {
high--;
}
array[low] = array[high];
while (low < high && array[low] < pivot) {
low++;
}
array[high] = array[low];
}
array[low] = pivot;
return low;
}
接下来一头一尾两个指针往中间走的做法了,2-4行做基本的设置。主元选在start位置,其实选end也行,留作练习吧。low和high是两个指针分别指向开头和末尾。
这里的partition目标有一些变化,按照那个标准也能写,无非就是小改动。
| 小于pivot | pivot | 大于或者等于pivot |
|---|---|---|
array[start...mid-1] |
array[mid] |
array[mid+1...end] |
5-14行使用while循环,因为使用赋值操作,所以叫做填充法
- 首先从high指针开始,如果high指向的元素
array[high] >= pivot,high本来就是从右边开始的,说明此时不需要交换,high所处的位置已经满足条件,内部是未知状态,high左移一位。 - 但是当
array[high] < pivot时,因此array[high]要放到左边去,注意到已经array[low]已经用pivot变量保存了,这个位置可以认为是空出来了(虽然没有真正空出来),因此第9行array[low] = array[high];,就这样放到了左边。 - low指针同理,但是当执行过
array[low]=array[high]后,array[high]相当于也空了出来。 - 因此当
array[low] >= pivot时,需要向右边填充就去找array[high]吧
注意到外层while循环的条件被附带到了内层,这是很常见的,内层循环改变变量的值,有的时候不可避免就不满足外层循环的条件了。内外层循环都附带
low<high的条件,保证退出循环一定low=high
low与high撞上的时候,这里实质上是一个空位,因为当循环结束时,空位左边都小于pivot,空位右边都大于等于pivot,并且循环遍历了除了pivot以外所有的元素。因此填入pivot即可。
返回low或者high都可以,这就是pivot应该在的位置。
交换法
感觉这个方法见的最多。先给一个不常见的写法
一个不常见的写法
public int swapPartition(int[] array, int start, int end) {
int pivot = array[end];
int low = start;
int high = end-1;
while (low < high) {
while (low < high && array[high] >= pivot) {
high--;
}
while (low < high && array[low] < pivot) {
low++;
}
if (low < high) {
swap(array, low, high);
}
}
if (pivot > array[low]) {
low++;
}
swap(array, end, low);
return low;
}
2-4行,设置pivot为最后一个元素,一般都是第一个元素,这里最后一个也无妨。如果是第一个可以当成作业完成。既然pivot已经指定,high可以从end-1开始往回走。
| 小于pivot | pivot | 大于或者等于pivot |
|---|---|---|
array[start...mid-1] |
array[mid] |
array[mid+1...end] |
partition的目标仍然与填充法相同。
5-15行是循环体,很容易看懂
- 右边找小的,左边找大的,两边都找到就交换,low往右边走,high往左边走。可以保证low左边都比pivot小,high右边都大于等于pivot
- low,high指针都要往中间走,碰上循环结束。
- 碰上的时候,左侧一定都小于pivot,右边都大于等于pivot,那么中间是不是应该填pivot?于是与
array[end]交换?
不能想的太直接,16-18行加了一个判断,当pivot <= array[low]时,那当然可以把array[low]换到右边去,因为大一些吗。如果pivot > array[low]呢?
| ...... | array[low] |
array[low+1] |
...... | array[end] |
|---|---|---|---|---|
| ....... | low==high |
low+1 |
...... | end |
那么array[low]应该在左边,可以考虑变通以下,array[end]与low+1位置的元素交换,这样就仍然满足条件。最后返回low+1即可,low++本身就是low自增,所以如16-20行所示。
low++会数组越界吗?不会。因为判断条件为array[high] >= pivot,因此high指针一定向左边移动一步
为了方便与下一节相对比,给出pivot取array[start]的另一个版本
public int swapPartition1(int[] array, int start, int end) {
int pivot = array[start];
int low = start + 1;
int high = end;
while (low < high) {
while (low < high && array[low] <= pivot) {
low++;
}
while (low < high && array[high] > pivot) {
high--;
}
if (low < high) {
swap(array, low, high);
}
}
if (pivot < array[low]) {
low--;
}
swap(array, start, low);
return low;
}
一个常见的写法
public int swapPartition1(int[] array, int start, int end) {
int pivot = array[start];
int low = start;
int high = end;
while (low < high) {
while (low < high && array[high] > pivot) {
high--;
}
while (low < high && array[low] <= pivot) {
low++;
}
if (low < high) {
swap(array, low, high);
}
}
swap(array, start, low);
return low;
}
这里pivot又换成开头了,但是不管在哪里,都要思路清晰,知道该做什么改动能让程序依旧正确。
但是这个写法很严格,几乎一点不能改动的。
流程与一个不常见的写法一节中基本类似,但是
low=start不能动,必须是这个,不能是low=start+1,这个举一个只有两个元素的数组的例子就知道了- 6-8行的内循环和9-11行的内循环不能对调
- 两个内循环中的判断必须如上面所写,
array[high] > pivot,array[low] <= pivot,>和<=不能轻易改成别的。也就是partition的目标是固定的。(一旦改动,low必须是start+1,但是这样写不可避免最后swap又要加判断,反而和不常见的写法一样了。)
| 小于或者等于pivot | pivot | 大于pivot |
|---|---|---|
array[start...mid-1] |
array[mid] |
array[mid+1...end] |
如此才能舍去一个不常见的写法中16-18行的判断条件
最关键的点在于循环位置不能对调。循环终止,low与high碰上,有以下几种情况
- high先停住,low增加碰上high停止。high停下来是因为
array[high] <= pivot当然能与array[start]交换换到左边去 - low先停住,high减少碰上low停止。low停下来是因为
array[low] > pivot,但是紧跟着swap(array, low, high);,所以low先停下来也无妨,low的位置的元素仍然满足array[low] <= pivot,换到左边去没有问题。
鉴于这个写法太过严格不建议这么写
完整代码
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] nums = {7,8,4,9,3,2,6,5,0,1,9,4,7,3};
quickSort(nums,0, nums.length-1);
System.out.println(Arrays.toString(nums));
}
public static void quickSort(int[] array, int start, int end) {
if(start < end){
int mid = swapPartition1(array, start, end);
quickSort(array, start, mid-1);
quickSort(array, mid+1, end);
}
}
public static int partition(int[] array, int start, int end) {
int firstHigh = start;
int pivot = array[end];
for(int j=start; j < end; j++) {
if(array[j] <= pivot) {
swap(array, firstHigh, j);
firstHigh++;
}
}
swap(array, firstHigh, end);
return firstHigh;
}
public static int insertPartition(int[] array, int start, int end) {
int pivot = array[start];
int low = start;
int high = end;
while (low < high) {
while (low < high && array[high] >= pivot) {
high--;
}
array[low] = array[high];
while (low < high && array[low] < pivot) {
low++;
}
array[high] = array[low];
}
array[low] = pivot;
return low;
}
/* 为啥这是正确的 */
public static int swapPartition1(int[] array, int start, int end) {
int pivot = array[start];
int low = start;
int high = end;
while (low < high) {
while (low < high && array[high] > pivot) {
high--;
}
while (low < high && array[low] <= pivot) {
low++;
}
if (low < high) {
swap(array, low, high);
}
}
swap(array, start, low);
return low;
}
/*这个肯定是正确的*/
public static int swapPartition(int[] array, int start, int end) {
int pivot = array[end];
int low = start;
int high = end;
while (low < high) {
while (low < high && array[low] < pivot) {
low++;
}
while (low < high && array[high] >= pivot) {
high--;
}
if (low < high) {
swap(array, low, high);
}
}
if (pivot > array[low]) {
low++;
}
swap(array, end, low);
return low;
}
public static void swap(int[] array, int i, int j) {
int tmp;
tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
浙公网安备 33010602011771号