经典排序java实现
介绍
排序算法分为内部排序和外部排序。以内存为区分界限,如果在内存中完成即为内部排序。常见排序算法有:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序。
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | In-place | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | In-place | 不稳定 |
插入排序 | O(n²) | O(n) | O(n²) | O(1) | In-place | 稳定 |
希尔排序 | O(n log n) | O(n log² n) | O(n log² n) | O(1) | In-place | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | Out-place | 稳定 |
快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | In-place | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | In-place | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | Out-place | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n²) | O(n+k) | Out-place | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | Out-place | 稳定 |
解释:n:数据规模k:“桶”的个数In-place:占用常数内存,不占用额外内存Out-place:占用额外内存稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
冒泡排序(Bubble Sort)
比较相邻的两个数,如果顺序不一致就交换,将小的元素浮到顶部。存在一种优化的算法即为设置flag,如果比较一遍没有需要交换的即为有序,但是元素直接有序的可能性不会太大。
步骤
1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 3、针对所有的元素重复以上的步骤,除了最后一个。 4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
最快
数据是正序。
最慢
数据是逆序。
实现
public static void BubbleSort(int[] arr) {
int temp; //临时变量
for (int i = 0, n = arr.length; i < n-1; i++) {
for (int j = n-1; j > i; j--) {
if (arr[j] < arr[j-1]){
temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
}
}
print(arr);
}
优化
数据的顺序排好之后,冒泡算法仍然会继续进行下一轮的比较,直到arr.length-1次,后面的比较没有意义的。因此设置标志位flag,如果发生了交换flag设置为true;如果没有交换就设置为false。 这样当一轮比较结束后如果flag仍为false,即:这一轮没有发生交换,说明数据的顺序已经排好,没有必要继续进行下去。
public static void BubbleSort1(int[] arr) {
int temp; //临时变量
boolean flag; //交换标志
for (int i = 0, n = arr.length; i < n-1; i++) {
flag = false;
for (int j = n-1; j > i; j--) {
if (arr[j] < arr[j-1]){
flag = true;
temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
}
if(!flag){
break;
}
}
print(arr);
}
选择排序
时间复杂度O(n²),不需要额外的空间。
步骤
1、首先在未排序序列中找到最小元素,存放到排序序列的起始位置。
2、再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。
3、重复第二步,直到所有元素均排序完毕。
实现
public static void SelectSort(int[] arr, int length) {
for (int i = 0 ; i < length-1; i++) {
int minIndex = i;
for (int j = i+1; j < length; j++) {
if (arr[j] < arr[minIndex]){
minIndex = j;
}
}
// 如果不相同进行交换
if ( minIndex != i){
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
print(arr);
}
插入排序
构建有序队列,对于未排序的从后向前扫描,找到位置进行插入。有种优化算法,折半插入。
步骤
1、将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2、从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
实现
private static void insertSort(int[] arr, int length) {
int temp ;
for (int i = 0; i < length - 1; i++) {
for (int j = i+1; j > 0 ; j--) {
if (arr[j] < arr[j-1]){
temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}/*else{
break;
}*/
}
}
}
希尔排序
希尔排序是插入排序的改进版,但是不稳定。改进主要体现在下面两个方面:1.插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率; 2.但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
步骤
1、选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
2、按增量序列个数 k,对序列进行 k 趟排序; 3、每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
private static void shellSort(int[] arr, int length) {
int temp;
int incre = length;
while (true){
incre = incre / 2;
for (int i = 0; i < incre; i++) { //根据增量分为若干子序列
for (int j = i+incre ; j < length; j += incre) {
for (int k = j; k > i ; k -= incre) {
if (arr[k] < arr[k - incre]){
temp = arr[k-incre];
arr[k-incre] = arr[k];
arr[k] = temp;
}else {
break;
}
}
}
}
if (incre ==1){
break;
}
}
}
归并排序(Merge sort)
采用分治法,可以有两种方式实现:1、自上而下的递归。2、自下而上的迭代。
步骤
1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
4、重复步骤 3 直到某一指针达到序列尾;
5、将另一序列剩下的所有元素直接复制到合并序列尾。
实现
private static void mergeArray(int[] arr1, int m, int[] arr2, int n, int[] arr) {
int i = 0, j = 0, k = 0;
while (i < m && j < n){
if (arr1[i] < arr2[j]){
arr[k++] = arr1[i++];
}else {
arr[k++] = arr2[j++];
}
}
while (i<m){
arr[k++] = arr1[i++];
}
while (j<n){
arr[k++] = arr2[j++];
}
}
快速排序
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。快速排序的最坏运行情况是 O(n²)。但它的数学期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
步骤
1、从数列中挑出一个元素,称为 “基准”(pivot);
2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作; 3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
思想: 先从数列中取出一个数作为key值;将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;对左右两个小数列重复第二步,直至各区间只有1个数。
实现
private static void quickSort(int[] arr, int l, int r) {
if (l >= r) return;
int i = l;
int j = r;
int key = arr[l]; //选择第一个数为key
while (i < j){ // 指针相遇,左边全部小于key,右边全部大于key
while ( i < j && arr[j] >= key){ //从右向左找第一个小于key的值
j--;
}
if ( i < j ){
arr[i] = arr[j];
i++;
}
while ( i < j && arr[i] < key){//从左向右找第一个大于key的值
i++;
}
if (i < j){
arr[j] = arr[i];
j--;
}
}
// i ==j
arr[i] = key;
// 对于key左边排序
quickSort(arr,l,i-1);
// 对key右边递归
quickSort(arr,i+1,r);
}
堆排序(Heapsort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为大顶堆和小顶堆。
步骤
1、创建一个堆 H[0……n-1];
2、把堆首(最大值)和堆尾互换;
3、把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
4、重复步骤 2,直到堆的尺寸为 1。
代码
package com.gugu.sort;
/**
* @author gugu
* @Classname MinHeapSort
* @Description 小頂堆排序
* @Date 2020/6/16 10:25
*/
public class MinHeapSort {
public static void main(String[] args) {
int arr[] = new int[]{1,7,5,2,9,6,3};
int n = arr.length;
minHeapSort(arr,n);
MyPrint.print(arr);
}
private static void minHeapSort(int[] arr, int n) {
int temp = 0;
makeMinHeap(arr, n);
for (int i = n-1; i >= 0 ; i--) {
temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
minHeapFixdown(arr, 0 , i);
}
}
//构建最小堆
private static void makeMinHeap(int[] arr, int n) {
for (int i = (n-1)/2; i >= 0 ; i--) {
minHeapFixdown(arr,i,n);
}
}
//从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2
private static void minHeapFixdown(int[] arr, int i, int n) {
int j = 2 * i + 1; //子节点
int temp;
while (j < n){
//在左右子节点中寻找最小的
if (j+1 < n && arr[j+1] < arr[j]){
j++;
}
if (arr[i] <= arr[j]){
break;
}
//较大节点下移
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i = j ;
j = 2*i + 1;
}
}
}
计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
1、计数排序是一种非常快捷的稳定性强的排序方法,时间复杂度O(n+k),其中n为要排序的数的个数,k为要排序的数的组大值。计数排序对一定量的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序。计数排序是消耗空间发杂度来获取快捷的排序方法,其空间发展度为O(K)同理K为要排序的最大值。
思想:为一组数在排序之前先统计这组数中其他数小于这个数的个数,则可以确定这个数的位置。
实现
private static void countingSort(int[] arr, int range) throws Exception {
if (range <= 0){
throw new Exception("最大值范围大于0");
}
int[] countArray = new int[range+1];
for (int i = 0; i < arr.length; i++) {
int value = arr[i];
if (value < 0 || value > range){
throw new Exception("value范围不正确");
}
countArray[value] += 1;
}
for (int i = 1; i < countArray.length; i++) {
countArray[i] += countArray[i-1];
}
int[] temp = new int[arr.length];
for (int i = arr.length - 1; i >= 0 ; i--) {
int value = arr[i];
int position = countArray[value] - 1;
temp[position] = value;
countArray[value] -= 1;
}
for (int i = 0; i < arr.length; i++) {
arr[i] = temp[i];
}
}
桶排序
桶排序可以说是计数排序的升级版。利用函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:1、在额外空间充足的情况下,尽量增大桶的数量 2、使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
最快
当输入的数据可以均匀的分配到每一个桶中
最慢
当输入的数据被分配到了同一个桶中。
实现
private static void bucketSort(Double[] arr) {
int n = arr.length;
// 创建链表(桶)集合并初始化,集合中的链表用于存放相应的元素
int bucketNum = 10;// 桶数
LinkedList<LinkedList<Double>> buckets = new LinkedList<LinkedList<Double>>();
for (int i = 0; i < bucketNum; i++) {
LinkedList<Double> bucket = new LinkedList<>();
buckets.add(bucket);
}
// 把元素放进相应的桶中
for (int i = 0; i < n; i++) {
int index = (int) (arr[i] * bucketNum);
buckets.get(index).add(arr[i]);
}
//对每个桶中的元素排序,并放进a中
int index = 0;
for (LinkedList<Double> linkedList :buckets) {
int size = linkedList.size();
if (size == 0){
continue;
}
//把LinkedList<Double>转化为Double[]的原因是,之前已经实现了对数组进行排序的算法
Double[] temp = new Double[size];
for (int i = 0; i < temp.length; i++) {
temp[i] = linkedList.get(i);
}
// 利用插入排序对temp排序
InsertSort.insertSort(temp);
for (int i = 0; i < temp.length; i++) {
arr[index] = temp[i];
index++;
}
}
}
基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
1、基数排序:根据键值的每位数字来分配桶; 2、计数排序:每个桶只存储单一键值; 3、桶排序:每个桶存储一定范围的数值;
代码
package com.gugu.sort;
import java.util.Arrays;
/**
* @author gugu
* @Classname RadixSort
* @Description 基数排序
* @Date 2020/6/16 15:04
*/
public class RadixSort {
public static void main(String[] args) {
int[] arr = new int[]{1,7,5,2,9};
MyPrint.print(sort(arr));
}
private static int[] sort(int[] sourceArray) {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int maxDigit = getMaxDigit(arr);
return radixSort(arr, maxDigit);
}
//获取最高位数
private static int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLength(maxValue);
}
private static int getNumLength(int num) {
if (num == 0){
return 1;
}
int length = 0;
for (long temp = num; temp != 0; temp /= 10) {
length++ ;
}
return length;
}
private static int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value:arr) {
if (maxValue < value){
maxValue = value;
}
}
return maxValue;
}
private static int[] radixSort(int[] arr, int maxDigit) {
int mod = 10;
int dev = 1;
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
//考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
int[][] counter = new int[mod * 2][0];
for (int j = 0; j < arr.length; j++) {
int bucket = ((arr[j] % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], arr[j]);
}
int pos = 0;
for (int[] bucket:counter) {
for (int value:bucket) {
arr[pos++] = value;
}
}
}
return arr;
}
//自动扩容,并保存数据
private static int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}