选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
时间复杂度
选择排序的交换操作介于 0 和 (n - 1)次之间。
空间复杂度
- 插入排序的空间复杂度为常数阶O(1)
Java实现代码
public int[] sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
//记录最小值的下标
int tmp = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[tmp] > arr[j]) {
tmp = j;
}
}
if (tmp != i) {
//交换数组中的两个数
SortUtil.swapArr(arr, i, tmp);
}
}
return arr;
}
插入排序
一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动
。
时间复杂度
- 在插入排序中,当待排序数组是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较N- 1次,时间复杂度为O(N) 。
- 最坏的情况是待排序数组是逆序的,此时需要比较次数最多,总次数记为:1+2+3+…+N-1,所以,插入排序最坏情况下的时间复杂度为 O(N^2) 。
空间复杂度
- 插入排序的空间复杂度为常数阶O(1)
Java代码实现
public int[] sort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
for (int j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
SortUtil.swapArr(arr, j, j - 1);
}
}
return arr;
}
冒泡排序
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
时间复杂度
- 最好情况下,时间复杂度为O(N) 。
- 最坏情况下的时间复杂度为 O(N^2) 。
空间复杂度
- 插入排序的空间复杂度为常数阶O(1)
Java代码实现
public int[] sort(int[] arr) {
for(int i = 0;i<arr.length -1;i++){
for(int j = arr.length-1;j > i;j--){
if(arr[j-1] > arr[j]){
SortUtil.swapArr(arr,j-1,j);
}
}
}
return arr;
}
希尔排序
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
时间复杂度
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。
空间复杂度
- 插入排序的空间复杂度为常数阶O(1)
Java代码实现
public int[] sort(int[] arr) {
if (arr == null || arr.length <= 1) {
return arr;
}
//inc为增量,当inc=1的时候,此次循环正好排序完成。
for (int inc = arr.length / 2; inc > 0; inc /= 2) {
for (int i = 0; i < inc; i++) {
for (int j = i + inc; j < arr.length; j += inc) {
//对每组进行插入排序
for (int k = j - inc; k >= 0 && arr[k] > arr[k + inc]; k -= inc)
SortUtil.swapArr(arr, k, k + inc);
}
}
}
return arr;
}
归并排序
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
时间复杂度
- 归并排序的时间复杂度为O(n log n)
空间复杂度
- 归并排序的空间复杂度为O(n)
Java代码实现
public int[] sort(int[] arr) {
return sort(arr, 0, arr.length - 1);
}
public int[] sort(int[] arr, int start, int end) {
if (start >= end) {
return new int[]{arr[end]};
} else if (end - start == 1) {
if (arr[start] > arr[end]) {
return new int[]{arr[end],arr[start]};
}
return new int[]{arr[start],arr[end]};
}
int mid = (end + start) / 2;
int[] left = sort(arr, start, mid); //左分支
int[] right = sort(arr, mid + 1, end); //右分支
return merge(left,right);
}
//二路归并
public int[] merge(int[] left,int[] right){
int[] arr = new int[left.length + right.length];
int i = 0,j=0,k = 0;
while(i < left.length && j < right.length){
if(left[i] < right[j]){
arr[k++] = left[i++];
}else{
arr[k++] = right[j++];
}
}
//判断左右分支,若还有漏网之鱼则继续归并
while (i < left.length){
arr[k++] = left[i++];
}
while (j < right.length){
arr[k++] = right[j++];
}
return arr;
}
示例
快速排序
快速排序(Quicksort)是对冒泡排序算法的一种改进。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
时间复杂度
- 快速排序法的平均复杂度为 O(nlogn)
空间复杂度
- 空间复杂度为O(logn)
Java代码实现
/**
* 1.先从数列中取出一个数作为基准数。
* 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
* 3.再对左右区间重复第二步,直到各区间只有一个数。
*/
public int[] sort(int[] arr) {
return sort(arr,0,arr.length - 1);
}
public int[] sort(int[] arr ,int start,int end){
if(start >= end){
return arr;
}
int key = arr[start];
int i= start,j = end;
while(i < j){
//从右向左找第一个小于key的数
while (i < j && arr[j] > key){
j--;
}
if(i < j)
SortUtil.swapArr(arr,i++,j);
//从左向右找第一个大于或等于于key的数
while (i < j && arr[i] <= key){
i++;
}
if(i < j)
SortUtil.swapArr(arr,i,j--);
}
//此时i必等于j,左右交汇的位置就是key的位置
arr[i] = key;
//对[start,i)区间进行快速排序
sort(arr, start, i - 1);
//对(i,end]区间进行快速排序
sort(arr, i + 1, end);
return arr;
}
示例
堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。 将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
时间复杂度
- 堆排序的复杂度为 O (nlgn)
空间复杂度
- 因为堆排序是直接原数组位置上排序,空间复杂度为常数:O(1)
Java代码实现
/**
* 1.创建一个堆 H[0……n-1];
* 2.把堆首(最大值)和堆尾互换;
* 3.把堆的尺寸缩小 1,并把新的数组顶端数据调整到相应位置;
* 4.重复步骤 2,直到堆的尺寸为 1。
*/
public int[] sort(int[] arr) {
if (arr == null || arr.length <= 1) {
return arr;
}
//创建一个大顶堆,此时arr[0]为最大值
buildMaxHeap(arr);
for (int i = arr.length - 1; i > 0; i--) {
//把最大值移到末尾
SortUtil.swapArr(arr, 0, i);
//对[0,i-1]区间重新建堆
heapify(arr, 0, i);
}
return arr;
}
public void buildMaxHeap(int[] arr) {
for (int i = (arr.length - 2)/ 2; i >= 0; i--) {
heapify(arr, i, arr.length);
}
}
public void heapify(int[] arr, int i, int len) {
int left = 2 * i + 1; //第i个节点下的左节点
int right = 2 * i + 2; //第i个节点下的右节点
int largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (i != largest) {
//将i节点与比其大的最大的那个子节点交换
SortUtil.swapArr(arr, i, largest);
heapify(arr, largest, len);
}
}
计数排序
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(nlog(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n)), 如归并排序,堆排序)
时间复杂度
- 对一定范围内的整数排序时,计数排序的复杂度为 Ο (n+k)
空间复杂度
- 计数排序的空间复杂度为Ο (n+k)
Java代码实现
public int[] sort(int[] arr) {
if (arr == null || arr.length <= 1) {
return arr;
}
//1.找出待排序的数组中最大和最小的元素
int minValue = arr[0];
int maxValue = arr[0];
for (int i = 1; i < arr.length; i++) {
if (minValue > arr[i]) {
minValue = arr[i];
}
if (maxValue < arr[i]) {
maxValue = arr[i];
}
}
int len = maxValue - minValue + 1;
//2. 创建一个新数组count,长度为待排序数组的取值范围的宽度+1
int[] count = new int[len];
//3.统计源数组中每个值为a的元素出现的次数,存入数组count的第(a-minValue)项
for (int a : arr) {
count[a - minValue] += 1; //其中minValue对应count数组中的下标即为0
}
//4.反向填充源数组:将C中不为0的下标为pos元素计算后放在源数组的第i项,每放一个元素就将count[pos]减去1,同时i加1
for (int pos = 0, i = 0; pos < len; pos++) {
while (count[pos]-- != 0) {
arr[i++] = pos + minValue;
}
}
return arr;
}
基数排序
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
时间复杂度
- 设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。
空间复杂度
- 需要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针。
Java代码实现
public int[] sort(int[] arr) {
if (arr == null || arr.length <= 1) {
return arr;
}
//获取最大的那个值的长度,也就是它的位数
int maxLen = getMaxLength(arr);
int radix = 10, level = 1;
for (int i = 0; i < maxLen; i++) {
//考虑负数的情况下创建20个桶,其中0-9的下标放负数,10-19放正数
//不考虑负数情况下只用创建10个桶即可
int[][] buckets = new int[20][arr.length];
int[] count = new int[20];
for (int j = 0, k = 0; j < arr.length; j++) {
//对level位以radix为基数取模
int mod = (arr[j] / level) % radix + 10;
buckets[mod][count[mod]++] = arr[j]; //count记录对应mod出现次数
}
int pos = 0;
//将桶中数据依次放回源数组中
for (int m = 0; m < buckets.length; m++) {
for (int n = 0; count[m]-- > 0; n++) {
arr[pos++] = buckets[m][n];
}
}
//位数增加一位
level *= 10;
}
return arr;
}
public int getMaxLength(int[] arr) {
int maxLen = (arr[0] + "").length();
for (int i = 1; i < arr.length; i++) {
int len = (arr[i] + "").length();
if (maxLen < len) {
maxLen = len;
}
}
return maxLen;
}
桶排序
桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
时间复杂度
- 桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。
空间复杂度
- 桶排序的空间复杂度为O(N+M)
Java代码实现
public int[] sort(int[] arr) {
if (arr == null || arr.length <= 1) {
return arr;
}
return sort(arr,5);
}
//bucketSize为初始化桶的个数
public int[] sort(int[] arr,int bucketSize){
int maxValue = arr[0];
int minValue = arr[0];
//找打源数组中最大值和最小值
for(int i = 1;i<arr.length;i++){
if(maxValue < arr[i]){
maxValue = arr[i];
}else if(minValue > arr[i]){
minValue = arr[i];
}
}
int[][] buckets = new int[bucketSize][arr.length];
int [] count = new int[bucketSize];
int range = (maxValue - minValue) / bucketSize +1;
for(int a : arr){
//按a的取值范围找到对应的桶
int b = (a - minValue) / range;
buckets[b][count[b]++] = a;
}
for(int[] bucket : buckets){
//此处对每个桶采用了插入排序,当然也可以替换为其它排序算法
for (int i = 1; i < count[i]; i++) {
for (int j = i; j > 0 && bucket[j] < bucket[j - 1]; j--) {
SortUtil.swapArr(bucket, j, j - 1);
}
}
}
//遍历每个桶,依次放到源数组中即完成排序
for(int i = 0,pos = 0;i< bucketSize;i++){
for(int j = 0;count[i]-- > 0;j++){
arr[pos] = buckets[i][j];
}
}
return arr;
}
关于面试
如果面试官问你,给你一个数组如何排序,请忘记上述所有算法,根据我多年开发经验,我可以很负责的告诉你,工作中几乎不会需要自己去实现上述任意算法,你只要记住下面一行代码就ok了,这也是我用的最多的方法。
Arrays.sort(arr);
实现说明: Arrays.sort(arr)将指定的数组按数字升序排序。
其用到的排序算法是由vladimiryaroslavskiy、jonbentley和joshuabloch提出的一个双基准快速排序算法。此算法在许多数据集上提供了O(nlog(n))性能,这些数据集会导致其他快速排序降级为O(N^2) 性能,并且通常比传统的(单基准)快速排序实现更快。
既然jdk提供了如此好的方法,我们又何必重复造轮子呢?
附录
SortUtil源码
//交换数组中两个下标为i,j的数
public static void swapArr(int[] arr, int i, int j) {
arr[i] ^= arr[j];
arr[j] ^= arr[i];
arr[i] ^= arr[j];
}
/**
* 初始化一个随机数组
*
* @param size 数组大小
* @param range 生成随机数范围
*/
public static int[] initArr(int size, int range) throws Exception {
if (size < 0 || range < 0) {
throw new Exception("输入非法参数");
}
int[] arr = new int[size];
Random random = new Random();
for (int i = 0; i < size; i++) {
if(random.nextInt(2) == 1) {
arr[i] = random.nextInt(range);
}else{
//二分之一的概率生成随机负数
arr[i] = -random.nextInt(range);
}
}
return arr;
}
//打印数组
public static void printArr(int[] arr, String message) {
StringBuilder sb = new StringBuilder(message);
for (int a : arr) {
sb.append(a).append(" ");
}
sb.append("\n" + message + (checkAsc(arr) ? "有序" : "无序"));
System.out.println(sb.toString());
}
public static void printArr(int size,int range,Sort sort) throws Exception {
int[] arr = SortUtil.initArr(size, range);
SortUtil.printArr(arr, "排序前:");
arr = sort.sort(arr);
SortUtil.printArr(arr, "排序后:");
//校验排序结果,不正确则抛出错误
if(!checkAsc(arr)){
throw new Exception("排序错误!");
}
}
//检查数组是否升序排列
public static boolean checkAsc(int[] arr) {
for (int i = 1; i < arr.length - 1; i++) {
if (arr[i - 1] > arr[i]) {
return false;
}
}
return true;
}
总结
名词解释:
- n:数据规模
- k:"桶"的个数
- In-place:占用常数内存,不占用额外内存
- Out-place:占用额外内存
- 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
GitHub源码地址:git@github.com:HelloWorld19930603/alien_cat.git
本文章源码对应目录:alien_cat/algorithm/src/main/java/com/aliencat/algorithm/sort