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