0 d0 ~0 T( l8 ?" x1 D: O# y9 C ' B& }: E! T& P# w- s/ J$ A3 P, ]f、栈 8 Q! s' M8 M8 j- s内存结构:看用数组实现,还是链表实现 9 X0 v1 a' Z) w实现难度:一般 / l( r/ T8 W# ]9 n8 \下标访问:不支持 ; r: i+ V/ R9 `0 Z分类:FILO、单调栈 # `9 X5 y+ I1 g- H- W& D- c5 B插入时间复杂度:O ( 1 ) O(1)O(1) . @2 c8 {& L& \& _- N9 v查找时间复杂度:理论上不支持9 `" Y, a$ W; n- q
删除时间复杂度:O ( 1 ) O(1)O(1)- b1 P. d% k5 [; r
+ Y. F% `! s" C2 ]6 g
- \7 _2 j% _- Y! }, b, [/ |
g、树% D \% y2 s2 F! S
内存结构:内存结构一般不连续,但是有时候实现的时候,为了方便,一般是物理连续,逻辑不连续6 @- p/ n# |) Y d7 z
实现难度:较难 q7 N% Q* L4 k. T' X9 ^$ }" F: H下标访问:不支持: w8 Z' Y9 J7 O' a2 g4 O" l
分类:二叉树 和 多叉树 0 a9 A4 j' I3 m: m7 f# h( k插入时间复杂度:看情况而定5 `1 ~( R/ C' m( r
查找时间复杂度:理论上 O ( l o g 2 n ) O(log_2n)O(log 6 D+ p( J# i) g( h2 9 u$ x7 \ W+ e7 j% }1 b& h $ H& e7 a- r8 J! G- Q8 r n) # B' ]% e8 G8 l; w0 Q3 p删除时间复杂度:看情况而定5 b* l4 P/ [+ c- p/ B$ R. S" L
* j; w0 |7 x$ f/ s( B! F" B+ U; T8 ~9 q, r' j
1、二叉树 ' j8 I, R' l( k- b5 L3 @二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。 4 b2 Z8 k' @. j0 T其中,堆也是一种二叉树,也就是我们常说的优先队列。* V7 e5 g8 L& m; [- j
2、多叉树 * G4 J0 e0 i4 m3 bB树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。 0 m& V: N. A! ~; o" ch、图 * C. A2 G, o5 \. X内存结构:不一定4 e2 R& m( ?+ q9 d- \3 z
实现难度:难2 E$ Q6 c1 K9 B2 S1 c4 m2 h/ |5 U
下标访问:不支持 ! l, t4 H, v( z$ R" t分类:有向图、无向图, S9 ~3 _# U: n+ H* r) H( _
插入时间复杂度:根据算法而定. M9 E* {3 _. x$ L
查找时间复杂度:根据算法而定3 i4 O1 N' p8 A& j& d' c. x
删除时间复杂度:根据算法而定5 Y0 s0 X: Z$ o. [7 E% V" v6 ~; L7 Y
1 z c7 m5 c9 V+ d
2 e7 ]- H6 W6 K3 a) B: ~7 O, @1、图的概念; a0 B' o' X! g0 h+ h
在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下: ; _# j, T$ ?) K2 L: H' P图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。: T. e h2 s( h7 M" t2 c3 p# z
对于无权图,边由二元组 ( 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 为权值,可以是任意类型。% [; w1 ^: P# W0 h, Z
图分为有向图和无向图,对于有向图, ( 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 E w7 ?( U1 _4 i7 @/ ]6 ?: h F
2、图的存储 2 G! S8 u9 p0 ` [& u/ ^对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。5 w* B0 a3 o8 F: i# Z, j# O1 O
% T7 V E& V( r3 [0 D- O
+ O7 K8 e j" g! a* ]& }1)邻接矩阵 & j) x* x# q$ H# @- `" U邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。3 M1 o) r4 m) ^! ^# S
它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。: k, `" M- @, s) z1 @) S7 M
[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[ 5 T* B8 i% g, u& h% c8 }6 w01∞9∞0∞8320∞∞∞30 ' U+ Z. c0 A2 o+ i A6 {: r0∞3∞102∞∞∞0398∞00 W! A7 K. x/ p- U
\right] " D: x; g. N5 x2 I) u⎣* Y5 t% [. I6 C1 N
⎢ . z3 l; S# R1 X1 ^' Y" F+ `: w6 @; Q⎢ 2 z- W+ |2 {2 N+ d4 J⎡ ( u; q# `) D* T) [ 3 H& A& h. g0 F5 d u$ z7 j 6 I1 a* [) p1 ]+ B& `- u$ Q/ u
0 0 V" P& N+ N8 G" u1 & ?7 S, ]! Y7 _' G6 j+ `∞2 Z+ Z& i7 J; v: \6 Z9 ~6 S
9 9 h# |7 F: t! S, p; T/ J' o4 { 2 s) T& \3 H4 n k% w2 o* E8 I
2 A# C% `* {: r1 [$ P- H6 L: f∞ * V$ R$ Y7 T6 d4 @, O6 A0 5 |0 o0 Q& N5 H2 I3 o∞ " _+ T3 L' r6 G# O* V8 4 G8 n3 A6 h% J 5 Q @. g- I- v+ S
) ]& k: t0 F/ A4 n
3 0 u1 a: P s3 n0 t |2 : X5 }0 b; |# |0; p& Y- O5 a/ d6 N ?
∞ ' `& j. N2 b3 _9 @ 0 ~6 ]3 A6 p2 }) o 0 ^# t* Y2 E/ a5 X- v$ U∞/ z7 i# Y. E2 \9 t( P6 P
∞ * R7 _( d1 h) E9 a3' o5 u% W7 ^' A, o0 v) e4 G; `
0" i0 O+ l i% V" |& l
3 w' ^8 D8 [0 j8 g) F! i: H8 _
- d2 g9 ]5 a4 z, ?) `4 P
⎦ % n' m3 ?1 p0 K" ^# H⎥ - ^+ F! k3 i9 |! l+ `2 F3 m0 ?⎥ 5 v. ]4 R/ R, e2 x2 O6 C8 b2 y8 O⎤ 6 r) a" e( J0 l& }* G / r6 A6 ^- n( p/ p' s) ]$ z / j1 b5 w8 ~4 w% K2)邻接表 9 U* Y# T( J8 T1 s, }0 n- N" t2 V3 T" s邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。 + T5 J" R7 a" S; D+ y# o6 Q4 U6 A它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。; ?0 Y2 C; o( J
如图所示,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) 二元组。 7 Q$ k/ g7 s; p" E- V 8 E+ B# k* k7 F+ O$ d 8 t' t4 I9 f1 \* y6 m" \在 C++ 中,还可以使用 vector 这个容器来代替链表的功能; : w7 ^7 D3 S, R* ^5 s( X9 Z vector<Edge> edges[maxn]; ; r" I3 t; E3 C6 w% u1 2 b0 X- d" |$ L3)前向星9 ?5 N0 X0 m2 Q: f
前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。7 ^ E0 P& G ~7 K$ _( L( v
它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。 ( ~; D" F6 j/ ]8 _! O如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。+ H! K- B4 X7 N# o6 P; \
( N& T& i" n8 B' d' K& c
& S8 H2 ^# ?$ N( m那么用哪种数据结构才能满足所有图的需求呢? 4 J. W; V! w1 }2 {' q2 d8 k接下来介绍一种新的数据结构 —— 链式前向星。' r: Y- k& J* S+ @/ Z' z: ~8 `
4)链式前向星5 y) M7 ` W! x/ ?0 P/ _; t7 g
链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 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 指向下一条边。 , U8 ~! m$ B) _7 |# X: c具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。 4 |/ l( T, j# V; l; I边的结构体声明如下:- |3 D- Y& ?( B4 T6 n
struct Edge {, z& W- l% b* b1 r
int u, v, w, next; * d; |; ~8 \, |) ^' K, y# s8 i/ A9 Z Edge() {}/ ?1 y: t: K- Q* j( h# F$ m
Edge(int _u, int _v, int _w, int _next) :9 {0 ^ x& t/ A. `
u(_u), v(_v), w(_w), next(_next) " y- ?! N' ?+ ~1 G4 x; g1 _
{ 6 z6 q0 g" G1 y' B$ T# K } 2 V6 U5 l, S: A6 z; e: H* a9 _$ X}edge[maxm];( c8 {2 }- X2 B9 l' z
1 2 _ r+ q; n% d: o& T8 X5 K2# ], l! J) c4 h0 {7 v
3 # }7 M. D2 q" W4 1 n U& ^7 c; P' P" n5. j% Z0 I5 E- \" g. z
6 3 L% i8 f3 r5 X5 M% a- T7 ; |7 z: O' ]# C% Y4 o( \% ~80 u/ l% _8 i8 G I& d
初始化所有的head = -1,当前边总数 edgeCount = 0;- q k* U9 [2 E
每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下:4 Y0 f2 [9 _& o6 e' j, D
void addEdge(int u, int v, int w) { 3 W5 X* e" J/ M3 M4 c ^ edge[edgeCount] = Edge(u, v, w, head); 4 I) D+ Q! J0 b6 R' z, R head = edgeCount++; , i7 h) z5 q9 K: I$ o: [7 k} ; h( U+ c$ W8 n: k1 9 d3 |8 K: u6 F. w+ `2) g" P2 F4 M/ b* _
3 ' j$ E% c$ K- N) C8 ?4# {7 H% G; H8 i4 s: @3 L
这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。 * Y0 L* V5 A& B/ l+ _6 K! A: O c7 G调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。0 {9 M" |" i8 W8 u2 M+ `5 z
for (int e = head; ~e; e = edges[e].next) {$ `+ D8 [0 J* ]1 y* w
int v = edges[e].v;( e; p. X) y( [7 s9 J
ValueType w = edges[e].w;2 ^4 d2 ?0 g2 t# R; f% b1 |
... % Y, K0 l. z; o! i4 a}: J" [% r8 i0 z
1 5 h8 i' `" Z( R9 D0 Q2 n2' j; i* W5 L( [1 J
3 # |: z7 X. T* J4 * M/ U3 Q0 x; w3 V t0 N/ m58 ?$ M6 V& `; M* I
文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。 0 o7 G3 a6 f2 H; e& L& \4 `; Q* }4、算法入门$ G! M8 O) K6 Y0 y6 E* _
算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。 8 n2 P; R* P6 }# m3 j, k8 Z% o - L+ S) l9 ?$ ~, p9 L! u! T- G$ s
入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。 6 f# _3 H" z# C3 Q5 m$ N3 g2 ]对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。3 s/ K2 O' S8 h; c! x: d; ]3 d
1、枚举; s8 H- n5 f5 J* W; v0 m6 w
枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。 , V" p: v$ v! }) R$ _+ h1 O) Z对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。 g, I" H( G5 |) D6 G2、排序 3 G$ B! B3 O7 |1 Z既然是入门,千万不要去看快排、希尔排序这种冷门排序。+ ]2 _4 P( @4 K" F! | E, U
冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。) C! J* ^$ C+ `
C中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。4 W1 r8 z! D7 C6 e
3、模拟 5 d8 c5 k6 q' y& C模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。- [* |( s7 j, a
不管时间复杂度 和 空间复杂度,放手去做! : ~. _( m2 S! A9 T1 H但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。 ( ?8 ]! v* f) B1 [4、二分: n* w L$ U) n' k
二分一般指二分查找,当然有时候也指代二分枚举。: ]+ W6 `* m2 m v7 L
例如,在一个有序数组中查找值,我们一般这个干:3 { R& V- |" s$ R. M
1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1;8 a, m2 d: h( s% m. U7 Z9 M- D
2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种: ) s& [( h6 a% ~6 H: t* T) { 2.a)目标值 等于 数组元素,直接返回 m i d midmid; - N0 [+ k4 u7 U0 [* i 2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1;. f( J% A2 a' o p
2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1; 2 o/ E* f) H) w$ m3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。 " ?# w% w, {5 F- [7 Z9 m% s- Z5、双指针 $ C; w( V8 K: s9 M7 {' _双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。6 Y8 v+ l! U) S
0 F8 I+ m4 F! a0 D+ |; t- U3 ?7 k
9 f0 O" O& c! F* e% j
6、差分法+ l9 z2 d1 Y" i9 `
差分法一般配合前缀和。 4 U. \' R {" X1 s+ G对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题;3 ~- y( p# R: v4 A* Q% s+ v
假设 [ 0 , x ] [0, x][0,x] 内的 g o o d n u m b e r good \ numbergood number 数量为 g x g_xg % h) q7 T8 C6 D% ]* r2 A7 P+ \x ! n8 P# P5 S& X- ^ T9 b) s 8 w8 U+ d: ~* E( x* e# e) \4 B
,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g 6 z" C: M3 p- ^+ \4 U7 l; L9 P) I
r + B% q- l' H. j) i+ _6 [ 0 f3 i" n/ T- T& o4 p- q −g . ?5 e( R, n( S' L; X' i4 i0 Y8 Yl−1& Z3 l/ l/ C+ `- Z. g
2 [" U: q& i: c3 o ;分别用同样的方法求出 g r g_rg 4 E7 s, @; l" r' D9 B/ q
r0 r1 i+ E/ A: s* c% r
; w; [7 Y$ h1 }
和 g l − 1 g_{l-1}g 2 i, T! p* ^1 x- L
l−18 ^ U) \$ T% y; F' T1 V7 W
. J- p3 j- @! `! n4 p
,再相减即可; * C8 Y! C5 u' L7 V : [3 k7 R& I& K8 N: s& n+ ?) a% r3 G $ H5 ]0 Z; w$ d! l9 L. W" ~7、位运算 ; L3 F. c& @0 t; d0 n位运算可以理解成对二进制数字上的每一个位进行操作的运算。2 M* K" z* ~5 Q5 C
位运算分为 布尔位运算符 和 移位位运算符。, \) _* U0 M8 H, a! F
布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。 i& g# C% k1 y& Y# M如图所示:* E6 u# T! V$ ^5 i7 V5 B
# P+ {7 K6 }" g' \/ G 6 f( t( v$ b0 Y( ?位运算的特点是语句短,但是可以干大事!$ p' c. y3 f; _' S
比如,请用一句话来判断一个数是否是2的幂,代码如下: : a# ^$ _% e6 Y4 k3 ?) d, a!(x & (x - 1))- Y {# @1 C' |" U- Z
1 # T' n! f7 Z$ G8、贪心* r. T Y3 Q4 N9 D( ?6 i* X
贪心,一般就是按照当前最优解,去推算全局最优解。- k2 V6 g4 `$ {& R6 n
所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。 $ |2 l/ `" }! s1 n( j& p- g9、迭代+ Z8 X5 ~3 K, ~! h& o
每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。* \! f; k& B+ ~$ f9 C4 U
10、分治 / a9 Y3 B2 C, y; _/ W分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。; B- e5 H" U. X- K& ~
5、算法进阶7 F! q2 ^& F9 s! `# y2 x: e( K
算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。8 P' P5 n: t) r8 X6 q
如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。( j5 p+ X, u8 i! V7 c" i
这个系列主要分为以下几个大块内容:& }) u1 S |+ t: T+ I( t
1)图论3 t& Z) E5 W8 u* ]5 p3 E
2)动态规划5 h4 {) T8 ~( m$ x1 f
3)计算几何 ! D4 k" a& Y& m) B- Z 4)数论# E4 n& q; y: a0 S2 f% R
5)字符串匹配1 M9 U- k2 {8 E) {3 P& ~' g
6)高级数据结构(课本上学不到的) ( b6 Q- Q3 ?$ R) T1 }( s 7)杂项算法+ z9 f# O" D- r, ]$ [
2 w1 e. G- a1 _- W+ C6 a, q" h
/ Q: P; \9 K0 C1 s! N
先来看下思维导图,然后我大致讲一下每一类算法各自的特点,以及学习方式: 9 T7 V U3 ]% Y% L# G/ c 5 [; x0 H! D0 z! V + z _/ e5 [4 b# v0 n. D, K% V' P. o7 t" E" O! c! q v2 ~, N