1:冒泡排序:
相邻元素两两比较,大的往后放,第一次完毕,最大值出现在了最大索引处。同理,其他的元素就可以排好。
public static void bubbleSort(int[] arr) {
for(int x=0; x <arr.length;x++){
for(int y=0; y<arr.length-1;y++){
if(arr[y]>arr[y+1]) {
int temp = arr[y];
arr[y] = arr[y+1];
arr[y+1] = temp;
} } } }
2:简单选择排序:
把0索引的元素,和索引1以后的元素都进行比较,第一次完毕,最小值出现在了0索引。同理,其他的元素就可以排好。
public static void selectSort(int[] arr) {
for(int x=0; x<arr.length-1;x++){
for(int y=x+1;y<arr.length;y++(){
if(arr[y]<arr[x]){
int temp=arr[x];
arr[x]=arr[y];
arr[y]=temp;
}}}}
3:插入排序:
效果图:
它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
代码:
4:快速排序:
快速排序的原理就是每次设置一个基准点,这个基准点可以是要排序的一趴数之间的任何数,然后将比基准点小的数放在基准点左边,比基准点大的数放在基准点右边
代码:
5:二分排序:
针对数组有序的情况(千万不要先排序,在查找)
public static int binarySearch(int[] arr,int value) {
int min = 0;
int max = arr.length-1;
int mid = (min+max)/2;
while(arr[mid] != value) {
if(arr[mid] > value) {
max = mid - 1;
}else if(arr[mid] < value) {
min = mid + 1;
}
if(min > max) {
return -1;
}
mid = (min+max)/2;
}
return mid;
}
堆排序
堆排序是借助堆来实现的选择排序,思想同简单的选择排序,以下以大顶堆为例。注意:如果想升序排序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。
首先,实现堆排序需要解决两个问题:
-
如何由一个无序序列键成一个堆?
-
如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
第一个问题,可以直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就需要自底向上从第一个非叶元素开始挨个调整成一个堆。
第二个问题,怎么调整成堆?首先是将堆顶元素和最后一个元素交换。然后比较当前堆顶元素的左右孩子节点,因为除了当前的堆顶元素,左右孩子堆均满足条件,这时需要选择当前堆顶元素与左右孩子节点的较大者(大顶堆)交换,直至叶子节点。我们称这个自堆顶自叶子的调整成为筛选。
从一个无序序列建堆的过程就是一个反复筛选的过程。若将此序列看成是一个完全二叉树,则最后一个非终端节点是n/2取底个元素,由此筛选即可。举个栗子:
49,38,65,97,76,13,27,49序列的堆排序建初始堆和调整的过程如下:
实现代码:
-
/**
-
*@Description:堆排序算法的实现,以大顶堆为例。
-
*/
-
public class HeapSort {
-
/**
-
* 堆筛选,除了start之外,start~end均满足大顶堆的定义。
-
* 调整之后start~end称为一个大顶堆。
-
* @param arr 待调整数组
-
* @param start 起始指针
-
* @param end 结束指针
-
*/
-
public static void heapAdjust(int[] arr, int start, int end) {
-
int temp = arr[start];
-
for(int i=2*start+1; i) {
-
//左右孩子的节点分别为2*i+1,2*i+2
-
//选择出左右孩子较小的下标
-
if(i ]) {
-
i ++;
-
}
-
if(temp >= arr[i]) {
-
break; //已经为大顶堆,=保持稳定性。
-
}
-
arr[start] = arr[i]; //将子节点上移
-
start = i; //下一轮筛选
-
}
-
arr[start] = temp; //插入正确的位置
-
}
-
public static void heapSort(int[] arr) {
-
if(arr == null || arr.length == 0)
-
return ;
-
//建立大顶堆
-
for(int i=arr.length/2; i>=0; i--) {
-
heapAdjust(arr, i, arr.length-1);
-
}
-
for(int i=arr.length-1; i>=0; i--) {
-
swap(arr, 0, i);
-
heapAdjust(arr, 0, i-1);
-
}
-
}
-
public static void swap(int[] arr, int i, int j) {
-
int temp = arr[i];
-
arr[i] = arr[j];
-
arr[j] = temp;
-
}
-
}
希尔排序
希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,如果待排序列是正序时,时间复杂度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。希尔排序就利用了这个特点。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。
举个栗子:
从上述排序过程可见,希尔排序的特点是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增量的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。由于前两趟的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,因此关键字较小的记录就不是一步一步地向前挪动,而是跳跃式地往前移,从而使得进行最后一趟排序时,整个序列已经做到基本有序,只要作记录的少量比较和移动即可。因此希尔排序的效率要比直接插入排序高。
希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。但是在大量实验的基础上推出当n在某个范围内时,时间复杂度可以达到O(n^1.3)。
实现代码:
-
/**
-
*@Description:希尔排序算法实现
-
*/
-
public class ShellSort {
-
/**
-
* 希尔排序的一趟插入
-
* @param arr 待排数组
-
* @param d 增量
-
*/
-
public static void shellInsert(int[] arr, int d) {
-
for(int i=d; i) {
-
int j = i - d;
-
int temp = arr[i]; //记录要插入的数据
-
while (j>=0 & arr[j]>temp) { //从后向前,找到比其小的数的位置
-
arr[j+d] = arr[j]; //向后挪动
-
j -= d;
-
}
-
if (j != i - d) //存在比其小的数
-
arr[j+d] = temp;
-
}
-
}
-
public static void shellSort(int[] arr) {
-
if(arr == null || arr.length == 0)
-
return ;
-
int d = arr.length / 2;
-
while(d >= 1) {
-
shellInsert(arr, d);
-
d /= 2;
-
}
-
}
-
}
归并排序
归并排序是另一种不同的排序方法,因为归并排序使用了递归分治的思想,所以理解起来比较容易。其基本思想是,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合并。。。最终形成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。
举个栗子:
实现代码:
-
/**
-
*@Description:归并排序算法的实现
-
*/
-
public class MergeSort {
-
public static void mergeSort(int[] arr) {
-
mSort(arr, 0, arr.length-1);
-
}
-
/**
-
* 递归分治
-
* @param arr 待排数组
-
* @param left 左指针
-
* @param right 右指针
-
*/
-
public static void mSort(int[] arr, int left, int right) {
-
if(left >= right)
-
return ;
-
int mid = (left + right) / 2;
-
mSort(arr, left, mid); //递归排序左边
-
mSort(arr, mid+1, right); //递归排序右边
-
merge(arr, left, mid, right); //合并
-
}
-
/**
-
* 合并两个有序数组
-
* @param arr 待合并数组
-
* @param left 左指针
-
* @param mid 中间指针
-
* @param right 右指针
-
*/
-
public static void merge(int[] arr, int left, int mid, int right) {
-
//[left, mid] [mid+1, right]
-
int[] temp = new int[right - left + 1]; //中间数组
-
int i = left;
-
int j = mid + 1;
-
int k = 0;
-
while(i right) {
-
if(arr[i] arr[j]) {
-
temp[k++] = arr[i++];
-
}
-
else {
-
temp[k++] = arr[j++];
-
}
-
}
-
while(i mid) {
-
temp[k++] = arr[i++];
-
}
-
while(j right) {
-
temp[k++] = arr[j++];
-
}
-
for(int p=0; p) {
-
arr[left + p] = temp[p];
-
}
-
}
-
}
计数排序
如果在面试中有面试官要求你写一个O(n)时间复杂度的排序算法,你千万不要立刻说:这不可能!虽然前面基于比较的排序的下限是O(nlogn)。但是确实也有线性时间复杂度的排序,只不过有前提条件,就是待排序的数要满足一定的范围的整数,而且计数排序需要比较多的辅助空间。其基本思想是,用待排序的数作为计数数组的下标,统计每个数字的个数。然后依次输出即可得到有序序列。
实现代码:
-
/**
-
*@Description:计数排序算法实现
-
*/
-
public class CountSort {
-
public static void countSort(int[] arr) {
-
if(arr == null || arr.length == 0)
-
return ;
-
int max = max(arr);
-
int[] count = new int[max+1];
-
Arrays.fill(count, 0);
-
for(int i=0; i) {
-
count[arr[i]] ++;
-
}
-
int k = 0;
-
for(int i=0; i) {
-
for(int j=0; j) {
-
arr[k++] = i;
-
}
-
}
-
}
-
public static int max(int[] arr) {
-
int max = Integer.MIN_VALUE;
-
for(int ele : arr) {
-
if(ele > max)
-
max = ele;
-
}
-
return max;
-
}
-
}
桶排序
桶排序算是计数排序的一种改进和推广,但是网上有许多资料把计数排序和桶排序混为一谈。其实桶排序要比计数排序复杂许多。
对桶排序的分析和解释借鉴这位兄弟的文章(有改动):http://hxraid.iteye.com/blog/647759
桶排序的基本思想:
假设有一组长度为N的待排关键字序列K[1….n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。bindex=f(key) 其中,bindex 为桶数组B的下标(即第bindex个桶), k为待排序列的关键字。桶排序之所以能够高效,其关键在于这个映射函数,它必须做到:如果关键字k1
举个栗子:
假如待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。这些数据全部在1—100之间。因此我们定制10个桶,然后确定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将所有关键字全部堆入桶中,并在每个非空的桶中进行快速排序后得到如图所示。只要顺序输出每个B[i]中的数据就可以得到有序序列了。
桶排序分析:
桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,希尔排序中的子序列,归并排序中的子问题,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
对N个关键字进行桶排序的时间复杂度分为两个部分:
(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。
(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。
很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:
(1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。
(2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。
对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O(N)+O(M(N/M)log(N/M))=O(N+N(logN-logM))=O(N+NlogN-N*logM)
当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。
总结: 桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。 当然桶排序的空间复杂度 为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
实现代码:
-
/**
-
*@Description:桶排序算法实现
-
*/
-
public class BucketSort {
-
public static void bucketSort(int[] arr) {
-
if(arr == null & arr.length == 0)
-
return ;
-
int bucketNums = 10; //这里默认为10,规定待排数[0,100)
-
List> buckets = new ArrayList>(); //桶的索引
-
for(int i=0; i) {
-
buckets.add(new LinkedList()); //用链表比较合适
- }
-
//划分桶
-
for(int i=0; i) {
-
buckets.get(f(arr[i])).add(arr[i]);
-
}
-
//对每个桶进行排序
-
for(int i=0; i) {
-
if(!buckets.get(i).isEmpty()) {
-
Collections.sort(buckets.get(i)); //对每个桶进行快排
-
}
-
}
-
//还原排好序的数组
-
int k = 0;
-
for(List bucket : buckets) {
-
for(int ele : bucket) {
-
arr[k++] = ele;
-
}
-
}
-
}
-
/**
-
* 映射函数
-
* @param x
-
* @return
-
*/
-
public static int f(int x) {
-
return x / 10;
-
}
-
-
}
基数排序
基数排序又是一种和前面排序方式不同的排序方式,基数排序不需要进行记录关键字之间的比较。基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不同的关键字。比如说成绩的排序,如果两个人总分相同,则语文高的排在前面,语文成绩也相同则数学高的排在前面。。。如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如果要进行升序排序,那么个位、十位、百位优先级一次增加。基数排序是通过多次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。
举个栗子:
实现代码:
-
/**
-
*@Description:基数排序算法实现
-
*/
-
public class RadixSort {
-
public static void radixSort(int[] arr) {
-
if(arr == null & arr.length == 0)
-
return ;
-
int maxBit = getMaxBit(arr)
-
for(int i=1; i) {
-
List> buf = distribute(arr, i); //分配
-
collecte(arr, buf); //收集
-
}
-
}
-
/**
-
* 分配* @param arr 待分配数组
-
* @param iBit 要分配第几位
-
* @return
-
*/
-
public static List> distribute(int[] arr, int iBit) {
-
List> buf = new ArrayList>();
-
for(int j=0; j) {
-
buf.add(new LinkedList());
-
}
-
for(int i=0; i) {
-
buf.get(getNBit(arr[i], iBit)).add(arr[i]);
-
}
-
return buf;
-
}
-
/**
-
* 收集
-
* @param arr 把分配的数据收集到arr中
-
* @param buf
-
*/
-
public static void collecte(int[] arr, List> buf) {
-
int k = 0;
-
for(List bucket : buf) {
-
for(int ele : bucket) {
-
arr[k++] = ele;
-
}
-
}
-
}
-
/**
-
* 获取最大位数
-
* @param x
-
* @return
-
*/
-
public static int getMaxBit(int[] arr) {
-
int max = Integer.MIN_VALUE;
-
for(int ele : arr) {
-
int len = (ele+"").length();
-
if(len > max)
-
max = len;
-
}
-
return max;
-
}
-
/**
-
* 获取x的第n位,如果没有则为0.
-
* @param x
-
* @param n
-
* @return
-
*/
-
public static int getNBit(int x, int n) {
-
-
String sx = x + "";
-
if(sx.length() n)
-
return 0;
-
else
-
return sx.charAt(sx.length()-n) - '0';
-
}
-
}
总结
在前面的介绍和分析中我们提到了冒泡排序、选择排序、插入排序三种简单的排序及其变种快速排序、堆排序、希尔排序三种比较高效的排序。后面我们又分析了基于分治递归思想的归并排序还有计数排序、桶排序、基数排序三种线性排序。我们可以知道排序算法要么简单有效,要么是利用简单排序的特点加以改进,要么是以空间换取时间在特定情况下的高效排序。但是这些排序方法都不是固定不变的,需要结合具体的需求和场景来选择甚至组合使用。才能达到高效稳定的目的。没有最好的排序,只有最适合的排序。
下面就总结一下排序算法的各自的使用场景和适用场合。
-
从平均时间来看,快速排序是效率最高的,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。
-
上面说的简单排序包括除希尔排序之外的所有冒泡排序、插入排序、简单选择排序。其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用。
-
基数排序的时间复杂度也可以写成O(d*n)。因此它最使用于n值很大而关键字较小的的序列。若关键字也很大,而序列中大多数记录的最高关键字均不同,则亦可先按最高关键字不同,将序列分成若干小的子序列,而后进行直接插入排序。
-
从方法的稳定性来比较,基数排序是稳定的内排方法,所有时间复杂度为O(n^2)的简单排序也是稳定的。但是快速排序、堆排序、希尔排序等时间性能较好的排序方法都是不稳定的。稳定性需要根据具体需求选择。
-
上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元素的时间。具体的存储结构在具体的实现版本中也是不同的。
附:基于比较排序算法时间下限为O(nlogn)的证明:
基于比较排序下限的证明是通过决策树证明的,决策树的高度Ω(nlgn),这样就得出了比较排序的下限。
首先要引入决策树。 首先决策树是一颗二叉树,每个节点表示元素之间一组可能的排序,它予以京进行的比较相一致,比较的结果是树的边。 先来说明一些二叉树的性质,令T是深度为d的二叉树,则T最多有2^片树叶。 具有L片树叶的二叉树的深度至少是logL。 所以,对n个元素排序的决策树必然有n!片树叶(因为n个数有n!种不同的大小关系),所以决策树的深度至少是log(n!),即至少需要log(n!)次比较。 而 log(n!)=logn+log(n-1)+log(n-2)+…+log2+log1 >=logn+log(n-1)+log(n-2)+…+log(n/2) >=(n/2)log(n/2) >=(n/2)logn-n/2 =O(nlogn) 所以只用到比较的排序算法最低时间复杂度是O(nlogn)。