Java十大排序算法
1.1 选择排序
选择排序算法的运作如下:
①从待排序序列中,找到关键字最小的元素;
②如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
③从余下的 N - 1 个元素中,找出关键字最小的元素,重复①、②步,直到排序结束。
/** * 选择排序 * * @param array * @return */ public static int[] selectSort(int[] array) { for (int i = 0; i < array.length - 1; i++) { //标记第一个为待比较的数 int index = i; //然后从后面遍历与第一个数比较 for (int j = i + 1; j < array.length; j++) { //如果小,就交换最小值 if (array[j] < array[index]) { //保存最小元素的下标 index = j; } } //找到最小值后,将最小的值放到第一的位置,进行下一遍循环 int temp = array[index]; array[index] = array[i]; array[i] = temp; } return array; } 复制代码
1.2 快速排序
快速排序算法的运作如下:
①从数列中挑出一个元素,称为"基准"(pivot)。
②重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
③递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
/** * 快速排序(递归) * * @param arr 待排序数组 * @param low 左边界 * @param high 右边界 */ public static int[] quickSort(int[] arr, int low, int high) { if (arr.length <= 0) { return arr; } if (low >= high) { return arr; } int left = low; int right = high; //挖坑1:保存基准的值 int temp = arr[left]; while (left < right) { //坑2:从后向前找到比基准小的元素,插入到基准位置坑1中 while (left < right && arr[right] >= temp) { right--; } arr[left] = arr[right]; //坑3:从前往后找到比基准大的元素,放到刚才挖的坑2中 while (left < right && arr[left] <= temp) { left++; } arr[right] = arr[left]; } //基准值填补到坑3中,准备分治递归快排 arr[left] = temp; quickSort(arr, low, left - 1); quickSort(arr, left + 1, high); return arr; } 复制代码
1.3 冒泡排序
冒泡排序算法的运作如下:
①比较相邻的元素。如果第一个比第二个大,就交换他们两个。
②对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
③针对所有的元素重复以上的步骤,除了最后一个。
④持续每次对越来越少的元素重复上面的步骤①~③,直到没有任何一对数字需要比较。
/** * 冒泡排序 * * @param array 将要被排序的数组 * @return 排序完成的数组 */ public static int[] bubbleSort(int[] array) { 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]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; flag = false; } } if (flag) { break; } } return array; } 复制代码
1.4 归并排序
归并排序算法的运作如下:
归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序可通过两种方式实现:
- 自上而下的递归
- 自下而上的迭代
一、递归法(假设序列共有n个元素):
①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素;
②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素;
③. 重复步骤②,直到所有元素排序完毕。
二、迭代法
①. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
②. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
③. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
④. 重复步骤③直到某一指针到达序列尾
⑤. 将另一序列剩下的所有元素直接复制到合并序列尾
代码
/** * 归并排序(递归) * * ①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素; * ②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素; * ③. 重复步骤②,直到所有元素排序完毕。 * @param arr 待排序数组 */ public static int[] mergingSort(int[] arr){ if(arr.length <= 1) return arr; int num = arr.length >> 1; int[] leftArr = Arrays.copyOfRange(arr, 0, num); int[] rightArr = Arrays.copyOfRange(arr, num, arr.length); System.out.println("split two array: " + Arrays.toString(leftArr) + " And " + Arrays.toString(rightArr)); return mergeTwoArray(mergingSort(leftArr), mergingSort(rightArr)); //不断拆分为最小单元,再排序合并 } private static int[] mergeTwoArray(int[] arr1, int[] arr2){ int i = 0, j = 0, k = 0; int[] result = new int[arr1.length + arr2.length]; //申请额外的空间存储合并之后的数组 while(i < arr1.length && j < arr2.length){ //选取两个序列中的较小值放入新数组 if(arr1[i] <= arr2[j]){ result[k++] = arr1[i++]; }else{ result[k++] = arr2[j++]; } } while(i < arr1.length){ //序列1中多余的元素移入新数组 result[k++] = arr1[i++]; } while(j < arr2.length){ //序列2中多余的元素移入新数组 result[k++] = arr2[j++]; } System.out.println("Merging: " + Arrays.toString(result)); return result; } 复制代码
1.5 基数排序
基数排序算法的运作如下:
①. 取得数组中的最大数,并取得位数;
②. arr为原始数组,从最低位开始取每个位组成radix数组;
③. 对radix进行计数排序(利用计数排序适用于小范围数的特点)
/** * 基数排序(LSD 从低位开始) * * 基数排序适用于: * (1)数据范围较小,建议在小于1000 * (2)每个数值都要大于等于0 * * ①. 取得数组中的最大数,并取得位数; * ②. arr为原始数组,从最低位开始取每个位组成radix数组; * ③. 对radix进行计数排序(利用计数排序适用于小范围数的特点); * @param arr 待排序数组 */ public static void radixSort(int[] arr){ if(arr.length <= 1) return; //取得数组中的最大数,并取得位数 int max = 0; for(int i = 0; i < arr.length; i++){ if(max < arr[i]){ max = arr[i]; } } int maxDigit = 1; while(max / 10 > 0){ maxDigit++; max = max / 10; } System.out.println("maxDigit: " + maxDigit); //申请一个桶空间 int[][] buckets = new int[10][arr.length-1]; int base = 10; //从低位到高位,对每一位遍历,将所有元素分配到桶中 for(int i = 0; i < maxDigit; i++){ int[] bktLen = new int[10]; //存储各个桶中存储元素的数量 //分配:将所有元素分配到桶中 for(int j = 0; j < arr.length; j++){ int whichBucket = (arr[j] % base) / (base / 10); buckets[whichBucket][bktLen[whichBucket]] = arr[j]; bktLen[whichBucket]++; } //收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞 int k = 0; for(int b = 0; b < buckets.length; b++){ for(int p = 0; p < bktLen[b]; p++){ arr[k++] = buckets[b][p]; } } System.out.println("Sorting: " + Arrays.toString(arr)); base *= 10; } } 复制代码
1.6 堆排序
此处以大顶堆为例,堆排序的过程就是将待排序的序列构造成一个堆,选出堆中最大的移走,再把剩余的元素调整成堆,找出最大的再移走,重复直至有序。
堆排序算法的运作如下:
①. 先将初始序列K[1…n]K[1…n]建成一个大顶堆, 那么此时第一个元素K1K1最大, 此堆为初始的无序区.
②. 再将关键字最大的记录K1K1 (即堆顶, 第一个元素)和无序区的最后一个记录 KnKn 交换, 由此得到新的无序区K[1…n−1]K[1…n−1]和有序区K[n]K[n], 且满足K[1…n−1].keys⩽K[n].keyK[1…n−1].keys⩽K[n].key
③. 交换K1K1 和 KnKn 后, 堆顶可能违反堆性质, 因此需将K[1…n−1]K[1…n−1]调整为堆. 然后重复步骤②, 直到无序区只有一个元素时停止.
代码
/** * 堆排序 * * 1. 先将初始序列K[1..n]建成一个大顶堆, 那么此时第一个元素K1最大, 此堆为初始的无序区. * 2. 再将关键字最大的记录K1 (即堆顶, 第一个元素)和无序区的最后一个记录 Kn 交换, 由此得到新的无序区K[1..n−1]和有序区K[n], 且满足K[1..n−1].keys⩽K[n].key * 3. 交换K1 和 Kn 后, 堆顶可能违反堆性质, 因此需将K[1..n−1]调整为堆. 然后重复步骤②, 直到无序区只有一个元素时停止. * @param arr 待排序数组 */ public static void heapSort(int[] arr){ for(int i = arr.length; i > 0; i--){ max_heapify(arr, i); int temp = arr[0]; //堆顶元素(第一个元素)与Kn交换 arr[0] = arr[i-1]; arr[i-1] = temp; } } private static void max_heapify(int[] arr, int limit){ if(arr.length <= 0 || arr.length < limit) return; int parentIdx = limit / 2; for(; parentIdx >= 0; parentIdx--){ if(parentIdx * 2 >= limit){ continue; } int left = parentIdx * 2; //左子节点位置 int right = (left + 1) >= limit ? left : (left + 1); //右子节点位置,如果没有右节点,默认为左节点位置 int maxChildId = arr[left] >= arr[right] ? left : right; if(arr[maxChildId] > arr[parentIdx]){ //交换父节点与左右子节点中的最大值 int temp = arr[parentIdx]; arr[parentIdx] = arr[maxChildId]; arr[maxChildId] = temp; } } System.out.println("Max_Heapify: " + Arrays.toString(arr)); } 复制代码
1.7 桶排序
桶排序的基本思想是:把数组 arr 划分为n个大小相同子区间(桶),每个子区间各自排序,最后合并
。
计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况。
桶排序算法的运作如下:
1.找出待排序数组中的最大值max、最小值min
2.我们使用 动态数组ArrayList 作为桶,桶里放的元素也用 ArrayList 存储。桶的数量为(max-min)/arr.length+1
3.遍历数组 arr,计算每个元素 arr[i] 放的桶
4.每个桶各自排序
5.遍历桶数组,把排序好的元素放进输出数组
代码:
public static void bucketSort(int[] arr){ int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; for(int i = 0; i < arr.length; i++){ max = Math.max(max, arr[i]); min = Math.min(min, arr[i]); } //桶数 int bucketNum = (max - min) / arr.length + 1; ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum); for(int i = 0; i < bucketNum; i++){ bucketArr.add(new ArrayList<Integer>()); } //将每个元素放入桶 for(int i = 0; i < arr.length; i++){ int num = (arr[i] - min) / (arr.length); bucketArr.get(num).add(arr[i]); } //对每个桶进行排序 for(int i = 0; i < bucketArr.size(); i++){ Collections.sort(bucketArr.get(i)); } System.out.println(bucketArr.toString()); } 复制代码
1.8 希尔排序
希尔排序算法的运作如下:
①选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;(一般初次取数组半长,之后每次再减半,直到增量为1)
②按增量序列个数k,对序列进行k 趟排序;
③每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
/** * 希尔排序 * * @param array * @return */ public static int[] shellSort(int[] array) { for (int gap = array.length / 2; gap > 0; gap = gap / 2) { //将整个数组分为若干个子数组 for (int i = gap; i < array.length; i++) { //遍历各组的元素 for (int j = i - gap; j >= 0; j = j - gap) { //交换元素 if (array[j] > array[j + gap]) { int temp = array[j]; array[j] = array[j + gap]; array[j + gap] = temp; } } } } return array; } 复制代码
1.9 插入排序
插入排序算法的运作如下:
①从第一个元素开始,该元素可以认为已经被排序
②取出下一个元素,在已经排序的元素序列中从后向前扫描
③如果该元素(已排序)大于新元素,将该元素移到下一位置
④重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
⑤将新元素插入到该位置后
⑥重复步骤②~⑤
/** * 插入排序 * * @param array * @return */ public static int[] insertSort(int[] array) { //长度不减1,是因为要留多一个位置方便插入数 for (int i = 0; i < array.length; i++) { //定义待插入的数 int insertValue = array[i]; //找到待插入数的前一个数的下标 int insertIndex = i - 1; //拿a[i]与a[i-1]的前面数组比较 while (insertIndex >= 0 && insertValue < array[insertIndex]) { array[insertIndex + 1] = array[insertIndex]; insertIndex--; } array[insertIndex + 1] = insertValue; } return array; } 复制代码
1.10 计数排序
需要三个数组:
待排序数组 int[] arr = new int[]{4,3,6,3,5,1};
辅助计数数组 int[] help = new int[max - min + 1]; //该数组大小为待排序数组中的最大值减最小值+1
输出数组 int[] res = new int[arr.length];
计数排序算法的运作如下:
1.求出待排序数组的最大值max=6, 最小值min=1
2.实例化辅助计数数组help,help用来记录每个元素之前出现的元素个数
3.计算 arr 每个数字应该在排序后数组中应该处于的位置,此时 help = [1,1,4,5,6,7];
4.根据 help 数组求得排序后的数组,此时 res = [1,3,3,4,5,6]
代码
public static int[] countSort(int[] arr){ int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; //找出数组中的最大最小值 for(int i = 0; i < arr.length; i++){ max = Math.max(max, arr[i]); min = Math.min(min, arr[i]); } int[] help = new int[max - min + 1]; //找出每个数字出现的次数 for(int i = 0; i < arr.length; i++){ int mapPos = arr[i] - min; help[mapPos]++; } //计算每个数字应该在排序后数组中应该处于的位置 for(int i = 1; i < help.length; i++){ help[i] = help[i-1] + help[i]; } //根据help数组进行排序 int res[] = new int[arr.length]; for(int i = 0; i < arr.length; i++){ int post = --help[arr[i] - min]; res[post] = arr[i]; } return res; } 复制代码
总结
排序算法的稳定性定义
:
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
口诀
:
冒泡、插入、希尔、桶拍最好都是n
选择最好n方
堆、归并、快速都是nlog2n
基数n*k计数n+k
冒泡、插入、归并、桶、计数、基数稳,其他都不稳
参考文章:
itimetraveler.github.io/2017/07/18/…