排序算法
排序算法是将一组数据依照指定的顺序进行排列的过程。
排序算法又分为内部排序和外部排序:
- 内部排序: 指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
- 外部排序法:数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。
此处只详述内部排序,如下图:
1. 冒泡排序
冒泡排序(Bubble Sort)的基本思想是: 通过对待排序序列从前向后(从下标较小的元素开始), 依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部。
优化:因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。
代码
下面代码实现冒泡排序和对冒泡排序进行速度测试
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int [] arr = {1, 2, 5, 4, 6, 3};
// 输出原始的数组
System.out.println("------------原始数组----------");
System.out.println(Arrays.toString(arr));
// 排序
bubbleSort(arr);
// 输出排序后的数组
System.out.println("------------有序数组----------");
System.out.println(Arrays.toString(arr));
// 测试一下冒泡排序的速度,测试 100000 个随机数字进行排序所需要时间
System.out.println("------------测试速度----------");
int [] arr2 = new int[100000];
for (int i = 0; i < 100000; i++) {
arr2[i] = (int) (Math.random() * 100000);
}
long start = System.currentTimeMillis();
bubbleSort(arr2);
long end = System.currentTimeMillis();
System.out.println("100000 个数字冒泡排序所需时间:" + (end - start) + " ms");
}
// 冒泡排序
public static void bubbleSort(int [] arr) {
int temp = 0;
boolean flag = true; // 标记数组是否发生变化
for (int i = 0; i < arr.length - 1; i++) {
flag = true;
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j+1]) {
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = false;
}
}
// 如果数组未发生变化说明已经排序完成了,就退出
if (flag) {
break;
}
}
}
}
/* Code Running Result
[1, 2, 5, 4, 6, 3]
------------有序数组----------
[1, 2, 3, 4, 5, 6]
------------测试速度----------
100000 个数字冒泡排序所需时间:14679 ms
*/
2. 选择排序
选择排序(Select Sort)的基本思想是: 第一次从 arr[0] ~ arr[n-1] 中选取最小值,与 arr[0] 交换,第二次从 arr[1] ~ arr[n-1] 中选取最小值,与 arr[1] 交换,第三次从 arr[2] ~ arr[n-1] 中选取最小值,与 arr[2]交换,…,第 i 次从 arr[i-1] ~ arr[n-1] 中选取最小值,与 arr[i-1]交换,…,第 n-1 次从arr[n-2]~arr[n-1]中选取最小值,与 arr[n-2] 交换,总共通过 n-1 次,得到一个按排序从小到大排列的有序序列。
代码
下面代码实现选择排序和对选择排序进行速度测试
import java.util.Arrays;
public class SelectSort {
public static void main(String[] args) {
int [] arr = {1, 2, 5, 4, 6, 3};
// 输出原始的数组
System.out.println("------------原始数组----------");
System.out.println(Arrays.toString(arr));
// 排序
selectSort(arr);
// 输出排序后的数组
System.out.println("------------有序数组----------");
System.out.println(Arrays.toString(arr));
// 测试一下选择排序的速度,测试 100000 个随机数字进行排序所需要时间
System.out.println("------------测试速度----------");
int [] arr2 = new int[100000];
for (int i = 0; i < 100000; i++) {
arr2[i] = (int) (Math.random() * 100000);
}
long start = System.currentTimeMillis();
selectSort(arr2);
long end = System.currentTimeMillis();
System.out.println("100000 个数字选择排序所需时间:" + (end - start) + " ms");
}
// 选择排序
public static void selectSort(int [] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i, min = arr[i]; // 最小值的索引和最小值,先初始化为i和arr[i]
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < min) { // 找到更小的值就更新最小值
min = arr[j];
minIndex = j;
}
}
if (minIndex != i) { // 如果最小值不是arr[i]时就交换位置
arr[minIndex] = arr[i];
arr[i] = min;
}
}
}
}
/* Code Running Result
[1, 2, 5, 4, 6, 3]
------------有序数组----------
[1, 2, 3, 4, 5, 6]
------------测试速度----------
100000 个数字选择排序所需时间:3591 ms
*/
3. 直接插入排序
插入排序(Insert Sort)的基本思想是: 把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
代码
下面代码实现插入排序和对插入排序进行速度测试
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int [] arr = {1, 2, 5, 4, 6, 3};
// 输出原始的数组
System.out.println("------------原始数组----------");
System.out.println(Arrays.toString(arr));
// 排序
insertSort(arr);
// 输出排序后的数组
System.out.println("------------有序数组----------");
System.out.println(Arrays.toString(arr));
// 测试一下插入排序的速度,测试 100000 个随机数字进行排序所需要时间
System.out.println("------------测试速度----------");
int [] arr2 = new int[100000];
for (int i = 0; i < 100000; i++) {
arr2[i] = (int) (Math.random() * 100000);
}
long start = System.currentTimeMillis();
insertSort(arr2);
long end = System.currentTimeMillis();
System.out.println("100000 个数字插入排序所需时间:" + (end - start) + " ms");
}
// 插入排序
public static void insertSort(int [] arr) {
for (int i = 1; i < arr.length; i++) {
int insertValue = arr[i]; // 插入的值
int insertIndex = i - 1; // 插入的位置
// 寻找插入位置
while (insertIndex >= 0 && arr[insertIndex] > insertValue) {
arr[insertIndex+1] = arr[insertIndex];
insertIndex--;
}
// 插入
if (insertIndex != i - 1) {
arr[insertIndex + 1] = insertValue;
}
}
}
}
/* Code Running Result
------------原始数组----------
[1, 2, 5, 4, 6, 3]
------------有序数组----------
[1, 2, 3, 4, 5, 6]
------------测试速度----------
100000 个数字插入排序所需时间:851 ms
*/
4. 希尔排序(缩小增量排序)
希尔排序(Shell Sort)是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序; 随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止,排序完成。
代码
下面代码实现希尔排序时分别用插入法和交换法对各组排序并对两种方法进行速度测试
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int [] arr = {1, 2, 5, 4, 6, 3};
// 输出原始的数组
System.out.println("------------原始数组----------");
System.out.println(Arrays.toString(arr));
// 排序
shellSort(arr);
// 输出排序后的数组
System.out.println("------------有序数组----------");
System.out.println(Arrays.toString(arr));
// 测试一下希尔排序的速度,测试 100000 个随机数字进行排序所需要时间
int [] arr2 = new int[100000];
for (int i = 0; i < 100000; i++) {
arr2[i] = (int) (Math.random() * 100000);
}
System.out.println("------------插入法速度----------");
long start = System.currentTimeMillis();
shellSort(arr2);
long end = System.currentTimeMillis();
System.out.println("100000 个数字希尔排序所需时间:" + (end - start) + " ms");
int [] arr3 = new int[100000];
for (int i = 0; i < 100000; i++) {
arr3[i] = (int) (Math.random() * 100000);
}
System.out.println("------------交换法速度----------");
long start2 = System.currentTimeMillis();
shellSort2(arr3);
long end2 = System.currentTimeMillis();
System.out.println("100000 个数字希尔排序所需时间:" + (end2 - start2) + " ms");
}
// 希尔排序(缩小增量排序) - 插入法
public static void shellSort(int [] arr) {
for (int i = arr.length / 2; i > 0; i /= 2) { // i 为增量,每次都要缩小增量
for (int j = i; j < arr.length; j++) {
int insertValue = arr[j]; // 插入的值
int insertIndex = j - i; // 插入的位置
// 寻找插入位置
while (insertIndex >= 0 && arr[insertIndex] > insertValue) {
arr[insertIndex+i] = arr[insertIndex];
insertIndex -= i;
}
// 插入
if (insertIndex != j - i) {
arr[insertIndex+i] = insertValue;
}
}
}
}
// 希尔排序(缩小增量排序) - 交换法
public static void shellSort2(int [] arr) {
int temp = 0, count = 0;
for (int i = arr.length / 2; i > 0; i /= 2) { // i 为增量,每次都要缩小增量
for (int j = i; j < arr.length; j++) {
// 遍历各组中所有的元素(共i组), 步长i
for (int k = j - i; k >= 0; k -= i) {
// 交换
if (arr[k+i] < arr[k]) {
temp = arr[k+i];
arr[k+i] = arr[k];
arr[k] = temp;
}
}
}
}
}
}
/* Code Running Result
------------原始数组----------
[1, 2, 5, 4, 6, 3]
------------有序数组----------
[1, 2, 3, 4, 5, 6]
------------插入法速度----------
100000 个数字希尔排序所需时间:19 ms
------------交换法速度----------
100000 个数字希尔排序所需时间:8021 ms
*/
5. 快速排序
快速排序(Quick Sort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
代码
下面代码实现快速排序和对快速排序进行速度测试
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int [] arr = {1, 2, 5, 4, 6, 3};
// 输出原始的数组
System.out.println("------------原始数组----------");
System.out.println(Arrays.toString(arr));
// 排序
quickSort(arr, 0, arr.length - 1);
// 输出排序后的数组
System.out.println("------------有序数组----------");
System.out.println(Arrays.toString(arr));
// 测试一下快速排序的速度,测试 100000 个随机数字进行排序所需要时间
System.out.println("------------测试速度----------");
int [] arr2 = new int[100000];
for (int i = 0; i < 100000; i++) {
arr2[i] = (int) (Math.random() * 100000);
}
long start = System.currentTimeMillis();
quickSort(arr2, 0, arr2.length - 1);
long end = System.currentTimeMillis();
System.out.println("100000 个数字快速排序所需时间:" + (end - start) + " ms");
}
// 快速排序
public static void quickSort(int [] arr, int left, int right) {
int l = left, r = right; // 左右下标
int reference = arr[(left + right) / 2]; // 以中间数字作为参照
int temp = 0;
while (l < r) {
// 先从右边找到小于等于 reference 的数
while (arr[r] > reference) {
r--;
}
// 再从左边找到大于等于 reference 的数
while (arr[l] < reference) {
l++;
}
if (l < r) { // 交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
} else {
break;
}
// 如果相等则跳一个,不然可能产生死循环(arr[l] == arr[r] == reference)
if (arr[l] == reference) {
r--;
}
if (arr[r] == reference) {
l++;
}
}
// 循环结束之后只有两种情况 l == r 或者 r + 1 = l,如果相等就各移动一个,如果 r + 1 = l,则 r往左都是小的,l往右都是大的
if (r == l) {
r--;
l++;
}
// 左右递归
if (left < r)
quickSort(arr, left, r);
if (right > l)
quickSort(arr, l, right);
}
}
/* Code Running Result
------------原始数组----------
[1, 2, 5, 4, 6, 3]
------------有序数组----------
[1, 2, 3, 4, 5, 6]
------------测试速度----------
100000 个数字快速排序所需时间:37 ms
*/
6. 归并排序
归并排序(Merge Sort)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。将一个序列分成两部分,对每部分分别排序之后,再归并成一个有序的序列。
代码
下面代码实现归并排序和对归并排序进行速度测试
import java.util.Arrays;
public class MergeSort {
public static void main(String[] args) {
int [] arr = {1, 2, 5, 4, 6, 3};
int [] temp = new int[arr.length];
// 输出原始的数组
System.out.println("------------原始数组----------");
System.out.println(Arrays.toString(arr));
// 排序
mergeSort(arr, 0, arr.length - 1, temp);
// 输出排序后的数组
System.out.println("------------有序数组----------");
System.out.println(Arrays.toString(arr));
// 测试一下快速排序的速度,测试 100000 个随机数字进行排序所需要时间
System.out.println("------------测试速度----------");
int [] arr2 = new int[100000];
int [] temp2 = new int[arr2.length];
for (int i = 0; i < 100000; i++) {
arr2[i] = (int) (Math.random() * 100000);
}
long start = System.currentTimeMillis();
mergeSort(arr2, 0, arr2.length - 1, temp2);
long end = System.currentTimeMillis();
System.out.println("100000 个数字归并排序所需时间:" + (end - start) + " ms");
}
// 归并排序
public static void mergeSort(int [] arr, int left, int right, int [] temp) {
if (left < right) {
int mid = (left + right) / 2;
// 左递归,使得左边有序
mergeSort(arr, left, mid, temp);
// 右递归,使得右边有序
mergeSort(arr, mid + 1, right, temp);
// 有序合并
merge(arr, left, mid, right, temp);
}
}
/**
* @description: 两个有序数组合并
* @Param: arr 原始数组
* @Param: left 左边索引
* @Param: mid 中间索引
* @Param: right 右边索引
* @Param: temp 做中转的数组
*/
public static void merge(int [] arr, int left, int mid, int right, int [] temp) {
int l = left; // 初始化左边序列的索引
int r = mid + 1; // 初始化右边有序序列的索引
int t = 0; // 初始化指向 temp 数组的索引
// 先把两边(有序)的数据按照规则填充到 temp 数组中,直到两边有一组处理完毕为止
while (l <= mid && r <= right) {
// 如果左边的有序序列的当前元素小于等于右边的有序序列的当前元素,把左边的元素拷贝到 temp 数组中
if (arr[l] <= arr[r]) {
temp[t++] = arr[l++];
} else { // 否则把右边的元素拷贝到 temp 数组中
temp[t++] = arr[r++];
}
}
// 把剩余数据的依次全部填充到 temp 中
while (l <= mid) {
temp[t++] = arr[l++];
}
while (r <= right) {
temp[t++] = arr[r++];
}
// 将 temp 数组拷贝到 arr 数组
System.arraycopy(temp, 0, arr, left, right - left + 1);
}
}
/* Code Running Result
------------原始数组----------
[1, 2, 5, 4, 6, 3]
------------有序数组----------
[1, 2, 3, 4, 5, 6]
------------测试速度----------
100000 个数字归并排序所需时间:15 ms
*/
7. 基数排序
基数排序(Radix Sort)是将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
代码
下面代码实现基数排序和对基数排序进行速度测试
import java.util.Arrays;
public class RadixSort {
public static void main(String[] args) {
int [] arr = {1, 2, 5, 4, 6, 3};
// 输出原始的数组
System.out.println("------------原始数组----------");
System.out.println(Arrays.toString(arr));
// 排序
radixSort(arr);
// 输出排序后的数组
System.out.println("------------有序数组----------");
System.out.println(Arrays.toString(arr));
// 测试一下插入排序的速度,测试 100000 个随机数字进行排序所需要时间
System.out.println("------------测试速度----------");
int [] arr2 = new int[100000];
for (int i = 0; i < 100000; i++) {
arr2[i] = (int) (Math.random() * 100000);
}
long start = System.currentTimeMillis();
radixSort(arr2);
long end = System.currentTimeMillis();
System.out.println("100000 个数字基数排序所需时间:" + (end - start) + " ms");
}
// 基数排序
public static void radixSort(int [] arr) {
// 基数排序是空间换时间经典算法
// 10个桶,分别放某位是 0-9 的数字
int [][] bucket = new int[10][arr.length];
// 记录每个桶存放了多少数据
int [] bucketElementCounts = new int[10];
// 寻找最大数
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
}
// 循环最大数的位数个数次即可
for (int mod = 1; max > 0; max /= 10, mod *= 10) {
// 将原数组中数据放到桶中
for (int j = 0; j < arr.length; j++) {
int temp = arr[j] / mod % 10;
bucket[temp][bucketElementCounts[temp]] = arr[j];
bucketElementCounts[temp]++;
}
// 将桶中数据放回原数组
int index = 0;
// 遍历每一个桶
for (int i = 0; i < 10; i++) {
// 如果桶有数据,就都放回原数组
if (bucketElementCounts[i] > 0) {
for (int j = 0; j < bucketElementCounts[i]; j++) {
arr[index++] = bucket[i][j];
}
}
// 清零 bucketElementCounts[i],为了下一次循环桶中数据放回原数组
bucketElementCounts[i] = 0;
}
}
}
}
/* Code Running Result
------------原始数组----------
[1, 2, 5, 4, 6, 3]
------------有序数组----------
[1, 2, 3, 4, 5, 6]
------------测试速度----------
100000 个数字基数排序所需时间:22 ms
*/
8. 堆排序
堆排序(Heap Sort)的基本思想是: 先将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值,然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次大值。如此反复执行,便能得到一个有序序列了。可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了。
代码
下面代码实现堆排序和对堆排序进行速度测试
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int [] arr = {1, 2, 5, 4, 6, 3};
// 输出原始的数组
System.out.println("------------原始数组----------");
System.out.println(Arrays.toString(arr));
// 排序
heapSort(arr);
// 输出排序后的数组
System.out.println("------------有序数组----------");
System.out.println(Arrays.toString(arr));
// 测试一下插入排序的速度,测试 100000 个随机数字进行排序所需要时间
System.out.println("------------测试速度----------");
int [] arr2 = new int[100000];
for (int i = 0; i < 100000; i++) {
arr2[i] = (int) (Math.random() * 100000);
}
long start = System.currentTimeMillis();
heapSort(arr2);
long end = System.currentTimeMillis();
System.out.println("100000 个数字堆排序所需时间:" + (end - start) + " ms");
}
public static void heapSort(int [] arr) {
// 将无序序列构建成一个堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
//
for (int i = arr.length - 1; i > 0; i--) {
// 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
// 重新调整堆
adjustHeap(arr, 0, i);
}
}
public static void adjustHeap(int [] arr, int i, int length) {
int temp = arr[i];
for (int j = 2 * i + 1; j < length; j = j * 2 + 1) {
// 找到左结点和右结点较大的(<构成大顶堆,>构成小顶堆)
if (j + 1 < length && arr[j] < arr[j + 1]) {
j++;
}
// 如果子结点大于父节点,就把较大的赋值给父节点(< 构成大顶堆,> 构成小顶堆)
if (arr[j] > temp) {
arr[i] = arr[j];
i = j; // 更新当前结点
} else { // 否则顶堆已调整完成
break;
}
}
// 把堆顶放在相应位置
arr[i] = temp;
}
}
/* Code Running Result
------------原始数组----------
[1, 2, 5, 4, 6, 3]
------------有序数组----------
[1, 2, 3, 4, 5, 6]
------------测试速度----------
100000 个数字堆排序所需时间:15 ms
*/
下面对一些排序的平均时间复杂度,空间复杂度等的比较,供参考。
In-place: 不占用额外内存;
Out-place: 占用额外内存;
稳定: 如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定: 如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
k: 桶的个数
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | In-place | 稳定 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | In-place | 不稳定 |
插入排序 | O(n2) | O(n) | O(n2) | O(1) | In-place | 稳定 |
希尔排序 | O(n log n) | O(n log2n) | O(n log2n) | O(1) | In-place | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | Out-place | 稳定 |
快速排序 | O(n log n) | O(n log n) | O(n2) | O(log n) | In-place | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | In-place | 不稳定 |
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(k) | Out-place | 稳定 |
桶排序 | O(n + k) | O(n + k) | O(n 2) | O(n + k) | Out-place | 稳定 |
基数排序 | O(n x k) | O(n x k) | O(n x k) | O(n + k) | Out-place | 稳定 |