原文地址:https://mp.weixin.qq.com/s/pN4RH4pPKtSkZJgcf2V-Vw
总结
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:
关于时间复杂度:
1、平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
2、线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
3、 O(nlogn)) 排序,logn 是介于 0 和 1 之间的常数。 希尔排序
4、线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
关于稳定性:
5、稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
6、不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
名词解释:
n:数据规模k:“桶”的个数In-place:占用常数内存,不占用额外内存Out-place:占用额外内存稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
一、冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
- 算法步骤
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 - 什么时候最快
当输入的数据已经是正序时。 - 什么时候最慢
当输入的数据是反序时。
二、选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
- 算法步骤
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3)重复第二步,直到所有元素均排序完毕。
三、插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
- 算法步骤
1、将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2、从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
四、希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率; 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
- 算法步骤
1、选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
2、按增量序列个数 k,对序列进行 k 趟排序;
3、每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
五、归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
1、自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
2、自下而上的迭代;
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
- 算法步骤
1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
4、重复步骤 3 直到某一指针达到序列尾;
5、将另一序列剩下的所有元素直接复制到合并序列尾。
六、快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
- 算法步骤
1、从数列中挑出一个元素,称为 “基准”(pivot);
2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
基本思想:(分治)
·先从数列中取出一个数作为key值;
·将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
·对左右两个小数列重复第二步,直至各区间只有1个数。
七、堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
1、大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
2、小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
- 算法步骤
1、创建一个堆 H[0……n-1];
2、把堆首(最大值)和堆尾互换;
3、把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
4、重复步骤 2,直到堆的尺寸为 1。
八、计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
1、计数排序是一种非常快捷的稳定性强的排序方法,时间复杂度O(n+k),其中n为要排序的数的个数,k为要排序的数的最大值。计数排序对一定量的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序。计数排序是消耗空间发杂度来获取快捷的排序方法,其空间发展度为O(K)同理K为要排序的最大值。
2、计数排序的基本思想为一组数在排序之前先统计这组数中其他数小于这个数的个数,则可以确定这个数的位置。例如要排序的数为 7 4 2 1 5 3 1 5;则比7小的有7个数,所有7应该在排序好的数列的第八位,同理3在第四位,对于重复的数字,1在1位和2位(暂且认为第一个1比第二个1小),5和1一样位于6位和7位。
九、桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
1、在额外空间充足的情况下,尽量增大桶的数量
2、使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
- 什么时候最快
当输入的数据可以均匀的分配到每一个桶中。 - 什么时候最慢
当输入的数据被分配到了同一个桶中。
十、基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
- 基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
1、基数排序:根据键值的每位数字来分配桶;
2、计数排序:每个桶只存储单一键值;
3、桶排序:每个桶存储一定范围的数值;
所有代码如下:
package sort;
import java.util.*;
public class SortTest {
//冒泡排序
public static void bubbleSort(int[] arr){
int temp = 0;
boolean flag;//这一趟是否进行交换
int n = arr.length;
int counter = 1;//趟数计数,可以删去
for(int j = 0;j<arr.length;j++){
flag = false;//在一趟开始设为false
System.out.println("趟数:"+ counter++);
for (int i = 0; i < n-1; i++) {
if (arr[i] > arr[i + 1]) {
temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
flag = true;//发生交换
}
}
if(!flag)
break;
n -= 1;
}
}
//选择排序
public static void selectSort(int[] arr){
for(int i=0;i<arr.length;i++){
int min = arr[i];
for(int j=i+1;j<arr.length;j++){
if(arr[j] < min){
arr[i] = arr[j];
arr[j] = min;
min = arr[i];
}
}
}
}
//插入排序
public static void insertSort(int[] arr){
for(int i=1;i<arr.length;i++){
int temp = arr[i];
int j = i;
if(temp<arr[j-1]){
while(j>0 && temp < arr[j-1]){
arr[j] = arr[j-1];
j--;
}
}
arr[j] = temp;
}
}
public static void insertSort(Double[] arr){
for(int i=1;i<arr.length;i++){
Double temp = arr[i];
int j = i;
if(temp<arr[j-1]){
while(j>0 && temp < arr[j-1]){
arr[j] = arr[j-1];
j--;
}
}
arr[j] = temp;
}
}
//希尔排序
public static void shellSort(int[] arr){
int length = arr.length;
int h;//步长
int i,j;
int temp;
for(h = length/2;h>0;h=h/2){
System.out.println("h="+h);
for(i = h;i<length;i++){
temp = arr[i];
for(j = i-h;j>=0;j-=h){
if(arr[j]>temp){
arr[j+h] = arr[j];
}
else
break;
}
arr[j+h] = temp;
}
}
}
//归并排序
public static void merge(int[] arr,int p,int q,int r){
int i,j,k;
int n1 = q-p+1;
int n2 = r-q;
int[] L = new int[n1];
int[] R = new int[n2];
for(i=0,k=p;i<n1;i++,k++)
L[i] = arr[k];
for(j=0,k=q+1;j<n2;j++,k++)
R[j] = arr[k];
for(k=p,i=0,j=0;i<n1&&j<n2;k++){
if(L[i]<=R[j]){
arr[k] = L[i];
i++;
}
else{
arr[k] = R[j];
j++;
}
}
if(i<n1){
for(int a = i;a<n1;a++,k++)
arr[k] = L[a];
}
if(j<n2){
for(int a = j;a<n2;a++,k++)
arr[k] = R[a];
}
}
public static void mergeSort(int[] arr,int p,int r){
if(p<r){
int q =(r+p)/2;
mergeSort(arr,p,q);
mergeSort(arr,q+1,r);
merge(arr,p,q,r);
}
}
//快速排序
public static void quick(int[] arr,int low,int high){
int i=low;
int j=high;
int key = arr[i];
if(i>=j)
return;
while(i<j){
while(i<j && arr[j] >= key)
j--;
if(i<j)
arr[i++] = arr[j];
while(i<j && arr[i] < key)
i++;
if(i<j)
arr[j--] = arr[i];
}
arr[i] = key;
if(i==low){//第一个就是最小的
quick(arr,i+1,high);
}else if(i==high){//第一个是最大的
quick(arr,low,i-1);
}else if(i>0 && i<high){
quick(arr,low,i-1);
quick(arr,i+1,high);
}
}
public static void quickSort(int[] arr){
quick(arr,0,arr.length-1);
}
//堆排序(降序,最小堆)
//构建最小堆
public static void makeMinHeap(int a[],int n){
for(int i=(n-1)/2;i>=0;i--){
minHeapFixdown(a,i,n);
}
}
public static void minHeapFixdown(int a[],int i,int n){
int j = 2*i + 1;//子节点
int temp = 0;
while(j<n){
//在左右节点中寻找最小的
if( j+1 < n && a[j+1] < a[j])
j++;
if(a[i]<=a[j]) break;
//较大节点下移
temp = a[i];
a[i] = a[j];
a[j] = temp;
i = j;
j = 2*i +1;
}
}
public static void minHeapSort(int a[]){
int temp =0;
makeMinHeap(a,a.length);
for(int i=a.length-1;i>0;i--){
//交换堆首和堆尾
temp = a[0];
a[0] = a[i];
a[i] = temp;
minHeapFixdown(a,0,i);
}
}
//堆排序(升序,最大堆)
public static void makeMaxHeap(int a[],int n){
for(int i=(n-1)/2;i>=0;i--)
maxHeapFixdown(a,i,n);
}
public static void maxHeapFixdown(int a[],int i,int n){
int j = 2*i+1;
int temp;
while(j<n){
//找左右节点中最大的数
if(j+1<n && a[j+1]>a[j])
j++;
if(a[i] > a[j]) break;
temp = a[i];
a[i] = a[j];
a[j] = temp;
i = j;
j = 2*i+1;
}
}
public static void maxHeapSort(int a[]){
int temp = 0;
makeMaxHeap(a,a.length);
for(int i = a.length-1;i>0;i--){
temp = a[0];
a[0] = a[i];
a[i] = temp;
maxHeapFixdown(a,0,i);
}
}
//计数排序
public static void countingSort(int arr[],int range) throws Exception{
if(range<=0)
throw new Exception("range can't be negative or zero");
if(arr.length <= 1)
return;
int length = arr.length;
int couterLength = range+1;
int[] counterArr = new int[couterLength];
for(int i = 0;i<length;i++){
int value = arr[i];
if(value<0 || value>range){
throw new Exception("array element overflow range.");
}
counterArr[value] += 1;
}
for(int j=1;j<couterLength;j++){
counterArr[j] += counterArr[j-1];
}
int[] tempArr = new int[length];
for(int i=0;i<length;i++){
int value = arr[i];
int position = counterArr[value]-1;
tempArr[position] = value;
counterArr[value]--;
}
for(int i=0;i<length;i++){
arr[i] = tempArr[i];
}
}
//桶排序
/**
* 桶排序假设输入元素均匀而独立的分布在区间[0,1)上;
* 桶排序的核心思想是,将[0,1)分为n个大小相同的子区间,
* 上一个区间里的元素都比下一个区间里的元素小,然后对
* 所有区间里的元素排序,最后顺序输出所有区间里的元素,
* 达到对所有元素排序的目的。
*
*/
public static void bucketSort(Double[] a){
int n = a.length;
int bucketNum = 10;//桶数
//创建链表(桶)集合并初始化,集合中的链表用于存放相应的元素
LinkedList<LinkedList<Double>> buckets = new LinkedList<LinkedList<Double>>();
for(int j=0;j<bucketNum;j++){
LinkedList<Double> ll = new LinkedList<Double>();
buckets.add(ll);
}
//把元素放入相应的桶中
for(int i=0;i<n;i++){
int index = (int)(a[i]*bucketNum);
buckets.get(index).add(a[i]);
}
//对每个桶中的元素进行排序并放入a中
int index = 0;
for(LinkedList<Double> ll : buckets){
int size = ll.size();
if(size == 0)
continue;
Double[] temp = new Double[size];
for(int j=0;j<size;j++)
temp[j] = ll.get(j);
//利用插入排序对temp进行排序
insertSort(temp);
for(int k=0;k<size;k++){
a[index] = temp[k];
index++;
}
}
}
//基数排序
/*
* 获取最高位数
*/
public int getMaxDigit(int[] arr){
int maxValue = getMaxValue(arr);
return getNumLength(maxValue);
}
private int getMaxValue(int[] arr){
int maxValue = arr[0];
for(int value:arr)
if(maxValue < value)
maxValue = value;
return maxValue;
}
private int getNumLength(int num){
if(num == 0)
return 1;
int length =0;
for(int temp=num;temp!=0;temp/=10)
length++;
return length;
}
//自动扩容,并保存数组
private int[] arrayAppend(int[] arr,int value){
arr = Arrays.copyOf(arr,arr.length+1);
arr[arr.length-1] = value;
return arr;
}
public void 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;
}
}
}
public static void main(String[] args) {
//int[] arr = {2,3,5,45,8,1,3,9,0,2,8,4,22,56,18};
//int[] arr = {3,26,53,67,48,57,13,48,32,60,50};
//int[] arr = {1,2,3,4,5,6,7,8,9,10};
//int[] arr = {5,4,3,2,1};
//int[] arr = {2,6,4,7,3,0,5,1};
//int[] arr = {1,2,3,4,5,7,8,6,9,10};
//int[] arr = {9,9,8,8,8,7,6,5,5,0,4,4,0,2,1};
//bubbleSort(arr);
//selectSort(arr);
//insertSort(arr);
//shellSort(arr);
//mergeSort(arr,0,arr.length-1);
//quickSort(arr);
//minHeapSort(arr);
//maxHeapSort(arr);
/* try {
countingSort(arr,9);
} catch (Exception e) {
e.printStackTrace();
}
*/
/* Double[] arr = {0.1,0.3,0.9,0.7,0.4};
bucketSort(arr);
*/
int[] arr = {100,156,89,56,100,73,91,25,6,4,1,0};
SortTest st = new SortTest();
int maxDigit = st.getMaxDigit(arr);
st.radixSort(arr,maxDigit);
for(int i=0;i<arr.length;i++)
System.out.print(arr[i]+" ");
}
}