" ~* \8 @5 R' S4 W* Q4 t! N5 u1 缘起 5 f X* }! t3 Y* W+ K5 I源于对排序算法的学习,9 _: }% z+ x9 @
算法的学习,绕不过理论学习,同时,学者会自己实现算法,以加深理解, # G& e0 D' t% @( S这是一个阶段,我们所学习的知识,最终是要应用的, + v x+ z/ {; Z+ L+ D" s, ]: X比如,学习了各种排序算法后,大部分学者,要么应对考试,要么应对面试, ) r: v+ x, q7 b) g. r在实际的工作中,可能用不到自己实现的算法,或者很少用到, 6 l& R+ w$ c) |为什么? 6 B3 O2 ~% c% p' u: Q因为,我们的科学巨人,已经将其工程化,久经考验,成为工业级应用工具了,2 J7 H, @% `, l* C" A2 E) v% W
所以,我们需要进一步看看科学巨匠们是如何应用这些算法的, & ]+ b5 Z- P( B这不,从Java的数组排序开始,学习前人的优秀实践, . y, Y" Z7 N' z8 S) G" u4 n: O帮助读者,进一步了解不同的排序算法在软件工业中的应用,提高设计和应对知识考核及交流的能力。 4 |4 k8 W$ ?( u6 G / Y& P: b) F, ]; Z N( M3 ?1 A2 前置知识! h* \ h' E- ^0 o; d2 p w6 w- B _
首先,需要了解时间复杂度、排序算法以及对应的时间复杂度。 . ^/ n- j9 R# {( m# ^8 }8 `其次,分析Java源码中的排序算法,了解不同情况下的排序算法选择。 ) n! c- q$ \1 l+ d: ?" y1 W最后,为自己设计排序算法或者测试时提供参考。# Y c0 f V$ K9 Q/ M
7 B b6 [* k- y3 e& S. J# V' ]: |
2.1 时间复杂度 1 f$ x: P6 }0 ?$ g6 \- D$ g时间复杂度用于度量某个程序随输入数据量级的变化运行所消耗时间的变化趋势。 ' Q7 f! W, p0 V9 h不同时间复杂度的定量趋势(平均时间复杂度),- h6 w1 M* m/ w3 R, O# d z' l
时间复杂度函数图像如下图所示,6 V: B1 F7 @. G5 b- `- H
这里横轴表示输入的数据量(如个数),纵轴没有单位(不过,可以使毫秒,秒等),! r2 s+ @. k2 d. G: m9 O8 G6 v
不同的时间复杂度曲线间对比,纵轴可以不设单位, # K: ^# ]' D& ?* G# i9 U6 I0 o只做趋势分析。# x2 e$ Y X/ _- ]$ d; `
由图可知,在数据量不是很多的情况下,! h/ N, f+ o6 R$ M
平均时间负载趋势差别不大, / }# t; r: F) h8 P, s, \# N但是,数据量增长到一定数量级后,差距尽现,- O1 s) U2 g% X8 ~% c
大家在使用过程中用,可以实际测试一下。' R' @/ j9 H9 q- E: J# z# q& z
* c3 B1 W4 ^- y/ ]3 ~
# Q" H8 A- s' T, G9 P. z1 X
2.2 排序算法的时间复杂度 5 X5 n1 ^2 R, z序号 排序算法 平均时间复杂度 稳定性 7 e, I1 L8 E( {' ?( P. T1 冒泡排序 ; p: f& L1 g- Y) C9 m* U4 eO ( n 2 ) O(n^2)/ a- R9 F4 u H
O(n ) e( W4 y. E; E& z- S+ {
2 8 ~& b" ^6 O$ Q) b% U ) ; \5 m& Y& V2 N( d稳定 / U& R- I+ U& i% ? e4 T2 快速排序 ' Q$ \- G7 U J- [6 [
O ( n l o g n ) O(nlogn)$ o1 C' ^. V# M& C1 a
O(nlogn)" c, x4 T4 }+ P: e- [1 l
不稳定 % F4 y- P+ S/ I$ X5 `6 T. {3 直接插入排序 1 N, d8 c7 @, Q& F
O ( n 2 ) O(n^2) $ Q. N0 m ~1 F6 EO(n : J, K a( e9 f; B# N2 x4 F( s
2 1 y# t( F4 S) b! q# w8 K: Z+ x )4 i+ T1 Z- C, Y$ q* w! t
稳定( \# ?5 N5 C" I& O F% k
4 希尔排序 ) {$ G; _% h/ ?3 U
O ( n 1.3 ) O(n^{1.3})4 ], a" w. Z0 R1 O I. p
O(n 6 w- v* O0 Y$ C4 e- T1.3! M- |& f& O: o& ?
)2 z _) a* j4 V! A
不稳定' G* ^8 ?+ I- G0 ?2 n! `5 R
5 选择排序 # |* Z; z, ~5 i9 A' lO ( n 2 ) O(n^2)# v- F( |8 A. ^; J) h0 S
O(n $ E# y1 \1 `) V
2 ! Y: I v8 ?7 z2 T" r. ^. A ) , K0 C% F" `- f, I D2 g$ M# a R不稳定 - s9 _7 j0 S9 H6 堆排序 6 B9 p8 `' {6 g5 A4 g
O ( n l o g n ) O(nlogn) * Q- {8 k: n4 R9 a) L( PO(nlogn) 6 a6 l) \7 H9 E" k+ v2 e- S不稳定) r9 I# j" F7 s3 H
7 快速排序 $ [ C6 i; P6 U# o! C3 pO ( n l o g n ) O(nlogn) 6 W7 T/ L6 Y% K' h0 Q* gO(nlogn) ) h4 ]8 |. ]1 n- R6 v不稳定, I/ w2 U" W2 |) p3 [" C7 ~% v# V& [
8 归并排序 , B( B$ e% U7 T$ \! ]O ( n l o g n ) O(nlogn)1 M; P! }; }2 H) M) T( q# a7 \! @
O(nlogn) % o5 {) l- y$ n8 |8 o+ k0 q: B稳定 ) V$ O) r; j8 f* j4 A" O9 计数排序 . J$ e0 n+ U. T- ?2 |4 i% O
O ( n + k ) O(n+k) ) S4 H/ \, ~; b* _9 eO(n+k) 4 Z" o8 B$ F/ N稳定! Q' O% l) k# ^* z$ x
10 桶排序 ! I6 s6 B4 P; r/ M9 x6 R* o# }/ P$ hO ( n + k ) O(n+k)" r$ d3 q& J) N1 t7 r4 z( y/ D
O(n+k) , {/ c( `4 A) _& k/ r稳定$ Z% z) F" x' z4 X# K/ ]) Z
11 基数排序 # H3 _$ s; _; J' V0 i3 E: X0 bO ( n ∗ k ) O(n*k)& Q6 O6 V/ s( H6 s
O(n∗k) 9 _2 [5 g! s7 ?$ x' p稳定 7 b8 _! Z4 D( X3 数组排序5 b% K! A( ?' _
Java源码中的数组排序分两大类: ) o$ v) ]0 e; o4 f7 k1 t6 |(1)基础数据类型数组排序; " Y- f' a1 R5 }/ U7 x* H* d! ^(2)包装数据类型数组排序; 7 t' ?3 p0 Y# e这两类数组排序方法,' }; ?; v$ u* U1 l) b/ n, U5 x
都依据排序的数量量级选择不同的排序算法, - ^- r+ R3 u7 J1 f1 M1 O- \以达到较优的计算性能。% O6 D* b/ t7 l, r( O* s
注意:这里仅分析不同情况使用的排序算法,并没有深究是如何实现的,我打算在后续的文章中一一分析。/ P# u6 {# A8 s' N: O$ a
( ? f5 A1 V4 Q8 \3.1 基础数据类型数组排序( `' t& I4 k/ S+ b: P
基础数据类型数组排序算法: ' Q" r3 N( M) J: n依据不同的数据量选择结果如下表所示,从源码中提炼而来,( s. r& |& y$ V# _1 C/ b
通过上面的基础知识可知,数据量较少时采用插入排序,平均时间复杂度为O ( n 2 ) O(n^2)O(n 7 X7 `! \4 Z; w D) o- d) \& m2 2 S; o2 d6 x) C ),数据量较多时采用双端快速排序,平均时间复杂度为O ( n l o g n ) O(nlogn)O(nlogn),但是不稳定,当数据量再多时,采用稳定的归并排序,平均时间复杂度为O ( n l o g n ) O(nlogn)O(nlogn),至于,阈值,是设计者按照实践得出的较优方案。/ p+ o n" a/ \. S
1 A4 x& e4 b) X8 E* C& @4 }序号 数据量 排序算法 9 e3 r5 i9 d4 ^: d1 [2, 47) 插入排序' z+ D4 v7 b+ D& }, v
2 [47, 286) 双端快速排序9 V" s9 x$ D D6 H
3 [286, 无穷) 归并排序 & Y3 f' u7 H( X3 w5 C& K+ F1 ]! a; q接下来,看看Java的数组排序源码, 9 J6 l8 A! u; K首先进入数组排序入口:sort, m6 q0 q! @7 z7 k& y; h
位置:java.util.Arrays#sort(int[])' o, f5 Q2 R' |- ]
源码如下图所示, ! z) q* T& N3 B3 U3 \由源码可知,入口方法调用双轴快速类的排序算法:DualPivotQuickSort.sort,3 k5 [0 O. o7 S+ n. H# P
通过名称可知,该方法使用快速排序, 0 I2 a# i M% I5 J但是,进入该方法后,可知,不单单使用快速排序,且看下面的介绍。1 y% `% y7 u2 L C& H' K; T
( f+ ?* B6 O# k# R
* R1 H9 A2 B2 w, T3 t0 L" c" o3 N: `
好了。进入DualPivotQuickSort类的sort方法,9 R' r6 o& n& v2 u
位置:java.util.DualPivotQuicksort#sort(int[], int, int, int[], int, int) 1 N$ d8 B7 _ X2 e. E由源码可知,当排序的数据量小于286时, " P `6 Q _ x8 e% p, G使用快速排序:sort,但是,这里同样嵌入了其他排序算法," i: I0 Y( K& {! R* X
数据量大于286时使用 归并排序。 * u ~" [8 ^7 ^* o9 B6 W' p: E先进入这里的快速排序看看。 ' M! V, K( V6 a* \' m* n0 _- R% \' w5 z7 K* z- v0 E
$ [' V. H0 J6 J- D' u6 ?& N下面进入满足286以内数据的快速排序算法:sort,/ J8 M8 G0 J) I! T2 C/ b% H
位置:java.util.DualPivotQuicksort#sort(int[], int, int, boolean)( l' ]0 Z. d' f
源码如下图所示, 1 g3 }' t0 G9 h4 y+ s由源码可知,这里的快速排序方法同样分分了两层,+ a! d+ z w# `5 b3 Y" l
当数据量小于47条时,使用插入排序, ) \. w1 A0 ]/ e: `9 b+ R大于等于47,小于286时,采用双轴快速排序。( A2 H5 m2 X2 T0 B" C
3 j& R% P3 R) j$ r + v5 E% D9 k5 }7 L( @/ X$ V a; q" o
3.2 包装类型数组排序( r$ ]* c0 i0 d2 T% f- w% V1 [7 o
和上一节一样,先上结论,如下表所示,$ a$ T# a2 x) e) v) ^2 K* M
新排序算法:8 |) P. z! n* n1 V b# s
- V1 @: V$ k- l* Z序号 数据量 排序算法 ( [) V; y+ E; ?- S5 v1 [2, 32) 二分法插入排序% }$ V, n! U/ q3 {* ?
2 [32, 286) 归并排序& [! p7 a* J' o; O1 P1 Q) O! u1 W/ `
旧排序算法: 5 M) c; Z6 f6 c# u 4 |. g0 Z4 g9 Q: h, s序号 数据量 排序算法 7 @; O% x" J9 p9 k3 {1 C! E. N1 [2, 7) 插入排序 3 _' Y. h7 V$ X9 Q+ K$ T2 [7, …) 归并排序 7 t6 Q9 U, W4 H0 q) H( @/ { I包装类型数组排序使用的排序方法入口与基础数据类型数组使用不同的方法,0 G: ?- O: h% i& b
位置:java.util.Arrays#sort(java.lang.Object[])! g6 S( M4 [# a# [% r: U% U
源码如下图图示,5 m. c) u2 P# q
由源码可知,这里有两个分支,8 a. b% }4 V1 O' D/ ]% a
一个是旧的归并排序,9 B6 n/ R8 m% x/ l7 r& t" @9 A
一个是新的归并排序,默认情况使用ComparableTimSort.sort,类ComparableTimSort复刻TimSort排序思路, 4 [& U, ?. h/ m, _- J$ i) X+ x接下来进入该排序方法一探。 , W$ e/ ^) C1 m# c 4 s' x, j2 f: b; x # M9 U* v- o0 A& X+ Z3.2.1 新版归并排序1 t& N$ o: Z |. U8 V/ S
位置:java.util.ComparableTimSort#sort+ ^# F% i& p/ b! U! {3 m# U
源码如下图所示," @# N' g, y4 E8 Z; ?5 D
由源码可知,! Q- n; b& Q4 J9 a z' L, L" y f
当排序数据量不足两个时,不排序,直接返回,( [" }1 ~7 r# b! z, [9 Z6 [, @- w
排序数据量32个以内时,使用二分插入排序,平均时间复杂度O ( n 2 ) O(n^2)O(n ' Z1 ]" P8 J5 N. N% c4 ~: ?1 B& k
2 " ^ {5 w7 X/ a ),稳定, , h$ r& P K4 D' B0 h排序数据量大于等于32时,使用归并排序,平均时间复杂度O ( n l o g n ) O(nlogn)O(nlogn),稳定。 . O4 g4 {* i4 p+ B1 _8 w+ p/ j, c; j8 ] B. p
" f/ ? X/ i+ x1 ~8 P3 \+ M
3.2.2 旧版归并排序 ; w9 B/ ~+ b# N) V6 p# c: |讲完新版排序后,顺便讲一下旧版的归并排序吧。1 r5 L+ B x0 p# l" u& f9 C
由名称可知,这是归并排序,( ~8 w% @5 g- S( O, t( {9 e, h
可是,按照聪明的设计者思考,9 s) ]( k0 T; V1 J7 e
肯定嵌入了其他的逻辑,先上结果: 2 M2 K( Q7 N! F/ o2 x# {1 K8 a. @9 e6 _( i. K& [. N
序号 数据量 排序算法 g6 ^3 g. `5 C% ]7 V8 h+ H
1 [2, 7) 插入排序) k) l: P2 v* d
2 [7, …) 归并排序 : Z0 l: b! Q) X. A+ S8 i" V插入排序,5 u: j R5 {9 G$ G# F- D
位置:java.util.Arrays#mergeSort(java.lang.Object[], java.lang.Object[], int, int, int)6 b2 O0 w3 F, h) {
源码如下图所示,# Q* k$ Y- S" _* l
由源码可知, - x8 i' D8 e" A% M2 A排序数据量不足7条使用插入排序算法,平均时间复杂度O ( n 2 ) O(n^2)O(n * t7 Q, p* Y5 {$ R+ j+ l. {" e3 a2 Q7 e2 % T2 f0 V- Q# a4 B" F* b ),稳定。* V& s9 L5 w6 I4 J* `& L& {7 y
其他情况使用归并排序,平均时间复杂度O ( n l o g n ) O(nlogn)O(nlogn),稳定。, b) j$ b. ^. [1 @: m1 k' O( ]
5 M& `/ ?% h H
! [, M3 x0 Q, G$ b
归并排序源码片段如下图所示。: {1 q$ C! E. g( w