% u+ a4 }. e$ x1、二叉树 + x4 r. S, l( d二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。* {6 L' G4 Z9 S8 ^" y* O# t
其中,堆也是一种二叉树,也就是我们常说的优先队列。 8 N+ p) ?% d' T' A8 s2、多叉树# p0 A! `6 }5 Y! {
B树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。 / [; o% K* {# I; c/ i2 Q& g+ k% Dh、图 / @' d9 o2 ]8 P内存结构:不一定 8 W6 j6 D+ ?+ D. l: N实现难度:难 4 b5 y. E& N& N, j4 I下标访问:不支持 0 x: L4 h& I3 J2 B# z Y y; p- F分类:有向图、无向图 ' ^6 m$ o7 A" U' h, B. @插入时间复杂度:根据算法而定- J3 [6 d+ l; N; n, X, S
查找时间复杂度:根据算法而定 ) e/ ]6 D6 `" i4 L" J删除时间复杂度:根据算法而定 5 @9 z, ` R' j1 `- U& I% J r4 C6 A
8 r1 A# K) a4 J7 X1、图的概念 " u, Y. x) u( \; a2 c" k, q在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下:; D3 W5 S3 J# h* n" E
图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。 # f( s% n- ~, z0 k对于无权图,边由二元组 ( 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 为权值,可以是任意类型。3 v e. h$ H) W( H- 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;, Q: C3 @9 G% g7 I
2、图的存储2 i5 P; d8 Z4 K3 \3 @* S
对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。3 \* b& }7 |8 ?$ H* A
1 } B7 c8 m8 @. H
b( |5 M0 C: T+ N7 [# G0 y
1)邻接矩阵: f4 {' P8 c5 H3 O. e5 b
邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。$ o9 J$ |, E$ p" x B( c! F
它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。 6 ~- P$ H0 D. F& K; Z x[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[- Z1 u$ J* S/ u5 _9 H( i
01∞9∞0∞8320∞∞∞300 X% g7 V. d- b+ z6 P) d' s
0∞3∞102∞∞∞0398∞0 ' n( G/ H* a7 E\right], ~' Y6 g5 G3 X4 J! i$ S0 K2 N
⎣ # {3 X/ Y( ~7 F" Q2 U3 `7 p1 l% d⎢' t' v' L9 T$ w! C& m0 X; z3 ^
⎢ ( Y! E3 ~% Q9 f, P& R⎡' L8 u3 U4 E/ `/ i
0 X6 J# }$ Q1 V ( q) n9 x# _, L" N0 5 P+ T0 \& u3 u1 U% O2 {+ e7 z1 * U7 l4 {4 f! w! {$ m( v∞ 2 q, A; N* H% ~$ M" m6 {- W* J/ e9" C( d' Y8 o- a9 e4 I$ Z
H# P0 a/ C6 r1 @" ~% [9 Z
5 n& v. g% S9 `/ G* x* W% |8 @
∞ * N( ^3 m* i" g$ i7 K0; Q3 z( N. C* ~, Z4 G4 x( L, g
∞# T H- ]) E, m, s& E/ ^
8# ~- L5 N0 T3 v9 J2 e
0 Z3 Q4 R# J: g9 L. t4 ]9 P " j6 K- b6 W5 F7 n3 0 u& h- d8 w0 Y* ~* g1 s- y2 & {- [2 w% b; V4 m) A9 _0 ( k+ v6 t' t) J0 T5 X) }∞ / C G7 B1 Y, C5 _ }% F % c- u- o( H* P% I1 t+ U1 |
9 f( o5 a$ M% q& l& i
∞+ U8 z$ @7 [5 Y6 q
∞+ b6 L, E, V& ?# C% _2 n" L9 T0 e
3 ' b$ n& I- ?1 c* w0 5 f6 D) i- R& h j " Y- G/ H2 `7 V; m9 C1 W " @9 J& e( B) L$ j b" ?# y2 J⎦6 R- V1 T7 c) S0 V5 f
⎥/ I. Z, {* v6 k6 b! r
⎥) F, O+ e ~6 d- R3 K
⎤9 R6 E& u% J+ o& x6 W, V
F4 m7 j( U: _0 G / `' W4 X8 I9 {* \2 Z( K5 |
2)邻接表 ) m2 \# j& [* q% J2 S' A邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。 : o" L Q6 x2 ]- q" U7 r, n3 B它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。 7 i: n; }; R, F3 r# u% h如图所示,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) 二元组。& C% n. `, N8 x9 E
: _# N3 l, `6 |4 T) s 4 N* Q; b* j0 C% ~, b* ^0 S在 C++ 中,还可以使用 vector 这个容器来代替链表的功能;; ?- v4 h9 @* c2 A, I K4 m
vector<Edge> edges[maxn]; 8 O9 {. S4 s; H2 j: ~ p; C6 J1+ a6 K; \: x8 O) B6 y4 t/ H5 C5 y
3)前向星" ~. f3 _, L8 w' n0 t6 U, J
前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。 , X9 r3 I# F% A它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。- C' W) @/ {0 q3 S$ s/ C) C
如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。 4 g1 q( P* T) r$ P$ q% C1 E# X7 D ; a( P( L) e9 j5 x4 N V. ? q- ~3 s) {
那么用哪种数据结构才能满足所有图的需求呢? ( a8 O6 Q8 o8 Q$ ]! W接下来介绍一种新的数据结构 —— 链式前向星。 ]" e6 k+ y8 F. q/ i. m
4)链式前向星 F& j0 r1 [9 V
链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 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 指向下一条边。- }( t6 _) U$ e
具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。4 M: @; g- r' U3 v1 l- `" z
边的结构体声明如下: R( N0 r) G% \6 l2 W) Z- Ystruct Edge {. J, B, D7 N6 a' J
int u, v, w, next; 5 L9 |: T: f, s( T Edge() {} $ i4 g3 h" B( \! a Edge(int _u, int _v, int _w, int _next) :+ \, j2 y) |" u, q. g
u(_u), v(_v), w(_w), next(_next) 5 l# V" k& {8 l) v+ u
{. _4 D e5 b7 D8 u6 A
}7 a+ ?9 z' C8 M( f. F; R
}edge[maxm]; % q9 ^. c" _6 E0 G% ~. Z1 ) F1 D0 e( D# V h2& i( D* x n% q
3 6 |% N7 s3 D: q' I4 9 w! K+ d. [2 e2 r4 G8 p5 % l6 z) J8 {% @" y$ g% X* c6 ' m3 W( Y9 X: n4 M# @/ w7 - U; c0 G- v7 B0 p- i8 6 X) ~2 J T1 Z; K初始化所有的head = -1,当前边总数 edgeCount = 0; H2 K+ A) A4 I3 F
每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下: & h3 _ a+ ~1 [1 J7 g, ~void addEdge(int u, int v, int w) {/ @' l5 F' g* u4 w( ~/ E c
edge[edgeCount] = Edge(u, v, w, head);3 K( Y% D: k: b) ^
head = edgeCount++; 0 \2 `+ N# z+ ?9 V( D! t; v} # Z( ~5 J# j2 J5 {1 7 Y, C4 u" V$ d' v4 R. b( D22 T7 }% ]% }! R! T" Y" h; s" e+ R; A1 ]/ A+ N
3 ! @$ M' X* n" `4 D0 D) G/ \4" d9 k- I5 E, p6 O$ G& Q6 g9 E
这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。, U5 j5 j$ ~8 |5 X" B! J4 y6 J. I
调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。 : W3 o4 T7 [# b- P5 `0 Ofor (int e = head; ~e; e = edges[e].next) { 2 L* X- x4 V- Z5 v$ R$ Z int v = edges[e].v; - v n$ r: `; N7 n, f$ g8 n- y ValueType w = edges[e].w; b/ T0 l* h- Q3 t, H9 q! G
..." S6 g& ^) R8 F% J
} . F3 b4 F; F9 Y- |9 p13 G7 B8 H, [6 ~' f
2 6 u! @9 p1 m! B5 E3/ P7 J |1 L! H. W
4 - s& u& P2 c% p7 a$ j53 e ~( e- k5 g4 p0 K
文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。 . A: _2 Q/ S# l# C) \# Y4、算法入门 . n4 q8 D6 R- a$ M2 e$ q算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。 : I) F4 H7 j1 K5 B$ p C4 e5 m" N
* j; P) \. p; ?! ^入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。0 w: G% B5 @* x9 x% x" \0 n
对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。 ( o' @* w- T5 Q% F2 i! h0 T1、枚举; D+ b+ Z3 D2 D. R. o) H
枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。0 M* L+ b# W8 V2 q% ~2 }
对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。' s0 l2 e1 A" G
2、排序 " H# k3 u% U, E; O既然是入门,千万不要去看快排、希尔排序这种冷门排序。4 q) U) @+ D* q) ]
冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。: Z E5 ~* a% | r2 G3 A
C中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。' g j% v$ ]; Q+ p, H
3、模拟# \2 @0 S( i2 F i: l
模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。 ' F% R- @ a! u i& Z不管时间复杂度 和 空间复杂度,放手去做! 2 [, ?& k) P4 o% U" V2 N但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。% w/ }# i6 y" e6 m* u
4、二分1 U8 p: V9 Z! w. n- D& k, u
二分一般指二分查找,当然有时候也指代二分枚举。 0 }5 \& c6 w- l1 e例如,在一个有序数组中查找值,我们一般这个干: ! \" c [% D$ N( T' E$ b0 A% Y1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1;! r D6 I; l2 G5 c2 X
2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种: * I. H/ g$ `* |. i' J$ B) e$ v 2.a)目标值 等于 数组元素,直接返回 m i d midmid; - V3 j1 |7 p; p5 [ 2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1; " k$ ]0 |' d" y4 f9 L! F8 ]9 {1 I 2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1;* Q; l+ w6 O& w/ [
3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。 ) I0 `7 \8 h: x* p& H5 O5、双指针$ n% e: i" q0 a: n6 M
双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。 ( s% n' O! ^. K9 j8 m" f. K0 Y) p9 n. v. B9 H X A+ A; E7 Z
" y) U5 H6 z4 [: a/ l4 \. Y6 t7 q4 w6、差分法 7 V5 ]( c% ]* b) ~$ V差分法一般配合前缀和。* R) m6 j `8 A
对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题; 1 _+ u( D6 `7 L- k( A# R4 u7 b% S假设 [ 0 , x ] [0, x][0,x] 内的 g o o d n u m b e r good \ numbergood number 数量为 g x g_xg & Z2 @" \! m7 K5 W' X8 fx " r' j7 ?* V( t+ j9 @( V. Q % W$ c4 D' G2 I0 o4 q( V
,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g 4 B5 K, |) @* t5 g6 {# P: hr 7 _9 O- u- F8 n$ r7 F2 n! E , p( @; I0 c0 L" j2 ^. X) g. W −g ' a S; U: j3 p. |9 k! ?l−1* E0 U8 m( U! K4 d
+ j% R3 X2 Q" [; s0 ]9 ^ ;分别用同样的方法求出 g r g_rg 5 v) y; w& F( v) C8 u
r# I9 o- p. v p6 y3 z1 J
+ A7 V9 |" @$ G2 M' d C 和 g l − 1 g_{l-1}g + w1 t$ J7 {: {5 V6 m( L6 tl−1 6 D" P; O* |. g2 X: O* l 8 q P5 L6 h3 ^3 w9 @! W0 ? _4 k& n
,再相减即可; / s N1 e% N6 e9 p9 b1 Y9 a! w7 s
% {, c6 u) t2 ?- F, H7 J
7、位运算9 E, L1 w4 Z5 p8 P5 a
位运算可以理解成对二进制数字上的每一个位进行操作的运算。 # I8 j! @% e. V; f. [位运算分为 布尔位运算符 和 移位位运算符。 0 v1 m' t# y2 Z* ?布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。% @# j C9 [( b% z% ~- x: e
如图所示:% J) ~3 n m2 \4 j( F2 V8 d/ ^
/ p3 I0 \( X- |' X+ i, p3 b. j0 j8 h2 p+ p6 Z! l
位运算的特点是语句短,但是可以干大事! . C- Y+ ^7 x& q. v比如,请用一句话来判断一个数是否是2的幂,代码如下:0 [0 ^* a3 l) Q6 |1 d
!(x & (x - 1)) . D; h2 O/ F# }1+ h3 ^- ~6 e5 Z
8、贪心 # L5 T" g/ z8 Y' e: d+ W贪心,一般就是按照当前最优解,去推算全局最优解。5 k7 Y$ q1 @5 u" Z! \5 ]- r
所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。 : c& A- m) f) l! @9、迭代( H2 T& \$ P2 v$ Z8 |
每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。 C& r3 O9 h: ^" N% d6 |5 ~- |
10、分治! H$ A/ G u9 M
分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。9 T0 j+ m1 \/ Q9 L8 L9 I. U3 z
5、算法进阶4 M5 \1 y- Z) f( A' y! X
算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。* ^$ m6 Y0 M0 R/ S& ]& m' |
如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。 1 t& a! v6 c7 V# v9 V: t这个系列主要分为以下几个大块内容:2 K; U5 l K9 g& q
1)图论 , \; c4 m: G! u 2)动态规划% X: c; L+ V+ J. e c3 f% @/ G
3)计算几何 # ^3 Q7 L! f! t5 m: e ~9 ? 4)数论$ H8 N" [2 V, T6 y" Z; L Z; E4 Q
5)字符串匹配1 u6 l: y( k: H7 i/ d! E7 J
6)高级数据结构(课本上学不到的) 9 T# Q9 N# p7 K( Z 7)杂项算法6 R) V) F; F& V, L7 Z. g