前言
众所周知Java里面是没有指针的,但是有自动垃圾回收机制。C++是有指针的,但是没有自动垃圾回收。
Golang是一种新的语言,很多地方的设计借鉴了以前的语言,例如它是有指针的,并且有自动垃圾回收机制。
(主要代表着我需要进行指针的学习…)
Go语言基础学习
这方面去看一些教程就好:
首先菜鸟教程,内容不多,比较简单和基础:
然后可以看看知乎上是怎么推荐学习Go语言的:
然后还有官方的教程,有英文版的,也有中文版的,非常详细:
Documentation - The Go Programming Language
当然看上面的东西估计很快就烦了,那么不想看上面的就直接看官方提供的这个,真是个有意思的东西:
牛顿法(练习:循环与函数)
在教程练习:循环与函数中,要求我们实现一个求解平方根的函数, 方法就是使用牛顿法,代码如下:
1 | package main |
输出为:
1 | 561708 |
可以看到通过牛顿法,基本在10次迭代以内就可以计算出十分精确的平方根。
牛顿法原理:
牛顿法可以用于求解方程的根,当一个方程的根不好直接求解时,就可以使用牛顿法来进行迭代求解。
对于一个方程,需要求解 $x$ 的值:
那么首先利用泰勒展开来对 $f(x)$ 进行展开,这里从 $x_0$ 进行展开,并且只展开到一阶:
那么就可以使用这个近似式来求解原方程的近似解:
注意到 $f’(x_0)$ 就是原函数 $f(x)$ 在 $x_0$ 这点的斜率,那么其实在这一点上的切线方程就可以写为:
那么对于 $x = x_0 - \frac{f(x_0)}{f’(x_0)}$ 这一点,将它代入切线方程,求出它在切线上所对应的 $y$ :
也就是对应切线上的点 $(x_0 - \frac{f(x_0)}{f’(x_0)}, 0)$ ,画出图可以看出对应如下:
从图中可以看到观察到,这里所需要求解的点为曲线与x轴所相交的那个点 $x$ (y为0,也就是 $f(x)=0$ )。 虽然图中的 $x_{n+1}$ 并不是所需要求得点 $x$ ,但是它比 $x_n$ 更加接近 $x$ 。
所以这里就可以通过迭代来求解原方程的根,这一次从 $x_{n+1}$ 进行泰勒展开来求得下一个点。
回到练习题:
求解平方根,即方程 $x^2 = y$ 的解,即 $f(x) = x^2 - y$ ,代入上面的式子 $x = x_0 - \frac{f(x_0)}{f’(x_0)}$ , 可以推出:
题目中用的上面的形式,使用下面的形式其实也可以求解,但是两种方法的结果有细微的差别。 估计是考虑到溢出的问题吧,减法在这里可以保证更好的精度。
最大公约数与最小公倍数(练习题:最小众倍数)
最小众倍数:即是三个数的最小公倍数。
题目给了 a,b,c,d,e 一共5个数,求它们的最小众倍数中最小的那一个。
方法一:暴力搜索。复杂度等于搜索下限减去上限:
- 搜索下限:第3大的数。
- 搜索上限:前3小的数的乘积。
方法二:寻找所有三个数的组合的最小众倍数,取其中最小的那个。复杂度等于O(n^3),n为输入数的个数,本题 n = 5。
如何求3个数a,b,c的最小众倍数?
第一种求法:
- 求出
b*c
,a*c
的最大公约数A; - 求出
a*c
,a*b
的最大公约数B; - 求出A,B的最大公约数k;
- 使用
(a*b*c) / k
即得到结果。
这个想法就是,对于 $C = abc$ :
假设最小众倍数为 $D$ ,那么 $C$ 一定是 $D$ 的整数倍 $k$,即 $C = kD$ :
也就是:
那么 $k$ 就是能同时整除 $bc,ac,ab$ 的最大的数,也就是它们的最大众约数, 也就可以通过 $bc,ac$ 的最大公约数 $A$ , $ac,ab$ 的最大公约数B,再求 $A,B$ 的最大公约数 $k$ 即可:
两个数的最大公约数求法在之前的博客中有写LeetCode 149 Max Points on a Line
第二种求法:
- 求出a,b的最小公倍数A;
- 求出A,c的最小公倍数D;
- D就是a,b,c的最小众倍数。
首先,对于a,b的最小公倍数A:
然后对于A,c的最小公倍数D:
上面合在一起,即:
注意到由于最小公倍数的性质,$x_1$ 与 $x_2$ 互质,$x_3$ 与 $x_4$ 互质。
如果 $D$ 是a,b,c的最小众倍数,则 $x_1 x_3$ ,$x_2 x_3$,$x_4$ 的最大众约数为 1 。
这里使用反证,假设 $x_1 x_3$ ,$x_2 x_3$,$x_4$ 的最大众约数不为 1 。
由于$x_3$ 与 $x_4$ 互质,那么只能 $x_1$ 、 $x_2$ 与 $x_4$ 存在不为 1 的最大公约数 $S$ :
也就是 $x_1$ 与 $x_2$ 存在最大公约数 $S$ ,与原条件 $x_1$ 与 $x_2$ 互质相矛盾。
所以 $x_1 x_3$ ,$x_2 x_3$,$x_4$ 的最大众约数为 1 ,即 $D$ 是a,b,c的最小众倍数。
同理,使用公约数的解法可以改写为:
对于两个数的最小公倍数:
对于a,b的最小公倍数A :
注意 $k_1$ 与 $k_2$ 互质,假如a,b的最大公约数为 $S$ :
注意到 $h_1$ 与 $h_2$ 互质,那么这里就可以推出:
注意 $k_1$ 与 $k_2$ 互质,$h_1$ 与 $h_2$ 互质,所以 $\frac{h_2}{h_1}$ 必然等于 $\frac{k_1}{k_2}$ ,所以:
所以:
即a,b的最小公倍数等于它们的乘积除以它们的最大公约数。
1 | func LCM(a, b int) int { |
代码,求一个数组的最小众倍数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main
const MinInt = int(^uint(0) >> 1)
func zbsUseGCD(seq *[]int) int {
min := MinInt
n := len(*seq)
for i := 0; i < n; i++ {
for j := i+1; j < n; j++ {
for k := j+1; k < n; k++ {
zbsTmp := findZBSUseGCD((*seq)[i], (*seq)[j], (*seq)[k])
if zbsTmp < min {
min = zbsTmp
}
}
}
}
return min
}
func zbsUseLCM(seq *[]int) int {
min := MinInt
n := len(*seq)
for i := 0; i < n; i++ {
for j := i+1; j < n; j++ {
for k := j+1; k < n; k++ {
zbsTmp := findZBSUseLCM((*seq)[i], (*seq)[j], (*seq)[k])
if zbsTmp < min {
min = zbsTmp
}
}
}
}
return min
}
func findZBSUseGCD(a, b, c int) int {
return (a*b*c) / GCD(GCD(a*b, b*c), a*c)
}
func findZBSUseLCM(a, b, c int) int {
return LCM(a, LCM(b, c))
}
func LCM(a, b int) int {
return (a*b) / GCD(a, b)
}
func GCD(a, b int) int {
var c int
for b != 0 {
c = a % b
a = b
b = c
}
return a
}
1 | package main |
练习:Web 爬虫
修改 Crawl 函数来并行地抓取 URL,并且保证不重复。
- 使用map和互斥锁来保证URL的唯一抓取。
- 使用管道或者
sync.WaitGroup
来实现主线程等待子线程结束。
代码一:
1 | package main |
这里实现了SafeCounter
结构体来实现线程安全的map
,并在set
函数中使用了defer
来将解锁放到return
之后。
Crawl
函数使用了管道来让主线程等待子线程结束,但是可以看到这样写十分复杂,每个return之前都需要记得向管道写入。
可以使用下面方法简化:
1 | func WriteToChan(ch chan int) { |
这里编写函数WriteToChan
,然后使用defer
来实现函数结束时往管道写入。
当然也可以使用sync.WaitGroup
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Crawl(url string, sc *SafeCounter, fetcher Fetcher, ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
if !sc.set(url) {
return
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
var wgNext sync.WaitGroup
wgNext.Add(len(urls))
for _, u := range urls {
go Crawl(u, sc, fetcher, ch, &wgNext)
}
wgNext.Wait()
}
1 | func Crawl(url string, sc *SafeCounter, fetcher Fetcher, ch chan int, wg *sync.WaitGroup) { |
素数求解
素数或者质数:大于1,并且只能被1和自身整除的数,例如:2,3,5,7…
判断一个数 $n$ 是不是素数,可以按照定义:
- 方法一:从2开始判断是否能够整除,直到 $\sqrt{n}$ 。
- 方法二:如果比 $n$ 小的数所构成的合数中没有 $n$,那么 $n$ 是素数。
问题一:如何快速求解[0,n]
范围内的素数。
如果n较大的时候,使用方法一复杂度也较大,这时使用方法二会更快:
1 | package main |
当然,这样的写法会有很多重复筛选,例如2*3
,3*2
都会被统计,但是其实没有必要。
另外,还有重复类似 4*2, 4*3, 4*4 ...
其实就是 2*4, 2*6, 2*8 ...
,也就是说对于不是素数的数,
筛选它们的倍数是重复的。更进一步,这里只需要筛选素数的倍数就可以了。
按照上面所述,可以改写如下:
1 | func findPrime(n int) { |
相当于这里只统计:
2*2
,2*3
,2*4
…3*3
,3*4
,3*5
…5*5
,5*6
,5*7
…
当然这里还是会出现重复,例如2*9
和3*6
,但是已经少了很多重复的筛选了。
更进一步,快速筛选法:
1 | func findPrime(n int) { |
这样在筛选过程中,可以不出现任何一个重复。
那么为什么这样可以不出现任何一个重复,且筛选出所有合数呢?
首先每一个合数都可以以唯一形式被写成质数的乘积,即分解质因数
,那么只要是不同的质数组合,得到的肯定是不同的合数,
例如合数 $A_1 = a \times b \times c$ ,$A_2 = d \times e \times f$,假设 $a \leq b \leq c ; d \leq e \leq f$,
那么只有在 a == d && b == e && c == f
时, $A_1$ 等于 $A_2$。
那么在给定一个素数集合 {a, b, ..., n}
,就可以使用下面的方法来组合出所有这个集合所能表示的合数,且不发生重复:
图中的集合为{2, 3, 5, 7}
,左边是包含2的所有合数的组合方法,右边是其中 2,3,3
这条路线的样子。
可以看到,第一层生成 {2,2},{2,3},{2,5},{2,7}
,对于后面的三个集合 {2,3},{2,5},{2,7}
来说,它们向下的路线无法再经过 2
,
所有它们所生成的集合中,永远不可能包含子集 {2,2}
,所以 {2,2}
与 {2,3},{2,5},{2,7}
向下所得到的集合永远不会相同,
同理可以知道 {2,2},{2,3},{2,5},{2,7}
互相之间向下不可能生成相同集合,也就保证了不会重复。
至于覆盖所有合数,则是显然的,因为任何合数都可以表示为图中一条路线。
当然,另一种组合顺序也是一样的,也是快速筛选法中所用的:
也就是刚才是限制向左,这里是限制向右,不过效果都是一样的。
为什么说算法中使用的就是这种组合方式呢?我们来跑100以内素数,打印一下输出就知道了:
1 | i = 2 |
可以看到,输出完全符合预期(因为本来就是按这个组合方法写的…):
- 当
i = 2
,那么由于限制往右,那么向下只能再组合出{2,2}
; - 当
i = 3
,那么向下只能再组合出{2,2},{3,2}
; - 当
i = 6
,其实就是组合{3,2}
,相当于图中的 3 走到 2,那么再向下也只有 2 能选择了,所以只能组合出{3,2,2}
; - …
所以这里的 i%primes[j] == 0
其实就是在判断 primes[j]
是不是 i
的最小因数,一旦判断为 true
,就相当于到达了右边界,
所以进行 break
。
所以这样的方法一定不会出现任何重复的筛选。
至于包含所有合数,那也是明显的,对于合数 $A = a \times b \times … \times m \times n$ ,假设 $ a \geq b \geq … \geq m \geq n$, 那么一定会先遍历到 $i = a \times b \times … \times m$ ,又因为 $i$ 向下组合的右边界为 $m$ ,且 $m \geq n$ ,所以,一定会由 $i$ 筛选出 $A$, 所以这里一定不会漏掉任何一个合数。
证明完毕。