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