0 概述
如下图所示,给出了常见排序算法,本文主要总结基于比较排序算法。
1 基于比较排序算法
基于比较排原地序算法常见的主要有冒泡排序、选择排序、插入排序。
1.1 冒泡排序
- 冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。冒泡排序包含两种基本操作:比较和交换。最好情况时间复杂度O(n),平均和最坏的时间复杂度为O(n2),是稳定排序。
/**
* 一次冒泡会让至少一个元素移动到它应该在的位置,所以最坏情况需要扫描n次
* 只会操作相邻的两个数据,内循环是比较相邻的两个元素
*/
public static void bubbleSort(int arr[]) {
if (arr == null || arr.length <= 1) {
return;
}
int len = arr.length;
//是否存在交换的标志位
boolean existSwap = false;
//需要n次冒泡
for (int i = 0; i < len; i++) {
for (int j = 0; j < len - i - 1; j++) {
//交换
if (arr[j] > arr[j + 1]) {
int tmp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = tmp;
existSwap = true;
}
}
// 没有数据交换,提前退出
if (!existSwap) {
break;
}
}
}
1.2 插入排序
插入排序算法基本思想将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。插入排序基本操作:比较和移动。最好情况时间复杂度O(n),平均和最坏的时间复杂度为O(n2),是稳定排序。
/**
* 插入排序
*
*/
public static void insertSort(int arr[]) {
if (arr == null || arr.length <= 1) {
return;
}
int len = arr.length;
//
for (int i = 1; i < len; i++) {
int value = arr[i];
int j = i - 1;
// 和前面进行比较,数据后移动
for (; j >= 0; j--) {
//数据移动,如果已经有序直接break
if (arr[j] > value) {
arr[j + 1] = arr[j];
} else {
break;
}
}
//插入合适的数据
arr[j + 1] = value;
}
}
1.3 选择排序
选择排序基本思想:第一次从待排序的数据元素选择最小(或最大)的一个元素,放到第一位置。第二次从待排序的数据元素选择最小(或最大)的一个元素,放到第二个位置。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。最好、平均和最坏的时间复杂度为O(n2)。
/**
* 选择排序
*
*/
public static void selectSort(int arr[]) {
if (arr == null || arr.length <= 1) {
return;
}
int len = arr.length;
// 每次选择一个元素
for (int i = 0; i < len; i++) {
int minIndex = i;
for (int j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
//交换选择出来的最小值,放到相应的位置
int tmp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = tmp;
}
}
排利用的也是分治思想。乍看起来,它有点像归并排序,但是思路其实完全不一样。我们待会会讲两者的区别。现在,我们先来看下快排的核心思想。快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
1.4 归并排序
归并排序使用的分治思想;就是将一个大问题逐步分解成小的子问题来解决,可以使用递归来实现归并排序。
下面给出递归公式&递归结束条件
mergeSort(a[],p,r)=merge(mergeSort(a[],p,q)+mergeSort(a[],q+1,r));
递归终止条件
p>=r
public static void mergeSort(int arr[]) {
if (arr == null || arr.length <= 1) {
return;
}
mergeSortUseRecursion(arr, 0, arr.length-1);
}
private static void mergeSortUseRecursion(int arr[], int p, int r) {
//递归结束条件
if (p >= r) {
return;
}
//取中间位置
int q = p + (r - p) / 2;
mergeSortUseRecursion(arr, p, q);
mergeSortUseRecursion(arr, q + 1, r);
//数据做merge(p,r)
int[] left = new int[q - p + 2];
int[] right = new int[r - q + 1];
for (int i = 0; i <= q - p; i++) {
left[i] = arr[p + i];
}
for (int i = 0; i < r - q; i++) {
right[i] = arr[q + 1 + i];
}
// 第一个数组添加哨兵(最大值)
left[q - p + 1] = Integer.MAX_VALUE;
// 第二个数组添加哨兵(最大值)
right[r - q] = Integer.MAX_VALUE;
int i = 0;
int j = 0;
int k = p;
while (k <= r) {
// 当左边数组到达哨兵值时,i不再增加,直到右边数组读取完剩余值,同理右边数组也一样
if (left[i] <= right[j]) {
arr[k++] = left[i++];
} else {
arr[k++] = right[j++];
}
}
}
1.5 快速排序
快速排序用的也是分治的思想,乍一看可能和归并有点相似,其实思路有很大差异的。其基本思想:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
quickSort(a[],p,r)=quickSort(a[],p,q)+quickSort(a[],q+1,r);
递归终止条件
p>=r
public static void quickSort(int arr[]) {
if (arr == null || arr.length <= 1) {
return;
}
quickSortUseRecursion(arr, 0, arr.length-1);
}
private static void quickSortUseRecursion(int arr[], int p, int r) {
if (p >= r) {
return;
}
int q = partition(arr, p, r);
quickSortUseRecursion(arr, p, q);
quickSortUseRecursion(arr, q + 1, r);
}
private static int partition(int arr[], int p, int r) {
int pivot = arr[p];
int leftIndex = p;
for (int index = p + 1; index <= r; index++) {
//比哨兵大不用处理
if (arr[index] < pivot) {
leftIndex++;
//如果不是一个位置,需要把小的放到左边大的放到右边
if (leftIndex != index) {
int tmp = arr[leftIndex];
arr[leftIndex] = arr[index];
arr[index] = tmp;
}
}
}
int tmp = arr[leftIndex];
arr[leftIndex] = arr[p];
arr[p] = tmp;
return leftIndex;
}
根据快速排序思想,求k大元素。
public static int kSmall(int arr[], int k) {
if (arr == null || arr.length < k) {
return -1;
}
int p = partition(arr, 0, arr.length - 1);
while (p + 1 != k) {
if (p < k) {
p = partition(arr, p + 1, arr.length - 1);
} else {
p = partition(arr, 0, p);
}
}
return arr[p];
}
2 总结
虽然冒泡排序和插入排序的时间复杂度都是 O(n2),都是稳定排序算法,但是从代码上可以看出,插入排序是优于冒泡排序的,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。
排序算法 | 是否是稳定排序 | 最好、平均、最坏 | 空间复杂度 |
---|---|---|---|
冒泡 | 是 | O(n)、O(n2)、O(n2) | O(1) |
插入 | 是 | O(n)、O(n2)、O(n2) | O(1) |
选择 | 否 | O(n2)、O(n2、O(n2) | O(1) |
归并 | 是 | O(nlogn)、O(nlogn)、O(nlogn) | O(n) |
快速 | 否 | O(nlogn)、O(nlogn)、O(n2) | O(logn) |