通常我们从下面几个方面来分析一个排序算法:
- 时间复杂度 :时间效率决定了算法运行多久,O(1)
- 空间复杂度:
- 比较次数&交换次数:排序肯定会牵涉到两个操作,比较和交换。
- 稳定性:相同的两个数排完序后,相对位置不变。
插入排序,希尔排序,归并排序这三种排序方式其实是依次优化的,希尔排序是插入排序的一种升级,归并排序是插入算法的升级,效率比希尔排序还好。
一、插入排序
1、算法描述
插入排序也是一种常见的排序算法。
插入排序的思想:
将初始数据分为有序部分和无序部分,每一步将一个无序部分的数据插入到前面已经排好序的有序部分中,直到插完所有元素为止。
插入排序的步骤如下:
- 将将数组分为两段,数组分成有序数组段和无序数组段。初始化时有序数组段只有一个元素。
- 每次从无序数组中取出一个元素,与有序数组中的元素从后向前依次进行比较,直到找到合适的位置,然后该元素插到有序数组中,即保证插入后有序数组段仍然有序。
- 重复执行上述操作,直到无序数组段全部插入完成。
时间复杂度: O(n^2)
稳定性 :稳定
2、示例
生活中有很多类似于插入排序的行为,例如打扑克。
比如我们对 7, 8, 6, 9, 0, 4, 3进行插入排序
- step1:7为有序数组段,8 9 0 4 3为无序数组段。
- step2:取出 8 和 7比较,发现 8>7,所以不用交换,即: 7 8 6 9 0 4 3
- step2:取出 6,依次与前面的7和8比较 ,则6放在 7的前面,即: 6 7 8 9 0 4 3
- step3:取出 9,依次与前面的比较,因为9大,所以不用交换,即:6 7 8 9 0 4 3
- …
- 最终得到:0 3 4 6 7 8 9
从以上操作中我们看到插入排序会经历一个元素的比较以及元素的移动。
当我们每次从无序数组中取出一个元素,与有序数组中的元素从后向前依次进行比较,直到找到合适的位置,然后该元素插到有序数组中,并将有序数组段中插入点之后的元素进行往后移动。
代码如下:
public static void main(String[] args) {
int[] array = {7, 8, 6, 9, 0, 4, 3};
insertionSort(array);
System.out.println("最终排序结果为:" + Arrays.toString(array));
}
private static void insertionSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
int length = array.length;
/**
* 2 层循环<br/>
* 时间复杂度:n^2 <br/>
* 最好的情况:O(n);
*/
for (int i = 1; i < length; i++) { // 下标从1开始,因为第一个数据不用排序,我们将数组分成了[0, i)已排好序和[i, length0未排好序数组。
// 取出下标为i的数,要拿这个数与i之前的数比较
int temp = array[i];
int j = i - 1;
for (; j >= 0; j--) { // 从尾到头比较
if (temp < array[j]) {
array[j + 1] = array[j]; // 数据往后移动
} else {
// 这个break特别关键。因为前面已经是排好序的,那么找到一个比它小的就不用再往前遍历了,因为前面的肯定更小。
break;
}
}
// 上面的 j在 for循环中其实就是temp的最终位置的前一个,所以这里需要 j+1
array[j + 1] = temp;
System.out.println("第 " + i + " 次的排序结果为:" + Arrays.toString(array));
}
}
插入排序时间复杂度:
最好的情况就是传进来的数组是已经有序的,每次都进入 break中,这个时候时间复杂度为 O(n)。
最坏的情况就是传进来的数组刚好是逆序的 ,每次都不会进入 break中,这个时候时间复杂度为 O(n^2)。
优化:
如果这个break执行的越多,那么插入排序效率就越高。所以优化插入排序的方式就是尽量让数组期望排序,让break多执行或者尽量少的次数让数组先有序,因此出现了希尔排序和归并排序。
二、希尔排序
1、算法描述
希尔排序是基于插入排序的基础上进行改进改进后的算法。因为当数据移动次数太多时会导致效率低下。所以我们可以先让数组整体有序(刚开始移动的幅度大一点,后面再小一点),这样移动的次数就会降低,进而提高效率。
希尔排序是把一个数组
按照一个增量分组,数组索引为 n和 n+d的倍数为一组
,每次分组增量都是按照 d = d/2 的方式递减。对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的元素越来越多,当增量减至1时,整个数据恰好被分成一组,算法便终止。
其实就是分成很多小组使序列尽可能的变成有序段,因为我们通过对插入排序分析可知,插入排序对已经排好序的序列速度是很快的。如果数据量小的话,希尔排序对插入排序的优化并不明显。
时间复杂度: O(nlogn)
稳定性 :不稳定
2、示例
比如:一个数组是 7, 8, 9, 0, 4, 3, 1, 2, 5, 10,我们取的增量分别是5,2,1。
- 第一次,d = 5,分为 5组,即:[7, 3],[8, 1],[9, 2],[0, 5],[4, 10],排序之后: 3, 1, 2, 0, 4, 7, 8, 9, 5, 10
- 第二次,d = 2,分为 2组,即:[3, 2, 4, 8, 5],[1, 0, 7, 9, 10],排序之后: 2, 0, 3, 1, 4, 7, 5, 9, 8, 10
- 第三次,d = 1,分为 1组,即:[2, 0, 3, 1, 4, 7, 5, 9, 8, 10],排序之后:0, 1, 2, 3, 4, 5, 7, 8, 9, 10
代码如下:
public static void main(String[] args) {
int[] array = {7, 8, 9, 0, 4, 3, 1, 2, 5, 10};
shellSort(array);
System.out.println("最终排序结果为:" + Arrays.toString(array));
}
public static void shellSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
//取增量遍历
for (int step = array.length / 2; step > 0; step /= 2) {
// 接下来的过程类似于插入排序
for (int i = step; i < array.length; i++) {
int temp = array[i];
int j;
for (j = i - step; j >= 0 && array[j] > temp; j -= step) {
array[j + step] = array[j];
}
array[j + step] = temp;
System.out.println("第 " + step + " 次的排序结果为:" + Arrays.toString(array));
}
System.out.println("第 " + step + " 次增量的排序结果为:" + Arrays.toString(array));
}
}
三、归并排序
1、算法描述
归并排序的思想:
归并排序是一种非常高效的排序算法,其核心用到了递归和分治思想
。
归并排序的过程分为 拆分 和 归并。
- 拆分:将一个等待排序数组拆分成更多更小的数组,直到不能拆分为止。
- 归并:将拆分后的数组合并,在合并的过程中通过插入算法进行合并。
归并排序的步骤如下:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
将另一序列剩下的所有元素直接复制到合并序列尾。
时间复杂度: O(nlogn)
稳定性 :稳定
2、示例
假设我们对 9, 5, 6, 8, 0, 3, 7, 1 进行归并排序。
代码如下:
public static void main(String[] args) {
int[] array = { 7, 8, 9, 0, 4, 3, 1, 2, 5, 10 };
mergeSort(array, 0, array.length - 1);
System.out.println("最终排序结果为:" + Arrays.toString(array));
}
/**
* 归并排序
*
* @param array
* - 数组
* @param left
* - 数组的左端
* @param right
* - 数组的右端
*/
public static void mergeSort(int[] array, int left, int right) {
// 将数组分段成只有一个元素,就不用再拆了
if (left == right) {
return;
}
int mid = (left + right) / 2;
// 1.左右两端递归
mergeSort(array, left, mid);
mergeSort(array, mid + 1, right);
// 2.拆分完毕之后合并
merge(array, left, mid, right);
}
/**
* 自顶向下合并的过程,也就是递归里面归的过程
*
* @param array
* @param left
* @param mid
* @param right
*/
private static void merge(int[] array, int left, int mid, int right) {
int[] temp = new int[right - left + 1];// 临时数组用来保存合并的数据
int point1 = left; // 表示左边的第一个数的位置
int point2 = mid + 1; // 表示右边的第一个数的位置
int loc = 0;
while (point1 <= mid && point2 <= right) {
if (array[point1] < array[point2]) {
temp[loc++] = array[point1++];
} else {
temp[loc++] = array[point2++];
}
}
// 复制左边数组剩余的值
while (point1 <= mid) {
temp[loc++] = array[point1++];
}
// 复制右边数组剩余的值
while (point2 <= right) {
temp[loc++] = array[point2++];
}
int index = 0;
// 把temp全部复制给数组
while (left <= right) {
array[left++] = temp[index++];
}
System.out.println("第 " + mid + " 次的拆分合并排序结果为:" + Arrays.toString(array));
}
– 求知若饥,虚心若愚。