对于排序算法一般考虑:java
是否稳定(相同值的两个数相对位置在修改先后是否会变) 和 时间复杂度(算法执行次数的规模量级)。至于说空间复杂度(算法在运行过程当中临时占用存储空间大小的量度)对于每种算法具体实现代码迥异。git
通常而言:
稳定的排序算法有:冒泡排序、插入排序、归并排序和基数排序;
不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
我的感受在具体实现时,是否稳定取决于在处理相同值的两个不一样数据下标是否swap。好比说:算法
等等诸如此类在实现的处理细节决定是否能够作成稳定, 可是一样的带来执行的次数会多。 而对于实际上只须要知道排序后的结果数组,固然执行规模越少,效率越快 越好喽。数组
快排就没办法达到稳定了, 由于是队首与队尾双边查找,而后置换。 相同的值 先查到就先置换,后查到就更靠近中间位置。app
冒泡、选择、插入: ide
平方阶(O(N2))排序ui
通过实际代码能够发现,这些都是在从前日后的轮询数组,只是边界在慢慢变小,边界每变化一次就轮询一次。 (冒泡与选择相同都在于轮询数组比较值取大小;不一样在于冒泡是每次比较后都swap,而选择只是记录须要的数据下标,最后才将下标与队尾swap)spa
快速、堆和归并: 指针
线性对数阶(O(n*log2n))排序日志
通过实际代码能够发现, 都相似于将排序数组切分红2部分递归处理。
基数: 线性阶(O(n))排序
我的感受须要控制好轮询边界和等值时如何处理问题。能够相应提升计算效率。
相邻的两个值比较,将最大/小的值逐步换至队尾/首。
须要注意的是:
public class BubbleSort extends AbstractSort { @Override public int[] sort(int[] param) { int count = 0; //已经排序过的次数 int max_count = param.length - 1;//最大的排序次数 while (count <= max_count) { boolean is_swap = false; // 断定本轮是否swap了,没有则说明数组已经有序, break for (int i = 1; i <= max_count - count; i++) { // 控制好边界 if (param[i - 1] > param[i]) { swap(param, i, i - 1); is_swap = true; } } count++; if (!is_swap) { break; } } System.out.println(String.format("总量:%s, 轮询次数: %s", param.length, count)); return param; } public static void main(String[] args) { int[] param = new BubbleSort().sort(new int[] { 1, 2, 3, 5, 8, 6, 4, 9, 7 }); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
选中最大/小值置于已经排序队列的队尾,在未排序队列中递推执行。
与冒泡的异同在于: 都是轮询比较值的大小,只是冒泡是每次比对后均可能作swap,而选择是记录下下标,与本次轮询数组的边界的队首/队尾一次swap,固然若是下标就是队首/队尾就不用swap。
public class SelectionSort extends AbstractSort { @Override public int[] sort(int[] param) { for (int i = 0; i < param.length; i++) { // 此处因是升序排序,因此i的值就是边界 int min_index = getMinIndex(param, i); swap(param, i, min_index); } return param; } /** * 在指定起始位置以后队列中查找最小值的下标 * * @param param * @param start * @return */ private int getMinIndex(int[] param, int start) { int min_index = start; for (int j = start + 1; j < param.length; j++) { if (param[j] < param[min_index]) { min_index = j; } } return min_index; } }
将待排序序列第一个元素看作一个有序序列,把第二个元素到最后一个元素当成是未排序序列。从头至尾依次扫描未排序序列,将扫描到的当前元素依次与已排序序列从后往前递推比较,若是当前元素小,则swap, 直到边界。
/** * 稳定 插入排序 1)将待排序序列第一个元素看作一个有序序列,把第二个元素到最后一个元素当成是未排序序列。 * <p/> * 2)从头至尾依次扫描未排序序列,将扫描到的每一个元素插入有序序列的适当位置。(若是待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面 * 。) Created by bear on 2016/2/29. */ public class InsertionSort extends AbstractSort { /** * 假定前面都是已从小至大排序的队列:若是当前下标的值小于前一下标的值,交换位置,并在已排序队列中从后向前递推再判断; * * @param param * @return */ public int[] sort(int[] param) { for (int i = 1; i < param.length; i++) { for (int j = i - 1; j >= 0; j--) { if (param[j] > param[j + 1]) { swap(param, j, j + 1); } else { break; // 若是后面的要不小于,则break; 边界 } } } return param; } public static void main(String[] args) { int[] param = new InsertionSort().sort(new int[] { 4, 5, 3, 5, 8, 6, 4, 9, 7 }); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
插入排序在对几乎已经排好序的数据操做时,便可以达到线性排序的高效率
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
按步长分组并插入排序,递减步长至0。步长的起始值能够按需定义
package com.noob.sort; /** * 不稳定(由于每次的排序队列都是在元队列基础上按步长从新分组后的,致使相同值的数位置发生变化。) * 按步长分组并插入排序,递减步长至0 * 希尔排序是基于插入排序的如下两点性质而提出改进方法的: * <p/> * 插入排序在对几乎已经排好序的数据操做时, 效率高, 便可以达到线性排序的效率 * 但插入排序通常来讲是低效的, 由于插入排序每次只能将数据移动一位 * 希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。 * <p/> * 算法步骤: * <p/> * 1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1; * <p/> * 2)按增量序列个数k,对序列进行k 趟排序; * <p/> * 3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列做为一个表来处理,表长度即为整个序列的长度。 * Created by bear on 2016/2/29. */ public class ShellSort extends AbstractSort { public int[] sort(int[] param) { int length = param.length; for (int gap = length / 2; gap > 0; gap /= 2) {//步长递减至1,核心思想与插入排序一致 for (int i = 0; i <= gap; i++) {//按步长分组[0, gap, 2gap, 3gap],[1, 1 + gap, 1 + 2gap, 3 + 3gap]... for (int m = i; m < length - gap; m += gap) {//在每个分组中进行排序,从第二个值m开始向前递推比较。比较完后m++ for (int j = m + gap; j >= i + gap; j -= gap) { if (param[j - gap] > param[j]) {//最小坐标为分组起始坐标,最大坐标不大于原数组最大坐标 swap(param, j, j - gap); } } } } } return param; } public static void main(String[] args) { int[] param = new ShellSort().sort(new int[] { 12, 3, 2, 3, 5, 8, 19, 6, 4, 9, 7, 0 }); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
选择一个基准元素,一般选择第一个元素或者最后一个元素, 经过一趟扫描,将待排序列分红两部分, 前比基准元素小,后大于等于基准元素, 此时基准元素在其排好序后的正确位置, 而后再用一样的方法递归地排序划分的两部分。
实现的细节:
以第一个元素做为基准数。(细节在于:前部分大于等于基准数的都置换到后半部分,最后留基准数与重合指针位置比对肯定好边界)
package com.noob.sort; /** * 不稳定(判断时,若相等,则出现不稳定情况) 快速排序:关键值排序 * (1)基本思想:选择一个基准元素,一般选择第一个元素或者最后一个元素,经过一趟扫描,将待排序列分红两部分,一部分比基准元素小,一部分大于等于基准元素, * 此时基准元素在其排好序后的正确位置,而后再用一样的方法递归地排序划分的两部分。 * <p> * Created by bear on 2016/3/2. */ public class QuickSort extends AbstractSort { @Override public int[] sort(int[] param) { return sort_core(param, 0, param.length - 1); } /** * 左边 < 关键值 <= 右边 * * @param param * @param start 开始下标 * @param end 结束下标 * @return */ private int[] sort_core(int[] param, int start, int end) { int radix = param[start]; //选开始位置为基准数 int low = start + 1; // 队首指针开始位置为除基准数后下标 int high = end; // 队尾指针开始位置 while (low < high) {// 确保两个指针不交叉!! 快排思想:后面部分是大于等于基准数的,因此 队尾指针的数值相等也左移,队首则不移动 /** * 跳出循环时的状态: param[high] < radix || low == high */ while (param[high] >= radix && low < high) { //队尾指针的值 >= 基准值 & 队首指针下标 < 队尾首指针下标 high--; //队尾指针前移 } /** * 跳出循环时的状态: param[low] >= radix || low == high */ while (param[low] < radix && low < high) { //队首指针的值 < 基准值 & 队首指针下标 < 于队尾首指针下标 low++; //队首指针后移 } if (low >= high) { // low 必定 <= high if (low > high) { System.out.println(String.format("error test: low %s > high %s : %s", low, high, low > high)); } break; } else { if (param[low] == param[high]) { //必定是param[low] > param[high] System.out.println("error1"); break; } swap(param, low, high); // 换好以后 param[high] 必定 > param[low] } } if (low == high) { // 基准数大, 则边界就是这个low; 若是基准数小, 则边界是low-1. 并swap. Integer limit = radix > param[low] ? low : low - 1; swap(param, start, limit); //分割成左右2部分 if (limit - 1 > start) { sort_core(param, start, limit - 1); //左部分 } if (limit + 1 < end) { sort_core(param, limit + 1, end); // 右部分 } } else { System.out.println(String.format("error test: low %s != high %s ", low, high)); } //System.out.println(toString(param)); return param; } public static void main(String[] args) { int[] param = new int[] { 5, 4, 3, 2, 1, 0, 2, 5, 6, 4, 9, 5 }; System.out.println(toString(new QuickSort().sort(param))); } }
运行结果:
没有打印出任何 low > high 的日志;证实了: low和high严格控制在 low<=high的状况!!
归并排序: 采用分治法,递归将数组分割成两部分红两个已排序队列,再合并至新的数组中。
两个数组都有一个从队首开始扫描的指针,依次对领一个数组指针值比较,小的先写入新的store数组中,指针后移:++,再循环比较。
package com.noob.sort; /** * 不稳定(由于每次的排序队列都是在元队列基础上按步长从新分组后的,致使相同值的数位置发生变化。) 按步长分组并插入排序,递减步长至0 * 希尔排序是基于插入排序的如下两点性质而提出改进方法的: * <p/> * 插入排序在对几乎已经排好序的数据操做时, 效率高, 便可以达到线性排序的效率 但插入排序通常来讲是低效的, 由于插入排序每次只能将数据移动一位 * 希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。 * <p/> * 算法步骤: * <p/> * 1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1; * <p/> * 2)按增量序列个数k,对序列进行k 趟排序; * <p/> * 3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 * 时,整个序列做为一个表来处理,表长度即为整个序列的长度。 Created by bear on 2016/2/29. */ public class ShellSort extends AbstractSort { public int[] sort(int[] param) { int length = param.length; if (length > 1) { int split = length / 2; int[] low = splitArray(param, 0, split - 1); int high[] = splitArray(param, split, param.length - 1); //切割数组后排序 param = sort_core(sort(low), sort(high)); } return param; } /** * 两个已从小至大排序的数组合并 * * @param low * @param hight * @return */ private int[] sort_core(int[] low, int[] hight) { int low_length = low.length; int high_length = hight.length; int[] result = new int[low_length + high_length]; int i = 0, j = 0, m = 0; while (i < low_length || j < high_length) {//只有当两个数组都取完才退出循环 if (i < low_length && (j >= high_length || low[i] < hight[j])) { //当low还没被取完时,若是high已经被取完或者low_value < hight_value时,将low_value置于新数组中,下标+1 result[m++] = low[i]; i++; } else { result[m++] = hight[j]; j++; } } return result; } /** * 切分数组 * * @param param * @param start 开始下标 * @param end 结束下标 * @return */ private int[] splitArray(int[] param, int start, int end) { int[] result = new int[end - start + 1]; for (int i = start, m = 0; i <= end; i++, m++) { result[m] = param[i]; } return result; } public static void main(String[] args) { int[] param = new ShellSort().sort(new int[] { 12, 3, 2, 3, 5, 8, 19, 6, 4, 9, 7, 0 }); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
先了解三个概念:
堆排序利用的是完满二叉树的特性。每一次的排序完成,都将剩下的数组值看成新的一颗数。
2*root + 1 与 2*root + 2 是root节点的2个子节点
package com.noob.sort; /** * 不稳定(由于相同的值最终有可能下标小的先置换到末尾的已排序队列中,可在比较父子节点大小的判断中增长若相等也置换,那应该能达到稳定排序的要求, * 但多了不少不必的置换) 二叉堆 彻底二叉树只是在最后一层要么满子节点,要么都是倒数第二层左边的节点开始满起来。 * <p/> * Created by bear on 2016/3/3. */ public class HeapSort extends AbstractSort { /** * 不管是构建大根堆仍是小根堆,循环的结束是指针指向了元数组的0下标位置。由于param[0]才是整个树的root。 2*root + 1 与 * 2*root + 2 都没办法在没有新数组的状况下成为整个树的root。 * 若在构建小根堆时,start--,虽然本父子节点组的顺序是对的。可是可能会致使父节点同级的兄弟节点间的值不按正序排列 * 因此,有必要将最大值或者最小值置换到最后一个end_index 上。 没有叶子节点是彻底能够的,根节点不变。 * * @param param * @return */ public int[] sort(int[] param) { sort_max_heap(param); return param; } /** * 从小到大排序 构建大根堆。获取最大数组下标在二叉树中的父节点。 * 从这个父节点开始直至树根节点,比较是否符合父节点要大于等于任意子节点的值,不然将最大值替换置父节点上 最终能使得最大值必定是在树根节点。 * * @param param */ private void sort_max_heap(int[] param) { for (int i = 0; i < param.length; i++) { int end_index = param.length - 1 - i; //未排序队列最大下标 if (end_index > 0) { createMaxHeap(param, end_index); // 每次循环后都是将当前未排序队列中最大值替换到最后。max_index 每次都少1 swap(param, 0, end_index); } } } /** * @param param * @param lastIndex 最后一个值的下标 */ private void createMaxHeap(int[] param, int lastIndex) { for (int i = (lastIndex - 1) / 2; i >= 0; i--) {//获取根节点的下标 int root = i; int bigger_index = 2 * root + 1;//左右子节点中值最大的下标 if (bigger_index < lastIndex) { // 若左节点的下标小于最大的坐标,说明有右节点 if (param[bigger_index] < param[bigger_index + 1]) { bigger_index++; } } if (param[root] < param[bigger_index]) { swap(param, root, bigger_index); } } } /** * 从大到小排序。 构建最小堆,从最后一组父子节点开始向树root节点递推,将最小值置于每组父子节点的父节点上,这样最小值必定会在树root节点上 * * @param param */ private void sort_min_heap(int[] param) { for (int i = 0; i < param.length; i++) { int end_index = param.length - 1 - i; //未排序队列最大下标 if (end_index > 0) { createMinHeap(param, 0, end_index); // 每次循环后都是将当前未排序队列中最小值替换到最后。end_index 每次都少1 swap(param, 0, end_index); } } } /** * 不必定有子节点,单必定有根节点。 一、判断是否有右节点; 二、获取最小子节点下标;三、最小子节点值是否比根父节点值小,若小则交换 * * @param param * @param start * @param end 每次的end */ private void createMinHeap(int[] param, int start, int end) { for (int root = (end - 1) / 2; root >= start; root--) { int min_index = 2 * root + 1; // 左节点 if (min_index < end) { // 判断是否有右节点 if (param[min_index] > param[min_index + 1]) { min_index++; } } if (param[min_index] < param[root]) { swap(param, min_index, root); } } } public static void main(String[] args) { int[] param = new int[] { 5, 7, 3, 9, 2, 6, 1 }; new HeapSort().sort(param); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
定义的radix与max_position都与数组的最大值有关系; 桶的数量是10, 由于阿拉伯数是0-9。
从个位开始, 扔进桶内肯定好位置,再交由下一位的桶排序。
package com.noob.sort; /** * 按个位、十位、百位...排序 -----> 当前版本对负值无效 * <p/> * 稳定排序 基数排序 Created by bear on 2016/3/6. */ public class RadixSort extends AbstractSort { private int[] radix = new int[] { 1, 1, 10, 100, 1000 }; // 与数组的最大值的量级有关 private int max_index = 10; //桶的数量 由于阿拉伯数是0-9 private int max_position = 3; //数组最大值的位数 public int[] sort(int[] param) { for (int position = 0; position < max_position; position++) { sort_core(param, position); } return param; } /** * 按指定位上的数值排序 * * @param param * @param end_index * @param position */ private void sort_core(int[] param, int position) { int cap = param.length - 1; int[] count = new int[max_index];//记录每一个桶统计个数 int[] store = new int[cap + 1]; // 桶的容量 // 置空各个桶的数据统计 for (int i = 0; i < max_index; i++) { count[i] = 0; } // 指定位的值与桶的编号一一对应。统计对应桶有多少数量 for (int i = 0; i <= cap; i++) { count[getDigit(param[i], position)]++; } for (int i = 1; i < max_index; i++) { count[i] = count[i] + count[i - 1]; // 经过累加能够肯定每一个指定位的值在store桶上的边界。 } // 这里要从右向左扫描,保证排序稳定性 for (int i = cap; i >= 0; i--) { int digit = getDigit(param[i], position); store[count[digit] - 1] = param[i]; //放入对应的桶中,count[j]-1是第j个桶的右边界索引 count[digit]--; // 对应桶的装入数据索引减一 } // 将已分配好的桶中数据再倒出来,此时已经是对应当前位数有序的表 for (int i = 0, j = 0; i <= cap; i++, j++) { param[i] = store[j]; } } /** * 获取指定位上的数值 */ private int getDigit(int param, int digit) { return (param / radix[digit]) % 10; } }