最近在学习数据结构和算法,沈询老师讲到世界上没有一个完美的数据结构和算法,否则就不会出现这么多的数据结构和算法了,所以想学好数据结构和算法,最基本的就是得弄清这个数据结构和算法出现的原因和背景。那用了这么多年的排序算法,他们之间的联系和递进关系是什么样的呢?All In Code。
package org.longtuteng.sort;
import java.util.Arrays;
import java.util.Random;
/**
* 最近在学习数据结构和算法,沈询老师讲到世界上没有一个完美的数据结构和算法,否则就不会出现这么多的数据结构和算法了,
* 所以想学好数据结构和算法,最基本的就是得弄清这个数据结构和算法出现的原因和背景,以及他带来了什么,牺牲了什么。
* 比如我们学习arraylist,知道它基于数组,下标查找很快,增删很慢;于是出现了linkedlist,基于链表的它可以快速增删,
* 但是查找效率变低了;那么我又想增删快,又想查找快怎么办,我们引入了树,二叉树,二叉排序树,足够了么,我们发现不够,
* 因为树形结构太复杂了,维护其平衡要付出额外的代价;于是后面又有了跳表skiplist,兼顾了性能要求和复杂性...
*
* 说下自己为什么要回顾下几个经典排序算法,其实就是上面说的知其形不知其然。首先当然是编码啦,在某个风和日丽的下午,我花
* 了大概3H时间写了下面6个排序,结果能正确运行的只有一个选择排序,苦笑,然后又花了2个小时调试运行和排序效率对比。感慨
* 下不同排序算法效率差距真的大。附测试结果:(i7 8核 16g)
*
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* + 排序算法 + 数组长度一万用时 + 长度十万用时 + 长度一百万用时 + 长度一千万用时 + 长度一亿用时 +
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* + 冒泡排序 + 225ms + 19608ms + 太久 + 太久 + 太久 +
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* + 选择排序 + 40ms + 2875ms + 太久 + 太久 + 太久 +
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* + 插入排序 + 17ms + 837ms + 92161ms + 太久 + 太久 +
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* + 希尔排序 + 4ms + 14ms + 171ms + 2400ms + 32318ms +
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* + 并归排序 + 3ms + 16ms + 144ms + 1201ms + 15014ms +
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* + 快速排序 + 3ms + 17ms + 128ms + 1126ms + 12938ms +
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*
* 铺垫了这么多,那我们看一个排序算法到底要看什么呢?(这里只做简单介绍,具体可以参考csdn上很多优秀的文章)
* 1.时间复杂度,空间复杂度。首先的我们会看算法的比较次数,交换次数,是否占用了额外内存空间。
* 2.是否稳定,通俗的讲,就是我们将数组[3,7,5,5,1]从小到大排列时,会不会交换相同元素5的位置。
*
* 一些概念补充:
* 什么是逆序度?
* 我们想将数组[3,7,5,5,1]从小到大排列时,他其中的逆序为[3,1] [7,1] [7,5] [7,5] [5,1] [5,1] 那么这个数组的
* 逆序度就是6。很显然天然有序的数组,逆序度为0。
*
* ps:写的时候为了统一思路和阅读全部使用了for循环,其中某些可以改用为while循环使代码更加简洁。^_^
*
*/
public class SortTest {
/**
* 冒泡排序
* 核心:这个位置应该放哪个元素
* 通过不停的交换来减少逆序度,每次循环不能维持局部有序。
*
* @param array
*/
public static void dubbleSort(int[] array) {
long start = System.currentTimeMillis();
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - 1; j++) {
// 将较小的值替换到前面
if (array[j] > array[j + 1]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
System.out.println("dubbleSort array length:" + array.length
+ ", cost:" + (System.currentTimeMillis() - start));
}
/**
* 选择排序
* 核心:这个位置应该放哪个元素
* 优化冒泡排序,减少交换次数,但是没有减少遍历次数,每次循环维持局部有序。
*
* @param array
*/
public static void selectionSort(int[] array) {
long start = System.currentTimeMillis();
for (int i = 0; i < array.length - 1; i++) {
// 选择出本次循环中最小的
int minIndex = i;
for (int j = i; j < array.length; j++) {
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
System.out.println("selectionSort array length:" + array.length
+ ", cost:" + (System.currentTimeMillis() - start));
}
/**
* 插入排序
* 核心:这个元素应该放哪个位置
* 将待排序元素位置空出,优化了交换元素的步骤:
* 1.冒泡和选择排序的交换步骤:int temp = array[j + 1]; array[j + 1] = array[j]; array[j] = temp;
* 2.插入排序的交换步骤:array[j] = array[j - 1];
* 插入排序最好情况的时间复杂度优于选择排序。
*
* @param array
*/
public static void insertSort(int[] array) {
long start = System.currentTimeMillis();
for (int i = 1; i < array.length; i++) {
// 把待插入的位置空出来
int insertValue = array[i];
int j = i;
for (; j - 1 >= 0; j--) {
if (array[j - 1] > insertValue) {
array[j] = array[j - 1];
} else {
break;
}
}
array[j] = insertValue;
}
System.out.println("insertSort array length:" + array.length
+ ", cost:" + (System.currentTimeMillis() - start));
}
/**
* 希尔排序
* 核心:这个元素应该放哪个位置
* 优化插入排序,通过增加步长gap,让插入算法保持在较好情况下的排序效率。希尔排序的效率由步长的计算方式决定。
*
* @param array
*/
public static void shellSort(int[] array) {
long start = System.currentTimeMillis();
// 计算增量gap
for (int g = getKruthGap(array.length); ;g = getKruthGap(g)) {
for (int i = g; i < array.length; i++) {
// 把待插入的位置空出来
int insertValue = array[i];
int j = i;
for (; j - g >= 0; j = j - g) {
if (array[j - g] > insertValue) {
array[j] = array[j - g];
} else {
break;
}
}
array[j] = insertValue;
}
// 最小增量
if (g == 1) {
break;
}
}
System.out.println("shellSort array length:" + array.length
+ ", cost:" + (System.currentTimeMillis() - start));
}
/**
* 希尔排序增量 Kruth算法 K(n) = K(n-1) * 3 + 1 eg: 1, 4, 13, 40, 121...
*
* @param pre
* @return
*/
private static int getKruthGap(int pre) {
int n = 1;
while (n * 3 + 1 < pre) {
n = n * 3 + 1;
}
return n;
}
/**
* 并归排序
* 核心:递归保证每个最小的元素组,比如两个元素都是有序的,那么整个数组肯定是有序
* 分治法拆分合并,合并过程需要借助额外内存空间
*
* @param array
*/
public static void mergeSort(int[] array) {
long start = System.currentTimeMillis();
mergeSort(array, 0, array.length);
System.out.println("mergeSort array length:" + array.length
+ ", cost:" + (System.currentTimeMillis() - start));
}
/**
* 排序下标从low 到high 部分元素(包前不包后)
* 递归思想:将待排序部分划分为左右两个部分,先将两个部分分别排序,再将两部分合并且排序
*
* @param array
* @param low
* @param high
*/
private static void mergeSort(int[] array, int low, int high) {
if (high - low > 2) {
int mid = (high - low) / 2 + low;
// 左边排序
mergeSort(array, low, mid);
// 右边排序
mergeSort(array, mid, high);
// 将两部分合并且排序
mergeSort(array, low, mid, high);
} else if (high - low == 2) {
// 待排序部分只有两个元素时,直接进行排序,递归结束
if (array[high - 1] < array[low]) {
int temp = array[high - 1];
array[high - 1] = array[low];
array[low] = temp;
}
} else {
// 待排序部分只有一个元素时,递归结束
// to do nothing
}
}
/**
* 合并两部分数组且排序
* 原数组的下标从low 到high 部分被mid 划分为左右两个部分,
* 且两个部分都是有序的,如何使下标从low到high变得整体有序
* 简单思考如何将两个数组[1, 4, 8, 10] 和[2, 3, 6, 7] 合并成一个有序的数组
*
* @param array
* @param low
* @param mid
* @param high
*/
private static void mergeSort(int[] array, int low, int mid, int high) {
// 拷贝一份待排序部分,把array 待排序部分空出来
// 拷贝整个数组会造成内存浪费,所以只拷贝一部分
int[] copy = Arrays.copyOfRange(array, low, high);
// 这里的left 和right 是两部分数组在拷贝数组中对应的起始下标
int left = 0;
int right = mid - low;
// 同样的下文中的left >= mid - low 和right >= high - low是两部分数组在拷贝数组中对应的结束下标
for(int index = low; index < high; index ++) {
if (left >= mid - low){
array[index] = copy[right++];
} else if (right >= high - low) {
array[index] = copy[left++];
} else {
if (copy[left] < copy[right]) {
array[index] = copy[left++];
} else {
array[index] = copy[right++];
}
}
}
}
/**
* 快速排序
* 核心:递归保证每个元素处于正确位置,比如左边都比他小,右边都比他大,那么整个数组肯定是有序
* 冒泡算法和并归算法的合并优化,对冒泡算法的交换增加了步长gap,降低交换次数。解决并归算法的合并部分复杂的问题
*
* @param array
*/
public static void quickSort(int[] array) {
long start = System.currentTimeMillis();
quickSort(array, 0, array.length);
System.out.println("quickSort array length:" + array.length
+ ", cost:" + (System.currentTimeMillis() - start));
}
/**
* 数组下标从low 到high 部分元素的哨兵位置设置正确(包前不包后)
* 递归思想:设置指数组定段哨兵位置,按哨兵位置将数组划分为左右两部分,分别设置两部分哨兵
*
* @param array
* @param low
* @param high
*/
private static void quickSort(int[] array, int low, int high) {
// 递归结束标志
if (low == high) {
return;
}
// 设置哨兵, 即标志位,左边比他小,右边比他大
// 设置左右起始位
int left = low;
int right = high - 1;
// 左右标志位相撞,说明寻找结束
for (; left < right;) {
// 右边找一个比哨兵位大的值
for (; left < right; right--) {
if (array[right] < array[low]) {
break;
}
}
// 左边找一个比哨兵位小的值
for (; left < right; left++) {
if (array[left] > array[low]) {
break;
}
}
// 交换
if (left != right) {
int temp = array[left];
array[left] = array[right];
array[right] = temp;
}
}
// 相撞位置即哨兵应在的位置
int temp = array[low];
array[low] = array[left];
array[left] = temp;
// 分别设置两部分哨兵
quickSort(array, low, left);
quickSort(array, left + 1, high);
}
/**
* 创建长度为length的数组
*
* @param length
* @return
*/
public static int[] createArray(int length) {
int[] array = new int[length];
Random random = new Random(System.currentTimeMillis());
int total = length << 4;
if (total < 0) {
total = Integer.MAX_VALUE;
}
for (int i = 0; i < length; i++) {
array[i] = random.nextInt(total);
}
return array;
}
/**
* 打印数组内容
*
* @param array
* @return
*/
public static void print(int[] array) {
StringBuffer sb = new StringBuffer("[");
int total = 0;
for (int i = 0; i < array.length; i++) {
sb.append(array[i] + " ");
total += array[i];
}
sb.append("] total:" + total);
System.out.println(sb.toString());
}
public static void main(String[] args) {
int[] array = createArray(23);
// 1.测试算法是否正确,以希尔排序为例
print(array);
shellSort(array);
print(array);
// 2.统计算法耗时
dubbleSort(Arrays.copyOf(array, array.length));
selectionSort(Arrays.copyOf(array, array.length));
insertSort(Arrays.copyOf(array, array.length));
shellSort(Arrays.copyOf(array, array.length));
mergeSort(Arrays.copyOf(array, array.length));
quickSort(Arrays.copyOf(array, array.length));
}
}