各个排序比对表:
排序算法 | 时间复杂度 | 空间复杂度 | 是否稳定 |
冒泡排序 | O(n²) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(logn) | 不稳定 |
堆排序 | O(nlogn) | O(1) | 不稳定 |
计数排序 | O(m+n) | O(m) 0-10 m=10 | 稳定 |
桶排序 | O(n) | O(n) | 稳定 |
根据时间复杂度的不同,主流的排序算法可以分为 3 大类
- 时间复杂度为O( n²)的排序算法
- 冒泡排序、选择排序、插入排序、希尔排序
- 时间复杂度为O(nlogn)的排序算法
- 快速排序 、归并排序、堆排序
- 时间复杂度为线性的排序算法
- 计数排序、桶排序、基数排序
根据其稳定性,可以分为稳定排序和不稳定排序
- 稳定排序:值相同的元素在排序后仍然保持着排序前的顺序
- 不稳定排序:值相同的元素在排序后打乱了排序前的顺序
快速排序
同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。
不同的是,冒泡排序在每一轮中只把
1
个元素冒泡到数列的一端,而快速排序则在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分,这种思路就叫作分治法。
基准元素的选择
基准元素,英文是
pivot
,在分治过程中,以基准元素为中心,把其他元素移动到它的左右两边
可以随机选择一个元素作为基准元素,并且让基准元素和数列首元素交换位置
元素的交换
选定了基准元素以后,我们要做的就是把其他元素中小于基准元素的都交换到基准元素一边,大于基准元素的都交换到基准元素另一边。
- 双边循环法
首先,选定基准元素
pivot
,并且设置两个指针
left
和
right
,指向数列的最左和最右两个元素
接下来进行第
1
次循环:
从right指针开始,让指针所指向的元素和基准元素做比较。如果大于或等于pivot,则指针向左移动;
如果小于pivot,则right指针停止移动,切换到left指针
轮到left指针行动,让指针所指向的元素和基准元素做比较。如果小于或等于pivot,则指针向右移动;
如果大于pivot,则left指针停止移动
左右指针指向的元素交换位置
由于left开始指向的是基准元素,判断肯定相等,所以left右移1位
由于7>4,left指针在元素7的位置停下。这时,让left和right指针所指向的元素进行交换。
接下来,进入第
2
次循环,重新切换到
right
指针,向左移动。
right
指针先移
动到
8
,
8>4
,继续左移。由于
2<4
,停止在
2
的位置
- 单边循环法
单边循环法只从数组的一边对元素进行遍历和交换。
开始和双边循环法相似,首先选定基准元素
pivot
。同时,设置一个
mark
指针指向数列起始位置,这个mark
指针代表小于基准元素的区域边界。
接下来,从基准元素的下一个位置开始遍历数组。
如果遍历到的元素大于基准元素,就继续往后遍历
如果遍历到的元素小于基准元素,则需要做两件事:
- 第一,把mark指针右移1位,因为小于pivot的区域边界增大了1;
- 第二,让最新遍历到的元素和mark指针所在位置的元素交换位置,因为最新遍历的元素归属于小于pivot的区域
首先遍历到元素
7
,
7>4
,所以继续遍历。
接下来遍历到的元素是
3
,
3<4
,所以
mark
指针右移
1
位
随后,让元素
3
和
mark
指针所在位置的元素交换,因为元素
3
归属于小于
pivot
的区域。
按照这个思路,继续遍历,后续步骤如图所示
代码实现
import java.util.Arrays;
/**
* 快速排序:双边循环法
*/
public class QuickSort1 {
public static void quickSort(int[] arr, int startIndex,int endIndex){
// 递归结束条件:startIndex大于或等于endIndex时
if (startIndex >= endIndex) {
return;
}
// 得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 根据基准元素,分成两部分进行递归排序
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
/**
* 分治(双边循环法)
* @param arr 待交换的数组
* @param startIndex 起始下标
* @param endIndex 结束下标
* @return
*/
private static int partition(int[] arr, int startIndex, int endIndex){
// 取第1个位置(也可以选择随机位置)的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while( left != right) {
//控制right 指针比较并左移
while(left<right && arr[right] > pivot){
right--;
}
//控制left指针比较并右移
while( left<right && arr[left] <= pivot){
left++;
}
//交换left和right 指针所指向的元素
if(left<right) {
int p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
//pivot 和指针重合点交换
arr[startIndex] = arr[left];
arr[left] = pivot;
return left;
}
public static void main(String[] args) {
int[] arr = new int[] {4,7,3,5,6,2,8,1};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
}
import java.util.Arrays;
/**
* 快速排序:单边循环法
*/
public class QuickSort2 {
public static void quickSort(int[] arr, int startIndex,int endIndex) {
// 递归结束条件:startIndex大于或等于endIndex时
if (startIndex >= endIndex) {
return;
}
// 得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 根据基准元素,分成两部分进行递归排序
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
/**
* 分治(单边循环法)
*
* @param arr 待交换的数组
* @param startIndex 起始下标
* @param endIndex 结束下标
* @return
*/
private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第1个位置(也可以选择随机位置)的元素作为基准元素
int pivot = arr[startIndex];
int mark = startIndex;
for (int i = startIndex + 1; i <= endIndex; i++) {
if (arr[i] < pivot) {
mark++;
int p = arr[mark];
arr[mark] = arr[i];
arr[i] = p;
}
}
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;
}
public static void main(String[] args) {
int[] arr = new int[]{4, 7, 3, 5, 6, 2, 8, 1};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
}
快速排序的时间复杂度是:
O(nlogn)