在线时间 1630 小时 最后登录 2024-1-29 注册时间 2017-5-16 听众数 82 收听数 1 能力 120 分 体力 569155 点 威望 12 点 阅读权限 255 积分 175969 相册 1 日志 0 记录 0 帖子 5313 主题 5273 精华 3 分享 0 好友 163
TA的每日心情 开心 2021-8-11 17:59
签到天数: 17 天
[LV.4]偶尔看看III
网络挑战赛参赛者
网络挑战赛参赛者
自我介绍 本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。
群组 : 2018美赛大象算法课程
群组 : 2018美赛护航培训课程
群组 : 2019年 数学中国站长建
群组 : 2019年数据分析师课程
群组 : 2018年大象老师国赛优
* F5 v: ]$ m: m5 x: D
❤️两万字《算法 + 数据结构》全套路线❤️(建议收藏)
0 m/ D3 Y7 F' B) y8 ~. J l/ y
: w6 y6 T0 F6 f2 \, \. Q, Z 前言
) s7 ^4 u+ `0 j8 p, t# V. Q 所谓活到老,学到老,虽然我感觉自己已经学了很多算法了,但是昨天熬夜整理完以后发现,自己还是个弟弟,实在忍不住了,打算把 算法学习路线 发出来,我把整个算法学习的阶段总结成了五个步骤,分别为: 基础语法学习(重要)、语法配套练习、数据结构、算法入门、算法进阶。本文梳理了这五个大项的思维导图,在下文会有详细介绍。
( a/ j- O. U5 }2 k7 _ 希望各位能够找到自己的定位,通过自己的努力在算法这条路上越走越远。
$ `" M" ~9 G$ m. ?5 G1 f 刚开始切勿心浮气躁,千万不要给自己立 flag,说一定要把这么多东西都学会。就算你的精力旺盛,日夜操劳,时间也是有限的。所以,首先是明确我们要做什么,然后制定好一个合理的 目标 ,再一点一点将要学习的内容逐步付诸实践才是最重要的。 8 F: Z+ j( [8 U
每日一篇C语言打卡,目前更新到:光天化日学C语言(20)- 赋值运算符与赋值表达式 | 让代码变得更加简介(建议收藏)。
a! K! X2 F: u8 q; r
0 t. z- H7 n( {* L2 ^; M6 R
; O8 }9 }' J$ s( V- S; z2 c* d
3 Y/ I& g5 [ ~0 q$ A4 Y1 T
5 Q( A* _5 ^" ^9 X' a
& Q$ A* q* L. J3 ~& v ; h' c' ]5 u& P S* I4 X5 o
5 m& A4 Q5 d- U Z( T3 E
% c4 D; ]& \: o3 L2 m4 s2 E2 J, c2 ~# v 图片较大,文章中有拆解,需要原图可以留言找我要哈 ) t/ e; Y L/ l9 N* w
1、基础语法学习
& v' R$ R6 b! H e; u 算法是以编程语言为基础的,所以选择一门编程语言来学习是必须的。 ! D+ B7 s& C4 ?/ z0 k4 L4 V
因为作者本身是C/C++技术栈的,所以就拿C语言来举例子吧。如果是 Java、Python 技术栈,可以跳过 C语言相关的内容。这一小节,先给出学习路线图,然后我再来讲,每部分应该如何去学。 6 S9 }- M8 B8 d- Q0 z6 R0 U
! M9 d/ H1 a! I ^
8 M( W* L5 {0 p: Q8 Y( e ( e; k7 p' c: W9 Z
2 k2 }0 C* h4 c/ B' Z3 o 1)HelloWorld
. l. u' u; z( ?9 \' @ B6 s* _ 无论是 Java、Python、C/C++,想要上手一门语言,第一步一定是 HelloWorld,先不要急着去配环境。如果环境配了几个小时,可能一开始的雄心壮志就被配环境的过程消磨殆尽,更加不要谈日后的丰功伟业了。 : g, ~# k. O/ Z, w2 p; K# q
2)让自己产生兴趣 $ _; D. s6 y! C. K3 Y
所以,我们需要让这件事情从一开始就变得 有趣,这样才能坚持下去。比如找一个相对较为有趣的教程,这里我会推荐这个:《光天化日学C语言》。听名字就比较搞笑,可能作者本身也不是什么正经人,哈哈哈!虽然不能作为一个严谨的教程去学,起码可以对搞笑的内容先产生兴趣。从而对于语言本身有学习下去的动力。
$ W c3 t% e' W 刚才提到的这个系列,可以先收藏起来。回头再去看,它讲述的是 对白式 的 C语言教学,从最简单的输出 HelloWorld 这个字符串开始讲起,逐渐让读者产生对C语言的兴趣。这个系列的作者是前 WorldFinal 退役选手,一直致力于 将困难的问题讲明白 。我看了他的大部分教程,基本都能一遍看懂。算了,不装了,摊牌了,因为我就是这个作者。
9 h8 D" S/ p) W% G# A: c. l! N1 G1 ^ 3)目录是精髓
2 m, K, i9 F8 R+ _# ]! v- s 然后,我们大致看下你选择的教程的前几个章节,那些标题是否有你认知以外的名词出现,比如以这个思维导图为例,前几个章节为: - Q! i# F% g0 S
1、第一个C语言程序
1 `( ~& K5 ]: \ 2、搭建本地环境
9 U1 [8 `& |# w 3、变量
" W5 b, y$ A7 Z4 L7 a( U 4、标准输出 ! D, {3 ^% c0 Y1 |
5、标准输入 4 w& u- p9 x, q' x$ k) q' _6 `5 N
6、进制转换入门
8 F& Y; |- t9 i& S. z' D) V9 K1 k 7、ASCII字符
! P5 T' f2 `8 N) d7 M 8、常量
6 |# P: [3 l; @, W4 A
: `4 q$ J% T1 r8 d* q / A1 R; ?& r' `' R7 \
如果你觉得这些名词中有 3 / 4 以上是没有什么概念的。那么,可能需要补齐一些数学、计算机方面的基础知识。反之,我们就可以继续下一步了。 * q7 ^0 \9 d2 c, [
4)习惯思考并爱上它
( l! a/ v: |, T4 G" C 只要对一件事情养成习惯以后,你就会发现,再难的事情,都只是一点一点积累的过程。重要的是,每天学习的过程一定要吃透,养成主动思考的好习惯。因为,越到后面肯定是越难的,如果前期不养成习惯,后面很可能心有余而力不足。
e2 t0 D1 }" b$ }7 z6 o 就像刷题,一旦不会做就去找解题报告,最后就养成了看解题报告才会做题的习惯。当然这也是一种习惯,只不过不是一种好习惯罢了。
5 |1 q0 D0 C, c" y0 J- K- C 5)实践是检验真理的唯一标准 # d$ L+ @: W u; v p/ g. i1 b
光看教程肯定是不行的,写代码肯定还是要动手的,因为有些语法你看一遍,必定忘记。但是写了几遍,永世难忘。这或许就是写代码的魅力所在吧。
; p1 m! i; I. ]! k" p. |; S 所以,记得多写代码实践哟 (^U^)ノ~YO / M6 i4 J7 m" P, e2 b( E
6)坚持其实并没有那么难 m, h: D8 L5 {) o
每天把教程上的内容,自己在键盘上敲一遍,坚持一天,两天,三天。你会发现,第四天就变成了习惯。所以坚持就是今天做了这件事情,明天继续做。
* b1 z- O, ]% A 7)适当给予正反馈
4 r6 @- ~6 `: [) R: D9 R% { 然而,就算再有趣的教程,看多了都会乏味,这是人性决定的,你我都逃不了。能够让你坚持下去的只有你自己,这时候,适当给予自己一些正反馈就显得尤为重要。比如,可以用一张表格将自己的学习计划记录下来,然后每天都去分析一下自己的数据。
" ^* N3 w1 P e& }( w 当然,你也可以和我一样,创建一个博客,然后每天更新博文,就算没有内容,也坚持日更,久而久之,你会发现,下笔如有神,键盘任我行!更新的内容,可以是自己的学习笔记,心路历程 等等。 7 O% ?$ @: x+ i4 J* X
看着每天的粉丝量呈指数级增长,这是全网对你的认可,应该没有什么会是比这个更好的正反馈了。
! x( J3 n3 n @! f! C$ C% c 8)学习需要有仪式感 ) H' M7 X% C) n! b( |- @- F/ S
那么,至此,不知道屏幕前的你感想如何,反正正在打字的我已经激情澎湃了。已经全然忘记这一章是要讲C语言基础的了!
1 g; _, e% L2 o 介于篇幅,我会把C语言基础的内容,放在这个专栏 《光天化日学C语言》 里面去讲,一天更新一篇,对啊,既然说了要坚持,要养成习惯,我当然也要做到啦~如果你学到了哪一章,可以在评论区评论 “打卡” ,也算是一种全网见证嘛! 9 T$ v) [9 o0 f' Q. r% c
我也很希望大家的学习速度能够超越我的更新速度。 " _1 |# L, b/ [( Y# }1 M
2、语法配套练习
: ?) f1 X- n9 D% X. | X, @9 c5 a 学习的过程中,做题当然也是免不了的,还是应征那句话:实践是检验真理的唯一标准。 ) s, e- k& s N
而这里的题库,是我花了大量时间,搜罗了网上各大C语言教程里的例题,总结出来的思维导图,可以先大致看一眼: 4 L, V7 s" C# Y
( u- T5 G! o2 m! I4 v+ T
) s2 t; ^" n6 I. D- c
6 M) Z, M; d D5 M1 s
- W* ]( A4 j6 ~+ Y; {3 K
从数学基础、输入输出、数据类型、循环、数组、指针、函数、位运算、结构体、排序 等几个方面,总结出的具有概括性的例题 100 道 《C语言入门100例》,目前还在更新中。
/ w6 f7 T! i$ E6 T1 p 这里可以列举几个例子:
" z' P$ i* t3 T: U+ g7 w6 \/ ` 1、例题1:交换变量的值 $ W+ X; |4 h N8 t$ Y
一、题目描述
7 T- D* S9 {, e, f( ~5 t1 |" h 循环输入,每输入两个数 a aa 和 b bb,交换两者的值后输出 a aa 和 b bb。当没有任何输入时,结束程序。
) |. Y$ y1 c9 O4 W " ^ P, `& L0 e- a
4 f- N! y6 G" w& {( R* L* y# Q
" q9 W, y3 f6 C& ]; V/ ]1 d: g- g 3 h% K( Y& t% W7 r4 h9 E) W, Y
二、解题思路
5 u' {) v0 h0 D' p) Z" F 难度:🔴⚪⚪⚪⚪
: O: x) ]; o' q& B
. f& h9 x D$ y7 R , w* j. M2 g4 {' o9 @* G4 y0 }1 @
这个题的核心是考察如何交换两个变量的值,不像 python,我们可以直接写出下面这样的代码就实现了变量的交换。 * @" Z% Y+ u2 h1 G4 S) D) W
a, b = b, a 5 o# |$ H4 c8 u X& i
1 9 N3 v0 D% a( b8 i
在C语言里,这个语法是错误的。
! b# h: f2 B# b3 s9 v 我们可以这么理解,你有两个杯子 a aa 和 b bb,两个杯子里都盛满了水,现在想把两个杯子里的水交换一下,那么第一个想到的方法是什么?
* ~7 B$ z, d; M( z, j1 h 当然是再找来一个临时杯子: 9 Z `9 o& c3 ?- p: T* H
1)先把 a aa 杯子的水倒进这个临时的杯子里; # B+ d& M4 N( U' J6 W
2)再把 b bb 杯子的水倒进 a aa 杯子里;
* D& ~/ q/ |- N' b5 ^ 3)最后把临时杯子里的水倒进 b bb 杯子; 1 G! m2 N3 B# E+ i5 ], R
" X3 D, U# T+ v7 W% Z
' e: _' W* Z( L4 f! n4 [+ ? 这种就是临时变量法,那么当然,还有很多很多的方法,接下来就让我们来见识一下吧。 , I& |; `/ e: g% o l$ l
$ L% t8 W+ X3 K! f3 Y ( R8 R- D% }9 `! h9 h! p1 u
三、代码详解 8 Y/ I3 |& {8 f$ ^$ z
1、正确解法1:引入临时变量 1 L$ u M2 I" K2 o& k. `/ E
#include <stdio.h> ) ^: S+ b( F" v; {, W+ E% D5 g( a
int main() {
1 V8 s5 _. |6 e7 c% b0 c+ Z3 g2 r int a, b, tmp; & H7 O8 v2 M; |
while (scanf("%d %d", &a, &b) != EOF) {
, D; ]8 a$ v4 m [( C! b tmp = a; // (1)
& E' [! \) a4 s- C5 p3 w, h a = b; // (2) 6 `& \4 _/ S/ B3 _) u! }! P2 S
b = tmp; // (3) / ~/ j R# N# {$ ~! Z
printf("%d %d\n", a, b);
+ a+ V1 s6 R* S! U0 _7 B1 O }
& D+ C: b1 I) q( \3 J return 0; ; N) w* x2 [; Y B$ H
}
1 H$ M8 |! a+ E4 X 1 # ~9 v Q! I( e. i
2
0 j( x; U) O7 r) P' q 3 : I3 h+ i6 e" z1 j
4
/ [% |4 h7 Z- X1 l7 n 5 # D. q! t: j" f- E5 k- F9 ]
6
9 A! j$ `) J# ^ ?( p8 ] 7
# v: _* l$ ^- Q9 ]2 | 8 ) w d! e6 Q$ N" ?; g6 }" w
9
" k6 I2 K E7 q7 ]; a 10 9 p# ~# o$ M( k
11 ; A1 D1 V+ H9 T4 w! }' o, X5 ]$ t! J" y1 D
( 1 ) (1)(1) tmp = a;表示把 a aa 杯子的水倒进这个临时的杯子里;
! T/ M) c# r2 i- x0 F5 W% } ( 2 ) (2)(2) a = b;表示把 b bb 杯子的水倒进 a aa 杯子里; * w7 V% P2 ]& E4 j: }8 m# @
( 3 ) (3)(3) b = tmp;表示把临时杯子里的水倒进 b bb 杯子里;
) Q, W, M/ U2 _ 这三步,就实现了变量 a aa 和 b bb 的交换。
+ y y) R& o+ \' m1 L$ y( U+ F5 N 2、正确解法2:引入算术运算 $ f& f! L5 X+ ? K, ^3 c1 E$ Q. b h6 X
#include <stdio.h> + m$ _9 E! H. v4 r9 Z! r2 _$ y, }
int main() { ! s4 J. g, H6 `! d$ z) |: [
int a, b; . O+ Q; D9 w" C% O2 j6 C5 [
while (scanf("%d %d", &a, &b) != EOF) { 8 i( }5 P$ w0 h9 ?6 r3 D
a = a + b; // (1) * F/ Y" d. }7 a7 c* `) w- Y+ |
b = a - b; // (2) : J" z. y0 b) }" q0 |! ]% z( X
a = a - b; // (3) - [) s/ G) r3 @1 i2 \3 H
printf("%d %d\n", a, b);
( h0 ~% G( p% _ } 5 f9 y3 a* N8 x9 C+ I4 j2 f& h
return 0;
5 b, Y/ s2 f/ X1 s8 f6 ~ }
: \7 d$ D" Y# H; U. z) F' R 1
% K) X7 P8 c9 w* S 2
; E% [7 u. x! q5 C# p0 T: q 3 . x. h5 y/ F: r
4 ) V T. T9 D+ Z6 H% f- z! x% X3 y1 n
5 - D. x- ]/ A4 c! l
6 6 L1 L6 Q- p& C+ l
7
8 `# j) Y& Z- h6 Q" B 8
, v8 D. i1 T7 M1 } 9
; U6 g% ^0 M7 O* `2 \ C 10 / R3 X- t: |2 a. V2 K
11 X6 ^( O3 a( r* Q
( 1 ) (1)(1) a = a + b;执行完毕后,现在最新的a的值变成原先的a + b的值; m$ u+ O( Z1 d7 }; s& K
( 2 ) (2)(2) b = a - b;执行完毕后,相当于b的值变成了a + b - b,即原先a的值; & c, J" k9 S4 K/ j0 R
( 3 ) (3)(3) a = a - b;执行完毕后,相当于a的值变成了a + b - a,即原先b的值;
" O9 z2 _9 R1 S$ ` 从而实现了变量a和b的交换。
4 J U6 s& h) K/ u' ~- T+ p9 M+ P 3、正确解法3:引入异或运算
+ F$ K+ E- I' p$ h R 首先,介绍一下C语言中的^符号,代表的是异或。 $ D& Z; z- b7 }) ]( R
二进制的异或,就是两个数转换成二进制表示后,按照位进行以下运算:
, F6 Q3 ]3 j/ n& F 左操作数 右操作数 异或结果
* h1 p( q# _* O. F4 P- N! [! m: u' _ 0 0 0
6 n; @9 Z" c- l+ j/ f 1 1 0
! F! R c; m3 d8 l 0 1 1 6 ~0 L& p5 S' P6 T2 k
1 0 1 / Q/ u. p L" _* B1 ~5 d
也就是对于 0 和 1,相同的数异或为 0,不同的数异或为 1。
) M8 S# `1 x3 k, C$ A$ g0 ` 这样就有了三个比较清晰的性质:
# x+ m0 A0 b4 K 1)两个相同的十进制数异或的结果一定位零。
+ X$ H( t2 g: I8 o u7 ~$ T; m 2)任何一个数和 0 的异或结果一定是它本身。 9 Z7 a3 v+ N9 _
3)异或运算满足结合律和交换律。 ; {8 e1 T5 v- e3 |8 i4 f
#include <stdio.h>
# u2 H' a9 i, j7 p1 Y/ \9 M3 _+ j8 X int main() { : g/ m# {8 }& r7 e
int a, b; ( V7 X4 ?7 u5 A# l
while (scanf("%d %d", &a, &b) != EOF) { 8 |* w: z( E5 D
a = a ^ b; // (1) # e: l$ R/ t% E/ c
b = a ^ b; // (2)
' n N4 C3 W7 \' @ a = a ^ b; // (3) , r: y" A' @! D9 p5 Q) i
printf("%d %d\n", a, b);
" m0 S# |8 T3 H. d+ D }
& J$ @0 E6 |( b return 0; 1 b& {: I# l+ V) |! o2 r
} % q6 f" _, c3 L( k+ C& T
1
4 T* n) T) G. Q) S6 G4 j 2 $ X6 G6 T& x! ~4 T6 n
3 " a; c( q5 H& T1 j
4 + V# z7 g/ u0 m9 `7 s* I
5
* }, D9 d! E m) ?, C% ]2 | 6 8 o3 B" d- K3 c7 W/ w
7 4 m* L) D/ d/ G. q% M
8 ( Y, U9 v' ^$ [% ^6 J
9
" N$ @- v0 `8 i" R/ C; n t 10
2 P; U+ X' Q0 Z2 M$ f; W 11
: K% |' r) L; [: t/ ?7 Y7 L2 Q3 B$ v 我们直接来看 ( 1 ) (1)(1) 和 ( 2 ) (2)(2) 这两句话,相当于b等于a ^ b ^ b,根据异或的几个性质,我们知道,这时候的b的值已经变成原先a的值了。 6 ~: W8 e7 n5 U# n
而再来看最后一句话,相当于a等于a ^ b ^ a,还是根据异或的几个性质,这时候,a的值已经变成了原先b的值。 7 t! a1 j' M, ~; o, O5 p, @8 ?
从而实现了变量a和b的交换。
* t# }3 U& k: {9 r0 u
8 V' Z/ P* m% K/ b: j4 ~ 1 _- ]9 G! h1 Q: L- {/ ] B
4、正确解法4:奇淫技巧
3 W" d! s9 \& q" }$ H1 N- }+ E9 @ 当然,由于这个题目问的是交换变量后的输出,所以它是没办法知道我程序中是否真的进行了交换,所以可以干一些神奇的事情。比如这么写: : M* G# L5 v( N- A$ L9 x
#include <stdio.h>
2 y# ?2 c; W4 T' V6 j2 W# f int main() { 7 d" c+ Z1 r$ W4 G4 y7 T- O3 w d
int a, b; # Y9 s, f) v( k' }9 _8 Q& h3 }1 u
while (scanf("%d %d", &a, &b) != EOF) { . V3 Q/ Z, s- O
printf("%d %d\n", b, a); ( s3 Y; C2 K9 H m! e4 D. j3 G$ n n
}
$ Q* O& j+ ^( v7 H7 U return 0;
! ^1 m) R3 u; S } ) V/ T3 h$ C% z6 n1 O
1 + ]7 i% I# K) W. E0 l% P. @0 i
2 6 N$ @% T0 i: l
3
( t f9 N: e+ ]2 G! z8 o' w" } 4 " ?/ z; M' K4 {& }- w4 N7 z8 Z
5
& H/ `8 ^! i0 `, W* n% r6 D$ b 6 1 {- ^8 }! a% r$ t0 `. z8 ^
7
. X+ d$ y& `( g |7 J 8 6 Q1 N1 U2 s) e5 e( p! x* D
你学废了吗 🤣? - ~1 E3 D6 m& H6 a) x7 q/ ]
2、例题2:整数溢出 & X6 g# m( F+ V4 l
一、题目描述
6 J$ K' P* {- w2 \# A9 ]+ @ 先输入一个 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
& `% a0 G8 f5 ~# }" X 62 " E l' Y" s" A7 d
),输出 a + b + c + d a+b+c+da+b+c+d 的值。
" `) A! I, G% S' K
, d8 j& X+ n% [: H$ B; L0 P 0 Y j8 Z3 Z1 T3 G
二、解题思路 $ A! ~$ V* ]8 D+ P3 S4 \ ~
难度:🔴🔴⚪⚪⚪ ! w) O! d/ L; N
6 v# l: c# {& X, @ . k& U/ H8 t( J/ X! h$ ]: i$ s5 V
这个问题考察的是对补码的理解。
b7 I' o: s# O$ f7 T 仔细观察题目给出的四个数的范围:[ 0 , 2 62 ] [0, 2^{62}][0,2 ! n+ K, K s" { l8 A, B
62
) u8 W0 D4 O. ? n# z6 j* Q ],这四个数加起来的和最大值为 2 64 2^{64}2
5 z! v8 R! k1 D2 ~$ L) ~ 64
2 j/ b0 p" O% U% w 。而C语言中,long long的最大值为:2 63 − 1 2^{63}-12 x- A0 w1 L: j+ ?$ h( B$ |/ L7 s
63 " R- m) I) K& Y. u3 u, D( ?
−1,就算是unsigned long long,最大值也只有2 64 − 1 2^{64}-12 # U6 c; [# E+ f6 F8 h/ q# G+ E$ M9 {* y
64 3 {* w0 K) m z: p6 {2 Z1 A
−1。 / k1 s1 z6 T& [
但是我们发现,只有当四个数都取得最大值 2 62 2^{62}2
6 {4 T4 `1 H$ D/ |% S+ D 62 ) Z. S8 {- _, A7 Y# x
时,结果才为 2 64 2^{64}2
; `5 k9 b7 ]4 m 64 ) B9 ~ R* Y! @- F5 H; R
,所以可以对这一种情况进行特殊判断,具体参考代码详解。 * V8 @' J S1 q% n0 ]6 `! ^( }0 s6 u
三、代码详解
- s5 M1 j) W1 E7 A# E( T" L" B #include <stdio.h>
* F6 f0 f7 q3 c typedef unsigned long long ull; // (1) ; }+ B4 P) g9 c2 C, M
const ull MAX = (((ull)1)<<62); // (2)
% e& D: M8 c, {9 h$ H
4 P! \. f9 E8 f/ |0 M 5 s, }, X" L% F, |5 ^& n
int main() {
6 s7 w' ]9 @" {& H$ K" ] int t; % @+ a3 v& A. Q- H
ull a, b, c, d;
* N$ h1 A- Z8 v- k) C scanf("%d", &t); " e$ p n# R8 J
while (t--) {
) I' }) O6 m( O# U! ^* v scanf("%llu %llu %llu %llu", &a, &b, &c, &d); // (3)
) `5 \0 W* L# l7 [ if (a == MAX && b == MAX && c == MAX && d == MAX) // (4)
7 u) W: u1 z' @& o5 ? printf("18446744073709551616\n"); // (5) + c, ~. h c, }8 B! I! V' t
else
2 Z q& _. l# t* |. c printf("%llu\n", a + b + c + d); // (6)
% d+ U8 `4 A$ r% _( A } ' G9 f. X" p6 l* E, e3 J* P* A
return 0; " u7 }% M# i3 _+ m. i
} 9 D( ^0 k6 \7 O1 q) f
1
$ C% z$ m% {, x6 A5 Z 2
# @( F" U5 G' U" B) r: Q 3
9 T- @* T& J, |: o* b 4
; l9 _7 |9 `% @ 5
2 `' m. S) `5 Y d5 u& e- o7 P/ y2 V 6
' z: O" q) C3 f4 V 7
0 k2 W j6 j" W/ A! t( q$ {0 Y 8
1 {9 }5 A# h4 t- x 9 0 \+ \7 G* l- I, P
10 , u. w. I/ y' k6 x* f% ^7 o
11 s' ~& W% R* }- _: f
12
4 p/ S) Y3 G+ w 13
% l: R5 a' _. Y3 w7 E; S; J 14 + z2 V: T5 t# a0 J9 \. q
15
* \4 f4 l( Q; |+ P- D; _ 16 . R1 y* p+ i6 r! R, Y; n
17 & |" m1 h5 b3 h/ R6 j
( 1 ) (1)(1) 由于这题数据量较大,所有数据都需要用64位无符号整型。ull作为unsigned long long的别名; * E6 `& E4 |2 f7 a# p/ N4 ?
( 2 ) (2)(2) 用常量MAX表示 2 62 2^{62}2 * @9 K- T6 q+ v' y" w9 d; c( x
62 ( z( `9 b- P* W5 A+ G6 d
,这里采用左移运算符直接实现 2 22 是幂运算; 5 V; v5 D$ n$ L% v' p4 v
数学 C语言
5 h' [0 S6 @. Y, z0 K5 v4 A% n 2 n 2^n2 2 x, x2 I1 q/ B# Q
n ) |# [2 D# |- u) z- ?3 D- b
1<<n
( V2 `" H$ q$ B/ d; { 需要注意的是,由于 1 是int类型,所以需要对 1 进行强制转换。(ull)1等价于(unsigned long long)1;
- a6 g" w4 l7 F5 N# ]$ F ( 3 ) (3)(3) %llu是无符号64位整型的输入方式; & z# V4 G4 I, |5 y- `" P
( 4 ) (4)(4) 这里是对所有数都等于最大值的特殊判断,&&运算符的优先级低于==,所以这里不加括号也没事;
$ L/ E- y7 m- N- l9 C9 w ( 5 ) (5)(5) 由于 2 64 2^{64}2 " u4 Z" s' [6 t V( m2 u/ l
64 5 f$ j( i; M# k/ U
是无法用数字的形式输出的,所以我们提前计算机算好以后,用字符串的形式进行输出;
' G3 n! Z1 Z8 W, N1 Q& _1 }( ~ ( 6 ) (6)(6) 其它情况都在 [ 0 , 2 64 − 1 ] [0, 2^{64}-1][0,2 3 o8 |! S0 |3 g
64 9 z" I) d& A* \8 Y h8 j
−1] 范围内,直接相加输出即可。
+ a( k0 t7 b g* Q# i& }1 \ 由于这个专栏是付费专栏,可能对学生党不是很友好,所以作者经过再三思考,打算放出 300 张 一折优惠券, 先到先得。只要拿这个图片来找作者即可享受,仅限前 300 名。
6 P/ y' f9 K' N, Q5 x 为了适当提高一定门槛,你至少需要学会如何下载图片或者截图并且发送到微信里 🤣。
* ~9 x$ U% l* u! ?4 t) V m
: _2 K0 ~$ H! K3 B+ Z8 z/ ~, Y5 V/ l o 0 U4 K V/ |1 R) `; s0 _. Y
3、数据结构 ; C0 r& p) c k0 v9 ~7 Q7 _/ n
《C语言入门100例》上的例题,如果能理解前面 25 道,那基本C语言的学习就可以告一段落了,接下来就要开始我们的数据结构的学习了。
: z, v0 K2 @ x! X' F" F 1、什么是数据结构 ; H: X; ^) W7 ^" _; E$ l
你可能听说过 数组、链表、队列、栈、堆、二叉树、图,没错,这些都是数据结构,但是你要问我什么是数据结构,我突然就一脸懵逼了。 ! E1 W- B+ @" e2 g
如果一定要给出一个官方的解释,那么它就是:
9 R" C2 u( ?* h( M& X 计算机存储、组织数据的方式。相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。往往同高效的检索算法和索引技术有关。 + [5 \6 E5 e7 H( K
: {4 e4 L1 N( o- p9 |) b5 e! M# v
2 y" q+ C4 S/ S! }6 c2 ?0 r
是不是还不如说它是堆,是栈,是队列呢?
2 i* O! s' W8 n' ^. I6 \ 是这样的,我们学习的过程中,跳过一些不必要的概念,能够节省我们更多的时间,从而达到更好的效果,当你还在理解数据结构是什么的时候,可能人家已经知道了栈有哪些操作了。 / J( u& w% i4 U- [2 E! G
2、数据结构和算法的关系
6 u2 e0 I! x, X" G- \% A 很多同学搞不明白,数据结构与算法有哪些千丝万缕的关系?甚至有些同学以为算法里本身就包含了数据结构。 $ ]4 S i! W7 N m7 ~, M) h: E
数据结构主要讲解数据的组织形式,比如链表,堆,栈,队列。
; X: g ^' y; b 而算法,则注重的是思想,比如链表的元素怎么插入、删除、查找?堆的元素怎么弹出来的?栈为什么是先进后出?队列又为什么是先进先出? 1 u* A! ?* y& `/ @5 X" l1 |8 a
讲得直白一点,数据结构是有实体的,算法是虚拟的;数据结构是物质上的,算法是精神上的。当然,物质和精神 缺一不可。
4 _' A8 y6 B& R% X" u5 C9 Q* |* r& V 3、数据结构概览
9 |9 z! }( ?' ^* U, B 周末花了一个下午整理的思维导图,数据结构: % n R, |* E0 K; i3 y
& v5 U! `6 Y0 h! _' r
' t4 a2 q3 I* N' R: A 常用的一些数据结构,各自有各自的优缺点,总结如下: + P$ Z2 P. J i
a、数组
% V& g: s- W7 b9 ]& B1 D+ H 内存结构:内存空间连续
) A3 v4 ]3 U2 O/ H 实现难度:简单 + I+ W ^2 M, k
下标访问:支持
4 G3 P: v9 f1 _ 分类:静态数组、动态数组 # C' N) a8 B9 F& ?( ^* E. J
插入时间复杂度:O ( n ) O(n)O(n)
( D( ?8 I }- {2 ^' O e) a. F 查找时间复杂度:O ( n ) O(n)O(n)
) ]7 ^8 Q! _+ D1 | 删除时间复杂度:O ( n ) O(n)O(n)
/ ~8 t4 U) I( h9 V& Q7 C3 M r9 ]
) t. t' ?2 \$ j- f3 ` 4 a5 R+ p* \8 `" I4 l
b、字符串
6 E2 l7 f2 ?- s$ N, v/ @& d 内存结构:内存空间连续,类似字符数组
! [, U, l& Y5 i: Y& m" { E$ B 实现难度:简单,一般系统会提供一些方便的字符串操作函数 2 F- Q# _, V! @1 E( I' p: @
下标访问:支持
/ X' T3 M3 J4 j5 y( `6 v 插入时间复杂度:O ( n ) O(n)O(n)
( O5 j: u+ U3 x* ~! v3 j I 查找时间复杂度:O ( n ) O(n)O(n)
$ ^* K+ _+ F7 u 删除时间复杂度:O ( n ) O(n)O(n) 8 c) f- I h, b2 L% J: t2 [0 J
1 S$ O9 M. [) X1 {8 Z
q1 d8 i; t7 i3 r7 V5 T9 F c、链表
4 D# _% n. ]$ b# ^ 内存结构:内存空间连续不连续,看具体实现
: ^7 o( D7 E1 k 实现难度:一般
3 s4 j! S) \8 Y 下标访问:不支持
/ L( U3 v2 B7 r! N/ ?8 w 分类:单向链表、双向链表、循环链表、DancingLinks 4 n4 h: M* D3 J1 v) [8 t( y
插入时间复杂度:O ( 1 ) O(1)O(1) . P4 j" Z7 }8 I) `0 A4 y! R
查找时间复杂度:O ( n ) O(n)O(n)
9 r. d) Z* R1 q L9 @6 G% M 删除时间复杂度:O ( 1 ) O(1)O(1) " l8 ~7 k$ }1 U
3 P w3 ], c- }* }
% f; z, F, S4 H d、哈希表 % s: F4 P' c4 z5 C8 g6 t" n! Y
内存结构:哈希表本身连续,但是衍生出来的结点逻辑上不连续
9 @) }- D. B2 m [ 实现难度:一般 P4 A8 u) C/ c! i; c
下标访问:不支持 5 X* b6 o' j \) f
分类:正数哈希、字符串哈希、滚动哈希
) A* T2 X, z) j 插入时间复杂度:O ( 1 ) O(1)O(1) % s, g' }/ N0 C+ t3 Z3 I5 Y
查找时间复杂度:O ( 1 ) O(1)O(1)
3 g: k3 R7 x! e& x4 o: v 删除时间复杂度:O ( 1 ) O(1)O(1) 4 y% p6 T) M* U$ i; F+ d
3 o/ }! S0 n6 `' W$ J
3 U! T$ o# ]* J' P* E e、队列 / H8 ^& J x8 V5 K8 G3 e
内存结构:看用数组实现,还是链表实现
1 s' s5 q4 s+ k* V. o, s 实现难度:一般 ! J" w1 T6 A+ _. U4 l/ |+ d2 v
下标访问:不支持
- f$ a6 e' ], n# b# r 分类:FIFO、单调队列、双端队列
& P" p4 n7 W" D: p8 F 插入时间复杂度:O ( 1 ) O(1)O(1) , Z5 B. ~) d. v; e5 }. Y( d
查找时间复杂度:理论上不支持
: d* W5 W" X2 k3 I. {5 r( l 删除时间复杂度:O ( 1 ) O(1)O(1)
" i6 ~ E. ?: z" E( K/ G
7 R0 G m1 w0 w* c3 S % j; m% d( ~- X0 A5 H
f、栈
# ^! Q- i- L: X5 o' M" E9 Z* h2 ` 内存结构:看用数组实现,还是链表实现
' ?. H% h& ~0 l. j6 ?) ~% O0 m 实现难度:一般 : H5 j) Y1 O6 V) q5 B, K7 ~7 ~0 v
下标访问:不支持
2 j: O' Y7 e2 _4 t7 y# Q' h 分类:FILO、单调栈 % u+ m( y0 l% ?- Y* v. `1 h
插入时间复杂度:O ( 1 ) O(1)O(1)
: c S7 Z# E% `3 F) J7 H! ^6 b 查找时间复杂度:理论上不支持 7 j+ h6 W2 I y% ^% r O* O
删除时间复杂度:O ( 1 ) O(1)O(1) 3 }& g3 C2 N0 V }: a5 f* G3 a
6 m7 v* S( g& w) M
c i3 L4 x: G. i4 ?
g、树 & A* @; J0 B+ N' K- u
内存结构:内存结构一般不连续,但是有时候实现的时候,为了方便,一般是物理连续,逻辑不连续
4 S( H9 k- V* E& J, u5 n d# w 实现难度:较难
3 I9 }7 c F% K6 X; l4 } 下标访问:不支持 * j+ Z: Y" H+ w$ a* k
分类:二叉树 和 多叉树
, L: d" E/ ^( i+ V6 r7 {/ N% u 插入时间复杂度:看情况而定
$ q! _# U' \) Z. n8 ? N" g 查找时间复杂度:理论上 O ( l o g 2 n ) O(log_2n)O(log : H& u. p7 h3 Z4 ~
2
5 H$ C6 s! {2 r9 S9 ^4 x t+ N 9 d! {4 K: w$ c# r
n)
9 c4 n6 Z% _: O' | Y 删除时间复杂度:看情况而定
% y! N- d: N6 z- A8 F$ {" L8 _ : @6 e* ], v$ {+ g
! A, P8 _$ ^4 b. t 1、二叉树
! ^( o. v* s+ @$ X, T2 U% G 二叉树的种类较多,比如:二叉搜索树、平衡树。平衡树又可以分为 AVL 树、红黑树、线段树、堆。最平衡的树莫过于满二叉树了。 0 B/ a/ H0 V7 p' ~ |$ Y
其中,堆也是一种二叉树,也就是我们常说的优先队列。 5 ^+ N: x& ~) m9 A
2、多叉树 & i/ s4 i. w j' |/ C# N5 W
B树和B+树是多叉树,当然我们平时学到的并查集其实也是个多叉树,更加严谨一点,应该称之为森林。 9 @' T% [2 c; ~/ y
h、图
$ P5 A6 q0 }* c4 [' k- T- S+ } 内存结构:不一定 / `0 V+ r2 q t% S( E/ b- @
实现难度:难
: m4 ]/ \' G2 \! O( ]3 e" Y% q( a 下标访问:不支持
3 N7 d' c! L& |1 D& U( O 分类:有向图、无向图 6 u) ` z* L$ |6 x" e
插入时间复杂度:根据算法而定 % M2 c6 Y' R' m6 C$ ], r
查找时间复杂度:根据算法而定
) Q* q" H) R1 O1 w" b. n* f 删除时间复杂度:根据算法而定 + v) o0 n) Y4 N/ U& h
/ `) r( X. z$ Y$ u& h
0 Y1 \; p8 o. s1 s9 Z9 @
1、图的概念 6 Q' i# i) K8 a+ ^3 Z1 F" J
在讲解最短路问题之前,首先需要介绍一下计算机中图(图论)的概念,如下:
: R# y* V; ^: `9 ]" v$ c3 L' d9 k 图 G GG 是一个有序二元组 ( V , E ) (V,E)(V,E),其中 V VV 称为顶点集合,E EE 称为边集合,E EE 与 V VV 不相交。顶点集合的元素被称为顶点,边集合的元素被称为边。 ( ] {# ]! L& w
对于无权图,边由二元组 ( 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 为权值,可以是任意类型。 ; B) {: H. z( w7 S, ^2 |
图分为有向图和无向图,对于有向图, ( 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;
; R2 z, y6 r# i 2、图的存储
* t, v$ N6 j+ a4 ^: m* L( f9 U 对于图的存储,程序实现上也有多种方案,根据不同情况采用不同的方案。接下来以图二-3-1所表示的图为例,讲解四种存储图的方案。
1 f9 L) \( p% | r) W- C
3 R- _ s$ q8 Q% [! Q, M
5 b& g5 W; k2 f1 W8 Y; V 1)邻接矩阵 + \# w: e) j% B, P4 O( [2 G
邻接矩阵是直接利用一个二维数组对边的关系进行存储,矩阵的第 i ii 行第 j jj 列的值 表示 i → j i \to ji→j 这条边的权值;特殊的,如果不存在这条边,用一个特殊标记 ∞ \infty∞ 来表示;如果 i = j i = ji=j,则权值为 0 00。 3 `5 F$ ?: `3 k* b$ F! \
它的优点是:实现非常简单,而且很容易理解;缺点也很明显,如果这个图是一个非常稀疏的图,图中边很少,但是点很多,就会造成非常大的内存浪费,点数过大的时候根本就无法存储。 6 Z$ o* W0 k* V a) w
[ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[ ) s, p: P1 W* J h- M: |
01∞9∞0∞8320∞∞∞30
7 |2 }" C) L' B; \1 J 0∞3∞102∞∞∞0398∞0
% e1 n+ E0 p2 [3 b4 b \right] & ^* f" f" f& W4 _( y3 S% T
⎣
8 [, b7 i& S) K. [% Q& }, U. Z ⎢
% s. k+ S' k5 k& @9 q ⎢ ( ^$ v* U- i# Y7 X
⎡ - ]# H. l9 R6 V8 j
6 }7 ?4 o+ p H* u , h; Y1 M* f, ~! V- ^
0 & r5 o/ W6 r3 G6 N+ b; `
1 ! D7 g5 g( b7 u6 ?/ O
∞
3 u/ d a, E& W) u 9
$ Y8 k/ l: ~: [2 _ 8 p, _4 C A2 N# J# ~4 O1 y1 B
+ z6 V# B7 c! d! ]7 z* V9 j) i ∞ 2 t0 U8 n4 r2 B& S3 i
0
- x& f' c# |2 m' L1 ` ∞
& S! E- I5 n; q- q 8 # P0 H: Z9 k: F5 r$ W+ z+ {2 `
: D, z- ^; m2 m% _5 O- c+ x# _
" `; |7 g% u- n& A+ g! j
3
3 G' @ h8 ?8 a8 E) H1 I; n$ Z 2 0 U# i' h/ B4 f+ `
0 / K a% H z, ?4 F
∞ * r n6 |" `4 Y7 }6 z! @
8 {/ |' w/ p" N4 V: A& R+ c
/ Z( `0 M: Q3 Z ∞ 2 B/ d) P# S7 K3 T3 W6 m
∞
+ i* o0 N. D t4 b& L! ]. z" S) m7 i 3 6 E) L) f: x# i; ]
0
' c v1 X1 I9 w( \
9 {0 E0 ~* ^3 r3 H
G/ f: Z! `% o3 O9 j& _ ⎦ 0 H7 o$ D0 ^* t8 R: r
⎥ ; x: v' m' s q( j- v( C+ [" g, r
⎥ 2 x& y; h5 b5 c5 N3 {/ z
⎤ 1 o$ X7 \: C! E" C- d' ^3 I# p
* d6 m* Q4 T3 B3 M2 \- |; C! n
U! K: _5 b# }$ w
2)邻接表 . d' w2 D, x5 p, s7 @% r
邻接表是图中常用的存储结构之一,采用链表来存储,每个顶点都有一个链表,链表的数据表示和当前顶点直接相邻的顶点的数据( v , w ) (v, w)(v,w),即 顶点 和 边权。 . e1 K. ^/ |! r, U6 m' n0 b; u3 [
它的优点是:对于稀疏图不会有数据浪费;缺点就是实现相对邻接矩阵来说较麻烦,需要自己实现链表,动态分配内存。
6 {$ n. Z% k E. L8 k0 D- O: }& V( C3 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) 二元组。 ! C; y6 V6 W/ s
8 w2 d6 S' w% L
2 T4 S# m: b& u: W& k: W9 I6 E 在 C++ 中,还可以使用 vector 这个容器来代替链表的功能;
- m4 d' l/ v) g3 r vector<Edge> edges[maxn];
7 y# o$ e! ]3 D$ y( N 1
! S* e( [7 b; j 3)前向星
7 ]3 o! a2 j" j" \5 t* T 前向星是以存储边的方式来存储图,先将边读入并存储在连续的数组中,然后按照边的起点进行排序,这样数组中起点相等的边就能够在数组中进行连续访问了。
( {5 v3 N7 s- S) E/ V7 b. ~9 _ 它的优点是实现简单,容易理解;缺点是需要在所有边都读入完毕的情况下对所有边进行一次排序,带来了时间开销,实用性也较差,只适合离线算法。
- i5 ~5 r$ y- g2 e4 N X+ s 如图所示,表示的是三元组 ( u , v , w ) (u, v, w)(u,v,w) 的数组,i d x idxidx 代表数组下标。
7 {/ ?" [5 ]; a$ {- L; X " \; o0 _' Z( I( E# p
; g+ `, L- n+ V1 ]0 P9 t7 L 那么用哪种数据结构才能满足所有图的需求呢?
% Y2 Q* x ?9 U7 E0 E% L/ Z9 Y 接下来介绍一种新的数据结构 —— 链式前向星。
! A+ h; V( u' A. O, o 4)链式前向星
, H" T2 f' D+ @% `* @6 c 链式前向星和邻接表类似,也是链式结构和数组结构的结合,每个结点 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 s: p c! p( i4 L9 H: U: `6 t 具体的,我们需要一个边的结构体数组 edge[maxm],maxm表示边的总数,所有边都存储在这个结构体数组中,并且用head来指向 i ii 结点的第一条边。 6 n, z n9 W8 }- h3 `+ Y9 a
边的结构体声明如下:
2 {4 P; E+ M8 Q& O. p struct Edge {
" L9 g$ h7 |* ~# ~ int u, v, w, next; + {$ b) Y0 f; L
Edge() {}
3 U) B$ T7 n/ G3 }* F7 p" Z Edge(int _u, int _v, int _w, int _next) : 1 K3 z8 }+ D5 b0 e7 ?* D7 V% _
u(_u), v(_v), w(_w), next(_next) & }* {, ~$ r( p8 H7 ?
{ 1 h6 L2 f: [1 h2 A
}
0 g7 I! O% x0 s. b& A9 p8 w+ a# ? }edge[maxm]; : M% J8 D$ F5 P
1 % c( V& B# X( K7 C6 T1 ~ D4 I$ o
2
* d. R0 ^- {2 X A 3
# Y' B/ R# @; L1 n: N6 k 4 , m+ @2 U6 c& R1 q$ Q5 i
5
9 Q! q8 e( G$ e. K7 a 6 9 \6 e5 H. b S" k- i1 n' a
7
' e0 `8 A2 ?) m4 d+ m) ] 8
( Z9 z+ p( T$ _ 初始化所有的head = -1,当前边总数 edgeCount = 0; " B" h. Z! l, ^1 Y7 i+ M' x
每读入一条 u → v u \to vu→v 的边,调用 addEdge(u, v, w),具体函数的实现如下: + ^/ b4 ]& [1 T8 P x2 f; Y
void addEdge(int u, int v, int w) { f4 ]" p( ?- n! [0 m
edge[edgeCount] = Edge(u, v, w, head); ' c2 ]' o7 h* z" H g- Z
head = edgeCount++; 0 M6 o R: N6 z1 `2 T. |3 O- |
}
, Y0 T( F- d; B, A$ V3 b 1 - f' f4 u4 l* G5 j' |
2 0 L1 D1 p# h! K* H5 N
3
3 J' s' C' R6 f7 P X 4 F* ?+ r, {; {
这个函数的含义是每加入一条边 ( u , v , w ) (u, v, w)(u,v,w),就在原有的链表结构的首部插入这条边,使得每次插入的时间复杂度为 O ( 1 ) O(1)O(1),所以链表的边的顺序和读入顺序正好是逆序的。这种结构在无论是稠密的还是稀疏的图上都有非常好的表现,空间上没有浪费,时间上也是最小开销。 2 P% e' N5 A O; r5 z! K
调用的时候只要通过head就能访问到由 i ii 出发的第一条边的编号,通过编号到edge数组进行索引可以得到边的具体信息,然后根据这条边的next域可以得到第二条边的编号,以此类推,直到 next域为 -1 为止。
# I( p" t9 W' T for (int e = head; ~e; e = edges[e].next) {
% d" \( x, u, d/ s4 c" U int v = edges[e].v;
& w+ W& Y+ N. z: c7 A6 m5 o( ^; K ValueType w = edges[e].w;
3 ]5 ], B5 H5 Q' I5 b( w- B7 }' N1 d ... $ _- a c5 F4 I I% ?4 Y& j5 c* Q/ x
} # U+ e# C" d7 z4 B! h( v; p, N: B/ ?
1 7 p% ^" ^3 [4 c- p( x$ `/ p
2 4 z! l1 m6 u2 D' v
3
* Z5 ~' Y$ c% l1 }# c/ I$ X. ` 4
! v9 H4 i ]2 A2 t/ J8 b 5 1 h6 G w3 v4 U8 n' s
文中的 ~e等价于 e != -1,是对e进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。
4 h1 B' Z% J9 d7 H; O 4、算法入门
7 Z$ O0 o5 k" Z) V) L0 j 算法入门,其实就是要开始我们的刷题之旅了。先给出思维导图,然后一一介绍入门十大算法。
7 I# t8 p1 N' g# J, c ( h% ]+ m/ o( R' @' ^
& P! N6 G. ]5 W! o [ 入门十大算法是 枚举、排序、模拟、二分、双指针、差分法、位运算、贪心、迭代、分治。
4 k; M( j5 l+ p7 _+ Y% g 对于这十大算法,我会逐步更新道这个专栏里面:《LeetCode算法全集》。
5 H+ w4 `3 [: `5 H* Z 1、枚举
+ j @4 o+ y: o* e) j 枚举可以简单理解成for循环,从一个数组中遍历查找一个值,就是枚举;从一个数组中找到一个最大值,就是枚举;求数组所有数的和,也是枚举。
! S; ? W4 h0 `5 T ?% M! p 对于枚举而言,基本就是循环语句的语法学会,这个算法就算学会了。
, Z/ }" r+ u* t5 e1 |. d6 w" H2 O 2、排序 - \, u& q4 [" V. q/ Y
既然是入门,千万不要去看快排、希尔排序这种冷门排序。
, Z9 M! [' r. _5 ?0 B" s% p# r 冒泡排序、选择排序、简单插入排序 原理好懂,先看懂再说,其他不管。因为这三者都是基于枚举的。 ; ~3 c) A# m8 S+ @% j- }5 ]
C中有现成qsort排序函数,C++中有现成 sort排序函数,直接拿来用,等算法进阶时再回头来看快速排序的算法实现。
7 U- I. {) ~& J l 3、模拟 6 H) B8 ?9 S$ d( ~- S* K, n
模拟就是要求做什么,你就做什么,完全不要去考虑效率问题。
& w, R3 o& U4 t" W( z; n7 W 不管时间复杂度 和 空间复杂度,放手去做! * |% P6 ?/ s3 c6 q4 a) X; S
但是,有时候模拟题需要一些复杂的数据结构,所以模拟题难起来也可以很男,难上加难。 % ^' E2 }8 m& a' X1 v( l
4、二分 2 z+ d* n- H6 {+ Z0 v2 o; z
二分一般指二分查找,当然有时候也指代二分枚举。 , _; |! u3 y2 X2 W" n2 _
例如,在一个有序数组中查找值,我们一般这个干:
! F4 B! l' |$ L8 D1 d* z: D! N 1)令初始情况下,数组下标从 0 开始,且数组长度为 n nn,则定义一个区间,它的左端点是 l = 0 l=0l=0,右端点是 r = n − 1 r = n-1r=n−1;
! m2 h a4 W1 n8 F8 F# w* z G 2)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2mid=(l+r)/2,并且判断 m i d midmid 对应的数组元素和给定的目标值的大小关系,主要有三种:
; }- `9 M0 {. P 2.a)目标值 等于 数组元素,直接返回 m i d midmid;
+ T( @' O% m: N. J* u 2.b)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r][mid+1,r],迭代左区间端点:l = m i d + 1 l = mid + 1l=mid+1;
: \/ |3 f: d' }7 X. i, |2 b, U 2.c)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1][l,mid−1],迭代右区间端点:r = m i d − 1 r = mid - 1r=mid−1;
% @( v2 E- E( q+ W 3)如果这时候 l > r l > rl>r,则说明没有找到目标值,返回 − 1 -1−1;否则,回到 2)继续迭代。
8 K i. A/ X; m7 s' r- C8 W 5、双指针 F1 y; q4 ~/ W% Z1 H1 p5 e
双指针,主要是利用两个下标在一个数组上,根据问题的单调性,进行指针偏移,由于每个指针只往后偏移,所以时间复杂度可以达到 O ( n ) O(n)O(n),由于思想非常简单,所以出题时,热度不低。 ) r w/ ~. P3 ^ b0 I% _
& z3 v0 E% h9 \$ I: x( J * U) B9 F2 R0 Q# H
6、差分法
/ q8 g' P* |0 F) i 差分法一般配合前缀和。 ' E. z8 J0 B; w; ^/ c
对于区间 [ l , r ] [l, r][l,r] 内求满足数量的数,可以利用差分法分解问题; 6 i% H9 [4 C7 G
假设 [ 0 , x ] [0, x][0,x] 内的 g o o d n u m b e r good \ numbergood number 数量为 g x g_xg 7 F/ ]3 u" d9 `. k, {) `8 e
x
) D& Z+ ?% v/ _8 D; e# k$ ~6 u
( y, c+ B3 r0 y/ z4 f3 F* P6 q2 _ ,那么区间 [ l , r ] [l, r][l,r] 内的数量就是 g r − g l − 1 g_r - g_{l-1}g * \( _( q8 n% s# E9 w# r
r
, a7 ^) Q9 I) i4 \ " u( q0 T4 e/ o; b+ h6 D: a
−g
* Y" F/ c( l, P& ] l−1
5 X& h; c3 ^2 ~0 L& F) T 7 m p) M' W! C1 R
;分别用同样的方法求出 g r g_rg
$ V5 \2 M, o4 \, ~0 z; [0 ] r * K6 L5 B1 p, b. K6 B
& ]/ G0 [/ d$ E9 M; k {
和 g l − 1 g_{l-1}g 7 T$ w$ y6 ]0 x; J& m
l−1 3 d( ^2 g0 G( @6 L$ P+ o
& V( O! m& y; {5 M* T
,再相减即可; 7 T% G3 S. ` |. U; X2 \
9 M1 N) ^& w' m# t8 [9 C ; @7 K% f( \1 T3 x8 A
7、位运算 : n7 k j6 t' M
位运算可以理解成对二进制数字上的每一个位进行操作的运算。
( f& s8 I& F0 g4 B6 H* n; A 位运算分为 布尔位运算符 和 移位位运算符。
+ Y5 K7 j: A3 ]- L* m 布尔位运算符又分为 位与(&)、位或(|)、异或(^)、按位取反(~);移位位运算符分为 左移(<<) 和 右移(>>)。
1 D6 r3 }0 }* O( j* R 如图所示: * r/ Z9 Z+ \- ^* {, r% ?% M
& ^ n, l" [" T$ C& u
( x. G* O' y# K/ s6 r' m 位运算的特点是语句短,但是可以干大事! + ]. K! ]8 n! o- Q& L
比如,请用一句话来判断一个数是否是2的幂,代码如下: - V0 l# }1 O+ X! P7 v3 `5 c- B
!(x & (x - 1)) 1 E9 s6 O$ r R7 Y2 e P; p
1 2 ?4 Z' I/ l/ G: o/ o7 z2 Z
8、贪心
6 p' {# [+ f3 s9 H 贪心,一般就是按照当前最优解,去推算全局最优解。 ( y1 j- t, D/ M8 u7 Z
所以,只有当当前最优解和全局最优解一致时才能用贪心算法。贪心算法的证明是比较难的,但是一些简单的贪心问题会比较直观,很容易看出来这个能够这么贪。
7 W+ ]+ O5 g4 g 9、迭代 ) t7 b% q1 ^0 B% q' @4 x
每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,周而复始,直到问题全部解决。
9 x7 {: q' l* B. I# h* P 10、分治
5 E! P, u: i0 @ 分治,就是把问题分成若干子问题求解,子问题解决后,问题就解决了。一般利用递归实现。属于初学者比较头疼的内容。递归一开始学习的时候,一定要注意全局变量和局部变量的关系。 / I9 O1 v: Q, V! f! T+ B$ \
5、算法进阶 0 A/ R1 W2 N6 R- e2 i
算法进阶这块是我打算规划自己未来十年去完成的一个项目,囊括了 大学生ACM程序设计竞赛、高中生的OI竞赛、LeetCode 职场面试算法 的算法全集,也就是之前网络上比较有名的 《夜深人静写算法》 系列,这可以说是我自己对自己的一个要求和目标吧。 % E% Z. P7 l3 o( H# ~
如果只是想进大厂,那么 算法入门 已经足够了,不需要再来看算法进阶了,当然如果对算法有浓厚兴趣,也欢迎和我一起打卡。由于内容较难,工作也比较忙,所以学的也比较慢,一周基本也只能更新一篇。 2 J. d8 h1 v$ r2 ?
这个系列主要分为以下几个大块内容:
( K$ _2 ?; p4 | 1)图论 . Y. u7 M: O2 e# S
2)动态规划
* s8 T: R: ~0 q7 m4 G% N, R 3)计算几何
: k1 N1 _! r# U( v. Y) y1 ] 4)数论
1 O7 F+ }3 S: \ 5)字符串匹配
6 r( u% \7 T: P( B 6)高级数据结构(课本上学不到的) 5 s# J. n5 s, P- k8 u
7)杂项算法
7 k1 o j- X) k5 x/ m6 ~( \
x: D* Z) w. U7 u; k2 X2 `" M. C " r" I+ k4 K" H1 F
先来看下思维导图,然后我大致讲一下每一类算法各自的特点,以及学习方式: ! G, w* U, F/ O$ D
7 v6 i( u, Z0 F7 H$ n - I/ A1 a& G' }8 c0 r
' L$ [0 o) ^1 ^3 p% |! n % v/ F. n8 \, g8 Y8 o7 c; K
1)图论
4 C3 T" F, ~4 }, g 1、搜索概览 ) {8 Q1 b) {7 Q1 Z4 L
图论主要围绕搜索算法进行展开。搜索算法的原理就是枚举。利用计算机的高性能,给出人类制定好的规则,枚举出所有可行的情况,找到可行解或者最优解。 . O. w& h% Z7 M0 P- g
* N, g" Z- t+ P8 O
2 \+ h4 F; r% C& e 比较常见的搜索算法是 深度优先搜索(又叫深度优先遍历) 和 广度优先搜索(又叫广度优先遍历 或者 宽度优先遍历)。各种图论的算法基本都是依靠这两者进行展开的。
$ k: P7 @- R% n1 o 2、深度优先搜索 / y/ Y3 i) X+ \2 W& B2 j
深度优先搜索一般用来求可行解,利用剪枝进行优化,在树形结构的图上用处较多;而广度优先搜索一般用来求最优解,配合哈希表进行状态空间的标记,从而避免重复状态的计算;
4 {* g7 m; {* G 原则上,天下万物皆可搜,只是时间已惘然。搜索会有大量的重复状态出现,这里的状态和动态规划的状态是同一个概念,所以有时候很难分清到底是用搜索还是动态规划。 7 h' d; m; ~9 y H; P& D8 S8 w. Q
但是,大体上还是有迹可循的,如果这个状态不能映射到数组被缓存下来,那么大概率就是需要用搜索来求解的。
4 l, e, T9 N% C6 L- B4 F+ v 如图所示,代表的是一个深度优先搜索的例子,红色实箭头表示搜索路径,蓝色虚箭头表示回溯路径。
4 ^: q% e n V& I( Q# {) m % S; s; T4 [6 T! w
" i& y7 z2 L( ^
红色块表示往下搜索,蓝色块表示往上回溯,遍历序列为: 3 D8 }0 z& k6 y) |
0 -> 1 -> 3 -> 4 -> 5 -> 2 -> 6
, k6 i2 t1 W- I5 z+ G0 @) b: z 1 , P4 L& F( c# u+ L; v+ Z/ L; {
同样,搜索的例子还有: : d1 {& ~, c3 p* ^( Y4 c( G, G8 R4 J- ]
A; i; d/ j! ~4 w7 d& H
3 J) P8 y8 m6 G$ c 计算的是利用递归实现的 n nn 的阶乘。 . ?( f6 `9 M2 B! Q% X
3、记忆化搜索 & |0 [2 b( p% z& t
对于斐波那契函数的求解,如下所示:
h8 U2 ~! e$ K f ( n ) = { 1 ( n = 0 ) 1 ( n = 1 ) f ( n − 1 ) + f ( n − 2 ) ( n > 2 ) f(n) =
+ b. E0 m2 q9 v ⎧⎩⎨11f(n−1)+f(n−2)(n=0)(n=1)(n>2) 9 l% \3 _. l5 I8 }7 e4 R3 W# F
{1(n=0)1(n=1)f(n−1)+f(n−2)(n>2) % Q3 q# V# Y! V" ? z* D
f(n)= 1 n" ?5 W) C7 T3 w# q
⎩
* Z, z$ F, G$ C' E$ r) \5 b ⎪ 3 x1 O. I# D3 u5 q
⎨
$ h, h+ W3 p' u9 S. D$ G7 y/ G# O; j ⎪ . H/ i% K/ _5 X- Q7 d$ ^
⎧
$ ^4 ?5 h3 A6 k% m8 _ 0 v' I) O- `8 T% `5 a& A
+ n; a! Z3 {/ W) Q6 O1 a
1 9 r/ J, q3 a* ~( x
1 - E7 u: ~0 ]# O( |: p# e u1 U
f(n−1)+f(n−2) - ?4 n7 u% Q: J7 {, L
0 u E. N# U2 d9 j, ?* W0 T! O
( F; R3 v6 g/ Z* E# z (n=0) 1 U9 r; h' u6 U, j( `/ o
(n=1)
- X+ p* x/ u+ k (n>2)
; }: W& C: L; n, W2 ^9 n & T, R- a5 f3 P% b7 `2 g) S+ c6 o! ?
/ L+ C& n6 {8 k* l0 q 对于 f ( 5 ) f(5)f(5) 的求解,程序调用如下:
$ b( c" M8 p6 M h+ U4 D8 S 3 t1 G5 e% r6 g' r7 `
. `" u9 s: x$ p& o 这个过程用到了很多重复状态的搜索,我们需要将它优化,一般将一些状态缓存起来。
3 d# R2 D9 B6 w# ` 我们通过一个动图来感受一下:
) R+ p6 d" \% f( `! F$ w7 W
4 C ^$ H. y8 O. I. f8 L3 D! D
9 ~9 K8 ^% I4 _6 V( d; { 当第二次需要计算 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表达式为真,直接返回,不再需要往下递归计算,这样就把原本的 “递归二叉树” 转换成了 “递归链”, 从而将原本指数级的算法变成了多项式级别。 - o" h) T( z6 Y6 Y
这就是记忆化搜索,像这种把状态缓存起来的方法,就是动态规划的思想了。
" `7 c& [. c- A 4、广度优先搜索
! F0 Z6 k0 _+ A" {1 f 单向广搜就是最简化情况下的广度优先搜索(Breadth First Search),以下简称为广搜。游戏开发过程中用到的比较广泛的 A* 寻路,就是广搜的加强版。 & `9 ?( \0 S3 D7 ~- P" u: t, N
我们通过一个动图来对广搜有一个初步的印象。 ! W$ `1 _1 b7 ]. T
: ]+ |$ e. U( _2 }5 q . h/ j* G" L: U
7 h9 Z6 i7 |. c" F/ F* \1 J
3 X1 G) M8 E, Z5 y: _" L 从图中可以看出,广搜的本质还是暴力枚举。即对于每个当前位置,枚举四个相邻可以行走的方向进行不断尝试,直到找到目的地。有点像洪水爆发,从一个源头开始逐渐蔓延开来,直到所有可达的区域都被洪水灌溉,所以我们也把这种算法称为 FloodFill。
& g6 D) `) F' T/ A) ~0 @1 N4 p 那么,如何把它描述成程序的语言呢?这里需要用到一种数据结构 —— 队列。 ( V' t: G: I: g; V; T% i# C7 N
这时候,算法和数据结构就完美结合了。 % k) K Y$ Q" P
2)动态规划
! o, ~9 S4 T+ ]; E 动态规划算法三要素: , w, s- @5 H+ _+ p+ J& w- u7 P
①所有不同的子问题组成的表; 0 A: `( a: \( X+ C j2 W
②解决问题的依赖关系可以看成是一个图;
, y; t( S" P0 ?' o5 `/ Q ③填充子问题的顺序(即对②的图进行拓扑排序,填充的过程称为状态转移); : t! Y6 Q; F( a; n- H
' z# U5 u' J2 Y! D1 }$ r
3 u T. l' ~! e i5 \ 如果子问题的数目为 O ( n t ) O(n^t)O(n ' N9 o& X# t( k. p4 M: b
t \3 a2 P) s- X- o- j- S, O
),每个子问题需要用到 O ( n e ) O(n^e)O(n
% f# R5 f* C* W6 L% m' w e + }1 r) f, f' [* V/ m+ U& d
) 个子问题的结果,那么我们称它为 tD/eD 的问题,于是可以总结出四类常用的动态规划方程:(下面会把opt作为取最优值的函数(一般取 m i n minmin 或 m a x maxmax ), w ( j , i ) w(j, i)w(j,i)为一个实函数,其它变量都可以在常数时间计算出来)。 g% X$ q% s" Z/ k/ D6 f
1、1D/1D
" L; H+ L! [0 @8 U* S$ J d [ i ] = o p t ( d [ j ] + w ( j , i ) ∣ 0 < = i < j ) d = opt( d[j] + w(j, i) | 0 <= i < j )
/ F; |6 g; T) l9 ? d=opt(d[j]+w(j,i)∣0<=i<j)
+ b- u& D8 }. X3 ~ 状态转移如图四所示(黄色块代表d [ i ] dd,绿色块代表d [ j ] d[j]d[j]): , a. o% e! t& f: Y9 U: c
" u) p% A. m e$ B7 e1 L4 v
; ~6 i* a+ g4 s/ o+ I6 N7 B: z 这类状态转移方程一般出现在线性模型中。 3 F+ G8 B6 g* r% f- ?
2、2D/0D
7 u! M! Q- f# F" r& ^0 j# h% R% K# c 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} ) 3 e' ?' H. A4 J2 D; K; Y
d[j]=opt(d[i−1][j]+x ! G3 R0 R6 W/ e7 k/ ]
i
0 f, O4 r1 w4 ] : R8 l: ]* `' E4 i
,d[j−1]+y
" ~, e: m9 Z2 p% R j
& q5 M1 |" P A9 n/ s; L& j2 W
! b' j8 M6 B. o! h3 t: ~/ z ,d[i−1][j−1]+z 3 H1 t t( W5 H4 i1 c8 \) @9 g
ij
2 \, r: W2 C! K3 q1 ?& T
( f% [& r: Q N )
" y3 T, H6 h4 [" f6 ^* p( @ 状态转移如图四所示: 2 [' d7 }+ s* l6 d9 H C1 b
' {3 ~0 i% ~8 ~8 ]" Q
, N0 F0 D0 K, x+ V, J2 B 比较经典的问题是最长公共子序列、最小编辑距离。 / S: `% o4 n& ?: ?6 D
有关最长公共子序列的问题,可以参考以下文章:夜深人静写算法(二十一)- 最长公共子序列 1 V4 V. y& f5 \' C1 x6 L- r
有关最小编辑距离的问题,可以参考以下文章:夜深人静写算法(二十二)- 最小编辑距离
9 y. H* o8 W* _ 3、2D/1D 4 H8 C' R! Z. ~
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] ) 6 _/ H! A2 D* p4 \
d[j]=w(i,j)+opt(d[k−1]+d[k][j]) " d1 }2 L: J% A& B# N' \ \7 ]0 ~
区间模型常用方程,如图所示: 7 M% A7 |& d! V9 U4 w3 N5 ^
2 w/ v- W. b5 m/ E5 l; N8 S, v
' |5 ]3 Q3 ?+ ~- L" P 另外一种常用的 2D/1D 的方程为: 2 c: d1 U5 t W
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 )
7 S( r/ {8 R' x- d d[j]=opt(d[i−1][k]+w(i,j,k)∣k<j) 9 [0 |. w/ M9 F2 W: R5 d6 r. z
区间模型的详细内容可以参考以下这篇文章:夜深人静写算法(二十七)- 区间DP % l4 |1 B1 g" ~: b6 p, _
4、2D/2D
% _7 d6 m2 l$ p9 V. v6 k2 J d [ 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)
: v+ b4 C- G) U d[j]=opt(d[i
/ ]- n5 b$ Z+ D. ~5 o% y ′
3 F$ a. d# j/ g. |$ Q- K. o ][j
/ j( s5 A$ @" W+ m9 L ′ 7 D! h3 y3 O+ N* {" Q0 Y
]+w(i 5 b. x0 m2 e2 X) U3 u1 ?" z
′ $ P% i `1 }- Y" \/ S
,j / Z p4 ]- K/ k) O
′ ! Y% g+ o8 D G
,i,j)∣0<=i . O: q; E; B3 U5 N: Q0 {0 o
′ 1 q; Q4 D. ]/ |' E5 y
<i,0<=j 5 b, s8 k. w' O* n; L- K
′
/ |. z+ S( q/ p6 e5 y <j)
3 x- u% d8 I- e# X 如图所示: " d, G, }- P/ M! T8 x
6 K' b% L9 l2 L* |( R7 P
, i: I( y! x1 a; m 常见于二维的迷宫问题,由于复杂度比较大,所以一般配合数据结构优化,如线段树、树状数组等。
& W! E+ i. ]) n 对于一个tD/eD 的动态规划问题,在不经过任何优化的情况下,可以粗略得到一个时间复杂度是O ( n t + e ) O(n^ {t+e})O(n 7 n% ?2 v; {' [' ~
t+e # T( ?( E0 T. Y, c. o
),空间复杂度是O ( n t ) O(n^t)O(n
& w: L% G' a; r ?: I t K$ [. x5 L8 N6 p& I
) 的算法,大多数情况下空间复杂度是很容易优化的,难点在于时间复杂度,后续章节将详细讲解各种情况下的动态规划优化算法。 ) D( S" c: V, ^
3)计算几何
8 p: W$ B/ Y. U; S+ o$ K3 Z 计算几何的问题是代码量最大的。它是计算机科学的一个分支,以往的解析几何,是用代数的方法,建立坐标系去解决问题,但是很多时候需要付出一些代价,比如精度误差,而计算几何更多的是从几何角度,用向量的方法来尽量减少精度误差,例如:将除法转化为乘法、避免三角函数等近似运算 等等。
- \8 w: ?5 z0 U5 H; G+ _. V0 D 如果一个比赛中,有一道计算几何的题,那么至少,它不会是一道水题。
6 [) {' p0 e% @+ H* b 1、double 代替 float
6 I5 S2 R- \5 C9 C( ` c++ 中 double 的精度高于 float,对精度要求较高的问题,务必采用 double; 9 c+ _$ x/ O l! M) z3 J. X) a
2、浮点数判定
; M0 z( H0 [$ H4 a 由于浮点数(小数)中是有无理数的,即无限不循环小数,也就是小数点后的位数是无限的,在计算机存储的时候不可能全部存下来,一定是近似的存储的,所以浮点数一定是存在精度误差的(实际上,就算是有理数,也是存在误差的,这和计算机存储机制有关,这里不再展开,有兴趣可以参见我博客的文章:C++ 浮点数精度判定);
& }& H: X$ h( F+ f 两个浮点数是否相等,可以采用两数相减的绝对值小于某个精度来实现:
4 p4 c2 [# m5 b( A const double eps = 1e-8;
; Y% K; G& Q. ]. r7 X bool EQ(double a, double b) {
" U; k) F3 y" y return fabs(a - b) < eps; * e) P6 P0 e9 D: s+ R, D
}
% n4 w9 l; y4 L/ A6 D 1
+ i: {9 E. }0 }$ Z4 f9 ^ p" m7 c 2
) E. x) ^+ k) @! R! n1 B8 Y 3 3 a- q7 N N- Z! d
4 % Q5 \: u7 D+ L$ _4 P5 S
并且可以用一个三值函数来确定某个数是零、大于零还是小于零:
- \, s2 o, @, b- d0 _" S$ ~ int threeValue(double d) {
% x* F" u& {6 C0 c) ^& T+ k& l1 u if (fabs(d) < eps)
: k O1 M! B# Z8 x N return 0;
. Y; q3 z6 z& ^4 O0 C return d > 0 ? 1 : -1; 2 @9 c. |; d# N4 ?2 Y' {
}
1 q* v. y( Z" z* V- S; d 1 . w8 L) n7 I2 e. O1 c: K! p( h
2
4 E8 ?* H, O: V u* n 3
+ v$ s0 t1 _. u$ |) ^& q0 ] 4 $ S `! T q6 F1 `1 }/ U
5 0 n- L' ?- ?* Y: Y6 `
3、负零判定 / F! M/ A/ s1 P; R! t
因为精度误差的存在,所以在输出的时候一定要注意,避免输出 -0.00: 5 @) p' ?! n# c
double v = -0.0000000001; / ?$ K' E% i* @, X7 x! H
printf("%.2lf\n", v); % G5 m: M0 r/ P( L8 d
1 6 f' |6 e1 V% _5 I5 t5 U
2 / m; G, X9 _- S: ^& S
避免方法是先通过三值函数确定实际值是否为0,如果是0,则需要取完绝对值后再输出: - h# f7 [: {5 C; X
double v = -0.0000000001; 1 q% J5 @/ M- E4 h$ A2 F: z+ b
if(threeValue(v) == 0) { % B' g8 p3 T3 v# |; B
v = fabs(v); M) D5 y- y, r9 {" h5 }4 p
}
4 ^! p1 S0 b; U& k3 n" ?% V printf("%.2lf\n", v);
9 T% Z( L _4 N7 r 1
2 X" X+ D! Q! M8 s" j1 N 2
" x- y$ z5 W4 B3 u 3 - U$ z8 @& J/ C; Z' m2 ]
4 : q& \! M" r# }, F: H
5
, S1 w4 Q/ s( e% D0 ` R0 T 4、避免三角函数、对数、开方、除法等 ; N" S% V4 ~3 u6 {, s0 a
c++ 三角函数运算方法采用的是 CORDIC算法,一种利用迭代的方式进行求解的算法,其中还用到了开方运算,所以实际的算力消耗还是很大的,在实际求解问题的过程中,能够避免不用就尽量不用。
$ `# x4 @3 i' g 除法运算会带来精度误差,所以能够转换成乘法的也尽量转换为乘法运算。
2 k) o T- A' g7 x! Z 5、系统性的学习
" W( i4 n- ^) G7 U 基础知识:点、向量、叉乘、点乘、旋转、线段、线段判交、三角形面积; . l2 q0 S# S. u9 n3 w' n
进阶知识:多边形面积、凸多边形判定、点在多边形内判定;
# P/ h4 J4 A! ?. B 相关算法:二维凸包、三维凸包、旋转卡壳、多边形面积交、多边形面积并、多边形面积异或、多边形和圆的面积交、半平面交、最小覆盖圆、最小包围球、模拟退火。 $ R5 x4 n: j6 o
/ v1 d( \+ {2 g5 A/ Q9 [
, V7 r8 q0 T t& G$ F0 D* m 学习计算几何,最好是系统性的,刷题的过程中不断提炼出自己的模板。
) g2 N+ R3 l+ H8 e% D# Q0 A9 V! N 4)数论
7 j' J! J, K+ z, K0 Q6 q 刷题的时候遇到不会的数论题,真的是很揪心,从头学起吧,内容实在是太多了,每个知识点都要证明吃透,不然下次遇到还是不会;不学吧,又不甘心,就是单纯的想把这个题过了,真是进退两难! ; a9 K2 w, G$ u8 W* w
数论对一个人的数学思维要求较高,但是一般也是一些固定的模式,所以把模板整理出来很重要。 4 e, p7 K; Z$ N. o) B
当然,数论也有简单问题,一般先做一些入门题提升信心。 ( {. Z7 Y, P; R4 L; z' K
1、数论入门 6 O. _9 m5 R4 s8 ^9 w( {
主要是一些基本概念,诸如: 3 n$ s: C- B) q$ d+ _7 M
整除性、素数与合数、素数判定、素数筛选法、因数分解、算术基本定理、因子个数、因子和、最大公约数 (GCD) 和 最小公倍数 (LCM)、辗转相除、同余、模运算、快速幂取模、循环节;
# U; x# n) v" ? 2、数论四大定理
( [ W: p' Y: a4 F' K$ v3 ^ 这四个定理学完,可以KO很多题: ! _- f, }9 W/ R& g( U& b
欧拉定理、中国剩余定理、费马小定理、威尔逊定理 ! N: _/ }2 Z$ _3 o
3、数论进阶
# p* u: u* n6 l: | 系统性的学习,基本也就这些内容了: & N" J& t. W- N1 K/ I( S
扩展欧几里得、逆元、欧拉函数、同余方程组、扩展欧拉定理、RSA、卢卡斯定理、整数分块、狄利克雷卷积、莫比乌斯反演、大数判素、大数因子分解、大步小步离散对数等等。
" ~. z4 }/ Z% _, ^ 5)字符串匹配
4 _9 x7 O2 E5 [# h& d: h$ { 字符串匹配学习路线比较明确。 3 `( [' [8 [' g2 U# b6 i
先学习前缀匹配:字典树。
* y2 J4 z& I4 i, y- ?# K6 a 然后可以简单看一下回文串判定算法:Manacher。
% I! i2 ?1 o( E& F% D0 k3 v4 U B 以及经典的单字符串匹配算法:KMP。 + E' l: C+ S! O
实际上平时最常用的还是 BM 算法,而ACM中基本不考察。 * c5 {9 k- y! E0 T' K' I
然后就是较为高阶的 前缀自动机、后缀数组、后缀树、后缀自动机了。
3 W c9 x* c0 F 关于 算法学习路线 的内容到这里就结束了。
z4 f, e- ?5 | U3 l) x8 _) E 如果还有不懂的问题,可以 想方设法 找到作者的微信进行在线咨询。 + r9 w/ e2 ^' D `4 k) t. l6 R: B
参考资料 1 [ @5 T$ Q* Q5 X
【阶段一】C语言学习资料:《光天化日学C语言》(日更)
& U& c0 j" ?: b4 {. W# \: W 【阶段二】C语言例题:《C语言入门100例》(日更) 2 [/ M. o, Q! a) h( l2 b
【阶段三】算法入门题集:《LeetCode算法全集》(日更) 6 ]: g2 ~6 h6 b/ I& z- m& q
【阶段四】算法进阶:《夜深人静写算法》(周更)
' s' b2 i$ c3 f* H% X ————————————————
4 L7 ^! `" W. W' Q5 @ 版权声明:本文为CSDN博主「英雄哪里出来」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 5 ~: O0 r: a& W' a, X1 u
原文链接:https://blog.csdn.net/WhereIsHeroFrom/article/details/118382228 % t( U- C5 O G4 R
! m+ M& r0 g k1 V( V* v0 ` 0 h6 I- _) q+ r- x3 {
zan