数学建模社区-数学中国

标题: ❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏) [打印本页]

作者: 杨利霞    时间: 2021-7-8 15:06
标题: ❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏)
# x8 D* I1 _; K) a+ r" j) L& t6 r
❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏)& f! y. n5 I8 A( N* D6 W
3 w  x6 A" [3 z- n3 ~6 n1 g
前言
8 Y. Z% w6 d/ |% u) b; s4 l, W' R  所谓活到老,学到老,虽然我感觉自己已经学了很多算法了,但是昨天熬夜整理完以后发现,自己还是个弟弟,实在忍不住了,打算把 算法学习路线 发出来,我把整个算法学习的阶段总结成了五个步骤,分别为: 基础语法学习(重要)、语法配套练习、数据结构、算法入门、算法进阶。本文梳理了这五个大项的思维导图,在下文会有详细介绍。
8 ]1 N) o; k* V( W  希望各位能够找到自己的定位,通过自己的努力在算法这条路上越走越远。
7 X" |! Q. q4 D2 ?  刚开始切勿心浮气躁,千万不要给自己立 flag,说一定要把这么多东西都学会。就算你的精力旺盛,日夜操劳,时间也是有限的。所以,首先是明确我们要做什么,然后制定好一个合理的 目标 ,再一点一点将要学习的内容逐步付诸实践才是最重要的。
# H1 S5 q% Z$ Q  每日一篇C语言打卡,目前更新到:光天化日学C语言(20)- 赋值运算符与赋值表达式 | 让代码变得更加简介(建议收藏)。
" Y2 R0 o( \  _; q# X. i( m) @
( y, I: C' A" Y0 P
( o! L" R0 \6 V  T5 i- Y- n
7 \* @9 ]. P& h" Y! {" [
* [- m9 n1 j/ g# O

' \1 V& w( N1 Z( l% K1 I

5 m* {% C$ b+ [$ E& W' B4 Z3 {& h
% N, r" i2 j5 O) p0 \) w& w1 P

* K# ]# h+ g$ Z- Y0 g图片较大,文章中有拆解,需要原图可以留言找我要哈0 p! o9 ]8 d+ W8 d, d9 V, o  g
1、基础语法学习  X. a7 d6 y) U, U
算法是以编程语言为基础的,所以选择一门编程语言来学习是必须的。
8 c3 I% d, i  h, C! K7 d" a* h因为作者本身是C/C++技术栈的,所以就拿C语言来举例子吧。如果是 Java、Python 技术栈,可以跳过 C语言相关的内容。这一小节,先给出学习路线图,然后我再来讲,每部分应该如何去学。4 E# a( j0 r+ G' O5 Z: Z

/ a& c5 N* L% o, s) M+ N" F3 L* y/ V

6 q* `3 l3 R9 i$ K" @2 W7 S
2 T+ q$ ~' I% \( S8 |3 C/ s) W

$ b! C9 H0 j9 u, X1)HelloWorld
* V7 x1 a' Z9 Z; v无论是 Java、Python、C/C++,想要上手一门语言,第一步一定是 HelloWorld,先不要急着去配环境。如果环境配了几个小时,可能一开始的雄心壮志就被配环境的过程消磨殆尽,更加不要谈日后的丰功伟业了。0 [7 H% B. _1 K9 A
2)让自己产生兴趣
. s" P, Z! @1 K4 x# |: \0 u9 z: }所以,我们需要让这件事情从一开始就变得 有趣,这样才能坚持下去。比如找一个相对较为有趣的教程,这里我会推荐这个:《光天化日学C语言》。听名字就比较搞笑,可能作者本身也不是什么正经人,哈哈哈!虽然不能作为一个严谨的教程去学,起码可以对搞笑的内容先产生兴趣。从而对于语言本身有学习下去的动力。
/ M' L# @. |& A7 A& k& n刚才提到的这个系列,可以先收藏起来。回头再去看,它讲述的是 对白式 的 C语言教学,从最简单的输出 HelloWorld 这个字符串开始讲起,逐渐让读者产生对C语言的兴趣。这个系列的作者是前 WorldFinal 退役选手,一直致力于 将困难的问题讲明白 。我看了他的大部分教程,基本都能一遍看懂。算了,不装了,摊牌了,因为我就是这个作者。
. b! f5 g" \7 o# C3 b3)目录是精髓
6 p& H( o+ Y7 B4 P2 M然后,我们大致看下你选择的教程的前几个章节,那些标题是否有你认知以外的名词出现,比如以这个思维导图为例,前几个章节为:0 y6 a# y: v5 F! e
1、第一个C语言程序
* j6 L) w4 M5 t" s2、搭建本地环境1 z! {) j. f" G$ q) ^* G3 H) K
3、变量' b2 k0 r4 V( ~& j. n3 N
4、标准输出
8 h3 K0 \2 `, h5、标准输入
; u* l; Y6 j3 Z9 `6、进制转换入门8 w) p! T, {- n+ c
7、ASCII字符
6 x' b0 J: B8 H3 s) s8、常量
, H" F7 h; ^* D
4 K# @, y0 y# y" P$ p
) d5 n) P. x3 I, H) K
如果你觉得这些名词中有 3 / 4 以上是没有什么概念的。那么,可能需要补齐一些数学、计算机方面的基础知识。反之,我们就可以继续下一步了。
" R" F( k$ B: z+ t4)习惯思考并爱上它( A3 P$ w: r* _! G+ l# p
只要对一件事情养成习惯以后,你就会发现,再难的事情,都只是一点一点积累的过程。重要的是,每天学习的过程一定要吃透,养成主动思考的好习惯。因为,越到后面肯定是越难的,如果前期不养成习惯,后面很可能心有余而力不足。
! y' H$ N2 _: s2 }) ?0 b& V1 W6 M就像刷题,一旦不会做就去找解题报告,最后就养成了看解题报告才会做题的习惯。当然这也是一种习惯,只不过不是一种好习惯罢了。
% f! ?( L. g( G0 z5)实践是检验真理的唯一标准) A1 }0 E$ Q% W. i+ @3 q
光看教程肯定是不行的,写代码肯定还是要动手的,因为有些语法你看一遍,必定忘记。但是写了几遍,永世难忘。这或许就是写代码的魅力所在吧。% E0 f- E( w3 \, j$ p2 D0 f5 F* D
所以,记得多写代码实践哟 (^U^)ノ~YO
- ~! L; y1 E( ^6)坚持其实并没有那么难7 ]$ T  Y6 a1 W# D/ M
每天把教程上的内容,自己在键盘上敲一遍,坚持一天,两天,三天。你会发现,第四天就变成了习惯。所以坚持就是今天做了这件事情,明天继续做。7 s6 q' B6 W+ Q0 u. r
7)适当给予正反馈3 i& @0 l* U+ I7 d
然而,就算再有趣的教程,看多了都会乏味,这是人性决定的,你我都逃不了。能够让你坚持下去的只有你自己,这时候,适当给予自己一些正反馈就显得尤为重要。比如,可以用一张表格将自己的学习计划记录下来,然后每天都去分析一下自己的数据。
5 M. z6 t5 `! f2 l8 Z当然,你也可以和我一样,创建一个博客,然后每天更新博文,就算没有内容,也坚持日更,久而久之,你会发现,下笔如有神,键盘任我行!更新的内容,可以是自己的学习笔记,心路历程 等等。
( `; x% y- P( O1 r3 n看着每天的粉丝量呈指数级增长,这是全网对你的认可,应该没有什么会是比这个更好的正反馈了。& n  v( Y% O5 ]- p& k% _; O& [
8)学习需要有仪式感# n- u' B8 v* a
那么,至此,不知道屏幕前的你感想如何,反正正在打字的我已经激情澎湃了。已经全然忘记这一章是要讲C语言基础的了!( a1 x% X" u7 Y8 y" l/ m: B/ T
介于篇幅,我会把C语言基础的内容,放在这个专栏 《光天化日学C语言》 里面去讲,一天更新一篇,对啊,既然说了要坚持,要养成习惯,我当然也要做到啦~如果你学到了哪一章,可以在评论区评论 “打卡” ,也算是一种全网见证嘛!- z6 t4 H$ C: ~/ S; Q; b
我也很希望大家的学习速度能够超越我的更新速度。
# W- k$ ]9 V7 \2、语法配套练习' _- ]5 ?! B" q" O0 P) k
学习的过程中,做题当然也是免不了的,还是应征那句话:实践是检验真理的唯一标准。( c8 r. N; ^3 ^9 Z$ Z
而这里的题库,是我花了大量时间,搜罗了网上各大C语言教程里的例题,总结出来的思维导图,可以先大致看一眼:
% @* c" D! i+ K0 U# |( F# M7 u, q! w9 O& o- ~" ^

/ c2 E9 ~/ n' i8 R7 @: V$ H- C8 \. T! E0 O5 {/ k* X
" N4 E; Z1 v& u3 P( @9 f
从数学基础、输入输出、数据类型、循环、数组、指针、函数、位运算、结构体、排序 等几个方面,总结出的具有概括性的例题 100 道 《C语言入门100例》,目前还在更新中。
. D: f+ |# q" d) Z! Q4 X( c. g* E6 J这里可以列举几个例子:
! V6 `) t8 y* B" J$ ?- a1、例题1:交换变量的值
3 A6 T$ _' W# @" p8 ~# H$ h% T一、题目描述. {7 ~9 j: c/ `1 w  Q; q
  循环输入,每输入两个数 a aa 和 b bb,交换两者的值后输出 a aa 和 b bb。当没有任何输入时,结束程序。
$ ^+ R9 E1 O- v+ A- D! P
  j5 @; l7 ?: N

; d+ l; L  t. u3 d3 {/ h4 L/ l) D  r0 l

: r6 O) p) \' n9 S& v" B: J二、解题思路" R. v6 n" t5 y
难度:🔴⚪⚪⚪⚪
" L! q, P1 v% z. E% d, ]0 A: O+ M: W2 t4 k

/ l. w! o- V6 j% l5 I这个题的核心是考察如何交换两个变量的值,不像 python,我们可以直接写出下面这样的代码就实现了变量的交换。1 _1 I. K* F/ `; r& d* W" {
a, b = b, a% K; Q; w7 x! r9 Z& N  A2 J" s2 [
12 }6 S: E- ~: g( _9 ?
在C语言里,这个语法是错误的。
6 t6 u$ C+ Z0 a2 o8 n: j9 s( h我们可以这么理解,你有两个杯子 a aa 和 b bb,两个杯子里都盛满了水,现在想把两个杯子里的水交换一下,那么第一个想到的方法是什么?
" P  @* M4 x3 A1 i0 B5 m. _当然是再找来一个临时杯子:
' p- d! v/ T0 [: C) E7 N( r% a  1)先把 a aa 杯子的水倒进这个临时的杯子里;) e) H) L9 u" G# R) Q
  2)再把 b bb 杯子的水倒进 a aa 杯子里;3 S& w+ R8 `1 a. n' ?" d% u; D6 E4 p
  3)最后把临时杯子里的水倒进 b bb 杯子;3 b, a$ ?% K0 t; R) V
5 ]) U1 j2 j. O' q6 z9 V0 U, Q1 T

& ?( P/ |7 m" }8 K( g这种就是临时变量法,那么当然,还有很多很多的方法,接下来就让我们来见识一下吧。
- r1 r; G) m9 V: a5 Q0 O3 ^: p, s/ J. v) w+ R' P( d

6 A& `/ ]6 `* S0 o8 a三、代码详解4 `" a; I2 x+ v, f
1、正确解法1:引入临时变量
2 a% z/ n0 M8 D0 O8 \5 \! J: @#include <stdio.h>  ~  q0 L# _! ~" A# J6 f
int main() {. v* t7 F5 t, k+ W6 }+ i3 `
    int a, b, tmp;8 h- Q2 Z6 d% B  O: h
        while (scanf("%d %d", &a, &b) != EOF) {! J. P6 O# K; ?& p8 @% n  P$ D
            tmp = a;   // (1), l1 I2 N" [/ f5 l. F" H
            a = b;     // (2). v& L" X+ g' L* `) _' t' A  a7 y
            b = tmp;   // (3)4 X9 T" z4 m7 `+ E6 e0 H
            printf("%d %d\n", a, b);5 S+ ^: F/ s- z
        }
8 h1 Y7 W+ ]' i        return 0;
9 g0 C/ p+ {$ ]7 Y; a}
0 l& k% |/ ]" ^0 T# _4 g1 R  Q1
6 o9 {; S2 F3 }) G2
. T5 y; m/ J( O- ]3& i% W. i6 i# Z
4* s; z9 ~( T- h
5, h* X9 V4 a+ t) H; p+ ?& b9 h7 a
6
" g8 I, c$ ~* P) v6 }7
6 o; ?$ q; S. `9 W85 q  z0 c4 u) m" _) C
91 s( K) t3 p, A9 j$ z8 n) \- w3 ?0 k
10
+ c/ p- x' @7 [$ O" O2 P) q8 b# Z11
: @; T2 ?# R; n( G* ?9 z( 1 ) (1)(1) tmp = a;表示把 a aa 杯子的水倒进这个临时的杯子里;2 U1 W& ^) s' A6 `: t. l2 ^
( 2 ) (2)(2) a = b;表示把 b bb 杯子的水倒进 a aa 杯子里;. P. ~7 Y: W1 U9 b3 X. C0 {
( 3 ) (3)(3) b = tmp;表示把临时杯子里的水倒进 b bb 杯子里;5 w* j6 T( e3 E4 m5 u) I4 g0 ?
这三步,就实现了变量 a aa 和 b bb 的交换。
$ o& _3 w# B6 i' o( L0 N4 W2、正确解法2:引入算术运算5 A. C! T( s, G- h
#include <stdio.h>5 {+ A' Q$ t- R9 \( ]6 J: ^
int main() {
3 s( X8 ^# B$ A5 P0 C4 j& b0 e    int a, b;" O+ J# J; G) z& c- A4 [! D
        while (scanf("%d %d", &a, &b) != EOF) {
# a& J# x7 f, G: `1 c- {- }2 V            a = a + b;   // (1)
. C( F$ s) L5 p            b = a - b;   // (2), I% i3 h0 M" B4 i( h$ O
            a = a - b;   // (3)" P6 B. Q. X* }$ F# D
            printf("%d %d\n", a, b);
2 G, J7 Q2 n: T+ J5 h  [- @3 A        }' _7 H+ E: O5 @* ?# c3 l: ?2 o
        return 0;
: c8 b9 y* Z6 V3 d( N* V& j$ G" s( P}
4 [6 j/ I. V  j' H  v4 D1
% J1 M- A  _- j/ E! c1 G" ~5 S3 b0 y" M2" ]4 c. ^1 G7 e! i
38 A4 \2 @5 q2 ]; l/ u; T3 \# y$ _/ G
4
9 T3 q6 _2 y% e0 O5
- p9 }" k9 K7 T, L: s- @6
5 E1 j1 m; y: C( Y" n" V, C7
7 A% l  @+ X$ s' |8
4 g8 M  K, f% a% U: O7 a+ L; y9
: ^& z% Z( F$ B" {10' F! v- b3 M. W5 L  b4 d" Z, ]: t+ T
11
* @" g1 F2 v) m  [! R, S( 1 ) (1)(1) a = a + b;执行完毕后,现在最新的a的值变成原先的a + b的值;
3 k+ M8 ]2 O; v; D( 2 ) (2)(2) b = a - b;执行完毕后,相当于b的值变成了a + b - b,即原先a的值;
6 d& ?. f& k" R: F  B( z6 X( 3 ) (3)(3) a = a - b;执行完毕后,相当于a的值变成了a + b - a,即原先b的值;+ K5 P; N9 `, r
从而实现了变量a和b的交换。: P. E+ `" D# @7 C; k& h
3、正确解法3:引入异或运算: ?; T( k, M& {% ~* [1 D; l7 c5 e
首先,介绍一下C语言中的^符号,代表的是异或。/ _0 l" E# q$ w9 A0 ]% e
二进制的异或,就是两个数转换成二进制表示后,按照位进行以下运算:
9 g4 W( s' Q' |* c' }左操作数        右操作数        异或结果
0 {5 P0 ^8 C" ]! E* R- C5 q0        0        04 d8 V4 j& z9 b. z6 N
1        1        0
& g" X6 T  N  P' V* x! I4 t0 g. V0        1        1
2 E" h$ h% `" {5 H% ]1        0        1
. q  P$ H$ k2 s# n也就是对于 0 和 1,相同的数异或为 0,不同的数异或为 1。
7 ]* @! \, r: z, N0 B" Z8 i5 G这样就有了三个比较清晰的性质:5 B( E2 W3 d. s+ ^( l
1)两个相同的十进制数异或的结果一定位零。. z1 k% E6 U2 X* {8 Y
2)任何一个数和 0 的异或结果一定是它本身。
' |$ r3 E& b6 |5 Q# a$ z, n3)异或运算满足结合律和交换律。
' n# z( L- `# {: ], _#include <stdio.h>
* r( Z8 m1 f; v* s# sint main() {5 x% w2 |7 w2 p/ x
    int a, b;& M  F5 \5 Z+ H+ e6 S6 H! _
        while (scanf("%d %d", &a, &b) != EOF) {3 d8 s" ?0 q) P
            a = a ^ b;   // (1)7 _1 w0 M/ @) W) Y
            b = a ^ b;   // (2)
/ r5 c5 [9 P  X7 y- p            a = a ^ b;   // (3)  W- e" l% f# x* U; P
            printf("%d %d\n", a, b);4 z: v1 K1 Z% ~  G! |- v% f
        }) |! j  \6 L6 F& |
        return 0;9 a; p5 h/ e1 G: T5 F$ w
}
( J6 e4 I" c* b, n" u/ }6 I1* \$ V7 J$ c: l
2
) |1 [! d* x* N9 x' P' T7 P3, c0 A6 s! p2 x2 S
49 D, J. A$ S: s. C0 N' P, o8 E7 f6 P
5
- [! P$ S# p$ N8 \% x/ X6 y62 W8 X  Q% Y9 ~! V7 X  O# f' r9 c
7
; I9 f, c; z! @% u7 o; M9 ^8* h/ E: b+ \# A9 L! n
9
3 x' C* K* E0 x$ n7 H6 e10
) X, N. m: W2 {& N5 H; M11
' O9 d% A2 x& k6 w; x$ W6 k, z8 V我们直接来看 ( 1 ) (1)(1) 和 ( 2 ) (2)(2) 这两句话,相当于b等于a ^ b ^ b,根据异或的几个性质,我们知道,这时候的b的值已经变成原先a的值了。  I' O+ w. O& c7 X" D2 V
而再来看最后一句话,相当于a等于a ^ b ^ a,还是根据异或的几个性质,这时候,a的值已经变成了原先b的值。
* M. ^8 Q) G. @6 F4 O从而实现了变量a和b的交换。
/ v/ g" h. H& p4 h. s" H) c" Q# E4 H, ]
2 w  {0 C& b  K+ p" A0 v, A6 V7 f8 ~
4、正确解法4:奇淫技巧0 I& o6 F3 C( ^3 R. Q/ D
当然,由于这个题目问的是交换变量后的输出,所以它是没办法知道我程序中是否真的进行了交换,所以可以干一些神奇的事情。比如这么写:
' ^. r9 B7 y% k0 s1 C) W2 i* [#include <stdio.h>
$ I& E' n! X7 V, X  ?# t$ L- ?int main() {
& p4 S- `* T6 g5 ]    int a, b;5 E+ [" ^4 c5 T. t, C  U
        while (scanf("%d %d", &a, &b) != EOF) {& j% ~% U# R$ X" p( z; \' ]" r
            printf("%d %d\n", b, a);
2 @8 v$ S8 t0 F: U3 E$ L3 H        }: ~; O( }: S5 A' r1 C( X1 M
        return 0;7 |2 X4 d8 {8 S2 f
}
4 k+ t+ \# B/ _; Z8 z+ b3 S1
* |. T$ g$ z, X" w! q29 d6 K7 k, x# g
37 f/ S% ?0 P- Y& Z
4) [3 i0 S, `2 E  F1 @  f; m
5* d$ k; T( O7 @4 v1 {6 R9 z% }8 {
6& @1 e3 M; k) @5 a
7
7 ?7 l9 A0 E  s9 F; L9 h8$ u0 r* X3 {3 `6 N8 l6 Q1 g
你学废了吗 &#129315;?
- R! b5 `( B: _4 X& e2、例题2:整数溢出
. ?% V9 S$ @' ^4 N' S4 u一、题目描述% M1 ]& }) D( E  V7 j& d- t) h
  先输入一个 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 2 v) x9 j7 E. ?" R2 F+ _( u! P
62
; y; ^- q2 f3 @/ u ),输出 a + b + c + d a+b+c+da+b+c+d 的值。
& d- a* P! l" t& a% T
3 j0 N4 m7 A2 e1 H- n+ r  K3 R

; J( C5 q! s- c' k( e' \0 Z二、解题思路
4 ?9 I; Z" u) M" Y难度:&#128308;&#128308;⚪⚪⚪
! a: d1 A- {- H9 {
" N. }$ h- |  e/ V' y/ M" M# D) w5 X' s
/ y" N/ y6 a% g4 s% w
这个问题考察的是对补码的理解。6 r1 Y) l- j' a* V1 }9 n
仔细观察题目给出的四个数的范围:[ 0 , 2 62 ] [0, 2^{62}][0,2
' L/ Q* I. Y* Z2 e2 r# \5 \62
' {9 w6 N4 G4 Y+ M ],这四个数加起来的和最大值为 2 64 2^{64}2 ) o2 I, [3 [! {; a* T, [
645 k+ @! }4 \, d! ?% y/ u* F8 v
。而C语言中,long long的最大值为:2 63 − 1 2^{63}-12
( o1 ~: g; L& B5 J* V; F7 T4 u63
2 c5 R0 g4 Q- ~" ` −1,就算是unsigned long long,最大值也只有2 64 − 1 2^{64}-12
$ f" i3 u; |1 ~# f64
; x1 H6 q3 L& z −1。
! p9 [  j# w9 ?0 k# O但是我们发现,只有当四个数都取得最大值 2 62 2^{62}2 ; H# m& n9 o% Z; W, a" K
62
) i' d' i% G/ w6 L  时,结果才为 2 64 2^{64}2 2 V8 T  R7 `% H' S. x7 j
649 D2 A- ~4 o' \1 ~* S" Z/ n5 M
,所以可以对这一种情况进行特殊判断,具体参考代码详解。
+ S$ d! B4 k+ O% d三、代码详解
) T. u, H2 R7 i- K0 a' j#include <stdio.h>) j5 P' ~# v, |3 {5 f+ D& s% {
typedef unsigned long long ull;                           // (1)
, z8 }1 R, n6 Nconst ull MAX = (((ull)1)<<62);                           // (2)
& N4 G1 m$ m4 m. {" Z
, {, O1 H4 q9 l$ \

* m$ R5 s0 }! y* gint main() {; m& v" h, e, W8 Q& o
        int t;
0 }' |8 d- Z0 ^" D  U& ^* A        ull a, b, c, d;6 D6 s  X$ f, Y3 k" v
        scanf("%d", &t);
# F/ n, |* }! M, c. Z8 {$ R6 a1 ~/ n        while (t--) {/ f* B1 Z& ]2 d1 ]6 ]% p) Y
                scanf("%llu %llu %llu %llu", &a, &b, &c, &d);     // (3)9 ~# Q, G( U5 }8 u. t
                if (a == MAX && b == MAX && c == MAX && d == MAX) // (4)+ B6 D  Q+ q8 n; R  l
                        printf("18446744073709551616\n");             // (5)$ z2 a% j5 f0 {1 a# u
                else) L" ^* L! M/ W1 t+ f, A- b
                        printf("%llu\n", a + b + c + d);              // (6); D  e9 L' y+ g
        }; s4 b* t3 I' Q  |- q
        return 0;
- C; T: B1 m/ D1 E}: z; S/ s# l2 L( ?" m$ V
1
5 g8 w5 c  ]* U/ h+ v# ?: s24 ~' r+ n/ y# }. e
3% m# r% {  Z( g
42 t- E" o( Q  R& r3 B$ ?
5
4 l0 f( Y) T* V( _& Z) ~6/ F& Q6 L4 R$ o3 e. ]6 h/ @
7
+ }2 t& N% M* g. k8
# R% T4 i" U% W& M6 B! M- V" }95 _+ w  x! W  A% G& k
10
8 ]# l) ]- |* g& |11
' B' E. n' v& M' n3 t12
0 ?' r" [. K9 {* U' ]- F132 Q4 Q7 I2 J$ t* G8 E, j
14
; f' u" x! U- `# f; R15/ f8 j3 z( Z, `
16
8 h: U  L, _9 G" s1 b0 v% q- d179 C( H# Q5 T  d) F6 Q
( 1 ) (1)(1) 由于这题数据量较大,所有数据都需要用64位无符号整型。ull作为unsigned long long的别名;3 z& x8 G' H! l4 E2 B  C
( 2 ) (2)(2) 用常量MAX表示 2 62 2^{62}2
0 t$ j1 x0 X8 g: q62
& a. J4 m9 c  t, G9 G2 n' b ,这里采用左移运算符直接实现 2 22 是幂运算;
; Q' A$ v9 E  J  S5 l数学        C语言
( h6 I' F+ g6 ^( G4 }' ?2 n 2^n2
6 j2 q) X6 L( e4 y  L& m1 |) jn
7 `/ _+ A* m% _6 h" w         1<<n% k4 L4 Q( g' A! u
需要注意的是,由于 1 是int类型,所以需要对 1 进行强制转换。(ull)1等价于(unsigned long long)1;! i. u9 p- W+ o+ Q; ?" V
( 3 ) (3)(3) %llu是无符号64位整型的输入方式;& g6 U* [5 W3 d4 i, _/ j6 |' }
( 4 ) (4)(4) 这里是对所有数都等于最大值的特殊判断,&&运算符的优先级低于==,所以这里不加括号也没事;4 q& `# t  a* P0 E' ^4 L: Y
( 5 ) (5)(5) 由于 2 64 2^{64}2 # R/ l7 j; l7 C+ m7 r2 n! L
649 @2 M  e) G$ C2 d
  是无法用数字的形式输出的,所以我们提前计算机算好以后,用字符串的形式进行输出;
% Z' D. s6 }) u& W: U( 6 ) (6)(6) 其它情况都在 [ 0 , 2 64 − 1 ] [0, 2^{64}-1][0,2 & H2 d+ Y  F/ A$ ?# R$ O2 N
64( N5 G2 v- y: O6 {9 k/ i4 Z
−1] 范围内,直接相加输出即可。
/ z. Y* z+ r: p, D. v& j! X! h2 A由于这个专栏是付费专栏,可能对学生党不是很友好,所以作者经过再三思考,打算放出 300 张 一折优惠券, 先到先得。只要拿这个图片来找作者即可享受,仅限前 300 名。& \9 \! J, d* N; H4 ]) x5 n
为了适当提高一定门槛,你至少需要学会如何下载图片或者截图并且发送到微信里 &#129315;。8 |% p# D' N: K6 {$ Q$ u) F* ?

$ N. D2 B% V$ p: q& r6 t
! w( [) G" a0 X; S; x% h7 @
3、数据结构3 [4 I3 V5 J5 N4 Z& l
《C语言入门100例》上的例题,如果能理解前面 25 道,那基本C语言的学习就可以告一段落了,接下来就要开始我们的数据结构的学习了。
& j& s( K6 B# o. K& G, W" L9 ~" l1、什么是数据结构
6 i2 ^# {; z& c* _7 w你可能听说过 数组、链表、队列、栈、堆、二叉树、图,没错,这些都是数据结构,但是你要问我什么是数据结构,我突然就一脸懵逼了。
+ M& t0 ^) k# A如果一定要给出一个官方的解释,那么它就是:+ ?( {. I; k4 m8 O" f9 g
计算机存储、组织数据的方式。相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。往往同高效的检索算法和索引技术有关。
0 a1 v" m" G( s  s% G# [. J2 _
2 h- l- K5 u. Q8 F
! p$ m/ k  R2 Y# P3 B2 d
是不是还不如说它是堆,是栈,是队列呢?9 B- m5 U" ~1 }$ ~2 T$ M( {8 g
是这样的,我们学习的过程中,跳过一些不必要的概念,能够节省我们更多的时间,从而达到更好的效果,当你还在理解数据结构是什么的时候,可能人家已经知道了栈有哪些操作了。! _- ^% O# M4 P- s) x$ g# ?
2、数据结构和算法的关系
3 C8 U6 ~; H2 O2 Y/ {1 i# @很多同学搞不明白,数据结构与算法有哪些千丝万缕的关系?甚至有些同学以为算法里本身就包含了数据结构。& S, {2 [  O+ _' b& k7 p* B
数据结构主要讲解数据的组织形式,比如链表,堆,栈,队列。, W, y4 `) h+ U- k( m' l
而算法,则注重的是思想,比如链表的元素怎么插入、删除、查找?堆的元素怎么弹出来的?栈为什么是先进后出?队列又为什么是先进先出?
% d% v6 m6 Q! @$ K; o/ J: |: i讲得直白一点,数据结构是有实体的,算法是虚拟的;数据结构是物质上的,算法是精神上的。当然,物质和精神 缺一不可。( n6 I8 s4 C0 E# `2 f
3、数据结构概览
$ @  N- b% S5 `. w- U2 e$ M& L周末花了一个下午整理的思维导图,数据结构:. L, u- W; j( G* Z! ~- a
* M- y! d9 r' t, G
, W/ B( [, T' c5 M
常用的一些数据结构,各自有各自的优缺点,总结如下:  J* X( j! N7 {3 C4 _1 z
a、数组/ V8 ~# E8 J0 U* [2 d& t7 K
内存结构:内存空间连续, K; s0 n4 M$ p, D
实现难度:简单
0 \  v) S/ k( `3 {) ~4 O9 G下标访问:支持
+ l/ g9 R: k- b; D  U分类:静态数组、动态数组
, J& C4 y% ]( s- j8 W4 U6 d& b插入时间复杂度:O ( n ) O(n)O(n)% N, E; z: b0 {1 O+ y0 W
查找时间复杂度:O ( n ) O(n)O(n)
% C  n' B  W+ E' u3 X删除时间复杂度:O ( n ) O(n)O(n)
% d& `9 ]: A3 r: ]6 |* X" Y! B8 o! n' ^4 w

4 ^4 P6 Y( L* j& h# p. tb、字符串1 L. v; e7 @# r( _8 L. {
内存结构:内存空间连续,类似字符数组3 e0 d% Q) g  s6 m0 P
实现难度:简单,一般系统会提供一些方便的字符串操作函数( @$ z1 p* R: z6 J
下标访问:支持8 a: [6 j: m) p+ x' J
插入时间复杂度:O ( n ) O(n)O(n)5 E5 Z. \# p! |# z: |; t3 N4 D. @- [$ W
查找时间复杂度:O ( n ) O(n)O(n)& r3 g. x3 k- T+ K2 [* D; |
删除时间复杂度:O ( n ) O(n)O(n)- k8 U9 t% v5 c4 Q( [

2 L8 a, Q  x, X* O; S" h2 V

2 p+ A6 h! w# v  z# ]( l: [+ Z! Zc、链表. o. R, M0 x9 ]- Z' X0 Z
内存结构:内存空间连续不连续,看具体实现4 L" q, w! V- ~) _& X
实现难度:一般6 C: m& X/ R" }4 c8 ]8 p/ J- ^9 X2 x
下标访问:不支持
, @- e/ k! _! U4 Z- p2 w) U1 `) u分类:单向链表、双向链表、循环链表、DancingLinks
1 }# b6 c+ S2 X8 t2 Y3 c2 o插入时间复杂度:O ( 1 ) O(1)O(1), e! a6 E* c1 A/ T- c
查找时间复杂度:O ( n ) O(n)O(n)+ N7 _+ x$ G* Z
删除时间复杂度:O ( 1 ) O(1)O(1)+ f" U% u" Z5 j5 d* Z' d5 _2 e

! A7 j" E$ X- S5 _3 W, A1 }

3 B) g( B+ M1 E0 k( ]d、哈希表
4 G# @' h3 l0 x$ c4 |. A8 ~- c' O内存结构:哈希表本身连续,但是衍生出来的结点逻辑上不连续
$ H# Y9 n. T8 t' ?& k4 N实现难度:一般: V' B( ~* I* Q" |9 L
下标访问:不支持
, u, }3 O3 b* Z$ {- B3 v分类:正数哈希、字符串哈希、滚动哈希0 E/ N. f$ h( Z
插入时间复杂度:O ( 1 ) O(1)O(1)
' ^) F- s5 S2 l5 P5 U  S查找时间复杂度:O ( 1 ) O(1)O(1)
/ r) F7 N2 J$ a' @7 m4 s删除时间复杂度:O ( 1 ) O(1)O(1)
. u* O$ @2 ~  m( s) p& R" C3 ^7 z1 L' o- ]4 t; O

& [4 D1 V+ m$ Y: me、队列6 v- r. T" H) _. _9 A( Z
内存结构:看用数组实现,还是链表实现
& {6 x! b) k  D7 f# {实现难度:一般, m, i* M9 B+ Q+ j, Q. Y+ g5 {
下标访问:不支持
) m2 X5 i  ~$ E2 j# A5 ~分类:FIFO、单调队列、双端队列) l6 C* Z' Q. e( m
插入时间复杂度:O ( 1 ) O(1)O(1)) L" ~0 p# z- Q! X8 q9 l; e
查找时间复杂度:理论上不支持
, _* ?0 E# [3 [& C& \( @8 i$ N5 q删除时间复杂度:O ( 1 ) O(1)O(1)
: q6 s2 B) O% n% z* I% G' h: f& L" M1 x0 ^2 K7 c1 Y

. P( F6 _, r6 d7 L' X" }( bf、栈& [( n: T" ^- k; M+ g# d2 ]
内存结构:看用数组实现,还是链表实现" f& X* z* ?- K( N" }4 i! c
实现难度:一般+ t2 K# A% \6 D: E& }& X
下标访问:不支持5 j! f  O) M" Y$ a# C
分类:FILO、单调栈. E8 O! R0 m8 S- ]8 t! m# S
插入时间复杂度:O ( 1 ) O(1)O(1)
+ s) @3 H3 |* J: [5 c查找时间复杂度:理论上不支持5 j4 H7 o# |/ c
删除时间复杂度:O ( 1 ) O(1)O(1)
/ q' q& l; B$ e- [; x* V) @: k6 J2 F- a7 v. W! m0 e, B- S

" p* j8 e# u; A6 k# gg、树' q( ^2 v% Z- |3 ^$ P- R
内存结构:内存结构一般不连续,但是有时候实现的时候,为了方便,一般是物理连续,逻辑不连续3 c! i4 L9 U) ~0 ~6 m: F
实现难度:较难* B. q/ `2 S; J- j9 j* V
下标访问:不支持
& X3 O* x0 c" L0 Z: t分类:二叉树 和 多叉树
( q) v: J, Q7 Z( n4 n插入时间复杂度:看情况而定
( m5 F! d! n  Z6 ]" v' t  j查找时间复杂度:理论上 O ( l o g 2 n ) O(log_2n)O(log
$ c3 F2 b& Z/ \! F8 S; x# w- H2. W7 ]4 z6 K2 V# d
​       
1 l0 J1 g: |0 O+ Q! _ n)
5 H1 B  N7 H- m8 l! X! G删除时间复杂度:看情况而定# l: S8 Q% a6 \* p; L
/ N, e+ Q- A4 w6 J4 o8 m. w) G

9 }( X* Q7 Y% y1、二叉树0 _+ u! k9 J  l" @
二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。6 Y% @% N' d! }" _; x# ]# s
其中,堆也是一种二叉树,也就是我们常说的优先队列。
% y3 ?/ O" U/ i: q( y' T9 Y4 N" p$ C: L2、多叉树) A+ Q/ b/ Q6 u% z2 B) _4 Y, a
B树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。$ J$ N/ W! t, Z% [
h、图# N9 t1 X/ Y' L- A  \- x
内存结构:不一定) y- t" P4 |9 ]/ l4 m* Y2 M
实现难度:难0 {  O* @. v8 E" b
下标访问:不支持+ n) a2 h, T: @1 p0 p/ z
分类:有向图、无向图
0 G$ Y/ F6 h( ^8 M插入时间复杂度:根据算法而定
( [' ?3 C: e, D查找时间复杂度:根据算法而定* u6 `4 m3 m* V
删除时间复杂度:根据算法而定1 o! I( n$ a  b! U/ j% R) f

" @1 s9 x" i3 |6 K, R

9 {7 q0 h) I. E; G. r( G: ?1、图的概念
) O7 h4 n4 Y# n0 y. f在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下:
" a& y7 S5 Y$ ~" U! e' p/ x图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。4 ~0 b& b1 f: w" ?/ O& {' _4 x- G' o
对于无权图,边由二元组 ( 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 为权值,可以是任意类型。  d, m( t# Y& F8 v
图分为有向图和无向图,对于有向图, ( 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 g) s1 e1 T# U& O8 ^% A( {! U9 A8 E- b2、图的存储
% C, C$ B2 [# H; V9 ?$ N# M对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。' j% K( f; `* h
; N) Z" w" _: p& p( W) A: Y& H

/ c3 K7 c0 A; n) N2 D9 ^1)邻接矩阵8 @6 T" J, K  ~
邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。8 E- _8 D9 t5 d7 S% w, R+ @
它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。
$ V9 d' g- ?& w( P$ `' {0 M3 C[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[
( T* N) l+ s) h+ i. l7 w" }  s01∞9∞0∞8320∞∞∞30* ?" h, D! b7 N7 J: c# ^
0∞3∞102∞∞∞0398∞0! M7 ]) P; D: t; J5 h' r
\right]
& n3 E# O' U4 @3 }/ g( v3 ~# U, x0 u0 Z

$ m$ d% T: W8 M& X8 }+ B6 C% g8 f
4 d8 T+ f* R$ {1 ~- m
* L$ z5 \0 f9 W0 W6 @/ Q​       
, |1 b: M# E3 ~( t  
5 }! z# h1 X+ Z# [1 [% J. p" q: n2 N0
7 }* [- ?, O8 D1 [# Y/ a! S13 b2 Z+ ], W! l1 U

, l$ J7 C6 N1 E# l! U9% c: a% b6 t4 ]% H* L) s
​        0 K2 T4 K/ N, B6 S" }
  
3 `3 ^1 y( L; b# ], [2 W1 ^1 y" B. F$ j1 f$ U
" w! v4 N+ ~; P" D! ?2 W6 B" U0
. F. t: `5 t, Z$ x. D. R$ ^& T* F6 @
# {% P1 S" Y' B" n; A( C/ ^: M8
6 `: u" l8 w+ }* ~  f; E7 d​       
, E+ o  z3 s8 y% [6 g: _- s  , O3 ~: z3 E! o# x& |4 ?6 a& K; c
31 c: f% ~- N, ?% {8 ^" a
2( e  d/ F# B4 Q1 E' B
0( l' |, p6 j% M( k& W# Y
" n+ {3 {% K5 P. F
​        - Z2 K" T' Y9 c+ z& B( ?0 ]
  " T9 h$ S7 g- X* a3 p
% ~0 r0 ?& m8 J8 h4 q) H+ u

& L- z& S* Q) W7 M: q! H3
* P4 u8 ]3 A6 I, _8 C0' L5 G( I3 v- N, A- p
​       
0 G8 @5 r1 K9 t  W3 M( F8 m  , R9 c% z# v- t  Z" \2 N6 u1 T& |1 [

+ Y8 W- M. V( W9 X" v4 b7 ?- h: A) S# E. h- ]  X
( ~7 F" ?" X) L0 q! s! B6 _' _
. \" X' m; N4 L0 {$ L8 q
​       
3 P* Y5 [3 |- ?4 P9 ^' _ ! T6 I- R, U7 E( {" v
2)邻接表! h# e5 `2 M3 F5 w* p
邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。
$ f9 R' S( m! m& V) i它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。
8 \( J5 i+ T" w# s+ ], J( w如图所示,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) 二元组。* H$ H0 _8 S8 v8 y' v. D  Z

% }, ~( m! H: K9 z0 Z& z
7 G2 t% ]4 T5 x8 ?3 D
在 C++ 中,还可以使用 vector 这个容器来代替链表的功能;' |. M# h3 {, j/ ?
    vector<Edge> edges[maxn];
  Z2 v4 H8 _7 Y1 L+ d) k: S. I- u1
* p1 v- W, _# x* d6 V$ u8 }) a, J0 j3)前向星5 o  V7 O  L; N0 \9 u. [- r
前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。
2 c+ n) A  }5 u+ ^+ f' x, g( D它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。
3 f- ?1 Y. T& z1 Q! g4 s  k如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。
! _8 O+ S0 l) m+ U9 n! a, o8 K; F9 H5 C4 |

+ g6 t0 D: z0 K  q' l; x' O那么用哪种数据结构才能满足所有图的需求呢?: i/ E( z7 j4 O" D7 [/ `
接下来介绍一种新的数据结构 —— 链式前向星。/ D$ R$ Z0 ?! v% j, e) q/ J) T
4)链式前向星
/ ]4 M* s: Z9 Y! y/ P链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 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 指向下一条边。
/ G7 T$ j" B! K/ ?7 U: s8 l具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。
2 P' w, b! D/ J3 r  ?4 d边的结构体声明如下:& T" i; D0 y4 x. y, g1 t
struct Edge {
4 ]4 |7 z1 I; y0 p; Q    int u, v, w, next;% d: q' A2 G5 c+ _! {3 A
    Edge() {}/ f  t# i, E% O$ S& ^
    Edge(int _u, int _v, int _w, int _next) :
2 r" i+ b0 ?5 c0 X) q+ a        u(_u), v(_v), w(_w), next(_next) & r1 _" y8 G0 s% V8 [- a
    {8 D; ?8 e4 E  A* M) A0 x3 {& e
    }9 _! }) t' g. n/ O6 g# T
}edge[maxm];$ n' S4 m2 D9 E& B- ]/ e. s/ V& y
1" q% \4 S/ |4 _1 v$ M
2; N+ \% a! \- n5 x  p& C, y
3$ m; D5 V1 P3 O6 F
4
: N  T+ O1 W. L1 u7 R! H+ A5- c! u' T( Z6 f! A% w+ r
6
0 ~- z. t2 L. F& |# f7
! H% ^( N% I  n6 J: @! c8
* P7 B( a- k; m初始化所有的head = -1,当前边总数 edgeCount = 0;
. V" J4 B3 k* }每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下:
) \, m; w8 e6 `" xvoid addEdge(int u, int v, int w) {
1 v( y3 k. \) @3 A& H# B    edge[edgeCount] = Edge(u, v, w, head);
+ \, F5 v5 `8 c$ Y    head = edgeCount++;0 F& \, C- V( b- I$ k6 O
}  l* H* Q1 L6 t8 X# J) D1 c
1
4 b( g2 L& Q6 S- \. u6 |) L# [2. ^1 L& P" j2 n' q; b% e0 |+ K, s  q
3' K2 _1 z, r) w! s) |
4
9 L  t+ k% {! @( U这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。8 Y6 s8 w6 }" i0 j7 f& p8 v$ O
调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。8 T# M$ b( j0 J8 ^) W
for (int e = head; ~e; e = edges[e].next) {* v- |- d3 ~9 N! E/ }. F8 [
    int v = edges[e].v;
, a0 P+ g- c* o5 P: j    ValueType w = edges[e].w;3 \! K8 [7 @$ F4 [9 s0 }- ^. _
    ...
3 ~- f: x9 W- m$ M) b$ S}
, A9 m+ q# S! i: o' F1
0 `# k& k0 |( O* s2
2 W! b; A+ p! `0 W! f3
9 p0 Q3 D+ G* W% Y) _4* y" H: K' j7 V  \, @$ {
56 j; z' p' ~  D4 `8 t/ F; d
文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。
& _3 F" g. S8 Z) E$ E9 j4、算法入门
7 w' `3 S9 t3 V: l' g$ r' {算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。
  `6 i! U' T# L# X0 C) {: v8 z4 O6 e1 j- y% c" h* f
6 e* P+ B  _' Q. T1 W
入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。
' ]( O7 I7 e- \9 x! ?( z* N对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。
  _0 G# y, W; V9 G1、枚举! T6 Z. X0 I5 v8 ]
枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。
' v( T) E8 E0 O4 U) d/ m% r对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。1 r: ~$ O8 ^- d
2、排序
& r5 M+ O7 U6 k! S7 ]; j6 X/ I既然是入门,千万不要去看快排、希尔排序这种冷门排序。7 P& q" R4 _8 @8 t- p- \
冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。
" c5 m/ ?3 B0 F0 u1 F2 U& V$ _% WC中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。: s2 f) w6 t9 P8 R
3、模拟
6 I' F! O1 `6 H! F模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。+ i! H5 n" b0 n
不管时间复杂度 和 空间复杂度,放手去做!
1 b, J2 |4 Y( ^+ N但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。1 ^- W8 R: e0 b
4、二分
  F4 K( T* T7 O5 k0 ^3 }二分一般指二分查找,当然有时候也指代二分枚举。3 l4 J  w, Z& ~; {2 \
例如,在一个有序数组中查找值,我们一般这个干:7 a) G5 A/ O2 P+ s! q* d3 o2 x
1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1;3 m7 o) b# u4 \) n* g1 V! @
2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种:
2 }  E; f) i# c/ o* ?! o* L  2.a)目标值 等于 数组元素,直接返回 m i d midmid;9 G- c  \  m. z7 ]8 |. n
  2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1;
# ]/ ?* L+ K" O+ H$ ]) T7 |% E  2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1;
8 t; ^1 n1 G0 n' H) Q3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。- B4 M+ P* m' G3 C
5、双指针
$ ^9 C2 P+ |- M5 o; Q! X1 H双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。0 \! k  ~- \+ Q" M. z3 p
: m* m# E5 w; g; h6 c: ]: P4 V* w

5 C0 e! K# b% f- @8 [1 O; l6、差分法
# p& E. H! W0 A; B差分法一般配合前缀和。* g1 Y, v' L3 A
对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题;4 r5 y6 D% \3 I" O/ m2 y* O
假设 [ 0 , x ] [0, x][0,x] 内的 g o o d   n u m b e r good \ numbergood number 数量为 g x g_xg
: T, G* \+ H7 ~; O5 F  j5 k' Vx
- o- b; b2 f6 m) X9 o4 J​       
) o& N& [- W; I6 u- i ,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g # p0 X# F8 O1 J. U, i
r2 h: K% R0 A, j& C: T
​       
' }( ]3 c1 R' G0 \3 C  I) t −g 7 _& }- G5 G! w! L8 m$ B
l−1! r/ H! M, q6 X* X
​       
) Z, P  M' q8 K* A1 v: ?/ N" W ;分别用同样的方法求出 g r g_rg
, G$ |. A! {- h" `4 G) ^r4 ^8 M1 Y, E5 Q. m
​        ) Y% R4 ~' k7 x$ J
  和 g l − 1 g_{l-1}g 6 r+ _* B, f+ l/ P
l−1
2 x7 r# j: D- O7 ]( `" _! {" V​        8 b2 c+ g/ E# Q+ y9 S9 l
,再相减即可;  u2 {+ E3 d/ E+ ]4 d

) p2 m5 i# i9 b

( l) Q% A6 H; T# Z: y7、位运算/ G/ [* ~+ f$ |; O
位运算可以理解成对二进制数字上的每一个位进行操作的运算。$ N7 P1 j/ T4 z1 |( H2 n' p4 H& E' y
位运算分为 布尔位运算符 和 移位位运算符。5 h- A4 }5 I4 ~
布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。
9 y% j1 \9 f- \, D9 t如图所示:
! x/ f, N2 s+ G$ W/ Z# t3 v- T: l
9 X( e4 c3 q. |, ]7 F, m

2 K' z6 w% q7 D- b$ I位运算的特点是语句短,但是可以干大事!
8 G6 \) X! d" [3 B4 s/ A比如,请用一句话来判断一个数是否是2的幂,代码如下:
% I% z* _" @: Y- M; Q7 b4 H!(x & (x - 1))2 w8 c$ H6 w' ^5 R' R1 U) S
1
$ P0 s& \- p2 `/ Z8、贪心
! h) Q4 ~  E* ~% y' B贪心,一般就是按照当前最优解,去推算全局最优解。
( M  G5 ?$ r- F9 w, b* u  M3 Z所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。9 s3 E5 n/ ?) v: c. d
9、迭代
# b* x1 i, S% h. p1 \, _每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。
8 c, x0 n( w) a10、分治
$ M) ]& A3 Z: k$ S分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。5 \9 ^1 Q% s7 O. Q9 ?) Q: ]
5、算法进阶
  e1 W7 J5 Z1 A+ D算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。+ R  s, f$ u# A* u  W3 a" j
如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。' A- e0 U- k' |3 l
这个系列主要分为以下几个大块内容:" O5 q5 a' M/ W8 ?# }4 }7 ?2 q/ v
  1)图论
3 q7 U/ i3 ^  e4 x) U* g& ]# F8 Y  2)动态规划4 o  O- M( Y- k; x
  3)计算几何( V4 A# p% l7 E1 C
  4)数论) b% w' p# v( E  c
  5)字符串匹配
2 d8 \3 n* m! L6 v  6)高级数据结构(课本上学不到的)( j4 X8 Z5 S, B; q
  7)杂项算法
7 n* C6 X8 u) X' k6 f
+ l6 M0 ?0 ^9 Q1 [

3 F- |2 ~# _# {2 K先来看下思维导图,然后我大致讲一下每一类算法各自的特点,以及学习方式:
# g" h% @& i6 [" s; ~' g5 W( }* I! g& ~. ^% `  T
: S9 }" T+ [0 z0 `3 }1 J- b
$ j( F. {& G5 v' I/ T, ~3 v6 ~

0 _4 A; S5 B0 C: M9 K: o1)图论- y* L' ^1 u& ], P, P3 }0 C5 l
1、搜索概览  Q) V7 b& l$ R. l( e
图论主要围绕搜索算法进行展开。搜索算法的原理就是枚举。利用计算机的高性能,给出人类制定好的规则,枚举出所有可行的情况,找到可行解或者最优解。7 u% K# l0 s" u+ D5 K- R& j0 T
: f, g$ f) W) p+ H* m  R

9 Z$ R: }. h" e6 \" ^1 J; Z! c  B  }比较常见的搜索算法是 深度优先搜索(又叫深度优先遍历) 和 广度优先搜索(又叫广度优先遍历 或者 宽度优先遍历)。各种图论的算法基本都是依靠这两者进行展开的。% {- @+ W$ v! L2 ]
2、深度优先搜索1 h2 o5 m# W" \5 y& v0 {
深度优先搜索一般用来求可行解,利用剪枝进行优化,在树形结构的图上用处较多;而广度优先搜索一般用来求最优解,配合哈希表进行状态空间的标记,从而避免重复状态的计算;+ j7 H5 C9 m+ J  D0 l
原则上,天下万物皆可搜,只是时间已惘然。搜索会有大量的重复状态出现,这里的状态和动态规划的状态是同一个概念,所以有时候很难分清到底是用搜索还是动态规划。+ w' ]7 X( \9 C1 ]1 U5 t! [
但是,大体上还是有迹可循的,如果这个状态不能映射到数组被缓存下来,那么大概率就是需要用搜索来求解的。
5 |- y) `$ x' C3 g如图所示,代表的是一个深度优先搜索的例子,红色实箭头表示搜索路径,蓝色虚箭头表示回溯路径。
3 _0 P: G/ W7 p9 r( b/ b. Z8 I# {: E' t' m7 E# E3 [
& \+ b' z# F; i9 ?- j9 \$ @
红色块表示往下搜索,蓝色块表示往上回溯,遍历序列为:
( X, m1 O, O1 Z- j' J: ?$ l        0 -> 1 -> 3 -> 4 -> 5 -> 2 -> 6/ B% F6 o2 q( W* P# z
1  \# B# n1 O. q6 m6 _
同样,搜索的例子还有:* T- ^" x2 |  J/ V3 I/ y4 ^
; f; K9 W  b8 b+ w

" Z0 @1 O1 ?* `# T3 e计算的是利用递归实现的 n nn 的阶乘。
+ y+ E$ x- H$ i- ^# E9 |- s3、记忆化搜索
9 d  N8 g5 o% q' K( b8 t对于斐波那契函数的求解,如下所示:
( t+ N, `! U# o' [; R# S) s0 xf ( n ) = { 1 ( n = 0 ) 1 ( n = 1 ) f ( n − 1 ) + f ( n − 2 ) ( n > 2 ) f(n) =
8 U- Y; q' |) D5 m⎧⎩⎨11f(n−1)+f(n−2)(n=0)(n=1)(n>2)3 T6 q" I* G2 g8 n  A+ j' K; s5 g
{1(n=0)1(n=1)f(n−1)+f(n−2)(n>2)
' D; T& X$ m1 g- jf(n)= 3 O. N) L3 [$ \. z
* i4 q2 e6 o% K5 q/ V" m; l: R( g
! z. w$ U2 P! ?$ j* d  {$ d

3 p; v8 D! O! X+ V5 D8 l! u
1 Z: B3 \( A/ i  T% w0 d/ A) a: E1 n4 c2 v+ _, U9 N; d
​        3 L" `4 w' B* ]3 a5 a
  ' F; I& Y' K/ ~- f' r7 _! Z! N
1
1 L9 X6 A% g9 F  B- Q8 D8 V1
# e; j/ X- i. P2 U* Vf(n−1)+f(n−2)
' \: s- X% u2 [3 i​       
. d" Q* ]. E7 q; j2 G  ) @9 U- b9 n! L- {
(n=0)
' R# j" A7 |+ M* C(n=1)
" n; e2 T) g* L6 t! p7 L(n>2)* D6 Y* N% e$ v  j
​        4 `' |" O3 f. m- o+ m2 |0 R: `
+ K. j! T" t( ]* i# R+ P* V& r
对于 f ( 5 ) f(5)f(5) 的求解,程序调用如下:# m% ^1 S, M2 A6 N% H
/ h0 {' O/ L7 l: l( ~

* f- U7 X* W' i6 |/ J% f7 K$ u这个过程用到了很多重复状态的搜索,我们需要将它优化,一般将一些状态缓存起来。
" p  B  Q3 X- [# y' ?  Z1 a我们通过一个动图来感受一下:7 }6 k' S% p8 L" J9 B# m! O

5 F. E3 M1 E% N+ _. u% ^

6 |9 B. m% t2 M: i. Q当第二次需要计算 f ( 2 ) f(2)f(2) 和 f ( 3 ) f(3)f(3) 时,由于结果已经计算出来并且存储在 h [ 2 ] h[2]h[2] 和 h [ 3 ] h[3]h[3] 中,所以上面这段代码的fib != inf表达式为真,直接返回,不再需要往下递归计算,这样就把原本的 “递归二叉树” 转换成了 “递归链”, 从而将原本指数级的算法变成了多项式级别。
, v, i( t) C$ J+ V! P+ f% g  w" j这就是记忆化搜索,像这种把状态缓存起来的方法,就是动态规划的思想了。
; B2 A2 H' b8 L1 k" @4、广度优先搜索$ g- ?& {! V. P: s" F, |: |) d' t
单向广搜就是最简化情况下的广度优先搜索(Breadth First Search),以下简称为广搜。游戏开发过程中用到的比较广泛的 A* 寻路,就是广搜的加强版。( r& a+ b8 J; W: z1 i$ C
我们通过一个动图来对广搜有一个初步的印象。
' S8 [# T- [& Z! F4 L8 a2 I7 n& I  e2 U  g3 c# \
* [2 c! n* \" A6 C" [- u' ]+ H) H

9 F' z& C0 i8 W# k4 _5 j# f" k

# v; ?* h) I, B7 N- _1 G5 {从图中可以看出,广搜的本质还是暴力枚举。即对于每个当前位置,枚举四个相邻可以行走的方向进行不断尝试,直到找到目的地。有点像洪水爆发,从一个源头开始逐渐蔓延开来,直到所有可达的区域都被洪水灌溉,所以我们也把这种算法称为 FloodFill。5 o' I8 L7 Y8 R5 f4 `. E) w+ A* @
那么,如何把它描述成程序的语言呢?这里需要用到一种数据结构 —— 队列。# @% m( s5 h; Y5 Z  S2 C
这时候,算法和数据结构就完美结合了。0 i6 z6 W5 [5 D0 i3 j
2)动态规划# ]! s) c/ U7 M; P- V
动态规划算法三要素:' a% ?* z4 D: E+ A: u; \. R4 z
  ①所有不同的子问题组成的表;+ B, F% b) [2 d" V! M
  ②解决问题的依赖关系可以看成是一个图;* b- E1 a: S" |5 i1 I+ y! l
  ③填充子问题的顺序(即对②的图进行拓扑排序,填充的过程称为状态转移);( h" _7 i3 \/ r1 m  g5 T" F) F' e

: \, m# ^* B: K

- Q7 H' o) b4 C8 ]/ T如果子问题的数目为 O ( n t ) O(n^t)O(n
, J* J4 }2 q5 Gt2 \' d1 i8 y8 m5 n
),每个子问题需要用到 O ( n e ) O(n^e)O(n
3 Q0 k# H  M5 [( W7 S. S3 t: `e
! o1 h1 u0 N0 r7 `# H9 t ) 个子问题的结果,那么我们称它为 tD/eD 的问题,于是可以总结出四类常用的动态规划方程:(下面会把opt作为取最优值的函数(一般取 m i n minmin 或 m a x maxmax ), w ( j , i ) w(j, i)w(j,i)为一个实函数,其它变量都可以在常数时间计算出来)。+ J" t1 f$ F# K3 ]
1、1D/1D+ c1 H& b+ M5 e' K+ v" T2 n) n1 H6 t. S
d [ i ] = o p t ( d [ j ] + w ( j , i ) ∣ 0 < = i < j ) d = opt( d[j] + w(j, i) | 0 <= i < j )9 B; v) S* {5 f9 J
d=opt(d[j]+w(j,i)∣0<=i<j)& }+ S( z5 w- `" e: ?7 o3 r
状态转移如图四所示(黄色块代表d [ i ] dd,绿色块代表d [ j ] d[j]d[j]):+ O( i. j4 C6 R" N9 W
, n  {/ h2 M- Q4 ]! M" O
0 Z6 p; F) O# J9 \: S$ v
这类状态转移方程一般出现在线性模型中。7 ~) K8 m4 ?' v) Y$ u
2、2D/0D
1 V3 u- R. V( A2 Y# N# t# J6 _d [ i ] [ j ] = o p t ( d [ i − 1 ] [ j ] + x i , d [ i ] [ j − 1 ] + y j , d [ i − 1 ] [ j − 1 ] + z i j ) d[j] = opt( d[i-1][j] + x_i, d[j-1] + y_j, d[i-1][j-1] + z_{ij} )
" o. q5 b7 H, p6 O: u$ s# X" }: ad[j]=opt(d[i−1][j]+x
" u1 Y. e7 M; G3 H! ]( X* Xi2 q1 v6 x  J( Q
​        8 _# k+ C( m5 ]" B8 D' q
,d[j−1]+y
; G& p) v8 O3 p+ P: `4 w( zj
' z, u. J6 q) P; w* q+ b​       
9 U' G# d+ M! H' L- }. T5 E4 E8 R ,d[i−1][j−1]+z 4 G8 ~" K# t, j' q  e
ij" W9 z; b+ R# Z3 N) @" a+ e
​       
8 y9 m2 R/ O8 _; C) M# ]( n3 ^9 `4 R )
' M. Q* Z9 B/ W% v0 w状态转移如图四所示:2 H# Q- ^9 r% r4 c8 i) Y" c

$ H5 H/ R4 _& z% Z7 `- Z
! M! L) E) R% o5 L* G
比较经典的问题是最长公共子序列、最小编辑距离。8 e. C$ w4 O2 m
有关最长公共子序列的问题,可以参考以下文章:夜深人静写算法(二十一)- 最长公共子序列
  S: b/ a! g. C+ @有关最小编辑距离的问题,可以参考以下文章:夜深人静写算法(二十二)- 最小编辑距离0 a: t3 A: N9 B- n
3、2D/1D
% }7 P/ n! p9 Z. N1 _d [ i ] [ j ] = w ( i , j ) + o p t ( d [ i ] [ k − 1 ] + d [ k ] [ j ] ) d[j] = w(i, j) + opt( d[k-1] + d[k][j] )
( K4 w8 ^6 x3 v; {5 xd[j]=w(i,j)+opt(d[k−1]+d[k][j])
3 J  D, r: z  A. C. d. Y+ U8 C5 j区间模型常用方程,如图所示:' j, i0 T9 B8 I5 S* U8 P

+ p- K; _* {5 _/ v+ T& \5 E

9 k' ?. ]/ m5 R4 I+ X; f另外一种常用的 2D/1D 的方程为:
2 W% s' b# n/ p" m& ?d [ i ] [ j ] = o p t ( d [ i − 1 ] [ k ] + w ( i , j , k ) ∣ k < j ) d[j] = opt( d[i-1][k] + w(i, j, k) | k < j )( D3 {: b' d% A, |& v9 ~/ `# `
d[j]=opt(d[i−1][k]+w(i,j,k)∣k<j). @0 N" w2 s: X" G$ d+ I
区间模型的详细内容可以参考以下这篇文章:夜深人静写算法(二十七)- 区间DP
5 @! f& I$ T. [4、2D/2D
3 n- z/ ]3 g1 j$ C+ V' Q7 q2 Hd [ i ] [ j ] = o p t ( d [ i ′ ] [ j ′ ] + w ( i ′ , j ′ , i , j ) ∣ 0 < = i ′ < i , 0 < = j ′ < j ) d[j] = opt( d[i'][j'] + w(i', j', i, j) | 0 <= i' < i, 0 <= j' < j)! V1 q3 D* U% Q$ ]  o" n
d[j]=opt(d[i
9 w. a/ u; ?) J. ?1 p
. E) s% u  V; ?& h, N ][j 7 R1 i. A$ L+ C' e9 _$ E9 v/ y
  U) H5 \+ p7 j$ a
]+w(i # U6 W* p" u0 H! e
/ u+ t. O! K1 X- y7 L8 i% J' }
,j % ^7 p2 H+ N2 K; P. c

" Q/ k, j) B! Z- f+ D" a' y ,i,j)∣0<=i   D! Y1 L& \0 N
4 H% O- x' R9 `) X% X7 a
<i,0<=j
7 I9 d: q) W6 d
1 E9 z& U) c( d- O <j)/ r- T. l# c* w& o/ @- g% @9 J
如图所示:' R) D& D, j, |3 p1 S. r8 o

4 A; T4 w8 k& X' D* T, x

8 [9 t" O8 c- N( B1 h- g; B7 X6 F常见于二维的迷宫问题,由于复杂度比较大,所以一般配合数据结构优化,如线段树、树状数组等。
2 z# J* A- f* i" j对于一个tD/eD 的动态规划问题,在不经过任何优化的情况下,可以粗略得到一个时间复杂度是O ( n t + e ) O(n^ {t+e})O(n # o8 L7 m6 S+ E) z0 L; @
t+e# E. M4 g7 j5 F8 b5 }2 H
),空间复杂度是O ( n t ) O(n^t)O(n + Q7 c8 z7 c  m8 y0 N
t
; W1 G7 N. I$ o$ O  P ) 的算法,大多数情况下空间复杂度是很容易优化的,难点在于时间复杂度,后续章节将详细讲解各种情况下的动态规划优化算法。
, |: b9 p2 Q2 U) C0 B5 ?" z$ X3)计算几何) T* f4 z5 [/ Y) X5 H% w7 `4 q
计算几何的问题是代码量最大的。它是计算机科学的一个分支,以往的解析几何,是用代数的方法,建立坐标系去解决问题,但是很多时候需要付出一些代价,比如精度误差,而计算几何更多的是从几何角度,用向量的方法来尽量减少精度误差,例如:将除法转化为乘法、避免三角函数等近似运算 等等。
: n. |$ ~6 m8 R) R/ ?$ X+ _如果一个比赛中,有一道计算几何的题,那么至少,它不会是一道水题。, `" z) ]' m4 u( E4 u1 M0 g
1、double 代替 float
8 y& X1 f) n6 L, k3 tc++ 中 double 的精度高于 float,对精度要求较高的问题,务必采用 double;8 r9 O' O2 l* |( g
2、浮点数判定+ h, X0 {: s  k0 F( q. {4 j
由于浮点数(小数)中是有无理数的,即无限不循环小数,也就是小数点后的位数是无限的,在计算机存储的时候不可能全部存下来,一定是近似的存储的,所以浮点数一定是存在精度误差的(实际上,就算是有理数,也是存在误差的,这和计算机存储机制有关,这里不再展开,有兴趣可以参见我博客的文章:C++ 浮点数精度判定);
8 s1 j6 n  R9 V3 X% {两个浮点数是否相等,可以采用两数相减的绝对值小于某个精度来实现:
9 i5 j# i0 s+ h7 i* N" z: ]) H4 b' Q4 Kconst double eps = 1e-8;
, O; x7 b6 a: dbool EQ(double a, double b) {; y$ k4 Y6 _% Y$ d
    return fabs(a - b) < eps;% I% D) i5 k+ Y( M9 J
}3 z, e" B( v8 [+ ]3 P
1
& ]0 ^8 F. x3 S2; k7 p9 I) V( m
3; _- e7 R' E1 f/ D3 I& |- x* z
43 u# }" a- ]+ C  C
并且可以用一个三值函数来确定某个数是零、大于零还是小于零:% q# w$ E; u7 i4 J0 d- q) Q
int threeValue(double d) {
5 [' L& h! h4 W) z' H) @1 w6 R    if (fabs(d) < eps)
- u6 f: p. P" Q- q& C, N        return 0;: v  ^& s0 ~+ W  O
    return d > 0 ? 1 : -1;
- b" m* P' A+ w5 R% L}( S3 x9 s* n' S# Y5 |
1
7 S" z1 u) T/ R, E  d* |2
3 U" L: x* Q1 f  m* _3, P4 z: A. b* G( V8 B0 o2 Y
4
, j1 A* ?2 n: r7 |' Y0 |  E5
( n8 w1 a" M3 Q0 S7 y9 k3、负零判定) K& }+ u: k, |" G( d; ~# A
因为精度误差的存在,所以在输出的时候一定要注意,避免输出 -0.00:
! O. E3 t3 e* ]8 }# A. K4 q# B* V    double v = -0.0000000001;
9 N+ N3 V5 ^1 l& X1 l( e7 a* ~1 v6 {    printf("%.2lf\n", v);  H9 g' V. m  S* K: u
1. p) I; w2 u0 K6 ?5 }
2
& g% H! x) a  i- l4 t  j避免方法是先通过三值函数确定实际值是否为0,如果是0,则需要取完绝对值后再输出:
: A4 E# V* I* q    double v = -0.0000000001;1 ~9 X& {2 D+ U' W
    if(threeValue(v) == 0) {
; [. C$ E7 f+ ^9 a        v = fabs(v);
0 a0 ~; `0 y+ ~    }+ C" z* G) ?+ C9 d9 O  a
    printf("%.2lf\n", v);; h1 d/ |7 A. a8 I
1. J4 K1 [2 i/ K) d7 ~; M/ h+ Y
20 T/ w3 x" V( y0 ~
3) `- s  L/ q' \" w/ b' p7 `( m
4; m) r; k$ B4 R3 ?) C
5
2 D& y1 c1 d/ a0 c6 }4、避免三角函数、对数、开方、除法等
' i$ ?- S7 U6 R" s' k' P' b* mc++ 三角函数运算方法采用的是 CORDIC算法,一种利用迭代的方式进行求解的算法,其中还用到了开方运算,所以实际的算力消耗还是很大的,在实际求解问题的过程中,能够避免不用就尽量不用。
- V) [' i, V! p: }. M# ~( g除法运算会带来精度误差,所以能够转换成乘法的也尽量转换为乘法运算。/ e: v3 w3 ?. K( L' F0 {
5、系统性的学习
* _- Y/ G) d! M  E2 s. N6 ~基础知识:点、向量、叉乘、点乘、旋转、线段、线段判交、三角形面积;
" h& c& m( L( s% s$ V进阶知识:多边形面积、凸多边形判定、点在多边形内判定;, ~( |8 L0 v& [( f5 {
相关算法:二维凸包、三维凸包、旋转卡壳、多边形面积交、多边形面积并、多边形面积异或、多边形和圆的面积交、半平面交、最小覆盖圆、最小包围球、模拟退火。
5 |( q) L( t/ E9 M2 a3 J2 x8 L: S( G+ L2 K9 ~& ]% B. p- L! U

, f8 x% c% S2 G8 S* C3 z学习计算几何,最好是系统性的,刷题的过程中不断提炼出自己的模板。
2 Y" [2 P- m% i" A* _8 T+ @+ P; X4)数论( k2 `) Q% ]% D7 P, |
刷题的时候遇到不会的数论题,真的是很揪心,从头学起吧,内容实在是太多了,每个知识点都要证明吃透,不然下次遇到还是不会;不学吧,又不甘心,就是单纯的想把这个题过了,真是进退两难!
; \' y% g) D1 x. [数论对一个人的数学思维要求较高,但是一般也是一些固定的模式,所以把模板整理出来很重要。
9 f/ b/ D2 C9 t$ }8 j" s当然,数论也有简单问题,一般先做一些入门题提升信心。
! g! C# x" ^( u& |. V1、数论入门
* i' F  k3 S+ Q: z1 s1 x' s主要是一些基本概念,诸如:
6 k" i3 o4 E; P  [* a5 a9 A# P整除性、素数与合数、素数判定、素数筛选法、因数分解、算术基本定理、因子个数、因子和、最大公约数 (GCD) 和 最小公倍数 (LCM)、辗转相除、同余、模运算、快速幂取模、循环节;7 p# S0 [7 a- A/ B! t; O8 T6 N6 y
2、数论四大定理
4 s& U6 U- G) d7 V- @9 ]这四个定理学完,可以KO很多题:6 C7 i" V; e' j& f2 Y) ~8 ]
欧拉定理、中国剩余定理、费马小定理、威尔逊定理
0 x4 i# u4 [# @3、数论进阶' z& ~3 x: t) B
系统性的学习,基本也就这些内容了:9 c# J4 ~8 ^5 Z- Z# H
扩展欧几里得、逆元、欧拉函数、同余方程组、扩展欧拉定理、RSA、卢卡斯定理、整数分块、狄利克雷卷积、莫比乌斯反演、大数判素、大数因子分解、大步小步离散对数等等。( o1 w0 k# F% a# i7 a
5)字符串匹配: @% g# K  L: p  {' i1 j/ E- d
字符串匹配学习路线比较明确。
  `  Z1 z) N6 h2 ]: P先学习前缀匹配:字典树。) B% @! M; s* X; f& ^& i
然后可以简单看一下回文串判定算法:Manacher。
; |8 y$ [$ j+ D6 \7 _- Z; w以及经典的单字符串匹配算法:KMP。
. G6 P5 M) E% F% y4 l1 d实际上平时最常用的还是 BM 算法,而ACM中基本不考察。
4 K2 L) h9 n" A1 ~( B+ E) ~然后就是较为高阶的 前缀自动机、后缀数组、后缀树、后缀自动机了。
6 @: y) U2 n0 b6 \* w* U! X关于 算法学习路线 的内容到这里就结束了。
7 A1 L  ?+ d+ X5 X3 e4 P如果还有不懂的问题,可以 想方设法 找到作者的微信进行在线咨询。& I# s5 \5 H+ m
参考资料7 O- ^1 z% \: m
【阶段一】C语言学习资料:《光天化日学C语言》(日更)
  o8 g9 ~6 |* K* l+ `【阶段二】C语言例题:《C语言入门100例》(日更): R, C  j4 f% i3 G2 b9 {* P9 l" p  j
【阶段三】算法入门题集:《LeetCode算法全集》(日更)8 o* y7 P) _& v- D0 T7 r2 T% q) V
【阶段四】算法进阶:《夜深人静写算法》(周更)( a0 B& ~( G9 l9 g* d! }1 ^% U+ C
————————————————
5 e) L$ L" Q  h# s/ B版权声明:本文为CSDN博主「英雄哪里出来」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
& s& a2 r, H" h' i原文链接:https://blog.csdn.net/WhereIsHeroFrom/article/details/118382228
3 z$ U! Y: F. O, ^  n% W+ B: t. \6 z4 M2 a; t

6 m$ f% P7 s2 T# ?" a
作者: 1051373629    时间: 2021-8-17 17:14
太难得了
5 R4 y* O( M* O




欢迎光临 数学建模社区-数学中国 (http://www.madio.net/) Powered by Discuz! X2.5