一、概述
不论是在面试中还是日常开发中,排序算法都是经常会用/考到的。常用的排序算法共8种,可分为5类:插入排序
、选择排序
、交换排序
,归并排序
和基数排序
。
二、插入排序
插入排序中比较常见的是直接插入排序
和希尔排序
:
-
直接插入排序
- 基本思想:
在要排序的一组数中,假设前面(n-1) [n>=2] 个数已经是排好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。 - 性能/稳定性
平均时间复杂度 空间复杂度 稳定性 O(n*2) O(1) 稳定 - 示例代码:
public static void straightInsertionSort(int[] arr) { int length = arr.length; //单独把数组长度拿出来 提高效率 int insertNum; //要插入的数 for (int i = 1; i < length; i++) { //第一个数不用排序,所以下标从1开始 insertNum = arr[i]; //取出要排序的这个数(后面移动时会丢失这个数) int j = i - 1; //序列元素个数(已排好序的元素个数) while (j >= 0 && arr[j] > insertNum) { //从后往前循环,将大于insertNum的数向后移动 arr[j + 1] = arr[j]; //元素向后移动 j--; } arr[j + 1] = insertNum; //找到位置,插入当前元素。 } }
- 基本思想:
-
希尔排序
针对直接插入排序的低效率问题,有人对其进行了改进与升级,这就是现在的希尔排序。希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。- 基本思想
将数的个数设为n,取奇数k=n/2,将下标差值为k的数分为一组,构成有序序列。再取k=k/2 ,将下标差值为k的数分为一组,构成有序序列。如此重复,直到k=1执行简单插入排序。 - 性能/稳定性
平均时间复杂度 空间复杂度 稳定性 O(n*1.3) O(1) 不稳定 - 示例代码:
public static void shellSort(int[] arr) { int length = arr.length; while (length != 0) { length = length / 2; for (int i = 0; i < length; i++) { //分组 for (int j = i + length; j < arr.length; j += length) {//元素从第二个开始 int k = j - length; //k为有序序列最后一位的位数 int temp = arr[j]; //要插入的元素 while (k >= 0 && arr[k] > temp) { //从后往前遍历 arr[k + length] = arr[k]; k -= length; //向后移动len位 } arr[k + length] = temp; } } } }
- 基本思想
三、选择排序
-
直接选择排序
- 基本思想:
遍历整个序列,将最小的数放在最前面。遍历剩下的序列,将最小的数放在最前面。重复第二步,直到只剩下一个数。 - 性能/稳定性
平均时间复杂度 空间复杂度 稳定性 O(n*2) O(1) 不稳定 - 示例代码:
public static void simpleSelectSort(int[] arr) { int length = arr.length; for (int i = 0; i < length; i++) { //循环次数 int value = arr[i]; //用于存储最小值 int position = i; //用于存储最小值下标 for (int j = i + 1; j < length; j++) { //找到最小值的位置 if (arr[j] < value) { value = arr[j]; position = j; } } arr[position] = arr[i]; //交换 arr[i] = value; } }
- 基本思想:
-
堆排序
- 基本思想
利用大顶堆设计出的一种算法,是对简单选择排序的优化。 - 性能/稳定性
平均时间复杂度 空间复杂度 稳定性 O(nlog2n) O(1) 不稳定 - 示例代码:
public static void heapSort(int[] arr) { int length = arr.length; //循环建堆 for (int i = 0; i < length - 1; i++) { //建堆 buildMaxHeap(arr, length - 1 - i); //交换顶堆和最后一个元素 swap(arr, 0, length - 1 - i); } } //交换元素 private static void swap(int[] arr, int i, int j) { arr[i] = arr[i] + arr[j]; arr[j] = arr[i] - arr[j]; arr[i] -= arr[j]; } /** * 对arr数组从0到lastIndex建大顶堆 * <p> * 首先获取lastIndex的父节点(k)并从其开始循环 * 判断k是否有子节点 -> 取子节点较大值下标 -> 交换父子节点 -> 循环 * * @param arr arr * @param lastIndex 最后一个节点 */ private static void buildMaxHeap(int[] arr, int lastIndex) { //从lastIndex处节点(最后一个节点)的父节点开始 //认为存储方式是按层次存储的,先左后右 for (int i = (lastIndex - 1) / 2; i >= 0; i--) { //k保存正在判断的节点 int k = i; //如果当前k节点的子节点存在 while (k * 2 + 1 <= lastIndex) { //k节点的左子节点索引 int biggerIndex = 2 * k + 1; //如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在 if (biggerIndex < lastIndex) { //如果右子节点的值较大 if (arr[biggerIndex] < arr[biggerIndex + 1]) { //biggerIndex总是记录较大子节点的索引 biggerIndex++; } } //如果k节点的值小于其较大的子节点的值 if (arr[k] < arr[biggerIndex]) { //交换他们 swap(arr, k, biggerIndex); //将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值 k = biggerIndex; } else { break; } } } }
- 基本思想
四、交换排序
-
冒泡排序
- 基本思想
依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。重复第一趟步骤,直至全部排序完成。 - 性能/稳定性
平均时间复杂度 空间复杂度 稳定性 O(n*2) O(1) 稳定 - 示例代码:
public static void bubbleSort(int[] arr) { int length = arr.length; for (int i = 0; i < length; i++) { //设置循环次数 for (int j = 0; j < length - i - 1; j++) { if (arr[j] > arr[j + 1]) { //交换位置 arr[j] += arr[j + 1]; arr[j + 1] = arr[j] - arr[j + 1]; arr[j] -= arr[j + 1]; } } } }
- 基本思想
-
快速排序
- 基本思想
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归整个数据变成有序序列。 - 性能/稳定性
平均时间复杂度 空间复杂度 稳定性 O(nlog2n) O(nlog2n) 不稳定 - 示例代码:
public static void quickSort(int[] arr, int start, int end) { if (start < end) { int baseNum = arr[start]; //选基准值 int midNum; //记录中间值 int i = start; int j = end; do { //从前往后找到大于baseNum的第一个数的索引(下标) while (arr[i] < baseNum && i < end) { i++; } //从后往前找到小于baseNum的第一个数的索引(下标) while (arr[j] > baseNum && j > start) { j--; } if (i <= j) { midNum = arr[i]; arr[i] = arr[j]; arr[j] = midNum; i++; j--; } } while (i <= j); //到这里时j < i了 if (start < j) { quickSort(arr, start, j); } if (i < end) { quickSort(arr, i, end); } } }
- 基本思想
五、归并排序
-
基本思想
建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。- 性能/稳定性
平均时间复杂度 空间复杂度 稳定性 O(nlog2n) O(1) 稳定 示例代码:
public static void mergeSort(int[] arr, int start, int end) {
int mid = (start + end) / 2;
if (start < end) {
mergeSort(arr, start, mid);
mergeSort(arr, mid + 1, end);
//左右归并
merge(arr, start, mid, end);
}
}
//进行归并操作
private static void merge(int[] arr, int start, int mid, int end) {
//开辟新空间用于存储
int[] temp = new int[end - start + 1];
int i = start;
int j = mid + 1;
int k = 0;
//把较小的数先移到新数组中
while (i <= mid && j <= end) {
if (arr[i] < arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
//把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = arr[i++];
}
//把右边剩余的数移入数组
while (j <= end) {
temp[k++] = arr[j++];
}
//把新数组中的数覆盖到arr数组
if (temp.length >= 0) System.arraycopy(temp, 0, arr, start, temp.length);
}
六、基数排序(桶子法)
基本思想
将整数按位数切割成不同的数字,然后按每个位数分别比较。-
性能/稳定性
平均时间复杂度 空间复杂度 稳定性 O(d(r+n)) O(rd+n) 稳定 示例代码
public static void radixSort(int[] arr) {
int max = findMax(arr, 0, arr.length - 1);
//需要遍历的次数由数组最大值的位数来决定
for (int i = 1; max / i > 0; i = i * 10) {
int[][] buckets = new int[arr.length][10];
//获取每一位数字(个、十、百、千位...分配到桶子里)
for (int j = 0; j < arr.length; j++) {
int num = (arr[j] / i) % 10;
//放入桶子里
buckets[j][num] = arr[j];
}
//回收桶子里的元素
int k = 0;
//有10个桶子
for (int j = 0; j < 10; j++) {
//对每个桶子里的元素进行回收
for (int l = 0; l < arr.length; l++) {
//如果桶子里面有元素就回收(数据初始化会为0)
if (buckets[l][j] != 0) {
arr[k++] = buckets[l][j];
}
}
}
}
}
//找到数组中最大值
private static int findMax(int[] arrays, int start, int end) {
//如果该数组只有一个数,那么最大的就是该数组第一个值了
if (start == end) {
return arrays[start];
} else {
int a = arrays[start];
int b = findMax(arrays, start + 1, end);//找出整体的最大值
if (a > b) {
return a;
} else {
return b;
}
}
}