十大基础排序算法[java源码+动静双图解析+性能分析]
一、概述
作为一个合格的程序员,算法是必备技能,特此总结十大基础排序算法。java版源码实现,强烈推荐《算法第四版》非常适合入手,所有算法网上可以找到源码下载。
PS:本文讲解算法分三步:1.思想2.图示3.源码4.性能分析
1.1 时间复杂度
算法的运行时间,在这里主要考量:比较和交换的成本。
1.2 空间复杂度
使用的内存,是否需要额外的存储空间。
1.3 稳定性
相等元素排序前后相对位置保持不变,就认为是稳定的。
稳定性何时有意义?
1)排序对象包含多个属性
2) 原始序列其它属性有序
3)期望最终排序后原来有序的属性还有序,原来没序的字段有序了。
二、常见排序算法
通用函数封装
由于排序算法都用到一些相同源码,所以提炼出来作为一个父类,算法只需要继承这个类即可使用通用方法:
1.less()比较元素大小
2.exch()交换元素
3.show()打印排序后数组
4.isSorted()校验排序是否正确
package algorithm; /**
*
* @ClassName:MySort
* @Description:自定义排序基类,封装常用方法
* @author denny.zhang
* @date 2018年2月5日下午3:12:03
*/
public class MySort {
protected static String[] a = new String[]{"a","d","c","b"}; /**
*
* @Description 打印排序后结果
* @param a
* @author denny.zhang
* @date 2018年2月5日下午3:11:46
* @since JDK1.8
*/
protected static void show(Comparable[] a){
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
System.out.println();
} /**
*
* @Description 比较是否v小于w
* @param v
* @param w
* @return
* @author denny.zhang
* @date 2018年2月5日下午3:11:14
* @since JDK1.8
*/
protected static boolean less(Comparable v,Comparable w){
return v.compareTo(w)<0;
} /**
*
* @Description 对于数组a,交换a[i]和a[j]。
* @param a
* @param i
* @param j
* @author denny.zhang
* @date 2018年2月5日下午3:09:41
* @since JDK1.8
*/
protected static void exch(Comparable[] a,int i,int j){
Comparable temp= a[i];
a[i]=a[j];
a[j]=temp;
//System.out.println("交换a["+i+"]"+",a["+j+"]");
} /**
*
* @Description 用于校验是否升序
* @param a
* @return
* @author denny.zhang
* @date 2018年2月5日下午3:10:57
* @since JDK1.8
*/
protected static boolean isSorted(Comparable[] a){
//只要有一个后数<前数,返回失败
for(int i=0;i<a.length;i++){
if(less(a[i], a[i-1])) return false;
}
//都没问题,返回成功
return true;
} /**
*
* @Description 用于校验是否升序
* @param a
* @param lo 起始下标
* @param hi 结束下标
* @return
* @author denny.zhang
* @date 2018年2月6日下午5:19:43
* @since JDK1.8
*/
protected static boolean isSorted(Comparable[] a, int lo, int hi) {
for (int i = lo + 1; i <= hi; i++)
if (less(a[i], a[i-1])) return false;
return true;
}
}
2.1 冒泡排序
思想:
最简单的排序,内外两遍循环。外循环简单遍历a[n-1]~a[1],内循环一遍确定一个最大值,类似"冒泡"。
1.外循环遍历a[n-1]~a[1]。例如选定a[n-1]
2.内循环,从a[0]-a[n-1] 从左往右,比较相邻的元素对。如果第一个比第二个大,就交换它们两个,一直到最后一对,这样在最后的元素a[n-1]是最大的数;
2.重复以上的步骤,一直到外循环遍历到a[1],内循环会比较a[0],a[1],至此排序完毕。
优化点:添加一个标记,当内循环一遍不交换,排序完毕。
动态图:
源码:
/**
* @author denny
* @Description 冒泡排序
* @date 2019/7/2 下午6:19
*/
public class Bubble extends MySort { public static void sort(Comparable[] a) {
int n = a.length;
// 标记内层遍历是否交换过
int flag = 0;
// 外层遍历n-1次,每次确定一个最大值 a[i]
for (int i = n - 1; i > 0; i--) {
// 内层遍历:比较a[0]a[1]...a[n-1-1]a[n-1]
for (int j = 0; j < i; j++) {
// 相邻元素两两对比,如果后<前,交换
if (less(a[j + 1], a[j])) {
// 元素交换
exch(a, j + 1, j);
flag = 1;
}
}
// flag=0,说明数列已有序不需要再交换了!!!
if(flag==0){
break;
}
}
} public static void main(String[] args) {
Bubble.sort(a);
show(a);
}
}
分析:
时间复杂度:外部循环O(n) * 内部循环O(n) -->O(n²)
空间复杂度:不需要借助外部空间-->O(1)
是否稳定:相等元素不交换-->是。
2.2 选择排序
思想:
最简单的排序,内外两遍循环。外循环简单遍历a[0]~a[n-1],内循环每次确定一个最小值
1.外循环假设第一个数是最小值
2.内循环拿后面所有元素和第一个元素比较,找到最小值,放在第一位(如果第一位不是最小值就交换),第一小的元素确定。
3.外循环从第一个元素遍历到最后,重复1,2操作,排序完毕。
图示:
动态图:
源码:
package algorithm; /**
*
* @ClassName:Selection
* @Description:选择排序:对于数组长度为N的数组<br>
* <ul>
* <li>比较次数:(n-1)+(n-2)+...2+1=n(n-1)/2,大约N²/2次</li>
* <li>交换次数:N次</li>
* </ul>
* @author denny.zhang
* @date 2018年2月5日上午11:13:26
*/
public class Selection extends MySort{ @SuppressWarnings("rawtypes")
public static void sort(Comparable[] a){
int n = a.length;
//遍历n次,每次确定一个最小值
for(int i=0 ; i<n ; i++){
int min=i;//初始化最小值下标为i
//把i之后的每一项都和a[min]比较,求最小项
for(int j=i+1;j<n;j++){
if (less(a[j], a[min])) min = j;
}
//a[i]和a[min]交换,第i位排序完毕
exch(a, i, min);
}
} public static void main(String[] args) {
Selection.sort(a);
show(a);
}
}
分析:
时间复杂度:外部循环O(n) * 内部循环O(n) = O(n²)
空间复杂度:O(1)不需要借助外部空间
是否稳定:否。只要遇到小的就交换,被交换对象如果有等值元素存在且在交换元素之前,打破相对顺序了。列子:58529-》第一个5和2交换,2个5的相对顺序变了。
2.3 插入排序
思想:
类似扑克牌抓牌一样,一次插入一张牌保证当前牌有序。
从第二张牌开始,插入第一张牌;第三张牌插入前两张牌...一直到最后一张牌。插入牌时,两两比较,遇到逆序交换。
外循环i++,内循环a[j]和a[j-1]比较,逆序交换。j--往左移动,两两比较。
图示:
动态图:
源码:
package algorithm; /**
*
* @ClassName:Insertion
* @Description:插入排序:对于数组长度为N的数组<br>就像扑克牌插纸牌一样
* <ul>
* <li>比较次数:N-1~N²/2 平均N²/4 </li>
* <li>交换次数:0~N²/2 平均N²/4</li>
* </ul>
* @author denny.zhang
* @date 2018年2月5日上午11:13:26
*/
public class Insertion extends MySort{ @SuppressWarnings("rawtypes")
public static void sort(Comparable[] a){
int n = a.length;
//遍历n-1次,a[i]插入前i个数
for(int i=1 ; i<n ; i++){
//System.out.println("i="+i);
//i之前的每两项比较,出现逆序,立即交换
for(int j=i;j>0 && less(a[j], a[j-1]);j--){
exch(a, j, j-1);
}
}
} public static void main(String[] args) {
Insertion.sort(a);
show(a);
}
}
分析:
时间复杂度:O(n~n²)如果元素有序就是n, 元素逆序就是n²
空间复杂度:O(1)
是否稳定:是
2.4 希尔排序
思想:
希尔排序又称缩小增量排序,是插入排序的升级版。核心思想是使数组中任意间隔为h的元素有序,针对每个h,取出间隔h的子数组并插入排序,h越来越小一直到1,即排序完成。这个间隔h的选择有考究。
图示:
动态图:
步长:3-2-1
源码:
递增序列有很多,只要满足最后一个数为1即可。即最少有间隔为1的插入排序即可保证排序。至于这个数列具体最优不在本文讨论范围内。
下图展示了2种序列,sort()方法是复杂序列,sort2()是简单序列。如果只是理解算法的话sort2()足矣。
package algorithm; /**
*
* @ClassName:Shell
* @Description:希尔排序:改进自插入排序,交换不相邻元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序<br>
*
* @author denny.zhang
* @date 2018年2月5日上午11:13:26
*/
public class Shell extends MySort{ /**
*
* @Description 更加符合要求的算法
* @param a
* @author denny.zhang
* @date 2018年3月7日下午5:35:07
* @since JDK1.8
*/
@SuppressWarnings("rawtypes")
public static void sort(Comparable[] a){
int n = a.length; // 3x+1 increment sequence: 1, 4, 13, 40, 121, 364, 1093, ...
int h = 1;
while (h < n/3) {//这里确保了每个子数组有>=3个元素,h间隔过大,每个子数组元素太少就没有排序的意义了
h = 3*h + 1; //1.如果小于n/3,h变大,直到h>=n/3为止
}
while (h >= 1) {
// h-sort the array 2.遍历从h->n
for (int i = h; i < n; i++) {
//3.从j开始往左每h间隔比较一次,逆序就交换
for (int j = i; j >= h && less(a[j], a[j-h]); j -= h) {
exch(a, j, j-h);
}
}
assert isHsorted(a, h);
h /= 3;//排完序再把h降低回来
}
assert isSorted(a);
} /**
*
* @Description d=n/2序列(向上取整)简单算法
* @param a
* @author denny.zhang
* @date 2018年3月7日下午5:33:38
* @since JDK1.8
*/
@SuppressWarnings("rawtypes")
public static void sort2(Comparable[] a){
int n = a.length;
int h = n;
//1.第一层循环 是h值计算
while (h >= 1) {
h=(int) Math.ceil(h/2);//向上取整
//2.第二层循环i++ 从h->n-1
for (int i = h; i < n; i++) {
//3.第三层循环 从j开始往左每h间隔比较一次,逆序就交换,做到局部有序
for (int j = i; j >= h && less(a[j], a[j-h]); j -= h) {
exch(a, j, j-h);
}
}
assert isHsorted(a, h);
}
assert isSorted(a);
} // is the array h-sorted?
private static boolean isHsorted(Comparable[] a, int h) {
for (int i = h; i < a.length; i++)
if (less(a[i], a[i-h])) return false;
return true;
} public static void main(String[] args) {
Shell.sort(a);
show(a);
}
}
分析:
时间复杂度:O(n的1~2次方,暂无法证明最优递增数列)
空间复杂度:O(1)
是否稳定:否
2.5 归并排序
思想:
将数组拆分为两部分(递归)分别排序,再将结果归并排序。
图示:
动态图:
源码:
package algorithm; /**
*
* @ClassName:Merge
* @Description:归并排序:自顶向下+自底向上。性能差不多:比较次数:1/2NlgN-NlgN 访问次数6NlgN
* @author denny.zhang
* @date 2018年2月6日下午5:09:57
*/
public class Merge extends MySort{ /**
*
* @Description 归并
* @param a
* @param aux
* @param lo
* @param mid
* @param hi
* @author denny.zhang
* @date 2018年2月6日下午4:55:25
* @since JDK1.8
*/
private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) { // 复制数组a->aux
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
} // 归并进a数组
int i = lo, j = mid+1;
//从aux数组找到小值并赋值给数组a
for (int k = lo; k <= hi; k++) {
if (i > mid) a[k] = aux[j++];//左半边用尽,取右半边元素aux[j]->赋值给a[k],并j++
else if (j > hi) a[k] = aux[i++];//右半边用尽,取左半边元素aux[i]->赋值给a[k],并i++
else if (less(aux[j], aux[i])) a[k] = aux[j++];//aux[j]小->赋值给a[k],并j++
else a[k] = aux[i++];//aux[i]小->赋值给a[k],并i++
}
} // 自顶向下归并排序 a[lo..hi] 使用辅助数组 aux[lo..hi]
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) {
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(a, aux, lo, mid);//左半边排序
sort(a, aux, mid + 1, hi);//右半边排序
merge(a, aux, lo, mid, hi);//归并结果
} /**
*
* @Description 自底向上归并排序
* @param a
* @author denny.zhang
* @date 2018年2月6日下午6:46:01
* @since JDK1.8
*/
public static void sortBU(Comparable[] a,Comparable[] aux) {
int n = a.length;
for (int len = 1; len < n; len *= 2) {//len子数组大小,有多少个子数组遍历多少回
//子数组从2个元素开始自己归并:22归并->44归并->88归并...最后一个大归并
for (int lo = 0; lo < n-len; lo += len+len) {//lo子数组起始下标
int mid = lo+len-1;
int hi = Math.min(lo+len+len-1, n-1);//最后一个数组可能小于len,即不能按照2的倍数分小数组,最后一个取最小值
merge(a, aux, lo, mid, hi);
}
}
} /**
*
* @Description 归并排序
* @param a
* @author denny.zhang
* @date 2018年2月6日下午4:52:45
* @since JDK1.8
*/
public static void sort(Comparable[] a) {
//定义一个辅助数组
Comparable[] aux = new Comparable[a.length];
//自顶向下归并
sort(a, aux, 0, a.length-1);
//自底向上归并
//sortBU(a,aux);
//校验是否排序成功
assert isSorted(a);
} public static void main(String[] args) {
Merge.sort(a);
show(a);
}
}
分析:
时间复杂度:O(NlogN)简单理解:看上图源码44行sort(),把数组拆分2个有序数组,然后再归并2个小数组。长度为N的数组递归“折半拆分成2个有序数组”需要logN步(二叉排序树的树高),每一步的归并耗时N,相乘得到NlogN即是时间复杂度。
空间复杂度:O(N)不用多说使用了额外的辅助数组aux[N]
是否稳定:是
2.6 快速排序
思想:
两向切分:取数组第一个元素作为切分元素,左边子数组全部小于等于切分元素,右边子数组全部大于等于切分元素,这样每切分一次就可以确定一个元素的位置。左右子数组再分别递归排序。
三向切分:对于数组中重复元素多的数组,更适合三向切分。也是第一个元素作为切分元素值=v,三向切分:第一段:lo~lt元素<v,第二段:lt~gt元素=v,第三段:gt~hi元素>v。递归给第一段和第三段排序。
注意:对于小数组(<=15),插入排序更适合。
图示:
下图是一个两向切分
动态图
源码:
package algorithm; import edu.princeton.cs.algs4.StdRandom; /**
*
* @ClassName:Quick
* @Description:快速排序
* <ul>
* <li>两向切分</li>
* <li>三向切分:重复元素多的时候</li>
* </ul>
* @author denny.zhang
* @date 2018年2月7日下午5:50:56
*/
@SuppressWarnings("rawtypes")
public class Quick extends MySort{ public static void sort(Comparable[] a) {
StdRandom.shuffle(a);//随机排列,避免数组有序出现最差排序
show(a);
sort(a, 0, a.length - 1);//两向切分
//sort3Way(a, 0, a.length - 1);//三向切分
assert isSorted(a);
} /**
*
* @Description 两向切分快排
* @param a
* @param lo
* @param hi
* @author denny.zhang
* @date 2018年2月7日下午4:36:48
* @since JDK1.8
*/
private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) return;//
int j = partition(a, lo, hi);//拆分,找到拆分下标
System.out.println("j="+j);
show(a);
sort(a, lo, j-1);//左边排序,递归
sort(a, j+1, hi);//右边排序,递归
assert isSorted(a, lo, hi);
} // 两向切分数组,找到拆分下标并返回,保证a[lo..j-1] <= a[j] <= a[j+1..hi]
private static int partition(Comparable[] a, int lo, int hi) {
System.out.println("lo="+lo+",hi="+hi);
int i = lo;//左指针
int j = hi + 1;//右指针
Comparable v = a[lo];//初始化一个切分元素
//一个大的自循环
while (true) { // 分支1:如果左指针元素小于v,++i,一直到右边界退出,或者左边有不小于v的元素停下,执行分支2
while (less(a[++i], v))
if (i == hi) break; // 分支2:如果右指针元素大于v,j--,一直到到左边界退出,或者右边有不大于v的元素为止,执行分支3
while (less(v, a[--j]))
if (j == lo) break; // 分支3:如果左右指针碰撞,甚至左指针大于右指针,退出
if (i >= j) break;
// 交换需要交换的a[i]>=v>=a[j],使得a[i]<a[j]
System.out.println("交换 a[i] a[j] i="+i+",a[i]="+a[i]+",j="+j+",a[j]="+a[j]);
exch(a, i, j); }
System.out.println("i="+i+",j="+j+",设置 a[j]="+a[j]+",替换为a[lo]="+a[lo]);
// a[j]=a[lo],即v
exch(a, lo, j); // 返回下标j,a[j]就是中轴
return j;
} //三向切分快排
@SuppressWarnings("unchecked")
private static void sort3Way(Comparable[] a, int lo, int hi) {
if (hi <= lo) return;
int lt = lo, gt = hi;
Comparable v = a[lo];
int i = lo + 1;
while (i <= gt) {
int cmp = a[i].compareTo(v);
if (cmp < 0) exch(a, lt++, i++);
else if (cmp > 0) exch(a, i, gt--);
else i++;
} // a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi].
sort(a, lo, lt-1);
sort(a, gt+1, hi);
assert isSorted(a, lo, hi);
} public static void main(String[] args) {
Quick.sort(a);
show(a);
}
}
分析:
时间复杂度:O(NlogN):长度与为N的数组,一颗二叉排序树左节点<根节点<右节点,一层一层拆分下去,层数=logN+1,每一层的时间复杂度都是o(N)所以NlogN+N,N>2时logN>1,所以时间复杂度为O(NlogN)
空间复杂度:O(1)
是否稳定:否
2.7 堆排序
思想:
1.构造一个有序堆(每个节点都大于等于2个子节点),2.下沉排序销毁堆:交换a[1]和a[n],即每次确认一个最大元素,后移除a[n],这样数组越来越小一直到元素为0.
图示:
动态图
源码:
package algorithm; /**
*
* @ClassName:Heap
* @Description:堆排序:对于数组长度为N的数组<br>
* <ul>
* <li>比较次数:2NlgN+2N</li>
* <li>交换次数:NlgN+N</li>
* </ul>
* @author denny.zhang
* @date 2018年2月5日上午11:13:26
*/
public class Heap extends MySort{ /**
*
* @Description 排序
* @param pq
* @author denny.zhang
* @date 2018年3月2日下午2:28:38
* @since JDK1.8
*/
@SuppressWarnings("rawtypes")
public static void sort(Comparable[] pq){
int n = pq.length-1;//这样数组下标好算一点。原来下标0~6==>1~6
//1.构造有序堆(父节点大于等于子节点),从k=n/2--》1
for (int k = n/2; k >= 1; k--)
sink(pq, k, n);
//2.下沉排序,升序排序
while (n > 1) {
exch(pq, 1, n--);//交换根节点和首节点,后n--交换完一次就剔除最后那个已排序的元素
sink(pq, 1, n);//修复有序堆
}
} /**
*
* @Description 从上至下的堆有序化的实现
* @param pq
* @param k 根节点
* @param n 堆长度
* @author denny.zhang
* @date 2018年3月2日上午10:28:36
* @since JDK1.8
*/
@SuppressWarnings("rawtypes")
private static void sink(Comparable[] pq, int k, int n) {
while (2*k <= n) {
int j = 2*k;//左子节点下标
if (j < n && less(pq[j], pq[j+1])) j++;//找到子节点大值(pq[j]如果小于pq[j+1]就j++一直找,一直到大于等于的j),即pq[j]为左右子节点最大值
if (!less(pq[k], pq[j])) break;//如果父节点大于等于子节点大值,则堆有序,退出当前循环
exch(pq, k, j);//否则交换根节点和子节点大值
k = j;//跟节点下标变为j,即下沉到j
}
} public static void main(String[] args) {
String[] a = new String[]{"0","d","c","b","e","a"};//第一个元素空着不用排序,这样数组下标好算一点。排序结果:0 a b c d e
Heap.sort(a);
show(a);
}
}
分析:
时间复杂度:O(NlogN):1.恢复堆:由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作;2.建堆操作:从len/2到0处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2) 其中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程(具体需要公式推导这里不再拓展),结果是O(n)。二次操作时间相加=(N-1)logN+N=O(NlogN)。
空间复杂度:O(1)
是否稳定:否
2.8 计数排序
思想:
利用数组下标有序,新构造一个bucket[max-min+1],计数后,把新数组的下标(与min的差值)反向赋值给原数组。
为什么要求min? 避免不必要的空间浪费。比如90~99,如果开辟99+1=100,浪费太多空间了。现在只需要99-90+1=10的空间
注:升级版可以保证稳定性(相同元素值排序前后相对位置保持不变),这里就不再多写。
动态图:
源码:
package study.algorithm.sorting; import study.algorithm.sorting.base.MySort; /**
* @author denny
* @Description 计数排序,利用数组下标有序,新构造一个bucket[max-min+1],计数后,把新数组的下标反向赋值给原数组。
* 为什么要求min? 避免不必要的空间浪费。比如90~99,如果开辟99+1=100,浪费太多空间了。现在只需要99-90+1=10的空间
* 时间复杂度:O[n+k],k = max-min+1
* 空间复杂度:O[n+k]
* @date 2019/7/8 上午10:21
*/
public class CountingSort extends MySort { public static void sort(Integer[] b) {
// 1.遍历一遍,求最大值,最小值
int max = b[0], min = b[0];
for (int i : b) {
if (i > max) {
max = i;
}
if (i < min) {
min = i;
}
}
int k = max - min + 1;
// 构造长度为max+1的数组bucket
int[] bucket = new int[k]; // 2.遍历一遍b[], 以b元素-min值作为下标,统计出现次数赋值给bucket[]
for (int value : b) {
bucket[value-min]++;
} int sortedIndex = 0;
// 3.遍历一遍bucket[],反过来给b赋值,j=元素与最小值差值
for (int j = 0; j < k; j++) {
// 重复元素赋值,bucket[j]元素重复出现次数,bucket的下标是b的元素值
while (bucket[j] > 0) {
// bucket的下标是b的元素值,j+min=元素值
b[sortedIndex++] = j+min;
bucket[j]--;
}
} } public static void main(String[] args) {
CountingSort.sort(b);
show(b);
}
}
分析:
计数排序是一种牺牲空间换取时间的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序表现良好。
时间复杂度:1.遍历一遍b[n],求最大值最小值=O(n) 2.遍历一遍b[n],给新数组赋值=O(n) 3.遍历一遍bucket(k=max-min+1)=O(k) O(2n+k),去掉系数=O(n+k)
空间复杂度:构造了额外空间int[] bucket = new int[k], 所以为O(k)。其中k=max-min+1
2.9 桶排序
思想:
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
- 1.求原数组max、min。
- 2.把原数组元素分发到有限数量个桶中。(桶个数可自定义,映射关系例如:Math.floor(arr[i] - min)/ bucketSize)
- 3.桶内元素排序(可自选排序算法)。
- 4.遍历所有桶,把桶内元素顺序赋值给原数组。
图示:
源码:
package study.algorithm.sorting.intSort; import study.algorithm.sorting.base.MyIntSort; import java.util.Arrays; /**
* @Description 桶排序
* @author denny
* @date 2019/7/9 下午4:03
*/
public class BucketSort extends MyIntSort { public static void sort(int[] arr) {
// 每个桶能存储5个元素
bucketSort(arr, 5);
} /**
* 桶排序
* @param arr 需要排序的数组
* @param bucketSize 每个桶的容量
*/
public static void bucketSort(int[] arr, Integer bucketSize){
// 1.遍历一遍,求最大值,最小值
int max = arr[0], min = arr[0];
for (int i : arr) {
if (i > max) {
max = i;
}
if (i < min) {
min = i;
}
}
// 桶个数
int bucketCount = (int) Math.floor((max - min) / bucketSize) + 1;
// 构造一个二维数组,行=桶个数,列=桶容量,初始为0
int[][] buckets = new int[bucketCount][0]; // 2.利用映射函数将数据分配到各个桶中
for (int i = 0; i < arr.length; i++) {
// arr[i]在哪个桶中
int index = (int) Math.floor((arr[i] - min) / bucketSize);
// 把arr[i]追加进桶 buckets[index]
buckets[index] = arrAppend(buckets[index], arr[i]);
} int arrIndex = 0;
// 3.遍历所有桶,赋值
for (int[] bucket : buckets) {
if (bucket.length <= 0) {
continue;
}
// 对每个桶内部元素排序,这里使用了插入排序
InsertionIntSort.sort(bucket);
// 遍历每个桶中的元素,赋值给原始数组
for (int value : bucket) {
arr[arrIndex++] = value;
}
} } /**
* 自动扩容1位,并保存数据
*
* @param arr
* @param value
*/
public static int[] arrAppend(int[] arr, int value) {
// 扩容1
arr = Arrays.copyOf(arr, arr.length + 1);
// 最后一位赋值
arr[arr.length - 1] = value;
return arr;
} public static void main(String[] args) {
// 桶排序
BucketSort.sort(a);
// 查看排序结果
show(a);
} }
分析:
时间复杂度:1.桶数量足够大时,一次分配完事,不需要桶内部排序O(n)。2.桶数量只有一个,都在一个桶内部排序,排序算法最差是O(n^2)
空间复杂度:使用了N个桶,桶初始容量固定。每个桶容量自动扩容,所以空间没浪费,但是有可能存在空桶。假设空桶有k个:O(n+k)
2.10 基数排序
思想:
1.求最高位
2.从低位开始排序,一直到高位。每一次排序都是稳定的。
动态图:
源码:
package study.algorithm.sorting.intSort; import study.algorithm.sorting.base.MyIntSort; /**
* @Description 基数排序:从低位到高位,一遍一遍排序。
* @author denny
* @date 2019/7/10 上午11:40
*/
public class RadixIntSort extends MyIntSort { public static void sort(int[] arr) {
// 求最高位
int maxDigit = getMaxDigit(arr);
// 基数排序
radixSort(arr,maxDigit); } /**
* 基数排序核心方法
* @param arr
* @param maxDigit
*/
private static void radixSort(int[] arr, int maxDigit) {
// 取模数
int mod = 10;
// 除数
int dev = 1;
// 依次是:个位有序、十位有序(十位相同的个位有序)、百位有序(百位相同的个位、十位先后有序)...
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0~(mod-1)]对应负数,[mod~(2mod-1)]对应正数 (bucket + 10),int[行][列] 行=桶 ,列=元素
int[][] buckets = new int[mod * 2][0];
// 遍历数组arr
for (int j = 0; j < arr.length; j++) {
// 求arr[j]在counter中的index,这里+mod,就是为了处理负数,例如mod=10,-1+10=9,在负数里面是最大的。
int index = ((arr[j] % mod) / dev) + mod;
// 把arr[j]追加进桶buckets[index]
buckets[index] = arrAppend(buckets[index], arr[j]);
}
int pos = 0;
// 遍历所有桶
for (int[] bucket : buckets) {
// 每个桶元素赋值给arr
for (int value : bucket) {
arr[pos++] = value;
}
}
}
} /**
* 求最高位数
* @param arr
* @return
*/
protected static int getMaxDigit(int[] arr) {
// 最大值
int maxValue = getMaxValue(arr);
// 求长
return getNumLenght(maxValue);
} /**
* 求数长度
* @param num
* @return
*/
protected static int getNumLenght(long num) {
if (num == 0) {
return 1;
}
int lenght = 0;
for (long temp = num; temp != 0; temp /= 10) {
lenght++;
}
return lenght;
} public static void main(String[] args) {
int[] a = new int[] {321,60,1,-1,21,577,-123,11,10,743,127};
RadixIntSort.sort(a);
show(a);
}
}
分析:
时间复杂度:假设d是最高位,每一位排序都需要O(n),赋值给数组要O(n),所以总=O(d*2n),当然一般来说d<<n,所以整体来说也是O(n)线性阶。
空间复杂度:1.每次遍历都构造了一个int[k][0]的二维数组,k代表桶的个数 2.每个元素都会全部插入桶中,即O(n), 所以空间复杂度=O(n+k)。
三、总结
要想吃透这些算法,特别是时间空间的复杂度。这些算法很多都有改进版,但是是否真实有效,是否值得改进,又是另一说(例如现代计算机的cache命中机制、算法优化后难以理解且提升可能也只是适合特定的特征入参)。看完了算法,有一个例子可以来看看前辈们是如何使用算法的:jdk源码Arrays.sort(),后续会写一篇。总体就一句话,没有最优的算法,只有最适合特定场景的算法。 最后一张表来总结下:
各种常用排序算法 |
|||||||
类别 |
排序方法 |
时间复杂度 |
空间复杂度 |
稳定性 |
特点 |
||
最好 |
平均 |
最坏 |
辅助存储 |
||||
插入 排序 |
直接插入 |
O(N) |
O(N2) |
O(N2) |
O(1) |
稳定 |
|
希尔排序 |
O(N) |
O(Ns) |
O(N2) |
O(1) |
不稳定 |
希尔排序的平均时间复杂度并未证明,和递增序列有关 |
|
选择 排序 |
直接选择 |
O(N) |
O(N2) |
O(N2) |
O(1) |
不稳定 |
|
堆排序 |
O(N*log2N) |
O(N*log2N) |
O(N*log2N) |
O(1) |
不稳定 |
实际效果并不如快排好!!! |
|
交换 排序 |
冒泡排序 |
O(N) |
O(N2) |
O(N2) |
O(1) |
稳定 |
1、冒泡排序是一种用时间换空间的排序方法,n小时好 |
快速排序 |
O(N*log2N) |
O(N*log2N) |
O(N2) |
O(log2n)~O(n) |
不稳定 |
1、n大时好,快速排序比较占用内存,内存随n的增大而增大,但却是效率高不稳定的排序算法。 比较排序中实际最快的排序了,原因: 1.内循环中指令很少,且能利用缓存(具有更好的引用局部性:下一个要访问的对象通常与您刚刚查看的对象在内存中很接近。) 2.有优化方案:小数组切换为插入排序效果更好;大量重复元素时使用三向排序可将排序时间从现行对数级别降低到线性级别。 |
|
归并排序 |
O(N*log2N) |
O(N*log2N) |
O(N*log2N) |
O(n) |
稳定 |
1、n大时好,归并比较占用内存,内存随n的增大而增大,但却是效率高且稳定的排序算法。 |
|
基数排序 |
O(2n) |
O(d*2n) |
O(d*2n) |
O(n+k) |
稳定 |
当d=1,即都是个位数,最快O(2n) |
|
计数排序 |
O(n+k) |
O(n+k) |
O(n+k) |
O(k) |
稳定 |
k=max-min+1 |
|
桶排序 |
O(n) |
O(n+k) |
O(n^2) |
O(n+k) |
稳定 |
时间复杂度: 1.桶数量足够大时,一次分配完事,不需要桶内部排序O(n)。 2.桶数量只有一个,都在一个桶内部排序,排序算法最差是O(n^2) |
=============
《算法第四版》
十大基础排序算法[java源码+动静双图解析+性能分析]的更多相关文章
- 6种基础排序算法java源码+图文解析[面试宝典]
一.概述 作为一个合格的程序员,算法是必备技能,特此总结6大基础算法.java版强烈推荐<算法第四版>非常适合入手,所有算法网上可以找到源码下载. PS:本文讲解算法分三步:1.思想2.图 ...
- 十大经典排序算法(java实现、配图解,附源码)
前言: 本文章主要是讲解我个人在学习Java开发环境的排序算法时做的一些准备,以及个人的心得体会,汇集成本篇文章,作为自己对排序算法理解的总结与笔记. 内容主要是关于十大经典排序算法的简介.原理.动静 ...
- 十大经典排序算法最强总结(含Java、Python码实现)
引言 所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作.排序算法,就是如何使得记录按照要求排列的方法.排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面 ...
- 一文搞定十大经典排序算法(Java实现)
本文总结十大经典排序算法及变形,并提供Java实现. 参考文章: 十大经典排序算法总结(Java语言实现) 快速排序算法—左右指针法,挖坑法,前后指针法,递归和非递归 快速排序及优化(三路划分等) 一 ...
- 十大经典排序算法最强总结(含JAVA代码实现)(转)
十大经典排序算法最强总结(含JAVA代码实现) 最近几天在研究排序算法,看了很多博客,发现网上有的文章中对排序算法解释的并不是很透彻,而且有很多代码都是错误的,例如有的文章中在“桶排序”算法中对每 ...
- python基础__十大经典排序算法
用Python实现十大经典排序算法! 排序算法是<数据结构与算法>中最基本的算法之一.排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大, ...
- 十大经典排序算法(Javascript实现)
前言 总括: 本文结合动图详细讲述了十大经典排序算法用Javascript实现的过程. 原文博客地址:十大经典排序算法 公众号:「菜鸟学前端」,回复「666」,获取一揽子前端技术书籍 人生有情泪沾衣, ...
- 十大经典排序算法的 JavaScript 实现
计算机领域的都多少掌握一点算法知识,其中排序算法是<数据结构与算法>中最基本的算法之一.排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大 ...
- 十大经典排序【Java实现,手工作坊式】
终于把排序这个硬骨头,但是又很基础的知识点,自己手撕了一遍!之前,使用Python看着算法导论的书手撕过一遍,印象不是很深刻,容易忘记!好记性不如烂笔头!多自己思考解决问题 1,交换类CAS[最简单] ...
随机推荐
- touch.js - 移动设备上的手势识别与事件库
Touch.js 是移动设备上的手势识别与事件库, 由百度云Clouda团队维护,也是在百度内部广泛使用的开发工具.Touch.js手势库专为移动设备设计.Touch.js对于网页设计师来说,是一款不 ...
- CSS选择器[attribute | = value] 和 [attribute ^ = value]的区别
前言 首先你需要知道[attribute | = value] 和 [attribute ^ = value] 分别是什么? ①:[attribute | = value] ②:[attribute ...
- java架构之路-(tomcat网络模型)简单聊聊tomcat(一)
tomcat使我们熟知的也是我们使用最多的web服务器了,至少我是使用最多的.常见的web服务器还有Apache,web logic,JBOSS等,对于tomcat的安装我就不再赘述了,简单的不能再简 ...
- C#中窗口关闭时没有取消事件订阅导致事件重复执行的解决方法
场景 C#中委托与事件的使用-以Winform中跨窗体传值为例: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/100150700 ...
- Winform 窗体皮肤美化_IrisSkin
1 先把IrisSkin2.dll文件添加到当前项目引用(解决方案资源管理器->当前项目->引用->右键->添加引用,找到IrisSkin2.dll文件.....之后就不用我说 ...
- BCP 运行错误
记录下使用bcp导出csv文件时的错误: [Microsoft][ODBC SQL Server Driver][SQL Server]对象名 '***'无效问题的解决方案器 我们要对student数 ...
- jQuery常用API之jQuery选择器
3.jQuery常用API 3.1 jQuery选择器 3.1.1 jQuery基础选择器 原生JS获取元素的方式很多.很杂,而且兼容性情况不一致,因此jQuery给我做了封装,是获取元素统一了标准 ...
- ANDROID培训准备资料之四大组件的简单介绍
Android四大组件是一个android app 最基本的组成部分,这篇博客主要给大家简单的介绍一下四种组件 (1)Activities (2)Services (3)BroadcastReceiv ...
- Java 打印HelloKitty
Java第一课 如何在控制台打印出"Hello Kitty" 如图所示,在IDE中使用 System.out.println(); 语句来实现打印 最后附上AIDE下载链接: Ja ...
- [b0018] python 归纳 (四)_运算符重载
# -*- coding: UTF-8 -*- """ 测试运算符重载 加法 总结: python 运算符表达式其实都是调用 类中方法 __xxx__ + <--- ...