8 o. y# A5 _/ v' `: Q一、冒泡排序. g& g& @6 A" p% _
思路:每一轮排序使得当前无序的部分的最大值冒到当前无序数组的末尾,下一轮中无序部分的长度等于上一轮减一,原数组的末端不断的变成有序的部分。3 u4 u( c, E2 f+ J+ A9 `' V2 ~
1 k; ?* ^5 S/ \6 o' D4 d
void bubble_sort(vector<int> &nums)5 t! l) v% S2 O+ C- o5 q1 T4 y x/ _* x
{ 1 c3 B1 n' U+ h) F. ]( v // 用于标记当前轮是否有交换发生 * i) S# j/ o- Q- \6 N; V4 N bool swaped; & K3 b5 j7 r" [3 O( d0 u' D$ Z+ { // 从第一个数到倒数第二个数(n个数需要冒泡n-1次)8 T7 z D7 i7 R; Z& j. Z0 V
for(int i = 0; i < nums.size() - 1; ++i)/ |9 F' {! j v! W+ L! x
{ / f& `0 S0 g' m5 \! J) H W swaped = false;1 T7 y; l1 \7 v2 F+ u' u
// 因为是当前的和后面的比,所以结束条件是一直到冒泡完毕的元素的前一个 + u. ?0 q9 T1 N' q# d* U3 k for (int j = 0; j < nums.size()-1-i; ++j)% N7 Z% h3 u% V
{ , }& O' l0 c) b5 d // 通过交换两个元素,使得最大的冒到数组的末尾去,并且标记交换过 # L9 L }; U' h6 P0 s% H \* n // 若要从大到小排序,只需要把这个 '>' 改成 '<' 即可; b5 h/ W: l7 ]( E+ O
if (nums[j] > nums[j+1]) 4 E* p# W4 v; F c) [. X$ n { Y! E& U- T6 Z. P
int tmp = nums[j]; 7 s, ~$ Z, w2 o0 O9 n* F- t/ B nums[j] = nums[j+1]; y- Y* F9 c- C) S9 D& J! x9 h7 C
nums[j+1] = tmp; & `1 K S8 r8 c% f7 ? swaped = true; 7 V" F5 e0 K& p' m# Z1 @2 V1 Q }1 N4 S3 K# a. Y& \! M
}' U, O8 y) r7 r! ]2 b$ J
// 若该轮没有交换发生,说明数组已经有序,跳出循环 - `3 m: {$ h! { if (!swaped) $ [* y! Z! o7 m- O. }0 i2 C break; 0 r+ n/ I- _* m2 w9 f* h5 T5 \ }) F7 H9 r- U; o1 V& O! V! R
}' q- M2 M! X( g. p) ~0 ^" R
% k! Q7 O% x+ m2 c
分析:时间复杂度最坏和平均情况是O(),最好的情况是已经有序所以是O(n),空间复杂度O(1),且因为两个相同的元素是不会交换位置的,因此是一种稳定的排序。6 `3 ] p2 l# f. r
, ~ Y% n n G: o
二、选择排序 1 c$ a! ?. ~% r' M4 O# w思路:每一轮都将未排序数组中最小的/最大的选择出来,然后与未排序数组的第一个交换。接着未排序数组的长度就应该减一,然后继续做这个操作,直到未排序数组的长度为1/ f+ ?% v( z, P9 J9 g7 b1 F
! K( e3 @) E" v4 q3 _* E! Z
void select_sort(vector<int> &nums) : p# q: L* h) `$ j% S) Q{ 3 X& S+ X5 P3 F$ @& V9 ^( u
// 用于存储每一轮中最小的数的下标 s* A7 T9 M! Y0 j- p C5 F0 K int min_idx; ! |& p+ q9 w0 k8 K // 循环从第一个数开始到倒数第二个数 6 M8 G v& \! p for(int i = 0; i < nums.size() - 1; ++i)5 G0 D) e/ X4 i: I
{( C, T u6 X; D# i* z3 _4 K
min_idx = i;/ j5 y* V1 V& t R4 G5 D
// 在未排序部分寻找最小的数 ! q9 F, w6 l- t$ t for (int j = i+1; j < nums.size(); ++j) 6 ^5 p9 w! s& P8 @* T, ], } { 7 @9 \- a9 {/ a( o4 U$ A // 这里将 '<' 换成 '>' 就可以从大到小排序- R$ I, m5 \! ^2 s6 @
if (nums[j] < nums[min_idx])( I/ f( E6 p$ c) L& T4 n
min_idx = j;; k. E( N7 E u
} ~& A3 z! O7 f1 [: ^+ b0 P // 将未排序部分最小的数与未排序部分的第一数交换位置& C& Y* X' K* M7 e* [. ]6 D+ `/ Y) x
int tmp = nums[min_idx];7 _2 R* R& G5 X* s, [. }. f8 l
nums[min_idx] = nums;# R; e6 U! v- R
nums = tmp;- a3 x9 k& ?6 O" d4 U& z, m
}5 _" W+ ~( X# {6 J3 R% I5 R) w8 i# r
} ) ~. G0 ^/ ~4 d7 u* H! V" n ( w$ j8 K, J# b H分析:时间复杂度最坏、最好和平均情况都是O(),空间复杂度O(1)。因为每次选择最小元素要与未排序部分的第一个元素进行交换,因此可能会出现两个相同元素相对位置发生改变的情况,因此者是一种不稳定的排序。! A* \# p6 I7 a+ @, K9 x" F
" L8 E% S5 f/ {7 }9 l三、插入排序) F8 J+ n% g- {( Q
思路:从第二个元素作为当前位置,往前看,判断前面的元素是否大于当前位置,是的话就把前面的元素往后移动一个,依次类推,直到到了数组头或者碰到一个元素比当前元素小则循环终止,此时的位置就应该是当前位置元素的排序合适的位置。接着向后移动,把第三个元素作为当前位置,一直到最后一个元素。 ( Y2 b" w/ L* O( u3 g' h& P! C; o 6 T* `% T7 U: F$ U6 [ [void insert_sort(vector<int> &nums)$ g3 v: [3 r5 C! ?, J
{7 U9 T# j1 @0 N% X$ R
// 从第二个元素到最后一个元素 $ P1 }5 _- V1 s4 S) S: q for(int i = 1; i < nums.size(); ++i)6 Z; L8 i; Q7 }" ~" D
{ 5 l C* L. o' v) I; e n9 n // tmp存下当前元素的值,将i赋给j1 I& ]- V8 F" N7 d! y3 ~( |% L
int tmp = nums; S1 I# D1 k. Y4 X7 O8 n int j = i; ) g* D8 M6 ?) ~' S C2 n // j从当前元素往前找,一直找到第二个元素位置 - y1 h+ W2 y( Q+ _ for (; j > 0; j--) [4 { K& p: h0 h {) }8 j8 C/ x( M7 F6 U
// 判断tmp是否小于j-1位置的元素,是的话j-1的元素往后移动 0 ~0 b% r/ q2 { // 要从大到小排序,只需要把 '<' 改为 '>' 即可+ D& R6 n* y# _8 _0 y; `$ }) d
if (tmp < nums[j-1]) * V9 T. |3 y d0 U8 q @! f: | @ nums[j] = nums[j-1]; ; B: \8 G9 U& h! R; W2 r // 否的话,j位置就应该是tmp的最终合适的位置+ u$ F! R" I* s3 |! W6 z/ S+ X* y
else' `3 |+ S% E( M7 [' o# T6 G! k
break; " q' r9 |4 j0 t2 L* K" u! w }+ b. f5 ~) t( g3 H& u. p+ C
// 将tmp赋给j位置的元素 ' W' N% ?; f% F; H, u nums[j] = tmp;4 v l. }2 q, p: W4 f# ?/ F$ }
} ! ^0 ^# O: g% n, O& c} 0 z9 O3 \7 R1 r& I' C5 ^0 j( b/ v( u* S0 o- N: o% o( l
分析: 时间复杂度最坏和平均情况都是O(),最好的情况是O(n)(最好情况就是已经有序,因此内层循环只需要判断一次就可以break),空间复杂度O(1)。因为是依次往前找的,元素移动也是相邻位置的移动,因此这个方法是稳定的。! n* `1 o- T0 r$ [# M
" p+ T0 w2 Z2 K- Y1 D/ n( K2 X" Q四、希尔排序 + _+ \) h# |- b1 E% r ?思路:希尔排序是插入排序的一种扩展,它不是一个的找然后插入。而是将与原数组根据间隔分成不同的组,比如十个数{1,2,3,4,5,6,7,8,9,10}可以设置gap为2,这样就分成了{1, 3, 5, 7, 9}和{2, 4, 6, 8, 10}两个组,分的组数就等于gap的数量,每组的包含的元素数量就等于数组的长度除以gap。通过这样操作,再最后gap为1的时候,数组就是基本有序的了,因此这时只需要很少的操作就可以将整个数组变为有序的。希尔排序gap的设置很关键,设置一组好的gap序列往往可以很大程度上提高希尔排序的效率 % [ `. a( y, W8 ~ ^% K' i 0 \* j% _0 J) u1 hvoid shell_sort(vector<int> &nums): p% [$ f! p' ?% `" r: Y
{ 0 a1 Y. r0 u) d# T- P3 r // 初始将gap设置为数组长度的一半,每次gap等于上次gap的一半,一直循环一直到gap等于1 $ G, f0 ]5 y. J& h5 z for(int gap = nums.size()/2; gap > 0; gap/=2) & o: e: z. e D { + m5 S% W8 I0 j* \0 Q7 M // 每一组的元素数量 * X9 U& n' _1 c; w, o# K: v- J2 b int ele_sz = nums.size() / gap; 2 j8 `7 r1 b W {
// 遍历每一组 % { k4 `4 X, d0 v n6 j- b! L for (int i = 0; i < gap; ++i) : b' V0 @# ?! ~+ J/ v {/ k4 F; O% X0 a% F' E+ N4 a
// 对每一个组进行插入排序,下面的代码和插入排序那基本一样 4 d; ~& z" _- r. M for (int j = 1; j < ele_sz; ++j). N( v2 f. B* U' |; v, ~* q
{7 _) Q$ f( d# A9 k
int tmp = nums[j*gap];% b1 g0 t8 Q) ]9 v+ _
int k = j; 8 |0 K2 d3 b0 ^& X for (; k > 0; --k) 1 f4 g; o) U. r' v { ' W; J4 k' C$ ~5 g // 注意这里的下标要符合希尔排序间隔\ / d+ m8 b9 M! i // 要从大到小排序,只需要把 '<' 改为 '>' 即可4 a4 d8 T6 V6 G7 F2 V4 Z& `- c
if (tmp < nums[(k-1)*gap]), |* t2 N- ~0 i, U9 [, W
nums[k*gap] = nums[(k-1)*gap];5 Z- I1 ]8 Y* I5 K" l
else% L8 E, p" A* R$ ?5 q5 _
break;4 J4 i3 ^& Z' c8 O; h5 L
}3 V' i8 x( m* q& R
// 找到了最终插入的位置 * {" M) D8 T n' ]1 U nums[k*gap] = tmp;1 j" N; m" B1 U; x6 w
}+ G6 s# ~, Z1 x# ]$ A6 |
}& K1 d+ H! \( F9 Y' w' }: H8 c
// 因为每次gap等于上次一半,当gap为1的时候就陷入死循环,所以这里设置一个判断用于跳出 * W* P! I9 m4 m5 K; ? if (gap == 1)# C( e) Z+ o. D; X
break; ) s9 O/ M- K6 Y1 h } & T( ?; q6 t) n/ R6 r} . d( S. t' \1 c7 k. y) d # U% Y% j' i1 h+ C9 H分析:希尔排序最好情况的时间复杂度为O(nlogn),而最坏情况和平均情况要根据它的增量序列来确定,好的增量序列可以把最坏情况的时间复杂度控制为O()。希尔排序是原地排序因此空间复杂度为O(1)。由于增量序列的存在,使得比较的元素存在间隔,因此就可能导致相同元素的相对位置发生改变,因此希尔排序是不稳定的。 3 M9 b2 H! C# Y6 p E2 e( i& q. b % v' E2 @7 d# h) [- P- s五、归并排序2 V" [0 ]; d9 @
思路:归并排序是分治法的典型应用之一。首先先把大的数组拆分成两半,再继续拆,直到只剩一个数位置,拆完之后要合并,合并的时候就需要对两边的数进行排序,合并完成的时候就变成了有序的数组。以此类推,对左边的已经有序和右边已经有序的数组进行合并,最终得到了整体有序的数组。 # L L L: @$ p3 B# d0 Y0 Q: m $ T7 ?- t* s: ?+ I! a8 B# H// 合的过程! y2 e+ X/ M! Y9 z/ I
vector<int> merge_vec(vector<int> ivec1, vector<int> ivec2)) T; x& H9 D& v- Z0 J
{/ X5 X5 Y" Q- H7 Y. v
// 定义结果数组res, i和j分别用于遍历ivec1和ivec2: T# L( g* U) ^# W! P* D
vector<int> res;: [4 l; Q5 V; |
int i = 0, j = 0; % \ b" j/ O( _" F( h. n# H/ C // 当两个数组都没遍历到底 ) K7 F* k7 u% u- b, E) A! u while(i < ivec1.size() && j < ivec2.size())# `: l# u% r1 h. _; F
{ 8 m8 Z3 g3 }' I6 I$ `4 h // 谁的当前数字小,谁就加入结果数组,并且下标后移 0 Q& M) E3 M/ `2 n, O // 这里将 '<' 改为 '>' 就可以从大到小排序' G9 `' p0 `/ y* t3 C# V& @& T
if (ivec1 < ivec2[j]) / G2 J6 N7 H- o res.push_back(ivec1[i++]);: W5 O0 x& g. h: o' N- u! I
else0 B, o+ z, |+ \. w' O1 k3 ~
res.push_back(ivec2[j++]);& h5 |# c9 p' b" T6 w/ o! P1 ~- I4 ~
} - S3 s! p% \. _8 Y6 N // 处理未遍历完的数组,直接全部加入最后面: ]4 P. F) y0 M3 B- h/ M# G2 P" O9 }
while (j < ivec2.size()): Q+ T- C0 Q( r9 Z6 ? A, J
res.push_back(ivec2[j++]);0 M* Q* ?. N$ R4 e0 ^, @0 M- {
while (i < ivec1.size()); `; o; q+ y* D; o! A* Y$ L; ^
res.push_back(ivec1[i++]); & n ~2 @3 D1 w% \7 V3 U7 M3 A$ Y9 @ // 返回合并好的数组 " v q6 n- B% u+ z! p9 ~1 c- w return res; ; x& ~! V1 ]& @) V} n. c& @& h* U7 a& @4 t - [/ g' T7 Q& E# a# I% C& `( `// 分的过程 2 e. m" N% E! k- ^; M" d Z2 jvector<int> merge_sort(vector<int> nums)( C; `8 `1 f% k: b3 j
{ 4 I* Y" Z$ U" f& `8 n // 递归结束条件是只剩一个元素,则无序再分,直接返回 , }% p+ L" P, K if (nums.size() == 1) 4 l3 z; l( R n return nums;( J0 {, ^: T5 E5 _5 ~7 g
$ |/ g" C/ U& c+ l% J // 将大数组分治为左边的和右边的 7 U( J5 T9 m, V4 {, B vector<int> ivec1 = merge_sort(vector<int> (nums.begin(), nums.begin()+nums.size()/2));2 d; _9 y. v% a2 R+ h
vector<int> ivec2 = merge_sort(vector<int> (nums.begin()+nums.size()/2, nums.end()));4 C6 L3 b: F' ^
// 返回合并后的大数组 ( V r, o6 d. U return merge_vec(ivec1, ivec2);- \9 o" ~8 S8 G0 @8 E
}% ~8 x; T8 L* ?# ]2 h8 `
0 a& C) r9 L, l9 m2 W
分析:拆的过程时间复杂度是O(logn),合的过程时间复杂度是O(n),因此总的最好、最坏和平均时间复杂度是O(nlogn)。这种排序方法并非是原地排序,所以其空间复杂度是O(n)。这种方法并不会破坏相等元素之间的相对位置因此这是一种稳定的排序算法。 ' |6 _$ v+ {" M& d 1 g1 Y0 c: D2 }6 b六、快速排序- z1 O+ h" k' d
思路:快速排序同样也是利用分治法的思想,但是快速排序可以实现原地排序因此无序额外的空间。快速排序和归并排序的分的过程不同,快速排序分是根据小于和大于某个元素来分成两部分,而归并排序总是以中间的地方来划分。 & g5 B3 A8 r! J$ ~8 T5 j6 j& b" ~
注意:算法中每一轮quicksort会确定一个元素的最终位置,我们递归处理这个元素左边的剩余数组和右边的剩余数组。lp和rp在走的时候,需要注意的只要rp指向的元素大于等于pivot就一直往左,lp指向的元素小于等于pivot就一直往右,若不加入等于这个判断条件可能就会在中途停下来而陷入死循环。而且pivot的选取也很关键,我们是以pivot大小来划分左右的,常见的方法可以以最左边或者最右边的元素作为pivot(但是这样在已经有序的数组中效率很差),或者随机选择pivot。 : D4 x2 e6 p6 `8 o s/ [. o+ l4 j& X6 z# K! Z- M
void quicksort(vector<int> &nums, vector<int>::iterator lp, vector<int>::iterator rp)5 N+ k# U- d; v0 N1 u
{ % [' A/ E( `7 {+ _5 L/ n. w/ M // 若左右相等,说明只有一个元素,就不需要排序6 }5 v+ H& U2 Y# o
if (rp - lp > 0)% X* K3 d$ o+ p. a$ g& A( \
{ 0 t# U8 S3 |" e$ K. v* T" E6 P // old和orp存取起始位置和结束为止; N; J4 y) j0 l$ x% s
auto olp = lp, orp = rp;) S; l, E+ z; F( R! R
0 |4 ?: G# ?7 w8 s. p0 Q
// 下面两行代码可以要也可以去掉,其作用是随机选定一个范围在lp和rp之间的数作为pivot 3 _1 Q9 p/ r8 P" l0 A% R* v1 p // 将这个数与最左边的数调换位置就可以通过*lp得到这个数 - Z2 I/ Y* c. C! X0 b/ g int random = rand() % (rp - lp + 1);% V8 X) \# e. g/ x( z2 B6 R2 c
swap(*lp, *(lp + random)); + P* p# s9 m( r _! t: q! z8 V7 Q3 i$ h
int pivot = *lp;; `, |6 q1 l2 F! T2 D0 R6 k
2 O- m9 ~# m V/ L1 o1 P
// 当lp和rp没遇到的时候 8 h3 Q5 h& T* d0 [6 ~7 b/ Y& _ while (lp != rp) ' z( ?' [1 H: B6 u3 W {* _5 x- j, t: Y _) `0 D/ t
// 只要rp指向的元素大于等于pivot而且lp和rp没遇到,rp自减 % L' `# k* V9 b // 将这里的 '>=' 换成 '<=' 并且把下面的 '<=' 换成 '>=' 就是从大到小 ! H) Q0 t$ r9 e5 h | while (*rp >= pivot && lp != rp) 8 U3 g" H" Y+ C --rp;& h3 t) g/ m4 P2 F2 X
// 只要lp指向的元素小于等于pivot而且lp和rp没遇到,lp自增% q7 i* g6 u0 K' U
while (*lp <= pivot && lp != rp) $ N! o, F1 d+ r' d6 O4 b6 P ++lp;' N6 }$ q4 E- B# @" J( `
// 停下来的时候交换值6 W3 f7 O `$ I( D- ]+ M7 S
swap(*lp, *rp);# E' K, ]! \8 \5 G1 n0 a
} ) q3 H# Y9 K" a# [; ?, s, X: Q0 a8 g/ q/ g2 H* @
// lp遇到rp说明比pivot小的已经放在左边,比pivot大的已经放在右边了 , u0 k o; {% m; W* ]+ Z5 ^. ` // 因为while循环中rp先走,说明rp所指的元素一定是小于pivot或者正好是pivot N! D- p. p6 g% P2 q0 Y8 s6 t // 因此需要交换二者的值,同时pivot就被放在了正确的排序位置上$ D) i+ }& f. J2 b
swap(*olp, *rp);. v' ]3 d; w: s2 J
// 递归处理pivot的左边和右边 1 _8 a: ~9 C% g, ]# c quicksort(nums, olp, lp-1);" M) d; T9 C$ o) {& j2 x X+ D
quicksort(nums, rp+1, orp);/ t* j% j& ^) K9 }6 w
}: X: Z$ U4 h0 v i% s$ X1 D
} % Q9 K6 |# ~4 l W8 v $ t' J! H; ]$ c ! V; s4 T- ] q5 d- h( u Z9 ` mvoid quick_sort(vector<int> &nums) / ?/ {) j/ u8 m{. m! V% }& S' h+ ^& h7 C, \
quicksort(nums, nums.begin(), nums.end()-1); 5 X/ G8 N1 p [+ l# @}! X3 P( S2 S/ e9 B0 w
, F7 T% l6 o& w/ {3 d. C3 G( M分析:快速排序算法的时间复杂度分是O(logn),治是O(n),因此其最好和平均的复杂度是O(nlogn)。考虑一种情况,当数组有序而且我们选取pivot的方法是选最左边的或者最右边的元素时复杂度退化为O()。原地排序因此不需要额外空间,但是由于是递归函数,因此需要栈来保存状态,最好和平均情况下需要O(logn),最坏的情况下需要O(n)。由于快速排序涉及到随机位置值的交换,因此快速排序是一种不稳定的排序方法。 1 Z. @( V- E# Z/ l) F ) Z/ C8 K7 F, J# _3 `七、堆排序 ! B z+ e# r. c思路:堆排序顾名思义需要一个堆,堆是一个完全二叉树,可以分为大顶堆和小顶堆,其中大顶堆是满足子节点的值小于父节点的堆,小顶堆是满足子节点的值大于父节点的堆。建好了堆之后我们就可以通过取堆顶元素再不断调整堆的方法来得到排序结果。因为堆是完全二叉树,因此其父节点和子节点的关系容易得到,所以我们可以用数组来模拟一棵树(一个堆),下面这个视频将堆排序讲的很清晰。 7 T6 I2 Z! N" g" P' l7 N. \. \& Q/ s5 D
【从堆的定义到优先队列、堆排序】 10分钟看懂必考的数据结构——堆_哔哩哔哩_bilibili/ Q8 @) n8 p. P1 V B& R
) F# u; b. B8 o, B" A8 @( ?
建堆:建立堆的方式可以分为从下往上和从上往下。以大顶堆为例,从下往上的话,对于每个父节点,判断其子节点值是否存在大于父节点的情况,是的话就交换这父节点和子节点的值,然后继续调整子节点(若子节点也存在节点的话则继续往下调整)。如果是从上往下建堆,就相当于是堆从从空的开始,不断插入元素再进行调整,判断当前插入的选择值是否大于其父节点的值,若是的话则交换当前节点与父节点,并继续向上调整直到无需调整或者到根节点。可以判断,从下往上的方式其时间复杂度为O(n),而从上往下调整的方式其时间复杂度为O(nlogn)。同时需要注意的是,对于同一个数组,采用从下往上和从上往下的方式建立可能会导致节点的排列方式不一样,但是都满足大顶堆/小顶堆的性质。. R+ ^4 x z0 R& E