一、鸡尾酒排序
1.1 原理
要是文艺点的话,可以说是搅拌排序,通俗易懂点的话,就叫“双向冒泡排序”,我想作为码农的话,不可能不知道冒泡排序。
冒泡是一个单向的从小到大或者从大到小的交换排序,而鸡尾酒排序是双向的,从一端进行从小到大排序,从另一端进行从大到小排序。
1.2 示例
1.3 代码实现
//鸡尾酒排序,O(n*n),冒泡排序的改进版, 双向冒泡排序
public void cockTailSort(int [] arr) {
if(arr == null || arr.length<2) {
return;
}
int len = arr.length, temp=0 ;
int head=0,tail=len-1;
while (head < tail) {
for(int i=head;i<tail;i++) {
if(arr[i]>arr[i+1]) {
temp = arr[i+1];
arr[i+1] = arr[i];
arr[i] = temp;
}
}
tail--;
if(head != tail) {
for(int i = tail;i>0;i--) {
if(arr[i]< arr[i-1]) {
temp = arr[i];
arr[i] = arr[i-1];
arr[i-1] = temp;
}
}
head++;
}
}
}
2、桶式排序
2.1 原理
桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再单独排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来即得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。
鸽巢排序是桶式排序的一种
基本思想
桶排序的思想近乎彻底的分治思想。
桶排序假设待排序的一组数均匀独立的分布在一个范围中,并将这一范围划分成几个子范围(桶)。
然后基于某种映射函数f ,将待排序列的关键字 k 映射到第i个桶中 (即桶数组B 的下标i) ,那么该关键字k 就作为 B[i]中的元素 (每个桶B[i]都是一组大小为N/M 的序列 )。
接着将各个桶中的数据有序的合并起来 : 对每个桶B[i] 中的所有元素进行比较排序 (可以使用快排)。然后依次枚举输出 B[0]….B[M] 中的全部内容即是一个有序序列。
补充: 映射函数一般是 f = array[i] / k; k^2 = n; n是所有元素个数
为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
实现逻辑
- 设置一个定量的数组当作空桶子。
- 寻访序列,并且把项目一个一个放到对应的桶子去。
- 对每个不是空的桶子进行排序。
- 从不是空的桶子里把项目再放回原来的序列中。
复杂度
- 平均时间复杂度:O(n + k)
- 最佳时间复杂度:O(n + k)
- 最差时间复杂度:O(n ^ 2)
- 空间复杂度:O(n * k)
- 稳定性:稳定
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
2.2 示例
2.3 代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/*
* Java程序使用基数排序算法对整数数组进行排序。
* input: [80, 50, 30, 10, 90, 60, 0, 70, 40, 20, 50]
* output: [0, 10, 20, 30, 40, 50, 50, 60, 70, 80, 90]
*
* 解决方案的时间复杂性:
* 最佳情况O(n); 平均情况O(n); 最坏情况O(n ^ 2)。
*
*/
public class BuckeSort {
public static void main(String args) {
System.out.println("Bucket sort in Java");
int input = { 80, 50, 30, 10, 90, 60, 0, 70, 40, 20, 50 };
System.out.println("integer array before sorting");
System.out.println(Arrays.toString(input));
// sorting array using radix Sort Algorithm
bucketSort(input);
System.out.println("integer array after sorting using bucket sort algorithm");
System.out.println(Arrays.toString(input));
}
/**
*
* @param input
*/
public static void bucketSort(int input) {
// get hash codes
final int code = hash(input);
// create and initialize buckets to ArrayList: O(n)
List buckets = new List[code[1]];
for (int i = 0; i < code[1]; i++) {
buckets[i] = new ArrayList();
}
// distribute data into buckets: O(n)
for (int i : input) {
buckets[hash(i, code)].add(i);
}
// sort each bucket O(n)
for (List bucket : buckets) {
Collections.sort(bucket);
}
int ndx = 0;
// merge the buckets: O(n)
for (int b = 0; b < buckets.length; b++) {
for (int v : buckets[b]) {
[/b][/i] input[ndx++] = v;
}
}
}
/**
*
* @param input
* @return an array containing hash of input
*/
private static int hash(int input) {
int m = input[0];
for (int i = 1; i < input.length; i++) {
if (m < input) {
m = input;
}
}
return new int { m, (int) Math.sqrt(input.length) };
}
/**
*
* @param i
* @param code
* @return
*/
private static int hash(int i, int code) {
return (int) ((double) i / code[0] * (code[1] - 1));
}
}
Output
Bucket sort in Java
integer array before sorting
<p>[80, 50, 30, 10, 90, 60, 0, 70, 40, 20, 50]
integer array after sorting using bucket sort algorithm
<p>[0, 10, 20, 30, 40, 50, 50, 60, 70, 80, 90]
3、计数排序
3.1 原理
计数排序(Counting Sort)算法由 Harold H. Seward 在1954年发明,它不是一种基于元素比较的排序算法,而是将待排序数组元素转化为计数数组的索引值,从而间接使待排序数组具有顺序性。
整个过程包含三个数组:待排序数组A、计数数组B和输出数组C。简单来说,就是通过统计待排序数组A中元素不同值的分布直方图,生成计数数组B,然后计算计数数组B的前缀和(此步操作可以看成计算待排序数组A中每个元素的位置信息),最后通过逆序循环将元素对应赋值到输出数组C中,输出数组C即是最终排序结果。
从整个过程也可以看到使用了额外的数组,所以它是一种以空间换时间的做法。
3.1.1 时间复杂度
计数排序的时间复杂度为Ο(n+k),其中n为待排序数组长度,k为计数数组长度(简单情况下可以认为k是待排序数组中最大值)。在整个计数排序过程中涉及到若干个循环操作,其中初始化计数数组与计算计数数组前缀和这两个循环每个最多执行(k+1)次,所以这里时间复杂度为O(k)。而初始化输出数组、统计待排序数组分布直方图、赋值到输出数组这三个循环每个执行n次,所以这里的时间复杂度为O(n)。于是,整个过程所有操作的时间复杂度为Ο(n+k)。
我们知道在所有基于比较的排序算法中,最低的时间复杂度为O(n * logn),所以可以看到计数排序的时间复杂度能够比基于比较的排序算法更优,但当k很大而n又较小时,计数排序的效率反而不如基于比较的排序算法。
3.1.2 执行步骤
设待排序数组为 A,计数数组为 B,输出数组为 C,则计数排序的操作步骤如下:
- 如果计数数组 B 的长度还没有确定,那么就先执行确定操作,其实就是寻找待排序数组中的最小值和最大值,然后用计数数组的所有元素用来表示最小值到最大值之间的所有值,比如最小值和最大值分别为20和30,则计数数组长度为 30-20+1=11,于是数组下标为0到10,分别表示20到30,即需要做一个偏移。假如计数数组长度事先已知道则省略此步
- 统计待排序数组A中不同元素值的分布直方图,即将不同元素值出现的次数赋值到计数数组B对应的元素上
- 对计数数组B执行计算前缀和操作,此步操作实际上就是计算小于或等于计数数组索引值的个数,比如 B[4]=5 表示小于等于4的元素有5个
- 根据计数数组B的位置信息,通过逆序循环将待排序数组中的所有元素赋值到输出数组C中指定位置,最终得到的输出数组C即是最终排序结果
3.2 示例(不考虑稳定性)
严格的计数排序算法一般认为具有稳定性,既不会打乱待排序数组中值相等的元素的顺序。但有时在不必考虑稳定性的情况下,我们可以简化算法的过程。比如我们在对单纯的整数数组排序时就可以不考虑排序的稳定性,因为一百个整数3中每个3都是相同的,不必区分哪个3要在另一个3的前面。
在不用考虑稳定性的情况下,我们只需要一个计数数组作为辅助即可,直接统计待排序数组的分布直方图,然后根据计数数组依次赋值待排序数组的元素即可完成排序工作。
现在假设我们有10个整数组成一个待排序数组A,元素分别为3,1,4,4,2,0,1,5,0,1。假设计数数组B长度已经确定为6,则它的索引值为0-5,刚好是待排序数组中元素的取值范围。
从头到尾循环一遍,待排序数组A[0]=3,对应到计数数组B[3],则执行B[3]累加1。
继续遍历A数组,更新B数组,最终结果如下:
目前为止工作已经完成了一大半了,我们得到了计数数组B,它表示的是什么呢?其实就是待排序数组元素出现的次数,比如B[0]=2表示0出现了2次,B[1]=3表示1出现了三次。所以最后一步就是按出现次数将值赋值回原来的数组中。
最终结果如下: