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
冒泡、插入、归并、桶、计数、基数稳,其他都不稳
参考文章: