目录
总结:
冒泡:在没有改进算法的情况下都是N*N,使用一个标志,一次遍历没有交换就结束(当数组是正序N)
插入:最好N(就是已经安装要求排好);最坏(和要求排序完全反向有序)N*N
选择:所以情况一样N*N
快速:最好的情况是数据分布均匀每一次划分左右两边那都是一半;最坏情况是数组正序或者逆序
堆排序:
希尔排序
归并排序:
基数排序:
桶排序:
计数排序:
关于稳定性:
-
选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,
-
冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。
-
常用时间复杂度的大小关系:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
一、冒泡(n*n)
基本思想:
设数组长度为N。
1.比较相邻的前后二个数据,如果前面数据大于后面的数据,就将二个数据交换。
2.这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1个位置。
3.N=N-1,如果N不为0就重复前面二步,否则排序完成。
public static void bubble_sort1(int[] a,int n){
//一般的方法
for(int i=0;i<n;i++){
for(int j=1;j<n-i;j++){
if(a[j-1]>a[j]){
int temp=a[j-1];
a[j-1]=a[j];
a[j]=temp;
}
}
}
}
public static void bubble_sort2(int[] a,int n){
//优化一:当一次遍历没有发生交换,说明排序已经排好,不再进行比较
boolean flag=true;
int k=n;
while(flag){
flag=false;
for(int j=1;j<k;j++){
if(a[j-1]>a[j]){
int temp=a[j-1];
a[j-1]=a[j];
a[j]=temp;
flag=true;
}
}
k--;
}
}
public static void bubble_sort3(int[] a,int n){
/*优化二:再做进一步的优化。如果有100个数的数组,仅前面10个无序,后面90个都已排好序且都大于前面10个数字,
那么在第一趟遍历后,最后发生交换的位置必定小于10,且这个位置之后的数据必定已经有序了,
记录下这位置,第二次只要从数组头部遍历到这个位置就可以了。*/
int mark=n;
while(mark>0){
int k=mark;
//mark不为0说明发生了交换
mark=0;
for(int j=1;j<k;j++){
if(a[j-1]>a[j]){
int temp=a[j-1];
a[j-1]=a[j];
a[j]=temp;
//记录交换的位置
mark=j;
}
}
}
}
二、选择(n*n)
时间复杂度O(n^2), 空间复杂度O(1)
排序时间与输入无关,最佳情况,最坏情况都是如此, 不稳定 如 {5,5,2}。
基本思想:
设数组为a[0…n-1]。
1. 初始时,数组全为无序区为a[0..n-1]。令i=0
2. 在无序区a[i…n-1]中选取一个最小的元素,将其与a[i]交换。交换之后a[0…i]就形成了一个有序区。
3. i++并重复第二步直到i==n-1。排序完成。
直接选择排序和直接插入排序类似,都将数据分为有序区和无序区,
所不同的是直接插入排序是将无序区的第一个元素直接插入到有序区以形成一个更大的有序区,
而直接选择排序是从无序区选一个最小的元素直接放到有序区的最后。
如下代码,每次外循环遍历一次,把后面无序中最小的放到有序的最后一个
public static void select_sort1(int[] s,int n){
int i,j,minIndex;
for(i=0;i<n;i++){
minIndex=i;
for(j=i+1;j<n;j++){
if(s[j]<s[minIndex]){
minIndex=j;
}
}
int temp=s[i];
s[i]=s[minIndex];
s[minIndex]=temp;
}
}
三、插入(n*n)
基本思想:类似于打扑克的起牌过程(在有序列表中找第一个比插入值小的)
设数组为a[0…n-1]。
1. 初始时,a[0]自成1个有序区,无序区为a[1..n-1]。令i=1
2. 将a[i]并入当前的有序区a[0…i-1]中形成a[0…i]的有序区间。
3. i++并重复第二步直到i==n-1。排序完成。
时间复杂度O(n^2), 空间复杂度O(1)
排序时间与输入有关:输入的元素个数;元素已排序的程度。
最佳情况,输入数组是已经排好序的数组,运行时间是n的线性函数; 最坏情况,输入数组是逆序,运行时间是n的二次函数。
public void sort(){
int temp;
for(int i = 1; i<arraytoSort.length; i++){
for(int j = i-1; j>=0; j--){
if( arraytoSort[j+1] < arraytoSort[j] ){
temp = arraytoSort[j+1];
arraytoSort[j+1] = arraytoSort[j];
arraytoSort[j] = temp;
}
}
}
}
通过不断的交换实现
自己
//插入
// for (int i = 1; i < A.length; i++) {
// int insertpos = i;
// int insertval = A[i];
// for (int j = i - 1; j >= 0; j--) {
// if (insertval < A[j]) {
// A[j + 1] = A[j];
// } else {
// break;
// }
// insertpos = j;
// }
// A[insertpos] = insertval;
// }
//插入(感觉最优)
for (int i = 1; i < A.length; i++) {
int insertval = A[i];
int j;
for (j = i - 1; j >= 0 && A[j] > insertval; j--) {
A[j + 1] = A[j];
}
A[j + 1] = insertval;
}
四、希尔排序(n*n)
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
平均:O(n log2 n)约等于n的1.3次方
最坏:n*n
希尔排序(分组的插入排序)(增量排序)
// 该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序
// ,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。
// 因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
//插入
// for (int i = 1; i < A.length; i++) {
// int insertval = A[i];
// int j;
// for (j = i - 1; j >= 0 && A[j] > insertval; j--) {
// A[j + 1] = A[j];
// }
// A[j + 1] = insertval;
// }
//shell
int gap = 1;
int i,j,temp;
int len = A.length;
while (gap < len / 3) {
gap = gap * 3 + 1;
}
for (; gap > 0; gap /= 3) { //步长
for (i = gap; i < len; i++) { //同插入排序 从第gap个位置开始插入元素
temp = A[i];
for (j = i - gap; j >= 0 && A[j] > temp; j -= gap) {
A[j + gap] = A[j];
}
A[j + gap] = temp;
}
}
五、快速排序( O(N*logN))
性能分析
时间复杂度 O(nlogn) 空间复杂度O(logn) 不稳定 【两个时间复杂度O(nlogn) 的排序算法都不稳定】
时间复杂度:
最坏O(n^2) 当划分不均匀时候 逆序and排好序都是最坏情况
最好O(n) 当划分均匀
partition的时间复杂度: O(n)一共需要logn次partition
空间复杂度:递归造成的栈空间的使用,最好情况,递归树的深度logn 空间复杂的logn,最坏情况,需要进行n‐1 递归调用,其空间复杂度为 O(n),平均情况,空间复杂度也为O(log2n)。
由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。
快速排序的每一轮就是将这一轮的基准数归位,直到所有的数都归为为止,排序结束。(类似冒泡). partition是返回一个基准值的index, index 左边都小于该index的数,右边都大于该index的数。
快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是 O(n^2),它的平均时间复杂度为 O(nlogn)。其实快速排序是基于 “二分” 的思想。
/* 基本思想
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。*/
//快排 分治+挖坑
static void quick_sort(int[] s,int l,int r){
//递归的结束条件
if(l<r){
//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换(当基准数去区间的中间值时)
//基准数为第一个坑
int x=s[l];
int i=l,j=r;
//完成一次交换数据
while(i<j){
//从右边找第一个比基准数小的数,然后填入前一个坑
while(i<j&&s[j]>=x) j--;
if(i<j) s[i++]=s[j];
//从左边边找第一个比基准数大或者等于的数,然后填入前一个坑
while(i<j&&s[i]<x) i++;
if(i<j) s[j--]=s[i];
}
s[i]=x;
quick_sort(s,l,i-1);
quick_sort(s,i+1,r);
}
}
迭代实现
public void sortIntegers2(int[] A) {
// write your code here
if (A == null || A.length == 0) {
return;
}
Stack<Integer> stack = new Stack<Integer>();
stack.push(0);
stack.push(A.length - 1);
while (!stack.isEmpty()) {
int right = stack.pop();
int left = stack.pop();
//如果最大索引小于等于左边索引,说明结束了
if (right <= left) continue;
int pos = helper(A,left,right);
if (left < pos - 1) {
stack.push(left);
stack.push(pos - 1);
}
if (right > pos + 1) {
stack.push(pos + 1);
stack.push(right);
}
}
}
public int helper(int[] arr,int left,int right) {
if(left < right) {
int l = left;
int r = right;
int base = arr[l];
while(l < r) {
while(l < r && arr[r] > base) {
r--;
}
if (l <r) {
arr[l++] = arr[r];
}
while(l < r && arr[l] < base) {
l++;
}
if (l < r) {
arr[r--] = arr[l];
}
}
arr[l] = base;
return l;
}
return left;
}
六、堆排序 O(N*logN)
地排序,时间复杂度小于归并的O(n+logn)
排序时间与输入无关,最好,最差,平均都是O(nlogn). 不稳定
堆排序借助了堆这个数据结构,堆类似二叉树,又具有堆积的性质(子节点的关键值总小于(大于)父节点) 堆排序包括两个主要操作:
- 保持堆的性质heapify(A,i)
时间复杂度O(logn)
- 建堆 buildmaxheap(A)
时间复杂度O(n)线性时间建堆
对于大数据的处理: 如果对100亿条数据选择Topk数据,选择快速排序好还是堆排序好? 答案是只能用堆排序。 堆排序只需要维护一个k大小的空间,即在内存开辟k大小的空间。而快速排序需要开辟能存储100亿条数据的空间,which is impossible.
堆这种数据结构的很好的应用是 优先级队列,如作业调度。
堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。在堆中定义以下几种操作:
- 最大堆调整(Max-Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
- 创建最大堆(Build-Max-Heap):将堆所有数据重新排序,使其成为最大堆
- 堆排序(Heap-Sort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
sort(arr,arr.length - 1);
/**
* 堆排序的主要入口方法,共两步。
*/
public void sort(int[] nums,int len){
/*
* 第一步:将数组堆化
* beginIndex = 第一个非叶子节点。
* 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
* 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
*/
int beginIndex = (len - 1) >> 1;
for(int i = beginIndex; i >= 0; i--){
maxHeapify(nums,i, len);
}
/*
* 第二步:对堆化数据排序
* 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。
* 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。
* 直至未排序的堆长度为 0。
*/
for(int i = len; i > 0; i--){
//swap(0, i);
int temp = nums[0];
nums[0] = nums[i];
nums[i] = temp;
maxHeapify(nums,0, i - 1);
}
}
/**
* 调整索引为 index 处的数据,使其符合堆的特性。
*
* @param index 需要堆化处理的数据的索引
* @param len 未排序的堆(数组)的长度
*/
private void maxHeapify(int[] nums,int index,int len){
int li = (index << 1) + 1; // 左子节点索引
int ri = li + 1; // 右子节点索引
int cMax = li; // 子节点值最大索引,默认左子节点。
if(li > len) return; // 左子节点索引超出计算范围,直接返回。
if(ri <= len && nums[ri] > nums[li]) // 先判断左右子节点,哪个较大。
cMax = ri;
if(nums[cMax] > nums[index]){
// 如果父节点被子节点调换,
int temp = nums[index];
nums[index] = nums[cMax];
nums[cMax] = temp;
maxHeapify(nums,cMax, len); // 则需要继续判断换下后的父节点是否符合堆的特性。
}
}
维基百科
package com.li.lintcode;
import java.util.Arrays;
public class HeapSort {
private int[] arr;
public HeapSort(int[] arr){
this.arr = arr;
}
/**
* 堆排序的主要入口方法,共两步。
*/
public void sort(){
/*
* 第一步:将数组堆化
* beginIndex = 第一个非叶子节点。
* 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
* 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
*/
int len = arr.length - 1;
int beginIndex = (len - 1) >> 1;
for(int i = beginIndex; i >= 0; i--){
maxHeapify(i, len);
}
/*
* 第二步:对堆化数据排序
* 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。
* 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。
* 直至未排序的堆长度为 0。
*/
for(int i = len; i > 0; i--){
swap(0, i);
maxHeapify(0, i - 1);
}
}
private void swap(int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 调整索引为 index 处的数据,使其符合堆的特性。
*
* @param index 需要堆化处理的数据的索引
* @param len 未排序的堆(数组)的长度
*/
private void maxHeapify(int index,int len){
int li = (index << 1) + 1; // 左子节点索引
int ri = li + 1; // 右子节点索引
int cMax = li; // 子节点值最大索引,默认左子节点。
if(li > len) return; // 左子节点索引超出计算范围,直接返回。
if(ri <= len && arr[ri] > arr[li]) // 先判断左右子节点,哪个较大。
cMax = ri;
if(arr[cMax] > arr[index]){
swap(cMax, index); // 如果父节点被子节点调换,
maxHeapify(cMax, len); // 则需要继续判断换下后的父节点是否符合堆的特性。
}
}
/**
* 测试用例
*
* 输出:
* [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9]
*/
public static void main(String[] args) {
int[] arr = new int[]{3,5,3,0,8,6,1,5,8,6,2,4,9,4,7,0,1,8,9,7,3,1,2,5,9,7,4,0,2,6};
new HeapSort(arr).sort();
System.out.println(Arrays.toString(arr));
}
}
七、归并排序 O(N*logN)
1、将有序数组a[]和b[]合并到c[]中, 以看出合并有序数列的效率是比较高的,可以达到O(n)。
//将有序数组a[]和b[]合并到c[]中
public static void mergeArray(int a[], int n, int b[], int m, int c[]){
int i=0,j=0,k=0;
while(i<n&&j<m){
if(a[i]<b[j]){
c[k++]=a[i++];
}else{
c[k++]=b[j++];
}
}
while(i<n){
c[k++]=a[i++];
}
while(j<m){
c[k++]=b[j++];
}
}
2、归并排序算法
归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
public static void mergeSort(int[] a,int n){
int [] temp=new int[n];
//注意下标,按数组的下标而不是长度
merge_sort(a, 0, n-1,temp);
}
public static void merge_sort(int[] a,int first,int end,int[] temp){
if(first<end){
int mid=(first+end)/2;
merge_sort(a, first, mid, temp);//左边有序
merge_sort(a, mid+1, end, temp);//右边有序
merge_arrays(a, first, mid, end, temp);//再将二个有序数列合并
}
}
//将有二个有序数列a[first...mid]和a[mid...last]合并。
public static void merge_arrays(int[] a,int first,int mid,int end,int[] temp){
int i=first,j=mid+1;
int m=mid,n=end;
int k=0;
//注意下标,按数组的下标而不是长度
while(i<=m&&j<=n){
if(a[i]<=a[j]){
temp[k++]=a[i++];
}else{
temp[k++]=a[j++];
}
}
//注意下标,按数组的下标而不是长度
while(i<=m){
temp[k++]=a[i++];
}
//注意下标,按数组的下标而不是长度
while(j<=n){
temp[k++]=a[j++];
}
//左边排好序把原数组的值给覆盖掉,通过借助temp数组实现
for(i=0;i<k;i++){
a[first+i]=temp[i];
}
}
维基百科
递归版:
static void merge_sort_recursive(int[] arr, int[] result, int start, int end) {
if (start >= end)
return;
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, result, start1, end1);
merge_sort_recursive(arr, result, start2, end2);
int k = start;
while (start1 <= end1 && start2 <= end2)
result[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 <= end1)
result[k++] = arr[start1++];
while (start2 <= end2)
result[k++] = arr[start2++];
for (k = start; k <= end; k++)
arr[k] = result[k];
}
public static void merge_sort(int[] arr) {
int len = arr.length;
int[] result = new int[len];
merge_sort_recursive(arr, result, 0, len - 1);}
迭代版:
public static void merge_sort(int[] arr) {
int len = arr.length;
int[] result = new int[len];
int block, start;
// 原版代码的迭代次数少了一次,没有考虑到奇数列数组的情况
for(block = 1; block < len*2; block *= 2) {
for(start = 0; start <len; start += 2 * block) {
int low = start;
int mid = (start + block) < len ? (start + block) : len;
int high = (start + 2 * block) < len ? (start + 2 * block) : len;
//两个块的起始下标及结束下标
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
//开始对两个block进行归并排序
while (start1 < end1 && start2 < end2) {
result[low++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
}
while(start1 < end1) {
result[low++] = arr[start1++];
}
while(start2 < end2) {
result[low++] = arr[start2++];
}
}
int[] temp = arr;
arr = result;
result = temp;
}
result = arr;
}
非比较排序: ,计数排序,基数排序,桶排序,时间复杂度能够达到O(n). 这些排序为了达到不比较的目的,对数据做了一些基本假设(限制)。如计数排序假设数据都[0,n] 范围内,且范围较小;基数排序假设数据都[0,n] 范围内;也是桶排序假设数据均匀独立的分布。
而且,非比较排序的空间要求比较高,用空间换取时间吧。当我们的待排序数组具备一些基数排序与桶排序要求的特性,且空间上又比较富裕时,桶排序与基数排序不失为最佳选择。
八、基数排序(正数)
基数排序(英语:Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献[1]。
它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
//获取位数public static int getMaxnum(int[] A) { int base = 10; int num = 1; for (int i = 0;i < A.length; i++) { while (Math.abs(A[i]) / base != 0) { base *= 10; num++; } } return num; }
public class RadixSort
{
public static void sort(int[] number, int d) //d表示最大的数有多少位
{
intk = 0;
intn = 1;
intm = 1; //控制键值排序依据在哪一位
int[][]temp = newint[10][number.length]; //数组的第一维表示可能的余数0-9
int[]order = newint[10]; //数组orderp[i]用来表示该位是i的数的个数
while(m <= d)
{
for(inti = 0; i < number.length; i++)
{
intlsd = ((number[i] / n) % 10);
temp[lsd][order[lsd]] = number[i];
order[lsd]++;
}
for(inti = 0; i < 10; i++)
{
if(order[i] != 0)
for(intj = 0; j < order[i]; j++)
{
number[k] = temp[i][j];
k++;
}
order[i] = 0;
}
n *= 10;
k = 0;
m++;
}
}
public static void main(String[] args)
{
int[]data =
{73, 22, 93, 43, 55, 14, 28, 65, 39, 81, 33, 100};
RadixSort.sort(data, 3);
for(inti = 0; i < data.length; i++)
{
System.out.print(data[i] + "");
}
}
}
为什么要用基数排序 ?
计数排序和桶排序都只是在研究一个关键字的排序,现在我们来讨论有多个关键字的排序问题。
假设我们有一些二元组(a,b),要对它们进行以a 为首要关键字,b的次要关键字的排序。我们可以先把它们先按照首要关键字排序,分成首要关键字相同的若干堆。然后,在按照次要关键值分别对每一堆进行单独排序。最后再把这些堆串连到一起,使首要关键字较小的一堆排在上面。按这种方式的基数排序称为 MSD(Most Significant Dight) 排序。
第二种方式是从最低有效关键字开始排序,称为 LSD(Least Significant Dight)排序 。首先对所有的数据按照次要关键字排序,然后对所有的数据按照首要关键字排序。要注意的是,使用的排序算法必须是稳定的,否则就会取消前一次排序的结果。由于不需要分堆对每堆单独排序,LSD 方法往往比 MSD 简单而开销小。下文介绍的方法全部是基于 LSD 的。
通常,基数排序要用到计数排序或者桶排序。使用计数排序时,需要的是Order数组。使用桶排序时,可以用链表的方法直接求出排序后的顺序。
1 性能分析
时间复杂度O(n) (实际上是O(d(n+k)) d是位数)
2 核心代码
RADIX-SORT(A,d)
for i = 1 to d
do use a stable sort to sort array A on digit i
3扩展
问题:对[0,n^2-1]的n 个整数进行线性时间排序。
思路 : 把整数转换为n进制再排序,每个数有两位,每位的取值范围是[0..n-1],再进行基数排序
http://blog.csdn.net/mishifangxiangdefeng/article/details/7685839
问题: 给定一个字符串数组,其中不同的串包含的字符数可能不同,但所有串中总的字符个数为 n。说明如何在 O(n) 时间内对该数组进行排序
九、桶排序
桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。
桶排序以下列程序进行:
- 设置一个定量的数组当作空桶子。
- 寻访序列,并且把项目一个一个放到对应的桶子去。
- 对每个不是空的桶子进行排序。
- 从不是空的桶子里把项目再放回原来的序列中。
数据结构:数组
最坏时间复杂度:{\displaystyle O(n^{2})}
平均时间复杂度:{\displaystyle O(n+k)}
空间复杂度:{\displaystyle O(n*k)}
/** * @param a 待排序数组元素 * @param step 步长(桶的宽度/区间),具体长度可根据情况设定 * @return 桶的位置/索引 */
private int indexFor(int a,int step){
return a/step;
}
public void bucketSort(int []arr){
int max=arr[0],min=arr[0];
for (int a:arr) {
if (max<a)
max=a;
if (min>a)
min=a;
}
//该值也可根据实际情况选择
int bucketNum=max/10-min/10+1;
List buckList=new ArrayList<List<Integer>>();
//create bucket
for (int i=1;i<=bucketNum;i++){
buckList.add(new ArrayList<Integer>());
}
//push into the bucket
for (int i=0;i<arr.length;i++){
int index=indexFor(arr[i],10);
((ArrayList<Integer>)buckList.get(index)).add(arr[i]);
}
ArrayList<Integer> bucket=null;
int index=0;
for (int i=0;i<bucketNum;i++){
bucket=(ArrayList<Integer>)buckList.get(i);
insertSort(bucket);
for (int k : bucket) {
arr[index++]=k;
}
}
}
//把桶内元素插入排序
private void insertSort(List<Integer> bucket){
for (int i=1;i<bucket.size();i++){
int temp=bucket.get(i);
int j=i-1;
for (; j>=0 && bucket.get(j)>temp;j--){
bucket.set(j+1,bucket.get(j));
}
bucket.set(j+1,temp);
}
}
桶排序的思想近乎彻底的分治思想。
桶排序假设待排序的一组数均匀独立的分布在一个范围中,并将这一范围划分成几个子范围(桶)。
然后基于某种映射函数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是所有元素个数
性能分析
平均时间复杂度为线性的 O(n+C) 最优情形下,桶排序的时间复杂度为O(n)。
桶排序的空间复杂度通常是比较高的,额外开销为O(n+m)(因为要维护 M 个数组的引用)。
就是桶越多,时间效率就越高,而桶越多,空间却就越大,由此可见时间和空间是一个矛盾的两个方面。
算法稳定性 : 桶排序的稳定性依赖于桶内排序。如果我们使用了快排,显然,算法是不稳定的。
桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的 f(k) 值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块 (桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
对 N 个关键字进行桶排序的时间复杂度分为两个部分:
(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是 O(n)。
(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(ni*logni) 。其中 ni 为第 i个桶的数据量。
很显然,第 (2) 部分是桶排序性能好坏的决定因素。这就是一个时间代价和空间代价的权衡问题了。
十,计数排序
计数排序(Counting sort)是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
当输入的元素是n个0到k之间的整数时,它的运行时间是Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序算法中,能够更有效的排序数据范围很大的数组。
通俗地理解,例如有10个年龄不同的人,统计出有8个人的年龄比A小,那A的年龄就排在第9位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去1的原因。算法的步骤如下:
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组 C 的第 i 项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
public class CountingSort { public static void main(String[] argv) { int[] A = CountingSort.countingSort(new int[]{16, 4, 10, 14, 7, 9, 3, 2, 8, 1}); Utils.print(A); } public static int[] countingSort(int[] A) { int[] B = new int[A.length]; // 假设A中的数据a'有,0<=a' && a' < k并且k=100 int k = 100; countingSort(A, B, k); return B; } private static void countingSort(int[] A, int[] B, int k) { int[] C = new int[k]; // 计数 for (int j = 0; j < A.length; j++) { int a = A[j]; C[a] += 1; } Utils.print(C); // 求计数和 for (int i = 1; i < k; i++) { C[i] = C[i] + C[i - 1]; } Utils.print(C); // 整理 for (int j = A.length - 1; j >= 0; j--) { int a = A[j]; B[C[a] - 1] = a; C[a] -= 1; } }}//针对c数组的大小,优化过的计数排序public class CountSort{ public static void main(String []args){ //排序的数组 int a[] = {100, 93, 97, 92, 96, 99, 92, 89, 93, 97, 90, 94, 92, 95}; int b[] = countSort(a); for(int i : b){ System.out.print(i + " "); } System.out.println(); } public static int[] countSort(int []a){ int b[] = new int[a.length]; int max = a[0], min = a[0]; for(int i : a){ if(i > max){ max = i; } if(i < min){ min = i; } } //这里k的大小是要排序的数组中,元素大小的极值差+1 int k = max - min + 1; int c[] = new int[k]; for(int i = 0; i < a.length; ++i){ c[a[i]-min] += 1;//优化过的地方,减小了数组c的大小 } for(int i = 1; i < c.length; ++i){ c[i] = c[i] + c[i-1]; } for(int i = a.length-1; i >= 0; --i){ b[--c[a[i]-min]] = a[i];//按存取的方式取出c的元素 } return b; }}
public static void jishu_sort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int max = arr[0];
for (int i = 1;i < arr.length;i++) {
if (arr[i] > max) {
max = arr[i];
}
}
int[] c = new int[max + 1];
int[] b = new int[arr.length];
for (int i = 0; i < arr.length;i++) {
c[arr[i]]++;
}
for (int i = 1; i < c.length;i++) {
c[i] = c[i] + c[i-1];
}
for (int i = 0;i < arr.length;i++) {
int a = arr[i];
b[c[a] - 1] = a;
c[a]--;
}
int k = 0;
for (int i = 0;i < arr.length;i++) {
arr[k++] = b[i];
}
}
我们希望能线性的时间复杂度排序,如果一个一个比较,显然是不实际的,书上也在决策树模型中论证了,比较排序的情况为nlogn的复杂度。既然不能一个一个比较,我们想到一个办法,就是如果在排序的时候就知道他的位置,那不就是扫描一遍,把他放入他应该的位置不就可以了。 要知道他的位置,我们只需要知道有多少不大于他不就可以了吗?
1 性能分析
最好,最坏,平均的时间复杂度O(n+k), 天了噜, 线性时间完成排序,且稳定。
优点:不需要比较函数,利用地址偏移,对范围固定在[0,k]的整数排序的最佳选择。是排序字节串最快的排序算法。
缺点:由于用来计数的数组的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
2 核心代码
public int[] countsort(int A[]){
int[] B = new int[A.length]; //to store result after sorting
int k = max(A);
int [] C = new int[k+1]; // to store temp
for(int i=0;i<A.length;i++){
C[A[i]] = C[A[i]] + 1;
}
// 小于等于A[i]的数的有多少个, 存入数组C
for(int i=1;i<C.length;i++){
C[i] = C[i] + C[i-1];
}
//逆序输出确保稳定-相同元素相对顺序不变
for(int i=A.length-1;i>=0;i--){
B[C[A[i]]-1] = A[i];
C[A[i]] = C[A[i]]-1;
}
return B;
}
3 扩展
请给出一个算法,使之对给定的介于 0到 k 之间的 n个整数进行预处理,并能在O(1) 时间内回答出输入的整数中有多少个落在 [a...b] 区间内。你给出的算法的预处理时间为O(n+k)。
分析:就是用计数排序中的预处理方法,获得数组 C[0...k],使得C[i]为不大于 i的元素的个数。这样落入 [a...b] 区间内的元素个数有 C[b]-C[a-1]。
计数排序的重要性质是他是稳定的。一般而言,仅当卫星数据随着被排序的元素一起移动时,稳定性才显得比较重要。而这也是计数排序作为基数排序的子过程的重要原因