/ l+ j1 x2 M, |9 g& K9 h) H; n! v从数学基础、输入输出、数据类型、循环、数组、指针、函数、位运算、结构体、排序 等几个方面,总结出的具有概括性的例题 100 道 《C语言入门100例》,目前还在更新中。1 P$ O' h6 h1 I, @: p
这里可以列举几个例子: 1 c6 j' F. C" \9 l1 Z, f5 ?, T l1、例题1:交换变量的值 / j2 u2 a7 `# M% j; |& X一、题目描述8 T' x8 W3 i" l$ z$ T$ X. ^: x
循环输入,每输入两个数 a aa 和 b bb,交换两者的值后输出 a aa 和 b bb。当没有任何输入时,结束程序。, l' q8 W9 a1 e
; A8 S9 n: D( N, ~5 C4 O. b
# D6 Q4 c' A1 J; y. I- @, a3 D, Q4 Z4 [9 ~, D2 N- o
$ |- L: L0 s8 ^0 d1 M8 ]1 T二、解题思路 - j/ a" ^- C- M$ D8 O7 m难度:🔴⚪⚪⚪⚪$ E; p/ A( g; Q0 ^3 e+ ?1 O
$ q- p' R. z' m. I* b% w. J+ L5 I0 h9 g
8 M* Z" }5 ?0 Y! ^/ @/ \! E) K
这个题的核心是考察如何交换两个变量的值,不像 python,我们可以直接写出下面这样的代码就实现了变量的交换。 1 n2 l3 o6 x9 t& {: A, m2 ^1 b" s+ R" Va, b = b, a2 f% q. G% n& _4 h2 E
1 8 i D: Y9 d% p4 J; s( r: f7 f! M在C语言里,这个语法是错误的。+ J" S: @+ v/ v) U# |2 f
我们可以这么理解,你有两个杯子 a aa 和 b bb,两个杯子里都盛满了水,现在想把两个杯子里的水交换一下,那么第一个想到的方法是什么? ( d* v6 p; O3 F! _5 n, S当然是再找来一个临时杯子: & u$ u) h% h- m5 I$ u% i6 h 1)先把 a aa 杯子的水倒进这个临时的杯子里;' j* M& w V4 [5 Z( L T* o
2)再把 b bb 杯子的水倒进 a aa 杯子里; # `( E) H/ O* r$ ] 3)最后把临时杯子里的水倒进 b bb 杯子; 4 d9 z1 m5 Q+ o, G4 A9 _+ q6 r( ^" i# T& V
7 l3 _& S% Q- w' m$ Y& Z+ S1 f# ?
这种就是临时变量法,那么当然,还有很多很多的方法,接下来就让我们来见识一下吧。* F; f* T2 ~8 }5 |) S) H# B
: |( e- |& z) p4 W3 P
, R- q8 L. g5 W: J& P! W9 z
三、代码详解2 t2 L4 i+ ]: P& R, @
1、正确解法1:引入临时变量# N G4 m# J# a- o7 J! s
#include <stdio.h>' w3 q8 s7 F# }. {
int main() { 4 h" o4 S6 z0 a6 r+ B int a, b, tmp;- d! P0 w6 q6 P6 |; o1 Z+ ^! l j
while (scanf("%d %d", &a, &b) != EOF) { : v& m. }: Q- I! V8 \+ f tmp = a; // (1)8 a/ S4 m- M# o% ]* D3 S" c
a = b; // (2) - e4 K( z& |- {. A4 ]: j b = tmp; // (3)2 y9 z/ r0 |, G. o
printf("%d %d\n", a, b);6 X7 u) I7 D# \. r* ]2 k
} % i% I% p% Z% I/ S5 Y# _( [4 x+ O return 0;& ?) E$ W, q- Z2 [
} 5 p7 L2 l5 O( a4 U) B/ ^7 u$ s13 v4 w) C( s& t: v4 U: e
2' D8 u0 U% |- B" {/ v7 y
3 8 N* p! a" @' R8 j2 U& |/ V5 \4( f, E6 J* i I' r0 M1 p: a
53 _4 i" m( m; c& Z9 b& d- d: }
6. C% [' h) H4 c9 t
7 ( H5 q! U: c( @8 ! N& ]: z: D' l% |9 d4 H' I9" j/ ]% T/ F& G3 w6 e! _; S4 J
10# K7 n* Y6 y f; O0 P, e! q8 g
111 e2 k* L; @5 T/ ]' P( u+ l
( 1 ) (1)(1) tmp = a;表示把 a aa 杯子的水倒进这个临时的杯子里;5 N' T! X, a8 b8 d! d- p2 b
( 2 ) (2)(2) a = b;表示把 b bb 杯子的水倒进 a aa 杯子里;* ^% u0 a8 v6 p8 ~
( 3 ) (3)(3) b = tmp;表示把临时杯子里的水倒进 b bb 杯子里;2 f0 ^ {$ H* d u, H& X& ~/ g9 o2 M
这三步,就实现了变量 a aa 和 b bb 的交换。, `. @5 Y; w. `; W" ~, x
2、正确解法2:引入算术运算8 C2 l6 M% k5 L
#include <stdio.h>* C: V- o# W7 {7 ^8 J3 l
int main() { $ X1 s# O1 s" X0 w; n+ K int a, b;5 U. N- _! z& S: ^3 {; m8 H
while (scanf("%d %d", &a, &b) != EOF) { & }4 O7 {) q- f a = a + b; // (1)3 i! `% u2 J" u* \1 X4 N2 O0 _
b = a - b; // (2)! R# t* u* y) E2 V" b5 Q j
a = a - b; // (3) % X9 P$ b& E J5 C5 o printf("%d %d\n", a, b);1 G4 ~7 {5 w# I/ r: G7 K
} " H1 O7 t& G# C" a return 0;& n4 J0 b5 i% u* v+ B6 p5 U$ S
} 9 m) |4 T1 n$ s! n1 1 x2 h; l# `7 P2 4 d' @0 \6 D2 c6 B5 C) N; c& j33 t2 K9 c# `! Y9 I9 R
4! v6 Q) }: J( {! D
58 x9 `/ s% q! C1 b4 n1 j
62 T: W J3 w* a! d' N( e% {7 ]
7 $ S3 L3 S" a% ~/ P! h: T' M8! b9 [& ~3 T: D. Z1 Y& e
9 7 c. }+ R! G- o# x2 y10 ( E8 @1 h. @7 c11 , X/ `# k! Y. B: `( 1 ) (1)(1) a = a + b;执行完毕后,现在最新的a的值变成原先的a + b的值;6 u: V9 i) } r9 W: ^/ J N: i) x
( 2 ) (2)(2) b = a - b;执行完毕后,相当于b的值变成了a + b - b,即原先a的值; ' T0 ~' \, y' M) v( 3 ) (3)(3) a = a - b;执行完毕后,相当于a的值变成了a + b - a,即原先b的值; 1 f2 _0 H" L; {' n' O2 B4 v- z从而实现了变量a和b的交换。 0 H; i1 F- `- Z% s% Z3、正确解法3:引入异或运算 + h) c w% Q/ w+ i! D首先,介绍一下C语言中的^符号,代表的是异或。 / p% ? T2 D# o二进制的异或,就是两个数转换成二进制表示后,按照位进行以下运算: ) w, ?7 h* D6 e2 o- z [: F+ F9 G左操作数 右操作数 异或结果3 F7 Q2 {, u! C7 _+ E! N
0 0 0 . s% p1 \! f+ `1 1 0 $ {2 A4 s" C0 }0 1 1* w7 o2 ^# ?6 g; I7 |6 h
1 0 15 c' I* ?' ~: O. @
也就是对于 0 和 1,相同的数异或为 0,不同的数异或为 1。' {7 _# `' d. p% J2 `9 }& g1 V
这样就有了三个比较清晰的性质: * P5 ]' Z0 `: r* M# q' w! |& p1)两个相同的十进制数异或的结果一定位零。 % i1 {; g- y- ~1 P: Q4 a5 `. n/ L Z2)任何一个数和 0 的异或结果一定是它本身。% `6 K: ^1 d- X/ @" B# a
3)异或运算满足结合律和交换律。 + {' U @8 n/ C1 I#include <stdio.h>/ ?* A' I' m6 i( V6 {9 A+ R
int main() {& l8 n5 e3 t: ~, C( F! t, ?
int a, b;$ f( w- O7 W* Q4 I7 E8 }: P: p5 U
while (scanf("%d %d", &a, &b) != EOF) { N9 b1 o, ]0 t4 U, B6 u, k a = a ^ b; // (1) 8 k" G( O2 q% w' } b = a ^ b; // (2)$ e6 X, X9 o$ ^% p& o% a" J! c
a = a ^ b; // (3)' E+ |) [1 G# e$ y' [% K* U' {' I7 ~
printf("%d %d\n", a, b); ( t. {* u" x3 {$ O! B1 T }1 X4 N; `6 j2 w+ m5 S, l0 J
return 0; ' Z4 G; j' v2 `; H7 h} b- p, X" i5 S/ B1 W1 $ V& e0 X; n& R9 l7 q" l% B$ z2, @* Q9 [1 w0 F& W0 j/ l. x
3; f: @4 g( [; {" C: G& H
4 8 \. N8 B* s% Q: ]* G5 - H' c6 A n: z$ T: f8 O$ b6 . ~% E; C! T4 T; P- T. G# Q7 7 u8 d/ S N2 r8 , f& Z4 ^; X8 ?% ^9 % n% X( o: d/ [% v" X1 L8 f10 * L# V2 H. T+ N- B' E/ k6 ?3 a! @7 S11 2 F5 r& r0 i4 p1 ^& a6 b0 G; l我们直接来看 ( 1 ) (1)(1) 和 ( 2 ) (2)(2) 这两句话,相当于b等于a ^ b ^ b,根据异或的几个性质,我们知道,这时候的b的值已经变成原先a的值了。 & ?' X/ M8 b; \而再来看最后一句话,相当于a等于a ^ b ^ a,还是根据异或的几个性质,这时候,a的值已经变成了原先b的值。2 g7 O1 ^, Z3 P m
从而实现了变量a和b的交换。 * w; f1 j2 z8 s5 q1 F k - ~" E# f% C8 L1 [) B3 ]3 v# k2 U8 G' K% H$ W; ]6 _
4、正确解法4:奇淫技巧" x, {" V' d* C$ o
当然,由于这个题目问的是交换变量后的输出,所以它是没办法知道我程序中是否真的进行了交换,所以可以干一些神奇的事情。比如这么写:9 {7 i4 b7 V9 [6 U# x/ ?
#include <stdio.h> . B) y) {+ I1 o0 x4 v: ]4 {int main() {5 G5 G9 x' X, S% g' b3 `& v) {+ G0 r
int a, b; . q( m/ \$ ~% M9 D9 k7 h while (scanf("%d %d", &a, &b) != EOF) { 5 c B+ n/ I; w0 E8 X5 L printf("%d %d\n", b, a);; Y# p @8 z- x3 s: `& R
} 7 S. B( w" Q( w6 } return 0;6 a3 t7 ~' [' u+ \' ^1 ?, o+ h
}5 e6 Y5 r6 @ \7 q0 y3 A2 r8 B# J8 B
1. Q: a; ]0 Q5 e0 B
20 `9 X; r% ?4 n, U3 o/ d W. H* \
3 ' @: g) ^5 {) l3 d* |; H3 k9 H* O4 * i4 N6 |) M2 ^5 ) V- s1 m* O% x; V" }. K' x8 f60 |6 Y S, s$ h! V% P
7 ' r7 X6 a- {: C% Z81 i! P* s! G. k- o8 o1 r4 j
你学废了吗 🤣? : `7 O* t5 \( |! J/ A, v+ j2、例题2:整数溢出 6 |9 ^( l# g+ D" o$ O一、题目描述/ c/ e% f$ Q9 z7 G
先输入一个 t ( t ≤ 100 ) t (t \le 100)t(t≤100),然后输入 t tt 组数据。每组输入为 4 个正整数 a , b , c , d ( 0 ≤ a , b , c , d ≤ 2 62 ) a,b,c,d(0 \le a,b,c,d \le 2^{62})a,b,c,d(0≤a,b,c,d≤2 # C1 `( V3 F9 }- j5 W
62 ^+ e2 ~8 v0 g/ z
),输出 a + b + c + d a+b+c+da+b+c+d 的值。 : [5 J2 _3 Z9 ` \: i2 z/ }8 a4 B8 H1 t
7 T2 A# ~, I% \
二、解题思路 5 Q1 W5 D! h, J Z* ~难度:🔴🔴⚪⚪⚪/ o7 f( J( o: [! u
+ D6 j7 ~% l( C7 f! t/ p
% P Q8 ~9 ^7 |1 C这个问题考察的是对补码的理解。 3 s! h% u, K% S) c) p仔细观察题目给出的四个数的范围:[ 0 , 2 62 ] [0, 2^{62}][0,2 ; Q. w2 b# r- C62 % b; v, B) \2 J) J ],这四个数加起来的和最大值为 2 64 2^{64}2 5 p$ Z6 R/ [/ I- u1 Z
64 1 d% X+ \. F4 r 。而C语言中,long long的最大值为:2 63 − 1 2^{63}-12 8 Z1 F! q' w) g
63 " i3 p" |9 G5 B2 R* w" j −1,就算是unsigned long long,最大值也只有2 64 − 1 2^{64}-12 & C) q/ {: @; l1 m64 ! `& c6 H0 m+ O: x. F2 M8 ?0 f −1。2 _" {& y- ]2 e+ {8 V2 |# e. j. w
但是我们发现,只有当四个数都取得最大值 2 62 2^{62}2 3 c! H* b; i. e- J2 M# W
62 4 h. e! W M% Q: d" D; `9 W& _ 时,结果才为 2 64 2^{64}2 / D' o; |+ g. q$ x8 y' N, w$ T) O
64 1 S# Q, A" N9 h* m ,所以可以对这一种情况进行特殊判断,具体参考代码详解。7 O N! B$ \6 \. j0 ^
三、代码详解 3 `) Y U, P* r% m" u, l#include <stdio.h>1 k/ E* E2 M1 |% z! w4 v
typedef unsigned long long ull; // (1) ; [( l& F+ R% yconst ull MAX = (((ull)1)<<62); // (2)* }" Z6 X0 d+ i* u2 K" D
% w2 s U4 k3 k3 ]9 l7 H $ `+ P1 X# J6 ?7 t+ |6 n% _6 Lint main() { ) N" D9 x2 X8 ~& ~+ j3 I( Y int t; 7 j; e: j: D. F9 |: ]. p ull a, b, c, d; ( k. Z1 Z0 L; U9 M% E# F scanf("%d", &t);: c. W( d% R* L: }- @
while (t--) {+ {) C4 r) I J3 E4 f( } D6 D4 y4 T5 m
scanf("%llu %llu %llu %llu", &a, &b, &c, &d); // (3) ; e" t$ s' W6 L2 f. w$ u if (a == MAX && b == MAX && c == MAX && d == MAX) // (4)3 t- y. a4 Z7 [$ }0 l, e
printf("18446744073709551616\n"); // (5)0 \5 h t1 Q% S. I' Y+ e# d
else: k# T0 X) O( h o. m- H
printf("%llu\n", a + b + c + d); // (6)8 L5 \ J$ l. L. t1 I2 u
} ) e8 F- C& ^* h, ]! Q q+ g return 0;5 w7 e5 Q1 [! R6 D
}; X% ?3 y& F" Q0 D& i
1! ^- U e. q) v3 f! e. g" V
23 D3 D3 r8 w# Q/ s
3" d1 [2 t6 w9 z' d- p5 H# S- w
4 L$ _* y( {5 I# |" S
52 h9 h: f# b8 r1 c( E: T. X
6 5 l! C. l8 c' J0 w+ L# o7( x. P- Z1 i9 G) Z# ^! j2 h
8 ) j2 O& R6 C2 @/ ]% K9 N5 |* ]$ ?" }9 ?. k
100 h* ?$ ^( {: s- ~+ k0 Q9 D, g
11 # r" g' D8 x# |* ~4 C1 k0 M$ ?12 9 A) }0 m8 }- c: a% u; N% P13& k) q2 n9 ?6 y% I, a
14 7 {( g* s8 {! j! z) [6 a }. v" ]15 , i3 W) Z" X5 m& y& M4 n6 m% u16 4 e8 n4 h, O& r( M+ h& `3 f4 y17' @) K7 p6 i% g: J
( 1 ) (1)(1) 由于这题数据量较大,所有数据都需要用64位无符号整型。ull作为unsigned long long的别名;% _$ R- N# i- S" m
( 2 ) (2)(2) 用常量MAX表示 2 62 2^{62}2 * C; T$ R, a- n0 [' G7 @
62" v% d# |" s( L
,这里采用左移运算符直接实现 2 22 是幂运算;) Q) h D+ u; Q2 k
数学 C语言! H8 g5 x$ Y: f$ _
2 n 2^n2 & \6 R" u/ Z: Y4 D& |+ A5 y
n 0 e* @8 |: B+ c/ N( R$ ~& C; ^ 1<<n9 q! U0 w `; D/ O7 U8 M
需要注意的是,由于 1 是int类型,所以需要对 1 进行强制转换。(ull)1等价于(unsigned long long)1;: D" k/ [% j$ q4 M/ J
( 3 ) (3)(3) %llu是无符号64位整型的输入方式; ) b6 T( k0 n! h( 4 ) (4)(4) 这里是对所有数都等于最大值的特殊判断,&&运算符的优先级低于==,所以这里不加括号也没事;$ w0 S, a& [2 g9 t
( 5 ) (5)(5) 由于 2 64 2^{64}2 , V5 K1 n# `5 v3 k
646 X" l) e* T! C' L
是无法用数字的形式输出的,所以我们提前计算机算好以后,用字符串的形式进行输出; " l9 X' G$ U/ Z& [5 Z' Y( 6 ) (6)(6) 其它情况都在 [ 0 , 2 64 − 1 ] [0, 2^{64}-1][0,2 5 L; i2 ~1 u' b. T& E642 L$ F& `0 f" V9 |
−1] 范围内,直接相加输出即可。 ; } D w* \* g! }) X由于这个专栏是付费专栏,可能对学生党不是很友好,所以作者经过再三思考,打算放出 300 张 一折优惠券, 先到先得。只要拿这个图片来找作者即可享受,仅限前 300 名。 : M) x) j) o$ |3 B为了适当提高一定门槛,你至少需要学会如何下载图片或者截图并且发送到微信里 🤣。 7 K* S- J8 h9 `- R# u 4 k7 H( S U8 \+ V' f; U. f1 B a+ ?# h& c5 z$ u$ @, e3、数据结构 & n1 I3 t z* j( m) G( y/ Y《C语言入门100例》上的例题,如果能理解前面 25 道,那基本C语言的学习就可以告一段落了,接下来就要开始我们的数据结构的学习了。 0 I( p5 H4 i- a/ h1、什么是数据结构 ! o* |/ F& G. |( U5 t7 }0 d你可能听说过 数组、链表、队列、栈、堆、二叉树、图,没错,这些都是数据结构,但是你要问我什么是数据结构,我突然就一脸懵逼了。5 [, H. z' B; |; U* r+ }
如果一定要给出一个官方的解释,那么它就是:$ i, R7 Y8 X% L2 N) l0 B
计算机存储、组织数据的方式。相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。往往同高效的检索算法和索引技术有关。( H" j4 V3 `/ V' ?6 K5 ]% m) B
4 I! f, ^/ ?* ?1 h 6 B) N1 w) w' T. B是不是还不如说它是堆,是栈,是队列呢? ; M, h- X- q+ l. `9 W是这样的,我们学习的过程中,跳过一些不必要的概念,能够节省我们更多的时间,从而达到更好的效果,当你还在理解数据结构是什么的时候,可能人家已经知道了栈有哪些操作了。 ( h8 J2 Q9 {" o: }9 }: q) F5 B2、数据结构和算法的关系 2 N: B# J4 V. N+ X; I z% E+ _很多同学搞不明白,数据结构与算法有哪些千丝万缕的关系?甚至有些同学以为算法里本身就包含了数据结构。& d* T8 v/ s" E% U& Q- B
数据结构主要讲解数据的组织形式,比如链表,堆,栈,队列。 & c3 x, D- V& v/ l而算法,则注重的是思想,比如链表的元素怎么插入、删除、查找?堆的元素怎么弹出来的?栈为什么是先进后出?队列又为什么是先进先出?& [1 i" B$ Z; I$ [+ f. |& c& I
讲得直白一点,数据结构是有实体的,算法是虚拟的;数据结构是物质上的,算法是精神上的。当然,物质和精神 缺一不可。% E: N* u2 ]5 y% O1 g4 u
3、数据结构概览" u2 [# O1 t0 u3 |- z
周末花了一个下午整理的思维导图,数据结构: 8 s6 k0 W. w% A- I/ m3 p' r6 K1 M, W7 y
# k/ K2 ^% ^1 X$ L: ^
常用的一些数据结构,各自有各自的优缺点,总结如下:4 y% X h$ W. P, e \" j \
a、数组 + k5 p ^1 B7 Z2 |/ d3 _& _& L内存结构:内存空间连续 " j2 k+ R1 h# ?; b- t实现难度:简单 ( w" B! K' Z: m! I下标访问:支持 : u, |+ }. Z" @ F: n1 E分类:静态数组、动态数组. g4 O# n; ] T8 Q
插入时间复杂度:O ( n ) O(n)O(n). h- q" l. j U/ N
查找时间复杂度:O ( n ) O(n)O(n) 4 [8 Q8 h+ \- C; n* R# k! f `删除时间复杂度:O ( n ) O(n)O(n) ! L5 g" V& r* {% [' j5 ^0 Z3 w2 n) s; c' c
8 u0 A+ `* j X2 }; F- Jb、字符串 ( R2 C, C) z1 W8 k Y( |/ L" {内存结构:内存空间连续,类似字符数组 ( J A( V- T7 g- z9 S实现难度:简单,一般系统会提供一些方便的字符串操作函数 7 q6 C1 R6 R9 f- F/ P' ?- n下标访问:支持 + O$ @6 Z- t- i y插入时间复杂度:O ( n ) O(n)O(n) 1 R3 y9 j7 x3 r$ w查找时间复杂度:O ( n ) O(n)O(n)1 }1 u3 ~" a& d s+ r2 D
删除时间复杂度:O ( n ) O(n)O(n); j( X9 {* \- F
4 o+ l( [, o6 V8 l% d, r3 |
* b) Y1 h. M6 [c、链表, ^6 L+ N6 B2 ^9 ?+ U6 j0 i
内存结构:内存空间连续不连续,看具体实现 4 _; e+ a: @* S- v实现难度:一般7 ]! B/ w1 B% h! k& \, S. t1 E
下标访问:不支持1 p" Q2 A% d: Z/ d. n, w
分类:单向链表、双向链表、循环链表、DancingLinks- L# F7 B9 Y1 }7 }
插入时间复杂度:O ( 1 ) O(1)O(1) * T3 ? o) X3 i F4 ?! o1 B查找时间复杂度:O ( n ) O(n)O(n) 6 r/ `/ x, b2 H% H+ J3 d删除时间复杂度:O ( 1 ) O(1)O(1)& a* @0 L% h! w8 x+ X/ K5 G. y0 i
" o" D8 v' b0 X* S
1 l, C- l, c0 F4 l) [ v
d、哈希表5 X6 m# W: `4 U! C, D9 b- |+ V" c
内存结构:哈希表本身连续,但是衍生出来的结点逻辑上不连续 3 @: `" [ C6 C J实现难度:一般 / h% { B( ]. N9 L( b |+ C6 A下标访问:不支持 / x) C0 \3 Y9 G5 _- \: d分类:正数哈希、字符串哈希、滚动哈希 6 w( ?3 J0 J2 M) ?( _' p插入时间复杂度:O ( 1 ) O(1)O(1) % \8 p- | b6 L9 M* A查找时间复杂度:O ( 1 ) O(1)O(1): J2 `" r- l: \7 N2 p- h$ Y: ]
删除时间复杂度:O ( 1 ) O(1)O(1) 0 U4 p" o2 ^; ^+ r% B* j4 u1 @8 q2 T. P
, E) z" P& G8 ^( h. r! S$ t4 ]( d# zf、栈; Y3 z# ]3 F0 R/ o
内存结构:看用数组实现,还是链表实现* f8 w! J6 U& T8 Q' Y8 d/ a
实现难度:一般3 r) s, u' Z: {
下标访问:不支持. ?1 E4 X& i2 L% p, x
分类:FILO、单调栈4 p8 ~7 y/ O* F& E! G- o" W
插入时间复杂度:O ( 1 ) O(1)O(1)9 f( V* Y6 j7 e
查找时间复杂度:理论上不支持0 `! d# ]" H ~/ c
删除时间复杂度:O ( 1 ) O(1)O(1): J- S: @/ R+ A6 y
: h- }2 y( Y, j( R
9 B# l+ G7 g- a' l& @: [/ bg、树3 q3 f0 Q2 X9 Z
内存结构:内存结构一般不连续,但是有时候实现的时候,为了方便,一般是物理连续,逻辑不连续 ' U0 c9 U( ~9 r e实现难度:较难9 Y: h: ]) I- w% R9 d! Q. M
下标访问:不支持6 `, Z7 c2 k1 Q
分类:二叉树 和 多叉树 3 |+ O4 J7 Y. R. [" K2 C9 |插入时间复杂度:看情况而定2 w% S* K7 A( g0 j% Q) c( w
查找时间复杂度:理论上 O ( l o g 2 n ) O(log_2n)O(log & _4 j8 M* Q. u! m2 / @! b$ U8 G4 u( P ! T. x, v" @: q+ t* x" X5 q9 M n) ) y# T2 |" o8 }0 H! W- p, \" }/ h删除时间复杂度:看情况而定1 o: V( ^4 Z9 s9 ]7 A; ^; J
3 {2 [/ d- E+ N( s
: [$ j7 z# q ~+ V# e1 E$ {1、二叉树 / v& m$ x4 Y/ T" d: P! k3 q Y二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。7 o" C9 S1 X7 G; z# b& ^/ L
其中,堆也是一种二叉树,也就是我们常说的优先队列。# D J5 d& f: J* s
2、多叉树 ) V4 i- U4 Y3 E& a0 c& M' \B树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。 $ \ o4 `& s) H+ W! W3 @/ Fh、图; q4 y: l& o% U+ |, f6 r3 [) R$ a* O
内存结构:不一定 7 i2 X) r" T- a. G; D1 E实现难度:难/ D& N$ S1 p' y+ |+ l( j
下标访问:不支持, m) E2 Z+ O4 P9 K3 l$ ^' q6 V
分类:有向图、无向图5 K3 Z. I+ I6 I8 m
插入时间复杂度:根据算法而定 6 @! G- k3 q: i2 z5 }% N查找时间复杂度:根据算法而定% x: [# f' v w1 u5 [; X
删除时间复杂度:根据算法而定 0 u( V' K" W9 b# t 0 [$ F5 z! y1 I4 o1 G, `# a% T+ N* J/ s( W6 x- M
1、图的概念 1 o- y6 j' F0 u7 p; J f7 v在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下:1 `! X; c" T; x% ~8 J: `6 Y
图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。 ; V, c3 N4 O 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 为权值,可以是任意类型。1 M1 V& q+ Z$ M1 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;/ E8 U% F1 Q/ H- G! k3 S
2、图的存储 ( u) e9 M- D5 j: K8 f对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。 $ Q# k8 N% F' `1 M7 ~ + L: P/ O+ k7 R' L/ Q( z L8 V- q0 E. [# c2 r x" z$ n$ f$ r1)邻接矩阵 / L, I, p I+ l) S; B$ W( I/ W邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。+ r- d% \' a! K, h1 d! X1 k
它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。 9 \0 U/ J* K. c; c3 W. i, Y[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[ j7 } Q$ k+ z2 _8 q8 E
01∞9∞0∞8320∞∞∞30 ; O9 f' y2 j; R' {8 K. e0∞3∞102∞∞∞0398∞0- t# c4 M- G* [: W
\right] % ]5 _, M. J& D9 Z f⎣" M3 r, Z7 y; [# z
⎢ ! A+ \9 v8 _# w1 t3 F+ I& u8 a( V⎢- y4 i0 T- s( O8 A6 Q+ l
⎡ j8 p2 O' l% B 9 d5 k3 s9 i7 X4 o . n0 B* y; [: }& \" E0: @- N$ I9 d8 L$ V8 z, ]
1 + g- {5 k) @( K9 u3 A+ A4 ]4 u$ i∞- f6 _, j x0 x; q: z
9+ n8 u/ J. S4 ~' a0 A
6 D) G2 S1 p! ` P 9 L. h6 N0 {9 B+ K, g9 X∞ 2 S( l$ Y5 y; w8 p0 - e) i ?! |. h( f1 n$ Y" x$ w∞ % S3 t: J5 R; E/ l8+ R- J1 F' e4 B% m6 m9 X3 R
4 p! S Z1 T$ M& ^8 n( e. p# \ + Z3 a- k9 ?. s" x. K2 F' S3 F; Q1 m
3+ a7 F7 I! b! F; \
2; S& o+ N8 I' }7 w4 A3 W% o
03 Z0 T/ m4 ^& _0 e. b+ @ {& ^
∞" c; m3 U$ r! [1 E% {% S; u
4 f( G& L/ ?, ?0 P3 S2 w) D6 a
# ]: q9 }. R. G& D. Q∞" I/ r9 Y9 b5 p) ]. b- M
∞8 G& s' i2 a0 B, D
3 6 O6 x% o+ c/ X! V7 \( ?: u1 L0$ p9 k3 K2 y' i5 e' L
, j* z! }8 C3 U6 f8 n
/ v0 v- Z" C3 [8 ^: ]
⎦ " h& m* D+ A9 X/ U; C⎥ + Z8 W- Q* A% i) c, Q( n7 q⎥% O5 o, B6 s. `6 J3 Q( O! ]1 b
⎤ 2 Q9 C& s) o, p+ O7 _ 7 b4 C% f- `, T8 T . g$ m! ^; C# ]% z* H
2)邻接表 6 y; s* X% b0 n$ R邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。# U S1 }6 d0 F; v! |+ ~+ f
它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。4 r1 N. @2 ~! _" {; S+ ~
如图所示,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) 二元组。3 _) _! d- a$ j, E1 u, O3 Z; o
( [4 f" y- ~& B" I( t* D, i: {5 }! x( o; L- b
在 C++ 中,还可以使用 vector 这个容器来代替链表的功能;7 `& A2 ]" g5 Z% c, l6 i
vector<Edge> edges[maxn]; U" F' ~2 ?8 [( e- t* {/ e
1 . _& S( e- E- q( C* w3)前向星7 E# q( |4 n! p1 l" R0 x" r, Y
前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。 & K7 W2 x7 |! s( Q5 G它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。5 w# m4 S8 }. ?/ C: X
如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。- {+ [* J" ?2 D# T7 `
- X0 p% J5 A$ U+ t Q( O. S$ r$ t4 a那么用哪种数据结构才能满足所有图的需求呢?) }1 e- O7 h. [0 a
接下来介绍一种新的数据结构 —— 链式前向星。 2 }1 O& X# _; H# k9 }3 g3 g4)链式前向星0 y9 w4 H' \* `6 u
链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 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 指向下一条边。; e' ? r _6 \7 e2 |2 f+ S# g5 r
具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。 3 K u- Y' l3 |边的结构体声明如下: 5 u: E- ]! T7 Q) g2 O0 e* P" X. @struct Edge {5 E7 l! E" }. s% _" P/ T! q$ X4 k6 p* ]
int u, v, w, next;5 ` h) a1 M7 c- s
Edge() {} 2 L6 z& D- V3 f+ q# K6 A Edge(int _u, int _v, int _w, int _next) : 9 X9 H7 L0 ]: u" J) i- A u(_u), v(_v), w(_w), next(_next) " |8 c( S P- i {6 v/ K& q; Z; c- e' d/ {7 M
} 0 j& c5 Y( B* E}edge[maxm];9 w, w4 @/ `4 r5 I) l/ }5 g
1/ M; `4 I8 l& S7 i. M2 M- { I
2 6 V1 ~9 A6 K: O \+ \/ i* @3 * ?$ s @7 A: H7 e4 % S1 |' F8 E" o! V% j: k57 l2 i% `8 f2 |$ S! T, Z6 w$ X8 @' r
6 u8 \- n A" L a' `* r" B- J: V
7 , c- U: J* I, w1 o' m/ y- Y0 H: W83 a# t6 ^* u/ r. J$ A& S2 V0 _' j
初始化所有的head = -1,当前边总数 edgeCount = 0;( [% P4 X8 d6 Y! }% X; f# R# E
每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下: ; p* Z+ }/ Y/ j' a! C1 Avoid addEdge(int u, int v, int w) {) }2 Z8 A7 }) S7 B/ P
edge[edgeCount] = Edge(u, v, w, head);0 e/ L- o; ?# F$ ^
head = edgeCount++; 5 Q" x, Y. g+ A7 v: |} 4 R0 l+ ?9 M# H- z1 . O+ y( u6 V$ D4 X4 u2 4 P7 G6 M& Q+ w8 q5 [/ Q3 t3 6 @% n$ m7 b# @3 K$ \4 6 ^; [; a7 h3 o- ?# g这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。! x5 k9 r- `3 k1 b" x# x# e' i8 A6 A
调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。 2 ~6 ], \4 m* w0 rfor (int e = head; ~e; e = edges[e].next) { % k6 g2 H% h+ ?" W int v = edges[e].v; 7 w5 B% R8 l+ X) {) v ValueType w = edges[e].w; 3 e0 u, u" [* p) N* U6 D ...) O' p4 b* R2 \1 o6 c
}. T' r8 l0 ~ @% L. k
1 * u2 D/ j2 y U% Q2 " h( R% U# L6 D3 . F8 \3 S) }% u+ }- i; M4 / U" e' H& I; |6 E5 3 O- w! x3 q5 X' K F文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。 + G! a# W: d V! i: U! Y4、算法入门 . v+ n2 _; b1 ^, w- }# u算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。2 B8 R. ?( s; ^( x; }% ?8 f
/ }+ N! s0 A5 U- n+ M2 A
8 A# \! r( e, d1 J% Y* g, r
入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。& q' {0 O8 Y2 X5 J/ D
对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。 % m' |, |/ }" c! a1、枚举 3 T; K4 I( U2 s枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。7 j0 p. {1 L4 W1 E; l! m3 f& }
对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。 9 P9 O6 G6 d% d2 w3 b9 q! ^1 T2、排序 g7 G- D2 Y1 L1 B既然是入门,千万不要去看快排、希尔排序这种冷门排序。 ! e9 n# I: v$ N5 p( j6 Z冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。7 V" M9 {' ?# g! m- ^+ z
C中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。 1 w1 w) n% O0 _# @0 }; {0 ]3、模拟 U" l( N: Z2 H ^8 i0 j模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。/ r, d" m# a6 W. g) ]
不管时间复杂度 和 空间复杂度,放手去做! ) S# n& o/ M9 u( q6 H. R6 n但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。5 D$ ~- D5 z) h- n/ h" f+ k
4、二分 , P* K4 V" q. B二分一般指二分查找,当然有时候也指代二分枚举。 ' d/ ?9 w2 l6 j. ? U/ b0 Q例如,在一个有序数组中查找值,我们一般这个干: 6 H, O$ o9 [* H# E+ L1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1;; F4 f o1 u4 a
2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种: - z8 ]% h$ {* Y. g6 N9 \ 2.a)目标值 等于 数组元素,直接返回 m i d midmid; & ~2 [& h. a% E 2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1; - q7 q$ j% S. Z% M 2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1;3 w6 c o4 w& e$ M- A0 F1 a
3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。; {1 H, ^" E; n0 L* S- k
5、双指针3 E- Y3 s$ {1 B: j- b9 F; S, W
双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。# ~& [7 }3 R9 N y& {: U* {
' |- U' g9 D3 ^8 s3 m6 Q$ O4 N5 h& | & e u) P! |; s# o4 K$ R6、差分法; `6 w' z9 h, J% V
差分法一般配合前缀和。 7 e4 ~! ^* g) H5 W对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题; 3 E' H& q) c) b" ^6 v$ q假设 [ 0 , x ] [0, x][0,x] 内的 g o o d n u m b e r good \ numbergood number 数量为 g x g_xg : a K& A7 v+ v8 ^6 i
x: }- F0 f' S/ b
; ~7 }0 C2 v+ z4 X- g ,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g 0 K8 V& r/ d; l- F* W
r " v1 ? c. I/ V: S / J6 u5 A9 H8 c( ]# j9 O7 `& l −g % I4 G+ ^7 g9 _- {0 w: ul−1 6 H* w" N4 R5 D- Q/ @! T6 u ) [0 q1 P+ E. s$ ^ ;分别用同样的方法求出 g r g_rg 9 d1 ]3 `8 G @( @# dr 7 o- z2 G" G h g( B' u/ ?. Q 9 t# v6 c& ?" ^* G 和 g l − 1 g_{l-1}g 7 U; j! ~! P+ g( T$ |3 l
l−15 L) Z0 K- P' \; X" b1 q6 a: z- k" U
; ~. g }. G7 p7 g ,再相减即可;' l L& f! t, x! C
8 q; E& I: S2 q f; O
8 O& E# N) z( ~3 d% t7、位运算3 f# g# b0 d; I# p: R( z1 ^
位运算可以理解成对二进制数字上的每一个位进行操作的运算。) {9 ~! W. G5 q' i% G3 `( R- J
位运算分为 布尔位运算符 和 移位位运算符。 ) d. Q7 m- W/ U. ?8 d布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。7 l' J+ D K9 K n) ^6 i% R
如图所示: s5 g/ n5 I% ?9 a
- T% l7 ~3 z: [$ m' |) t4 F