1. 冒泡排序
Bubble Sorting 基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
优化:因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。
public static void bubbleAsc(int[] array) {
int temp;
for (int i = 0; i < array.length - 1; i++) {
boolean flag = true;//是否发生过交换
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {//如果逆序,则交换
flag = false;
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
if (flag) {//如果没有发生交换,说明排序完毕
break;
}
}
}
2. 选择排序
Select Sorting基本思想是:第一次从 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 次,得到一个按排序码从小到大排列的有序序列。
public static void selectAsc(int[] array) {
int min, minIndex;
for (int i = 0; i < array.length - 1; i++) {
min = array[i];//假定最小值是第一个元素
minIndex = i;//假定最小值索引是第一个
for (int j = i + 1; j < array.length; j++) {
if (min > array[j]) {//如果后面的元素比最小值还要小,交换
min = array[j];
minIndex = j;
}
}
if (minIndex != i) {//小小的优化
array[minIndex] = array[i];
array[i] = min;
}
}
}
3. 插入排序
Insertion Sorting 基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
public static void insertAsc(int[] array) {
int insertValue, insertIndex;
for (int i = 1; i < array.length; i++) {
insertValue = array[i];//待插入值
insertIndex = i - 1;//待插入位置
while (insertIndex >= 0 && array[insertIndex] > insertValue) {//如果待插入位置元素比待插入值大,将值向后移动,继续向前搜索
array[insertIndex + 1] = array[insertIndex];
insertIndex--;
}
if (insertIndex != i - 1) {//小小的优化
array[insertIndex + 1] = insertValue;
}
}
}
4. 希尔排序
插入排序可能存在的问题:数组 arr = {2, 3, 4, 5, 6, 1} 这时需要插入的数 1 (最小),需要交换 5 次,以及 1 次赋最小值;当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
Shell Sorting 基本思想是:把记录按下标的一定增量分组,对每组使用插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
只需要将插入排序稍微修改一下即可:在外部嵌套一层循环,该循环引入步长,然后将插入排序中 +1 或 -1 部分改为 +步长 或 -步长。
public static void shellAsc(int[] array) {
int insertValue, insertIndex;
//只是加一层步长的循环嵌套,然后把 1 替换成步长
for (int stepSize = array.length / 2; stepSize > 0; stepSize /= 2) {
for (int i = stepSize; i < array.length; i++) {
insertValue = array[i];
insertIndex = i - stepSize;
while (insertIndex >= 0 && array[insertIndex] > insertValue) {
array[insertIndex + stepSize] = array[insertIndex];
insertIndex -= stepSize;
}
if (insertIndex != i - stepSize) {
array[insertIndex + stepSize] = insertValue;
}
}
}
}
5. 快速排序
Quick Sorting 是对冒泡排序的一种改进,其基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程递归进行,以此达到整个数据变成有序序列。
public static void quickAsc(int[] array) {
quickAsc(array, 0, array.length - 1);
}
private static void quickAsc(int[] array, int left, int right) {
if (left >= right) {
return;
}
//以左边元素为基准
int key = array[left], l = left, r = right;
while (l < r) {
//先从右向左搜索(因为是以左边元素为基准)
while (l < r && array[r] >= key) {
r--;
}
//此时从右边找到比基准小的值
array[l] = array[r];
while (l < r && array[l] <= key) {
l++;
}
//此时从左边找到比基准大的值
array[r] = array[l];
}
//遍历完毕,此时 l = r,将基准赋值给 l 位置
array[l] = key;
//左边继续快排
quickAsc(array, left, l - 1);
//右边继续快排
quickAsc(array, l + 1, right);
}
6. 归并排序
Merge Sorting 利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
说明:可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程。
分比较简单,不对数据进行任何操作;
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
public static void mergeAsc(int[] array) {
mergeDivideAsc(array, 0, array.length - 1, new int[array.length]);
}
public static void mergeDivideAsc(int[] array, int left, int right, int[] tempArray) {
if (left >= right) {
return;
}
int mid = (left + right) / 2;
//向左继续分
mergeDivideAsc(array, left, mid, tempArray);
//向右继续分
mergeDivideAsc(array, mid + 1, right, tempArray);
//分完后,开始合并
mergeConquerAsc(array, left, mid, right, tempArray);
}
public static void mergeConquerAsc(int[] array, int left, int mid, int right, int[] tempArray) {
int leftIndex = left, rightIndex = mid + 1, index = 0;
//比较左边元素与右边元素大小,将小的放入临时数组
while (leftIndex <= mid && rightIndex <= right) {
if (array[leftIndex] > array[rightIndex]) {
tempArray[index++] = array[rightIndex++];
} else {
tempArray[index++] = array[leftIndex++];
}
}
//此时,左边和右边必定有一个已经遍历完毕
//如果左边还有元素,将元素依次放入临时数组
while (leftIndex <= mid) {
tempArray[index++] = array[leftIndex++];
}
//如果右边还有元素,将元素依次放入临时数组
while (rightIndex <= right) {
tempArray[index++] = array[rightIndex++];
}
index = 0;
//将临时数组数据放入目标数组中
for (int i = left; i <= right; i++) {
array[i] = tempArray[index++];
}
}
7. 基数排序
Radix Sorting 属于“分配式排序”,又称“桶子法”(Bucket Sorting或Bin Sorting),顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用;
基数排序法是1887年赫尔曼 · 何乐礼发明的,是效率高的稳定性排序法,是桶排序的扩展。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
基数排序基本思想:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列,图文说明如下:
说明:
-
基数排序是对传统桶排序的扩展,速度很快;
-
基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError;
-
基数排序时稳定的【注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的】;
-
有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考:code.i-harness.com/zh-CN/q/e98…。
public static void radixAsc(int[] array) { //定义 10 个桶 int[][] bucket = new int[10][array.length]; //10 个桶内元素下一个位置的索引,或者说是桶内元素个数 int[] bucketSize = new int[10]; for (int div = 1; ; div *= 10) { boolean zeroFlag = true;//是否所有元素除以 div 后都等于 0 for (int i = 0; i < array.length; i++) { int d = array[i] / div; if (d > 0) { zeroFlag = false; } int mod = d % 10; bucket[mod][bucketSize[mod]++] = array[i];//放入桶的对应位置 } if (zeroFlag) {//都等于 0,则说明排序完毕 break; } int index = 0; //将桶中元素放回数组,一次排序完毕 for (int i = 0; i < bucketSize.length; i++) { for (int j = 0; j < bucketSize[i]; j++) { array[index++] = bucket[i][j]; } //注意需要将桶内元素个数清 0 bucketSize[i] = 0; } } }
8. 堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它也是不稳定排序。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系;每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
大顶堆举例说明:
小顶堆举例说明:
一般升序采用大顶堆,降序采用小顶堆。
Heap Sorting 基本思想是:将待排序序列构造成一个大顶堆;此时,整个序列的最大值就是堆顶的根结点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余 n-1 个元素重新构造成一个大顶堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了。
public static void heapAsc(int[] array) {
//最后一个非叶子结点的计算公式:array.length / 2 - 1 //从后往前遍历,将堆变成大顶堆
for (int i = array.length / 2 - 1; i >= 0; i--) {
heapAsc(array, i, array.length);
}
int temp;
for (int i = 0; i < array.length - 1; i++) {
//此时第一个元素就是最大值,将其与最后一个位置交换
temp = array[0];
array[0] = array[array.length - 1 - i];
array[array.length - 1 - i] = temp;
//继续构造大顶堆,此时只有 0 位置不满足大顶堆条件
heapAsc(array, 0, array.length - 1 - i);
}
}
/**
* 本方法对 index 位置的元素进行大顶堆排序。
* 调用本方法有个前提:假定 index 位置的左右子树都是大顶堆,
* 只有 index 位置根结点不满足大顶堆条件。
* 如何保证该前提成立?
* 只需要从后往前构建大顶堆就可以了。
*
* @param array 堆
* @param index 待排序的根结点
* @param length 待排序的长度
*/
private static void heapAsc(int[] array, int index, int length) {
int target = array[index];
for (int i = index * 2 + 1; i < length; i = i * 2 + 1) {
//如果右结点比左结点大,取大的那个位置
if (i + 1 < length && array[i + 1] > array[i]) {
i++;
}
if (array[i] > target) {//如果子结点比待排序的大,将子结点值赋给父结点
array[index] = array[i];//此时 index 就是父结点,i 是子结点
index = i;//继续向下遍历,当前位置赋值给 index,在下一次循环时,i 会变成左子结点位置,继续满足 index 是父结点 i 是子结点
} else {//否则退出
break;
}
}
//循环完毕时,index 指向的位置要么已经替换到前面,要么就是原先的位置,再将值赋给 index
array[index] = target;
}
9. 常用排序算法总结和对比
- 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
- 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间;
- 空间复杂度:运行完一个程序所需内存的大小;
- n:数据规模;
- k:“桶”的个数;
- In-place:不占用额外内存;
- Out-place:占用额外内存。