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