本文内容基于《数据结构与算法分析 Java语言描述》第三版,冯舜玺等译。
- 外部排序:不能在主存中完成而必须在磁盘或磁带上完成的排序。
- 内部排序:整个排序工作能够在主存中完成;
排序方法 | 时间复杂度(最好) | 时间复杂度(平均) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n) | O(nlogn) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(nlogn) | 不稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
1. 插入排序
插入排序由N-1趟排序组成,对于p=1到N-1趟,插入排序保证从位置0到位置p上的元素为已排序状态。在第p趟,将位置p上的元素向左移动,直到它在前p+1个元素中的正确位置被找到的地方。
public class MySort {
public static void main(String[] args) {
Integer[] a = {34, 8, 64, 51, 32, 21};
System.out.println(Arrays.toString(a));
insertionSort(a);
System.out.println(Arrays.toString(a));
}
/**
* 插入排序
* 参考待排序数据:34, 8, 64, 51, 32, 21
* 第一步,取出8
* 第二步,倒序遍历8以前的序列,如果值比8大,则后移一位:34, 34, 64, 51, 32, 21
* 第三步,如果值等于或小于8,或者遍历结束,将结束位置的值替换为取出的8:8, 34, 64, 51, 32, 21
*
* @param a
* @param <AnyType>
*/
public static <AnyType extends Comparable<? super AnyType>> void insertionSort(AnyType[] a) {
for (int i = 1; i < a.length; i++) {
AnyType temp = a[i];
int j = i - 1;
for (; j >= 0 && a[j].compareTo(temp) > 0; j--) {
a[j + 1] = a[j];
}
a[j + 1] = temp;
}
}
}
由于嵌套循环的每一个都花费N次迭代,因此插入排序为O(N^2),如果输入数据已预先排序,那么运行时间为O(N)。
2. 希尔排序
它通过比较相距一定间隔的元素来工作,各趟比较所用的距离随着算法的进行而减小,知道只比较相邻元素的最后一趟排序为止,也叫做缩减增量排序。
public class MySort {
public static void main(String[] args) {
Integer[] a = {34, 8, 64, 51, 32, 21};
System.out.println(Arrays.toString(a));
shellSort(a);
System.out.println(Arrays.toString(a));
}
/**
* 希尔排序
* 第一步,取增量,每次取上次的一半
* 第二步,以增量进行插入排序
*
* @param a
* @param <AnyType>
*/
public static <AnyType extends Comparable<? super AnyType>> void shellSort(AnyType[] a) {
//取增量
for (int gap = a.length / 2; gap > 0; gap /= 2) {
//插入排序
for (int i = gap; i < a.length; i++) {
AnyType temp = a[i];
int j = i - gap;
for (; j >= 0 && a[j].compareTo(temp) > 0; j -= gap) {
a[j + gap] = a[j];
}
a[j + gap] = temp;
}
}
}
}
希尔排序使用一个序列h1, h2, ..., hi,叫做增量序列,一个流行的选择是使用Shell建议的序列:hi=N/2。使用希尔增量的最坏情况运行时间为O(N^2)。
希尔排序的性能在实践中是完全可以接受的,即使是对数以万计的N仍是如此,是大量输入数据经常选用的算法。
3. 堆排序
堆(优先队列)排序可以用于以O(NlogN)时间的排序,主要问题在于它使用了一个附加的数组,因此存储需求增加一倍。
public class MySort {
public static void main(String[] args) {
Integer[] a = {34, 8, 64, 51, 32, 21};
System.out.println(Arrays.toString(a));
heapSort(a);
System.out.println(Arrays.toString(a));
}
/**
* 堆排序
*
* @param a
* @param <AnyType>
*/
public static <AnyType extends Comparable<? super AnyType>> void heapSort(AnyType[] a) {
//构建max堆
for (int i = a.length / 2 - 1; i >= 0; i--) {
percDown(a, i, a.length);
}
//删除最大元
for (int i = a.length - 1; i > 0; i--) {
swapReferences(a, 0, i);
percDown(a, 0, i);
}
}
private static <AnyType extends Comparable<? super AnyType>> void percDown(AnyType[] a, int i, int n) {
int child;
AnyType tmp;
for (tmp = a[i]; leftChild(i) < n; i = child) {
child = leftChild(i);
if (child != n - 1 && a[child].compareTo(a[child + 1]) < 0) {
child++;
}
if (tmp.compareTo(a[child]) < 0) {
a[i] = a[child];
} else {
break;
}
}
a[i] = tmp;
}
private static int leftChild(int i) {
return 2 * i + 1;
}
public static <AnyType> void swapReferences(AnyType[] a, int index1, int index2) {
AnyType tmp = a[index1];
a[index1] = a[index2];
a[index2] = tmp;
}
}
4. 归并排序
归并排序是以O(NlogN)最坏情形时间运行而使用的比较次数几乎是最优的,是递归算法的一个实例。
基本操作是合并两个已排序的表,因为这两个表是已排序的,所以若输出放到第3个表中,则该算法可以通过对输入数据一趟排序来完成。
合并两个已排序的时间显然是线性的,因为最多进行N-1次比较,其中N是元素的总数。
public class MySort {
public static void main(String[] args) {
Integer[] a = {34, 8, 64, 51, 32, 21};
System.out.println(Arrays.toString(a));
mergeSort(a);
System.out.println(Arrays.toString(a));
}
/**
* 归并排序
* 参考待排序数据:34, 8, 64, 51, 32, 21
* 第一步:假设前三个值是一个数组A(34, 8, 64),后三个值是一个数组B(51, 32, 21),另外还有一个空数组C
* 第二步:数组A再分为两个数组A1(34, 8)、A2(64),数组B再分为两个数组B1(51, 32)、B2(21)
* 第三步:数组A1再分为两个数组A11(34)、A12(8),数组B1再分为两个数组B11(51)、B12(32)(目前数组都不可再分)
*
* @param a
* @param <AnyType>
*/
public static <AnyType extends Comparable<? super AnyType>> void mergeSort(AnyType[] a) {
AnyType[] temp = (AnyType[]) new Comparable[a.length];
mergeSort(a, temp, 0, a.length - 1);
}
public static <AnyType extends Comparable<? super AnyType>> void mergeSort(AnyType[] a, AnyType[] temp, int left, int right) {
//递归终止条件
if (left < right) {
int center = (left + right) / 2;
mergeSort(a, temp, left, center);
mergeSort(a, temp, center + 1, right);
merge(a, temp, left, center + 1, right);
}
}
private static <AnyType extends Comparable<? super AnyType>> void merge(AnyType[] a, AnyType[] tmpArray, int leftPos, int rightPos, int rightEnd) {
int leftEnd = rightPos - 1;
int tmpPos = leftPos;
int numElements = rightEnd - leftPos + 1;
while (leftPos <= leftEnd && rightPos <= rightEnd) {
if (a[leftPos].compareTo(a[rightPos]) <= 0) {
tmpArray[tmpPos++] = a[leftPos++];
} else {
tmpArray[tmpPos++] = a[rightPos++];
}
}
while (leftPos <= leftEnd) {
tmpArray[tmpPos++] = a[leftPos++];
}
while (rightPos <= rightEnd) {
tmpArray[tmpPos++] = a[rightPos++];
}
for (int i = 0; i < numElements; i++, rightEnd--) {
a[rightEnd] = tmpArray[rightEnd];
}
}
}
public static int[] mergeSort(int[] a) {
int length = a.length;
if (length < 2) {
return a;
}
int middle = (int)Math.floor(length / 2);
int[] left = Arrays.copyOfRange(a, 0, middle);
int[] right = Arrays.copyOfRange(a, middle, length);
return merge(mergeSort(left), mergeSort(right));
}
public static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
int index = 0;
//将已有序的两个子序列合并
while (left.length > 0 && right.length > 0) {
if (left[0] < right[0]) {
result[index++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
} else {
result[index++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
}
while (left.length > 0) {
result[index++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
}
while (right.length > 0) {
result[index++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
return result;
}
虽然归并排序的运行时间是O(NlogN),但是它有一个明显的问题,即合并两个已排序的表用到线性附加内存,与其他的O(NlogN)排序算法比较,归并排序的运行时间严重依赖于比较元素和在数组中移动元素的相对开销。
例如在Java中,当执行一次泛型排序(使用Comparator)时,进行一次元素比较可能是昂贵的,因为比较可能不容易被内嵌,从而动态调度的开销可能会减慢执行的速度,但是移动元素则是省时的,因为它们是引用的赋值,而不是庞大对象的拷贝。
归并排序使用所有流行的排序算法中最少的比较次数,因此是使用Java通用排序算法中的上好的选择,它就是标准Java类库中泛型排序所使用的算法。
5. 快速排序
快速排序是实践中的一种快速的排序算法,在Java中,快速排序用作基本类型的标准库排序。它的平均运行时间是O(NlogN),最坏情形性能为O(n^2)。快速排序也是一种递归算法。
随便选取任一项,然后形成三个组,小于被选项的一组,等于被选项的一组,大于被选项的一组。递归地对第一和第三组排序,然后把三组接龙。
参考数组a:[46, 79, 56, 38, 40, 84]。
假设取第一个数46为枢纽元。
第一次递归:left = 0, right = a.length() - 1 = 5。取i = left, j = right。
- 比较a[j]即a[5]即84 > 46,无操作,j--即4;
- 比较a[j]即a[4]即40 < 46,交换40与46,即[40, 79, 56, 38, 46, 84];
- 比较a[i]即a[0]即40 < 46,无操作,i++即1;
- 比较a[i]即a[1]即79 > 46,交换79与46,即[40, 46, 56, 38, 79, 84];
- 比较a[j]即a[4]即79 > 46,无操作,j--即3;
- 比较a[j]即a[3]即38 < 46,交换38与46,即[40, 38, 56, 46, 79, 84];
- 比较a[i]即a[1]即38 < 46,无操作,i++即2;
- 比较a[i]即a[2]即56 > 46,交换56与46,即[40, 38, 46, 56, 79, 84];
- 比较a[j]即a[3]即56 > 46,无操作,j--即2;
- 此时i=j,结束第一次递归,此时枢纽元左边的数都小于它本身,右边的数都大于它本身,所以之后将左右两边分别作为两个数组进行递归。
第二次递归:left = left, right = i - 1 = 1。
第三次递归:left = right + 1 = 3, right = right。
public class MySort {
public static void main(String[] args) {
Integer[] a = {34, 8, 64, 51, 32, 21};
System.out.println(Arrays.toString(a));
quicksort(a);
System.out.println(Arrays.toString(a));
}
/**
* 快速排序
*
* @param a
* @param <AnyType>
*/
public static <AnyType extends Comparable<? super AnyType>> void quicksort(AnyType[] a) {
quicksort(a, 0, a.length - 1);
}
private static final int CUTOFF = 3;
private static <AnyType extends Comparable<? super AnyType>> void quicksort(AnyType[] a, int left, int right) {
if (left + CUTOFF <= right) {
AnyType pivot = median3(a, left, right);
// Begin partitioning
int i = left, j = right - 1;
for (; ; ) {
while (a[++i].compareTo(pivot) < 0) {
}
while (a[--j].compareTo(pivot) > 0) {
}
if (i < j) {
swapReferences(a, i, j);
} else {
break;
}
}
// Restore pivot
swapReferences(a, i, right - 1);
// Sort small elements
quicksort(a, left, i - 1);
// Sort large elements
quicksort(a, i + 1, right);
} else {
// Do an insertion sort on the subarray
insertionSort(a, left, right);
}
}
private static <AnyType extends Comparable<? super AnyType>> AnyType median3(AnyType[] a, int left, int right) {
int center = (left + right) / 2;
if (a[center].compareTo(a[left]) < 0) {
swapReferences(a, left, center);
}
if (a[right].compareTo(a[left]) < 0) {
swapReferences(a, left, right);
}
if (a[right].compareTo(a[center]) < 0) {
swapReferences(a, center, right);
}
// Place pivot at position right - 1
swapReferences(a, center, right - 1);
return a[right - 1];
}
private static <AnyType extends Comparable<? super AnyType>> void insertionSort(AnyType[] a, int left, int right) {
for (int p = left + 1; p <= right; p++) {
AnyType tmp = a[p];
int j;
for (j = p; j > left && tmp.compareTo(a[j - 1]) < 0; j--) {
a[j] = a[j - 1];
}
a[j] = tmp;
}
}
}
public static void quickSort(int[] a, int left, int right) {
//递归退出判断
if (left >= right) {
return;
}
int i = left, j = right, k = a[left];
while (i != j) {
//从右边向左边找比标志位小的值
while (i < j) {
if (a[j] >= k) {
j--;
} else {
//交换
int temp = a[i];
a[i] = a[j];
a[j] = temp;
break;
}
}
//从左边向右边找比标志位大的值
while (i < j) {
if (a[i] <= k) {
i++;
} else {
//交换
int temp = a[i];
a[i] = a[j];
a[j] = temp;
break;
}
}
}
//递归,这里i==j
quickSort(a, left, i - 1);
quickSort(a, i + 1, right);
}
事实上,如果表中含有大量重复项,以及相对较小的不同项,其表现是非常好的。然后,它会产生额外的列表,并且还是递归地这么做。
5.1 选取枢纽元
5.1.1 一种错误的方法
选择将第一个元素用作枢纽元。
5.1.2 一种安全的做法
随机选取枢纽元。
5.1.3 三数中值分割法
一组N个数的中值,也叫做中位数,是第N/2个最大的数。中值的估计量可以通过使用左端、右端和中心位置上的三个元素的中值作为枢纽元。例如8, 1, 4, 9, 6, 3, 5, 2, 7, 0,左边元素是8,右边元素是0,中心位置(left + right) / 2 = 4是6,于是枢纽元则是6。
5.2 小数组
对于很小的数组(N<=20),快速排序不如插入排序。
6. 选择排序
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾,以此类推,直到所有元素均排序完毕。
public static int[] selectionSort(int[] data) {
int[] a = Arrays.copyOf(data, data.length);
for (int i = 0; i < a.length; i++) {
int minIndex = i;
for (int j = i + 1; j < a.length; j++) {
if (a[j] < a[minIndex]) {
minIndex = j;
}
}
int temp = a[minIndex];
a[minIndex] = a[i];
a[i] = temp;
}
return a;
}
7. 冒泡排序
专门对于已部分排序的数据进行排序的一种算法,它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
public static int[] bubbleSort(int[] data) {
int[] a = Arrays.copyOf(data, data.length);
for (int i = 0; i < a.length - 1; i++) { //-1是为了提高效率
for (int j = 0; j < a.length - 1 - i; j++) { //-i是为了提高效率
if (a[j] > a[j + 1]) { //前一个大于后一个
//交换
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
return a;
}