; v) o, k) b7 u8 q4 J: r8 E. D+ o, b
e、队列 S3 D$ A9 u5 H) q9 m0 g9 ^
内存结构:看用数组实现,还是链表实现; u' m& ^- f$ w1 m% t
实现难度:一般1 X: S1 A X3 r: T' E' I5 l
下标访问:不支持 0 ]# D! u/ P0 y0 B' \6 G$ j9 g" V分类:FIFO、单调队列、双端队列+ o; z- y' ?0 b9 _- D# ^
插入时间复杂度:O ( 1 ) O(1)O(1) v: v2 x* s# V w
查找时间复杂度:理论上不支持1 U& F, g4 J8 h$ f
删除时间复杂度:O ( 1 ) O(1)O(1) 9 X& f( z( h9 n9 b' x# O! o1 W3 o' y( L2 j+ z" c
0 r: Y0 W9 _6 {$ Z Ef、栈2 u7 o4 T) {& \' T$ d, C
内存结构:看用数组实现,还是链表实现/ t& c& {( `4 V0 X
实现难度:一般, t" m, ~( Q1 b" C8 a
下标访问:不支持" p! g" m) l& g" p/ A3 s5 H
分类:FILO、单调栈! }* d7 _1 w3 ^9 y$ n; L
插入时间复杂度:O ( 1 ) O(1)O(1)! A% |; C0 S+ R" V' Z3 b
查找时间复杂度:理论上不支持( O" w8 u3 G0 W+ G- ~+ M
删除时间复杂度:O ( 1 ) O(1)O(1) # [6 D8 [# D9 Z+ X' A0 Z: N& O) h* I, F1 z, }
. T6 Q" F) K2 o1 G; }g、树 }: @# o1 |) Y" Y内存结构:内存结构一般不连续,但是有时候实现的时候,为了方便,一般是物理连续,逻辑不连续 - V8 W& y# {; z/ \% N0 ]9 y2 O实现难度:较难$ t; ]* Y# _ q4 d. G
下标访问:不支持# q! m9 x+ v$ f/ s
分类:二叉树 和 多叉树 * a* n( U# [1 T& e插入时间复杂度:看情况而定 5 q0 M4 n9 O2 D- N查找时间复杂度:理论上 O ( l o g 2 n ) O(log_2n)O(log " Y9 `3 `) }+ G9 J$ v% D2- Y1 S8 x+ s u3 ]/ [
% v8 } z% B8 |9 a n) $ b. Z( l9 B3 s" J/ }删除时间复杂度:看情况而定7 B3 j& M8 v5 W' o
' l* ]1 O0 S: O' X. `7 `) a' x
1、二叉树9 t( o& C! N7 R; Y6 C9 \; U
二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。/ v ?( L, a8 K& z% x8 I G
其中,堆也是一种二叉树,也就是我们常说的优先队列。7 H; y6 f( x! E9 ]2 M
2、多叉树" m; Y/ N. Q) G% Z2 |6 ]% }+ i
B树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。4 r4 w h. d* N' B
h、图 , ]. X2 H2 E2 I. n! R3 ]8 i内存结构:不一定 ) q( u7 y- f' ~实现难度:难 $ J0 E* O, {- V/ K) P下标访问:不支持% n5 |# O0 i4 a& K+ c1 g) |& V
分类:有向图、无向图 - j% p( K2 I$ I& v. s/ p插入时间复杂度:根据算法而定 - q( k) N$ |' G. p$ @2 u5 D' P) z5 m$ y4 o查找时间复杂度:根据算法而定 + q; p3 w; |# f* V+ ?. @删除时间复杂度:根据算法而定) a7 v) H% @6 F( Y% S
( u: `8 Y/ F. A5 ]$ ^
# O8 x* o" N5 v: c( |
1、图的概念 % m4 D+ {& Y8 r4 g在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下: 5 l# I6 H; d, q( }9 [图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。0 M' Q- p, p( M
对于无权图,边由二元组 ( u , v ) (u,v)(u,v) 表示,其中 u , v ∈ V u, v \in Vu,v∈V。对于带权图,边由三元组 ( u , v , w ) (u,v, w)(u,v,w) 表示,其中 u , v ∈ V u, v \in Vu,v∈V,w ww 为权值,可以是任意类型。 9 F1 B1 T( A7 z3 A# b$ U图分为有向图和无向图,对于有向图, ( u , v ) (u, v)(u,v) 表示的是 从顶点 u uu 到 顶点 v vv 的边,即 u → v u \to vu→v;对于无向图,( u , v ) (u, v)(u,v) 可以理解成两条边,一条是 从顶点 u uu 到 顶点 v vv 的边,即 u → v u \to vu→v,另一条是从顶点 v vv 到 顶点 u uu 的边,即 v → u v \to uv→u;5 K- m( X H- z3 e
2、图的存储- j; b9 N8 G/ |+ ^& r) H/ y- }2 L
对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。 & v+ k5 T8 L- E2 \# a7 R R7 r# J9 f: o6 y& X4 [
' \0 F" P; {; {" t: j7 ?- b2 I* k: ^1)邻接矩阵 % b/ b8 P" R- X9 D( I邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。 ' {& f4 t& F: A! l它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。+ k5 k6 ^! c! Q( S, a7 G3 @
[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[ 3 e5 v9 j* K' _8 o. P+ x/ G! a) i01∞9∞0∞8320∞∞∞30 . P F, V: ~$ G/ W0∞3∞102∞∞∞0398∞0 : r! E. ]( E2 P1 F Y7 S% e\right] 1 q$ X- N5 W& n/ k. ? c4 h⎣4 y' ?/ F0 _+ |- [; a
⎢( ?, N& I# I( z Y( M* x
⎢ / j4 d0 r0 A3 v" W' `% J ]9 [⎡1 @+ Q( T9 d+ k t
' F1 m2 H9 D" y: ~
; f. f& } s, ]) e) ^- a⎦, B A. N1 W( V8 l
⎥ 1 i/ A) q, c; \) v⎥ ( X( I( z5 i) O2 V* _; @6 [⎤& Y( I4 m) Y- L
: D5 `7 C' ~& `$ O
! G8 t0 A$ P7 `4 U0 ?- B8 W' Q! E
2)邻接表; c' m D( N" U- y6 |+ `: E6 u
邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。 ! {' }1 T) H6 W# E它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。 ; b5 B& j+ z R& ?! B如图所示,d a t a datadata 即 ( v , w ) (v, w)(v,w) 二元组,代表和对应顶点 u uu 直接相连的顶点数据,w ww 代表 u → v u \to vu→v 的边权,n e x t nextnext 是一个指针,指向下一个 ( v , w ) (v, w)(v,w) 二元组。 " T3 v: ^3 I* N+ o; H$ t s) s3 ]2 u0 t9 v( l 9 x! {: \: v Q5 Z7 x在 C++ 中,还可以使用 vector 这个容器来代替链表的功能; / B W# O4 y6 @5 C p: M vector<Edge> edges[maxn];) ?- i- W M5 w" r/ h+ u% N/ g9 i1 v
1- q6 b$ k. u* a$ F; ^$ Y/ y
3)前向星 2 ?5 z6 S7 x6 p' E6 E/ v前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。 1 n4 U, }3 H/ Q: _它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。% r9 U* \% x' Y+ M5 ~9 X
如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。, J1 \% K3 G6 @0 F7 c
+ t3 ^& Q: Y- {7 s; O & _. F7 Q! F9 r/ q$ v1 m1 a: t那么用哪种数据结构才能满足所有图的需求呢?9 |) h( w% F; n
接下来介绍一种新的数据结构 —— 链式前向星。 & j4 t# F$ D" h# R' m2 X; G4)链式前向星 % G& R1 Y! B! S7 w8 F* w1 P5 l链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 i ii 都有一个链表,链表的所有数据是从 i ii 出发的所有边的集合(对比邻接表存的是顶点集合),边的表示为一个四元组 ( u , v , w , n e x t ) (u, v, w, next)(u,v,w,next),其中 ( u , v ) (u, v)(u,v) 代表该条边的有向顶点对 u → v u \to vu→v,w ww 代表边上的权值,n e x t nextnext 指向下一条边。" x' l% l9 _, C
具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。8 ` f5 a F& e" s
边的结构体声明如下: * {2 Q: l7 @ f7 g2 u8 P9 Pstruct Edge {" b$ a! W& B4 j6 z+ I
int u, v, w, next; $ H y1 j B$ d* t' m1 B Edge() {}1 K' }7 c, H& U% S& N
Edge(int _u, int _v, int _w, int _next) :1 _0 [0 _& b0 m* Q; ~0 Q
u(_u), v(_v), w(_w), next(_next) & y s: Q9 i6 U+ K
{ . H3 T8 B" f, {$ H }- [ u$ f+ w2 A' a7 ^6 _( k
}edge[maxm]; + Q0 E3 ]6 n7 R* b5 F# H6 E1' N* G7 `; y( \' O1 A& R
2- a" k% g% _9 d. W0 @) }% I' m
3 : N) {" Q: t; ]7 ]1 k45 O2 b! V" U9 o- S% U3 Q
5: ~6 Q( ?4 @! A3 X
60 e1 W- a8 l( I, Z
72 l! W4 D* d- b* T
87 O4 e7 @$ f2 e$ a R1 K+ A
初始化所有的head = -1,当前边总数 edgeCount = 0;; E0 P, s( x4 z0 S) p0 _- L" W
每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下:/ R$ A& j$ d! n4 J7 F9 b6 A; Y
void addEdge(int u, int v, int w) { ; ^) N3 T; ]" S; M9 D edge[edgeCount] = Edge(u, v, w, head); ( ?+ s( x& {+ z! C# X ]8 Q head = edgeCount++;" M$ c y D: R/ h, [" j
} / |1 ^* |5 m. t* X6 p1 1 }! U# q& u! W: D( p/ x2 ' ]- O {0 _) e; Z; q3 - i+ K% a O7 ?4 {4 : m B# d" O' o; I这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。 7 L2 N& G. ~3 Q! L3 d* }调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。 $ O% k1 q7 E6 |3 wfor (int e = head; ~e; e = edges[e].next) { # \4 w( e B, O0 j( K9 ^* {' d6 t int v = edges[e].v; " |. t, s7 T7 Z& t ValueType w = edges[e].w; ; e& E9 b! S2 |6 m' ]& P$ S ... & J5 i4 `( Q7 F( j& ~} ; e" n5 x5 m* i0 R: n! |5 E- ]1 - [# S! N6 c. r0 b8 f3 g1 {2 ; T+ R) S+ ~) c* I; \2 A3' }* E% R6 d, y
43 H6 e) |1 _- z. J9 }6 `2 A( P. s
5 3 @8 D) Z$ R! C* l5 Q文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。 / i& M! K H) c C. G$ K$ S4、算法入门) r- [/ p$ g7 s
算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。( [/ I- [5 f0 d. c
; y2 d; z# K4 o4 H & Q# Y. b$ h( I& q3 b入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。 ( [4 Y4 }" s3 L* }对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。 + N, {! t9 o/ @& @ [+ a1、枚举! a/ M% Y m1 N0 ]. S1 I
枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。 + ?/ F; w0 F( P对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。 # U. M1 H! L0 E0 v* K2、排序( y% Y$ R5 `$ E
既然是入门,千万不要去看快排、希尔排序这种冷门排序。 ) q2 |+ _3 ?$ K, d冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。. d! Q. n% C4 N; w, P, M& B! f
C中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。 # M3 a3 ~" o$ r% h( o @3、模拟 % [: ^& B7 \: R模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。 ! ?4 v8 L0 L! {/ F ]不管时间复杂度 和 空间复杂度,放手去做!# p B4 `% y; R0 C
但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。6 g. u: _2 c& z; ?
4、二分) r' V. y. _2 [- Q' M/ f
二分一般指二分查找,当然有时候也指代二分枚举。9 t: R8 a" O# {2 T% I
例如,在一个有序数组中查找值,我们一般这个干:8 I! T0 C) j& u
1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1; 5 ^4 a7 K& N! Z. r+ r2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种: : J& X! p7 t8 ]1 O8 R 2.a)目标值 等于 数组元素,直接返回 m i d midmid; / t& s' T; E+ v3 g0 N, o W 2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1;- |+ d- r* T u) R
2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1;& q- t/ R3 P% r& P" Q6 t
3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。 2 Q$ w& R* w2 L5、双指针8 x9 i! V; b$ H
双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。 9 E, o; m# Y h) N8 ? # y9 f% [! A$ P7 R" w 1 c# w+ h# r) y2 `; K6、差分法 / m+ R7 k* F3 Q0 S差分法一般配合前缀和。 , R5 m; I- q8 ^# m0 k对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题; / \9 v" M, I/ J1 I7 B6 M+ t1 r假设 [ 0 , x ] [0, x][0,x] 内的 g o o d n u m b e r good \ numbergood number 数量为 g x g_xg . e# g2 s' Y. {! s
x . O0 l7 I z+ x+ P , s5 i& d6 l$ p, F5 u ,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g : p7 K: P5 }/ @' L+ ^4 I
r# S% V# K: }" L- `9 c
: d( [) S* E5 O; M, @
−g ! n" k2 ?, v* s+ e% ?0 y
l−1' W8 n* \. ^' x: w u' m) w% D
! h' o0 u6 ^. J# z0 O2 ]5 ~
;分别用同样的方法求出 g r g_rg + n# U3 Z! j' y$ Wr: {9 ?& I& T+ h- E6 u. S' A* L
" L9 s! |6 o& N% @" y$ I
和 g l − 1 g_{l-1}g # |. H5 R0 ?4 S9 r A3 j" wl−1: Y2 A$ ~9 Q! g+ s: n: X. P/ s/ ^- Q8 r
" r& }9 e8 l- q! @$ b
,再相减即可;) I7 L3 l9 {# d' W- \7 ^
& M! o* w0 J+ h- [6 q6 Z, t
0 g: h. q$ [) d: z& w4 k6 ~
7、位运算 7 ^: ^3 \, x! x: _位运算可以理解成对二进制数字上的每一个位进行操作的运算。 6 @. F' b. s: w$ S' v" }位运算分为 布尔位运算符 和 移位位运算符。) h# K/ Y- s |# H9 {4 x/ N
布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。/ x Y! J2 O% l' y5 x: L
如图所示: ( a X+ T. j# k! i1 H5 p & u, N% D) \9 T2 e8 l4 P* S+ ?5 K% {. ~! L+ K9 k
位运算的特点是语句短,但是可以干大事! " I& B% x! n' b0 |9 l" z7 g x9 a( |比如,请用一句话来判断一个数是否是2的幂,代码如下: # I% @+ R3 \$ b& g!(x & (x - 1))9 q7 Z: k* C' p/ u& Q
1$ r4 o; L) `0 E) V! I
8、贪心 : u, \8 w) l E" u2 f贪心,一般就是按照当前最优解,去推算全局最优解。% A3 ~) a" ^5 R$ @* S* V
所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。, ^' R9 M' r1 W( q, e
9、迭代/ x% ]5 h7 y7 |- c s4 R
每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。 / x" F1 X; C2 N( f, [! N10、分治 8 t( s/ _+ g' } V( H分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。4 f4 A0 {1 t& d+ |9 J
5、算法进阶 * S# Y( N q4 _* z# @算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。 ) e4 X8 \% b4 i5 G% N, S2 F/ E如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。5 h0 d8 v/ P5 K! p% Q9 h; M
这个系列主要分为以下几个大块内容: * c& V: O8 N% r& P" M. F3 F5 x 1)图论5 E/ P/ {- `1 l) {/ L( C* \6 \
2)动态规划& U: A+ n2 K9 ?) o x: y
3)计算几何7 @! u6 B$ x' G. O3 U4 q
4)数论 2 B Z; i3 X' x* g6 i B9 S | 5)字符串匹配 ( q' K4 o2 b% z# T) O 6)高级数据结构(课本上学不到的)" v7 v3 [$ G; }0 }# \
7)杂项算法 l! W' _, [0 I
( n7 e. k+ `; ^/ d : e5 f! O; z+ U/ s* M/ a先来看下思维导图,然后我大致讲一下每一类算法各自的特点,以及学习方式: - F7 ]" M; b! e 6 M* m! B4 F: E& ^' m1 J' z6 \. n6 c" P" b- X/ [/ H# G [' z
1 O. A- g0 T' o* N4 i