查找和排序算法是算法的入门知识,其经典思想能够用于不少算法当中。由于其实现代码较短,应用较常见。因此在面试中常常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。通常在面试中最常考的是快速排序和归并排序,而且常常有面试官要求现场写出这两种排序的代码。对这两种排序的代码必定要信手拈来才行。还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。面试官对于这些排序可能会要求比较各自的优劣、各类算法的思想及其使用场景。还有要会分析算法的时间和空间复杂度。一般查找和排序算法的考察是面试的开始,若是这些问题回答很差,估计面试官都没有继续面试下去的兴趣了。因此想开个好头就要把常见的排序算法思想及其特色熟练掌握,在必要时能熟练写出代码。ios
接下来咱们就分析一下常见的排序算法及其使用场景。限于篇幅,某些算法的详细演示和图示请自行寻找其它参考资料。面试
冒泡排序是最简单的排序之一了,其大致思想就是经过与相邻元素的比较和交换来把小的数交换到最前面。这个过程相似于水泡向上升同样,所以而得名。举个例子,对5,3,8,6,4这个无序序列进行冒泡排序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3.这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会获得一个有序序列。冒泡排序的时间复杂度为O(n^2),空间复杂度为O(1)。算法
冒泡排序:shell
实现代码:数组
/** *@Description:冒泡排序算法实现 */ public class BubbleSort { public static void bubbleSort(int[] arr) { if(arr == null || arr.length == 0) return ; for(int i=0; i<arr.length-1; i++) { for(int j=arr.length-1; j>i; j--) { if(arr[j] < arr[j-1]) { swap(arr, j-1, j); } } } } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
选择排序的思想其实和冒泡排序有点相似,都是在一次排序后把最小的元素放到最前面。可是过程不一样,冒泡排序是经过相邻的比较和交换。而选择排序是经过对总体的选择。举个例子,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5之外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4.对剩下的序列一次进行选择和交换,获得3,4,5,8,6, 以此类推,最终就会获得一个有序序列。其实选择排序能够当作冒泡排序的优化,由于其目的相同,只是选择排序只有在肯定了最小数的前提下才进行交换,大大减小了交换的次数。选择排序的时间复杂度为O(n^2),空间复杂度为O(1)。函数
示意图:性能
实现代码:大数据
/** *@Description:简单选择排序算法的实现 */ public class SelectSort { public static void selectSort(int[] arr) { if(arr == null || arr.length == 0) return ; int minIndex = 0; for(int i=0; i<arr.length-1; i++) { //只须要比较n-1次 minIndex = i; for(int j=i+1; j<arr.length; j++) { //从i+1开始比较,由于minIndex默认为i了,i就不必比了。 if(arr[j] < arr[minIndex]) { minIndex = j; } } if(minIndex != i) { //若是minIndex不为i,说明找到了更小的值,交换之。 swap(arr, i, minIndex); } } } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
插入排序不是经过交换位置而是经过比较找到合适的位置插入元素来达到排序的目的的。相信你们都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理本身的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是同样的。举个例子,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置是正确的,想一下在拿到第一张牌的时候,不必整理。而后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。而后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。简单插入排序的时间复杂度也是O(n^2),空间复杂度为O(1)。优化
示意图:ui
实现代码:
/** *@Description:简单插入排序算法实现 */ public class InsertSort { public static void insertSort(int[] arr) { if(arr == null || arr.length == 0) return ; for(int i=1; i<arr.length; i++) { //假设第一个数位置时正确的;要日后移,必需要假设第一个。 int j = i; int target = arr[i]; //待插入的 //后移 while(j > 0 && target < arr[j-1]) { arr[j] = arr[j-1]; j --; } //插入 arr[j] = target; } } }
快速排序一听名字就以为很高端,在实际应用当中快速排序确实也是表现最好的排序算法。快速排序虽然高端,但其思想是来自冒泡排序,冒泡排序是经过相邻元素的比较和交换把最小的冒泡到最顶端,而快速排序是比较和交换小数和大数,在把小数冒泡到上面的同时也把大数沉到下面。
为何必定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,由于在最后两个指针相遇的时候,要交换基准数到相遇的位置。通常选取第一个数做为基准数,那么就是在左边,因此最后相遇的数要和基准数交换,那么相遇的数必定要比基准数小。因此j指针先移动才能先找到比基准数小的数。
快速排序是不稳定的,其平均时间复杂度是O(nlgn),空间复杂度是O(nlgn)。
示意图:
实现代码:
/** *@Description:实现快速排序算法 */ public class QuickSort { //一次划分 public static int partition(int[] arr, int left, int right) { int pivotKey = arr[left]; int pivotPointer = left; while(left < right) { while(left < right && arr[right] >= pivotKey) right --; while(left < right && arr[left] <= pivotKey) left ++; swap(arr, left, right); //把大的交换到右边,把小的交换到左边。 } swap(arr, pivotPointer, left); //最后把pivot交换到中间 return left; } public static void quickSort(int[] arr, int left, int right) { if(left >= right) return ; int pivotPos = partition(arr, left, right); quickSort(arr, left, pivotPos-1); quickSort(arr, pivotPos+1, right); } public static void sort(int[] arr) { if(arr == null || arr.length == 0) return ; quickSort(arr, 0, arr.length-1); } public static void swap(int[] arr, int left, int right) { int temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; } }
其实上面的代码还能够再优化,上面代码中基准数已经在pivotKey中保存了,因此不须要每次交换都设置一个temp变量,在交换左右指针的时候只须要前后覆盖就能够了。这样既能减小空间的使用还能下降赋值运算的次数。优化代码以下:
/** *@Description:实现快速排序算法 */ public class QuickSort { /** * 划分 * @param arr * @param left * @param right * @return */ public static int partition(int[] arr, int left, int right) { int pivotKey = arr[left]; while(left < right) { while(left < right && arr[right] >= pivotKey) right --; arr[left] = arr[right]; //把小的移动到左边 while(left < right && arr[left] <= pivotKey) left ++; arr[right] = arr[left]; //把大的移动到右边 } arr[left] = pivotKey; //最后把pivot赋值到中间 return left; } /** * 递归划分子序列 * @param arr * @param left * @param right */ public static void quickSort(int[] arr, int left, int right) { if(left >= right) return ; int pivotPos = partition(arr, left, right); quickSort(arr, left, pivotPos-1); quickSort(arr, pivotPos+1, right); } public static void sort(int[] arr) { if(arr == null || arr.length == 0) return ; quickSort(arr, 0, arr.length-1); } }
总结快速排序的思想:冒泡+二分+递归分治,慢慢体会。。。
堆排序是借助堆来实现的选择排序,思想同简单的选择排序。
1.堆
堆其实是一棵彻底二叉树,其任何一非叶节点知足性质:
Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2]
即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。
堆分为大顶堆和小顶堆,知足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,知足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字确定是全部关键字中最大的,小顶堆的堆顶的关键字是全部关键字中最小的。
2.堆排序的思想
利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。
其基本思想为(大顶堆):
1)将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区;
2)将堆顶元素R[1]与最后一个元素R[n]交换,此时获得新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且知足R[1,2...n-1]<=R[n];
3)因为交换后新的堆顶R[1]可能违反堆的性质,所以须要对当前无序区(R1,R2,......Rn-1)调整为新堆,而后再次将R[1]与无序区最后一个元素交换,获得新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
操做过程以下:
1)初始化堆:将R[1..n]构造为堆;
2)将当前无序区的堆顶元素R[1]同该区间的最后一个记录交换,而后将新的无序区调整为新的堆。
所以对于堆排序,最重要的两个操做就是构造初始堆和调整堆,其实构造初始堆事实上也是调整堆的过程,只不过构造初始堆是对全部的非叶节点都进行调整。
下面举例说明:
给定一个整形数组a[]={16,7,3,20,17,8},对其进行堆排序。
首先根据该数组元素构建一个彻底二叉树,获得
而后须要构造初始堆,则从最后一个非叶节点开始调整,调整过程以下:
20和16交换后致使16不知足堆的性质,所以需从新调整
这样就获得了初始堆。
即每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换(交换以后可能形成被交换的孩子节点不知足堆的性质,所以每次交换以后要从新对被交换的孩子节点进行调整)。有了初始堆以后就能够进行排序了。
此时3位于堆顶不满堆的性质,则需调整继续调整
这样整个区间便已经有序了。
从上述过程可知,堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1...n]中选择最大记录,需比较n-1次,而后从R[1...n-2]中选择最大记录需比较n-2次。事实上这n-2次比较中有不少已经在前面的n-1次比较中已经作过,而树形选择排序刚好利用树形的特色保存了部分前面的比较结果,所以能够减小比较次数。对于n个关键字序列,最坏状况下每一个节点需比较log2(n)次,所以其最坏状况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。堆排序空间复杂度为O(1)。
示意图:
实现代码:
/*堆排序(大顶堆)*/
#include <iostream>
#include<algorithm>
using namespace std;
void HeapAdjust(int *a,int i,int size) //调整堆
{
int lchild=2*i; //i的左孩子节点序号
int rchild=2*i+1; //i的右孩子节点序号
int max=i; //临时变量
if(i<=size/2) //若是i是叶节点就不用进行调整
{
if(lchild<=size&&a[lchild]>a[max])
{
max=lchild;
}
if(rchild<=size&&a[rchild]>a[max])
{
max=rchild;
}
if(max!=i)
{
swap(a[i],a[max]);
HeapAdjust(a,max,size); //避免调整以后以max为父节点的子树不是堆
}
}
}
void BuildHeap(int *a,int size) //创建堆
{
int i;
for(i=size/2;i>=1;i--) //非叶节点最大序号值为size/2
{
HeapAdjust(a,i,size);
}
}
void HeapSort(int *a,int size) //堆排序
{
int i;
BuildHeap(a,size);
for(i=size;i>=1;i--)
{
//cout<<a[1]<<" ";
swap(a[1],a[i]); //交换堆顶和最后一个元素,即每次将剩余元素中的最大者放到最后面
//BuildHeap(a,i-1); //将余下元素从新创建为大顶堆
HeapAdjust(a,1,i-1); //从新调整堆顶节点成为大顶堆
}
}
int main(int argc, char *argv[])
{
//int a[]={0,16,20,3,11,17,8};
int a[100];
int size;
while(scanf("%d",&size)==1&&size>0)
{
int i;
for(i=1;i<=size;i++)
cin>>a[i];
HeapSort(a,size);
for(i=1;i<=size;i++)
cout<<a[i]<<"";
cout<<endl;
}
return 0;
}
希尔(Shell)排序又称为缩小增量排序,它是直接插入排序算法的一种增强版。
该方法因DL.Shell于1959年提出而得名。
希尔排序的基本思想是:
把记录按步长gap分组,对每组记录采用直接插入排序方法进行排序。
随着步长逐渐减少,所分红的组包含的记录愈来愈多,当步长的值减少到 1 时,整个数据合成为一组,构成一组有序记录,则完成排序。
咱们来经过演示图,更深刻的理解一下这个过程。
在上面这幅图中:
初始时,有一个大小为 10 的无序序列。
在第一趟排序中,咱们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,能够分为 5 组。
接下来,按照直接插入排序的方法对每一个组进行排序。
在第二趟排序中,咱们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,能够分为 2 组。
按照直接插入排序的方法对每一个组进行排序。
在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。
按照直接插入排序的方法对每一个组进行排序。此时,排序已经结束。
须要注意一下的是,图中有两个相等数值的元素 5 和 5 。咱们能够清楚的看到,在排序过程当中,两个元素位置交换了。
因此,希尔排序是不稳定的算法。
希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。可是在大量实验的基础上推出当n在某个范围内时,时间复杂度能够达到O(n^1.3)。空间复杂度O(1)
示意图:
实现代码:
/** *@Description:希尔排序算法实现 */ public class ShellSort { /** * 希尔排序的一趟插入 * @param arr 待排数组 * @param d 增量 */ public static void shellInsert(int[] arr, int d) { for(int i=d; i<arr.length; 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(nlogn),空间复杂度为O(n)
示意图:
举个例子:
实现代码:
/** *@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 <= mid && j <= 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<temp.length; p++) { arr[left + p] = temp[p]; } } }
若是在面试中有面试官要求你写一个O(n)时间复杂度的排序算法,你千万不要马上说:这不可能!虽然前面基于比较的排序的下限是O(nlogn)。可是确实也有线性时间复杂度的排序,只不过有前提条件,就是待排序的数要知足必定的范围的整数,并且计数排序须要比较多的辅助空间。其基本思想是,用待排序的数做为计数数组的下标,统计每一个数字的个数。而后依次输出便可获得有序序列。
计数排序很是基础,他的主要目的是对整数排序而且会比普通的排序算法性能更好。例如,输入{1, 3, 5, 2, 1, 4}给计数排序,会输出{1, 1, 2, 3, 4, 5}。这个算法由如下步骤组成:
例子:
输入{3, 4, 3, 2, 1},最大是4,数组长度是5。
创建计数数组{0, 0, 0, 0}。
遍历输入数组:
{3, 4, 3, 2, 1} -> {0, 0, 1, 0}
{3, 4, 3, 2, 1} -> {0, 0, 1, 1}
{3, 4, 3, 2, 1} -> {0, 0, 2, 1}
{3, 4, 3, 2, 1} -> {0, 1, 2, 1}
{3, 4, 3, 2, 1} -> {1, 1, 2, 1}
计数数组如今是{1, 1, 2, 1},咱们如今把它写回到输入数组里:
{0, 1, 2, 1} -> {1, 4, 3, 2, 1}
{o, o, 2, 1} -> {1, 2, 3, 2, 1}
{o, o, 1, 1} -> {1, 2, 3, 2, 1}
{o, o, o, 1} -> {1, 2, 3, 3, 1}
{o, o, o, o} -> {1, 2, 3, 3, 4}
这样就排好序了。
实现代码:
/** *@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<arr.length; i++) { count[arr[i]] ++; } int k = 0; for(int i=0; i<=max; i++) { for(int j=0; j<count[i]; 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<k2,那么f(k1)<=f(k2)。也就是说B(i)中的最小数据都要大于B(i-1)中最大数据。很显然,映射函数的肯定与数据自己的特色有很大的关系。
举个例子:
假如待排序列K= {4九、 38 、 3五、 97 、 7六、 73 、 2七、 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+N*logN-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<List<Integer>> buckets = new ArrayList<List<Integer>>(); //桶的索引 for(int i=0; i<10; i++) { buckets.add(new LinkedList<Integer>()); //用链表比较合适 } //划分桶 for(int i=0; i<arr.length; i++) { buckets.get(f(arr[i])).add(arr[i]); } //对每一个桶进行排序 for(int i=0; i<buckets.size(); i++) { if(!buckets.get(i).isEmpty()) { Collections.sort(buckets.get(i)); //对每一个桶进行快排 } } //还原排好序的数组 int k = 0; for(List<Integer> bucket : buckets) { for(int ele : bucket) { arr[k++] = ele; } } } /** * 映射函数 * @param x * @return */ public static int f(int x) { return x / 10; } }
基数排序又是一种和前面排序方式不一样的排序方式,基数排序不须要进行记录关键字之间的比较。基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不一样的关键字。好比说成绩的排序,若是两我的总分相同,则语文高的排在前面,语文成绩也相同则数学高的排在前面。。。若是对数字进行排序,那么个位、十位、百位就是不一样优先级的关键字,若是要进行升序排序,那么个位、十位、百位优先级一次增长。基数排序是经过屡次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。
举个例子:
第一步
假设原来有一串数值以下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
第二步
接下来将这些桶子中的数值从新串接起来,成为如下的数列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再进行一次分配,此次是根据十位数来分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
第三步
接下来将这些桶子中的数值从新串接起来,成为如下的数列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这时候整个数列已经排序完毕;若是排序的对象有三位数以上,则持续进行以上的动做直至最高位数为止。
时间效率:设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。 空间效率:须要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针。
实现代码:
/** *@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<=maxBit; i++) { List<List<Integer>> buf = distribute(arr, i); //分配 collecte(arr, buf); //收集 } } /** * 分配 * @param arr 待分配数组 * @param iBit 要分配第几位 * @return */ public static List<List<Integer>> distribute(int[] arr, int iBit) { List<List<Integer>> buf = new ArrayList<List<Integer>>(); for(int j=0; j<10; j++) { buf.add(new LinkedList<Integer>()); } for(int i=0; i<arr.length; i++) { buf.get(getNBit(arr[i], iBit)).add(arr[i]); } return buf; } /** * 收集 * @param arr 把分配的数据收集到arr中 * @param buf */ public static void collecte(int[] arr, List<List<Integer>> buf) { int k = 0; for(List<Integer> 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'; } }
在前面的介绍和分析中咱们提到了冒泡排序、选择排序、插入排序三种简单的排序及其变种快速排序、堆排序、希尔排序三种比较高效的排序。后面咱们又分析了基于分治递归思想的归并排序还有计数排序、桶排序、基数排序三种线性排序。咱们能够知道排序算法要么简单有效,要么是利用简单排序的特色加以改进,要么是以空间换取时间在特定状况下的高效排序。可是这些排序方法都不是固定不变的,须要结合具体的需求和场景来选择甚至组合使用。才能达到高效稳定的目的。没有最好的排序,只有最适合的排序。
下面就总结一下排序算法的各自的使用场景和适用场合。
1. 从平均时间来看,快速排序是效率最高的,但快速排序在最坏状况下的时间性能不如堆排序和归并排序。然后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。
2. 上面说的简单排序包括除希尔排序以外的全部冒泡排序、插入排序、简单选择排序。其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,所以常将它和其余的排序方法,如快速排序、归并排序等结合在一块儿使用。
3. 基数排序的时间复杂度也能够写成O(d*n)。所以它最使用于n值很大而关键字较小的的序列。若关键字也很大,而序列中大多数记录的最高关键字均不一样,则亦可先按最高关键字不一样,将序列分红若干小的子序列,然后进行直接插入排序。
4. 从方法的稳定性来比较,基数排序是稳定的内排方法,全部时间复杂度为O(n^2)的简单排序也是稳定的。可是快速排序、堆排序、希尔排序等时间性能较好的排序方法都是不稳定的。稳定性须要根据具体需求选择。
5. 上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元素的时间。具体的存储结构在具体的实现版本中也是不一样的。
总结二:
关于时间复杂度:
(1)平方阶(O(n2))排序
各种简单排序:直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlog2n))排序
快速排序、堆排序和归并排序;
(3)O(n1+§))排序,§是介于0和1之间的常数。
希尔排序
(4)线性阶(O(n))排序
基数排序,此外还有桶、箱排序。
关于稳定性:
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。