数据结构与算法学习(一):排序算法

最近在学习数据结构和算法,沈询老师讲到世界上没有一个完美的数据结构和算法,否则就不会出现这么多的数据结构和算法了,所以想学好数据结构和算法,最基本的就是得弄清这个数据结构和算法出现的原因和背景。那用了这么多年的排序算法,他们之间的联系和递进关系是什么样的呢?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)); } }

 

你可能感兴趣的:(Java,数据结构预算法)