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