用Python3的Dict作为函数参数的一个陷阱

今天遇到了个大坑。所以要记录一下。
在python3中,如果给函数参数添加默认值,在用到Dict时候一定要小心。

1
2
3
4
5
6
def f(value, key, hash={}):
hash[value] = key
return hash

print(f('a', 1))
print(f('b', 2))

你觉得是

1
2
{'a': 1}
{'b': 2}

但结果确是

1
2
{'a': 1}
{'a': 1, 'b': 2}

因为python会直接把你更改过的default的值保存到下一次call。

使用None可以避免这个问题。

参考

[刷题笔记]187 Repeated DNA Sequences

题目

原题地址

思路

很多Discussion里面的解法就是直接10个字符为Key进行Hash了。

很显然,这个题目,出题者的意图是让你找到一种Mapping的方法,既能够保证Key足够短,又能够保证从Key可以还原成原来的字符串。

如果满分是100分,下面介绍的集中方法我都进行相应的打分。

20分解法

直接hash,略过不说了。

50分解法

借用md5 hash,把md5值的前几位作为key。缺点是我也不知道该取前几位,所以得一次次失败提交之后增加位数。

80分解法

我们知道

1
2
3
4
A => 65 => 1000001
C => 67 => 1000011
G => 71 => 1000111
T => 84 => 1010100

所以我们可以分别用三个bit代表ACGT这四个字符。

1
2
3
4
A => 001
C => 011
G => 111
T => 100

于是每10个字符可以用30个bit表示。例如

1
2
3
AAACCCCCAA
可以表示为
001001001011011011011011001001

当我们移动到下一个10字长字符串,我们只需要在现在的基础上右移3位(表示移除左边的第一个字符),然后或上表示下一个字符的3个bit。

由于我们需要30个bit表示key。而int一般长度为32, 所以就可以用一个int来表示key。

为了在左移过程中防止混入我们已经移除的bit。(想象一下,我们第30,29 位在左移之后还存在与我们的32bit int中),所以我们需要一个mask把这最高的两位去掉。

这个mask可以是111111111111111111111111111111(0x3fffffff),我们把它与上我们的30位bit就是所得到的偶为key的int。

当然为了能用3位bit表示我们的字符,我们需要将其他位省略,我们需要将每个字母与上7(111)。

这样我们逐个在给定的字符串s上移动,每移动一格计算一下key,并在hash[key]中加1。

当hash[key]大于1,我们知道当前字符串重复了,所以将其放入一个结果的set中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def find_repeated_dna_sequences(s)
str = s[0..9]
key = 0
str.split("").each do |c|
key = key << 3
key |= (c.ord & 7)
end

hash = {}
hash[key] = 1
set = []
for i in 10...s.length
key = key << 3
key |= (s[i].ord & 7)
key &= 0x3fffffff
if(hash[key])
set << s[i-9..i]
else
hash[key] =1
end
end
return set.uniq
end

100分的解法

在80分解法的基础上,我们知道我们的key是可以还原成原来的字符的。所以并不需要一个set。我们可以直接在hash中count,然后把左右count大于1的key还原。

怎么还原?我们可以将key不断除以8.通过获得的余数可以知道

1
2
3
4
余1 => A
余3 => C
余7 => G
余4 => T

于是最后的解答为

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
def find_repeated_dna_sequences(s)
str = s[0..9]
key = 0
str.split("").each do |c|
key = key << 3
key |= (c.ord & 7)
end

hash = {}
hash[key] = 1
for i in 10...s.length
key = key << 3
key |= (s[i].ord & 7)
key &= 0x3fffffff
if(hash[key])
hash[key] +=1
else
hash[key] =1
end
end
result = []
hash.each do |k,v|
if v > 1
str = ""
while k > 0
left = k % 8
str = "A" + str if left == 1
str = "C" + str if left == 3
str = "G" + str if left == 7
str = "T" + str if left == 4
k = k / 8
end
result << str
end
end
return result
end

反思

这个题目要求对二进制十分熟悉。当然如果不熟悉就得变得熟悉起来。毕竟不去克服的话就永远安于现状了。

[刷题笔记] 554 Brick Wall

问题

思路

这个问题解决的关键一下子就看到是累加数组了。

建立一个hash表,记录经过每个缝隙的砖块的count。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def least_bricks(wall)
hash = {}
wall.each do |level|
sum = 0
if level.length == 1
next
end
level[0..-2].each do |width|
sum += width
if hash[sum]
hash[sum] +=1
else
hash[sum] = 1
end
end
end
max = 0
hash.each do |k,v|
max = max > v ? max : v
end

return wall.length - max
end

一波三折的解题过程

上面的答案看似很简单,但是一开始,我却第一个想用dp来做。

dp[i][j] 来表示第i行第j个数经过的砖块个数。

后来发现其实dp只和上一行相关。于是把i省去了。不过还是内存溢出。

再仔细一想,那么多dp,其实只有缝隙的地方我们需要记录。于是把不是缝隙的地方又都给排除了。

但是结果还是Timeout。

所以还是回过头来看一下,是不是有些细节可以稍微修改利用。

[刷题笔记] 139 Copy List with Random Pointer

题目

思路过程

第一次失败

粗略扫了一边题目,准备用三个hashMap分别存储node,next关系,以及random关系。

第一次提交一看,Wrong Anwser,原来,每个Node的val可以相同,所以没办法单独用val来做hashMap的Key。

第二次失败

仔细分析,可以推敲到,如果每个old node能有一个指针指向new node,那么复制random关系的时候就是

1
2
3
newNode = currentOldNode.getNewNode()
newNode.random = currentOldNode.random.getNewNode()
newNode.next = currentOldNode.next.getNewNode()

那么我们需要重新定义一个class吗?那样还不如把node自身作为HashMap的Key算了。(最不动脑经的做法)

仔细一想,我们可以用old node的next指针指向new node,然后用一个数组按顺序保存old node。这样等我们建立好了new node之间random关系的同时,可以参照数组建立next关系。

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
def copyRandomList(head)
arr = []
return nil unless head
current = head
while current
arr.push current
next_node = current.next
node = Node.new(current.val)
# borrow it
current.next = node
current = next_node
end
head = arr[0].next
for i in 0...arr.length
if arr[i].random
arr[i].next.random = arr[i].random.next
else
arr[i].next.random = nil
end
if arr[i+1]
arr[i].next.next = arr[i+1].next
else
arr[i].next.next = nil
end
end

return head
end

提交,Wrong anwser。 一看提示,还是没有仔细审题。原来的old node之间的next 关系和 random关系不能改变。

当我们把next借来指向new node的时候,这个关系破坏了。好在我们有数组,可以用来恢复这个关系。

成功代码

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
def copyRandomList(head)
arr = []
return nil unless head
current = head
while current
arr.push current
next_node = current.next
node = Node.new(current.val)
# borrow it
current.next = node
current = next_node
end
head = arr[0].next
for i in 0...arr.length
if arr[i].random
arr[i].next.random = arr[i].random.next
else
arr[i].next.random = nil
end
if arr[i+1]
arr[i].next.next = arr[i+1].next
else
arr[i].next.next = nil
end
end
# recover
for i in 0...arr.length
if arr[i+1]
arr[i].next = arr[i+1]
else
arr[i].next = nil
end
end

return head
end

反思

参考一下别人的做法,有一种是不需要建立数组。而是把 currentOld 的next 指向 currentNew,然后把currentNew的next指向nextOld。
这个方法不需要额外的数组保存顺序,只需要最后用两个指针把通过next连接在一起的新老list分成两个就好了。

[刷题笔记] Best Time to Buy and Sell Stock III

题目

思路

主体思想还是动态规划。

如何写出状态矩阵。dp ?

1
2
3
4
5
dp[i][j][k]

i: 当前prices中的第i个元素
j: 还可以出售j次, j in [0,1,2]
k: 0 表示持有股票状态,1 表示不持有股票状态

注意这里的j表示的是可以出售的次数。所以在状态转移的过程中,买入不需要减少j,只有出售时候需要变动。

接下来就是状态转移矩阵

1
2
3
4
5
6
for i in 1...prices.length
for j in 0..1
dp[i][j][0] = max(dp[i-1][j+1][1]+prices[i], dp[i-1][j][0])
dp[i][j][1] = max(dp[i-1][j][0] - prices[i], dp[i-1][j][1])
end
end

以及初始状态

1
2
3
4
dp[0][0][0] = 0
dp[0][0][1] = - 1 * prices[0]
dp[0][1][0] = 0
dp[0][1][1] = -1 * prices[0]

当然还需要更多初始状态

1
2
3
4
5
for i in 0...prices.length
dp[i][0][0] = 0
dp[i][2][0] = 0
dp[i][2][1] = -1 * min_of(prices)
end

TimeOut的提交

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
def max_profit(prices)
dp = Array.new(prices.length){Array.new(3){Array.new(2)}}
# 2 digial can sell count
dp[0][0][0] = 0
dp[0][0][1] = - 1 * prices[0]
dp[0][1][0] = 0
dp[0][1][1] = -1 * prices[0]

for i in 0...prices.length
min = prices[i] if prices[i] < min
dp[i][0][0] = 0
dp[i][2][0] = 0
dp[i][2][1] = -1 * prices[0..i].sort.first
end


for i in 1...prices.length
for j in 0..1
dp[i][j][0] = max(dp[i-1][j+1][1]+prices[i], dp[i-1][j][0])
dp[i][j][1] = max(dp[i-1][j][0] - prices[i], dp[i-1][j][1])
end
end

return dp[prices.length - 1][0][0]


end

Timeout的原因在于dp[i][2][1] = -1 * prices[0..i].sort.first, 其实我们在遍历的时候可以顺便求最小值。

Accepted

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
def max_profit(prices)
dp = Array.new(prices.length){Array.new(3){Array.new(2)}}
# 2 digial can sell count
dp[0][0][0] = 0
dp[0][0][1] = - 1 * prices[0]
dp[0][1][0] = 0
dp[0][1][1] = -1 * prices[0]

min = prices[0]
for i in 0...prices.length
min = prices[i] if prices[i] < min
dp[i][0][0] = 0
dp[i][2][0] = 0
dp[i][2][1] = -1 * min
end


for i in 1...prices.length
for j in 0..1
dp[i][j][0] = max(dp[i-1][j+1][1]+prices[i], dp[i-1][j][0])
dp[i][j][1] = max(dp[i-1][j][0] - prices[i], dp[i-1][j][1])
end
end

return dp[prices.length - 1][0][0]


end


def max(a,b)
return a > b ? a : b
end

[刷题笔记] 115.Distinct Subsequences

题目

解析

第一个如果没想到动态规划就out了。

首先确定dp的定义。

dp[i][j]表示 s[0…i] 与 t[0…j] 之间的Distinct Subsequences的个数。

然后确定初始状态。

dp[0][0] 自然是比较 s[0] t[0] 是否相等。

dp[i][0] 可以通过遍历s,数出有多少s[i] 与 t[0] 相等。

tp[0][j] 相对比较简单,除了 dp[0][0] 之外,都是 0。

接着确定状态转移。

当 s[i] == s[j] 的时候,dp[i][j] = dp[i-1][j-1] + dp[i-1][j](这个是蒙的,不过也不难蒙)

解法

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
def num_distinct(s, t)
ls = s.length
lt = t.length
dp = Array.new(ls){Array.new(lt)}

count = 0
for i in 0...ls
count += 1 if s[i] == t[0]
dp[i][0] = count
end

for i in 1...lt
dp[0][i] = 0
end

for i in 1...ls
for j in 1...lt
if s[i] == t[j]
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
else
dp[i][j] = dp[i-1][j]
end
end
end
return dp[ls-1][lt-1]
end

双百分。

[刷题笔记] minimum depth of binary tree

题目

https://leetcode.com/problems/minimum-depth-of-binary-tree/submissions/

思路

这个题目标榜的难度是easy,然而通过率只有39%到底是为什么呢?

第一遍的答案

1
2
3
4
5
6
7
8
def min_depth(root)
if root.left == nil && root.right == nil
return 1
end
left_min = min_depth(root.left)
right_min = min_depth(root.right)
return left_min > right_min ? 1+right_min : 1+left_min
end

Exception. 没有处理root是nil的情况。

第二遍,加上处理

1
2
3
4
5
6
7
8
9
def min_depth(root)
return 0 unless root
if root.left == nil && root.right == nil
return 1
end
left_min = min_depth(root.left)
right_min = min_depth(root.right)
return left_min > right_min ? 1+right_min : 1+left_min
end

Wrong answer.

1
2
3
4
5
testCase: 
[2,null,3,null,4,null,5,null,6]

actual: 1
expected: 5

如果只有左右中的一个child的情况,一个child深度为0,另一个child深度为计算的值,这个时候不能认为0是正确的深度,因为只有当左右都为nil的时候才是我们获取深度的时候。

最终结果

1
2
3
4
5
6
7
8
9
10
11
12
13
def min_depth(root)
return 0 unless root
if root.left == nil && root.right == nil
return 1
elsif root.left == nil
return 1 + min_depth(root.right)
elsif root.right == nil
return 1 + min_depth(root.left)
end
left_min = min_depth(root.left)
right_min = min_depth(root.right)
return left_min > right_min ? 1+right_min : 1+left_min
end

[刷题笔记] 109. Convert Sorted List to Binary Search Tree

题目

套路

首先找到中间节点mid,接下来,从head到mid之前的节点为left child,从mid节点之后到结尾为right child。

递归生成left child 和 right child 即可。

但是怎么找到中间节点呢?

这里用到了双指针技巧,即两个指针一快一慢,快指针一次前进两格,慢指针一次前进一格,这样快指针遍历到结尾的时候,慢指针就正好指向中间的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def sorted_list_to_bst(head)
return nil unless head
if head.next == nil
return TreeNode.new(head.val)
end
head = head
mid = head
last = head
while last
last = last.next
break unless last
first = mid
mid = mid.next
last = last.next
end
root = TreeNode.new(mid.val)
root.right = sorted_list_to_bst(first.next.next)
first.next = nil
root.left = sorted_list_to_bst(head)
return root
end

用Swift写一个MAC上的截图OCR工具

想法来源

工作中,很多人喜欢把截图添附到Confluence里面,而这些图片里面的文字,一旦被需要,却很难像文本一样简单选择复制出来。

于是自然而然想到,要是有一个截图工具,能够对截取的图中的文字进行识别就好了。

这样就产生了一个需求: 编写一个能够在MacOS上运行的截图并识别的工具。

对这个需求再具体话一点,可以得到以下几个关键要素。

  1. 能运行在我现在的OS 10.15.4 上

  2. 这个工具不需要界面,只需要显示在右上角status bar中。

  3. 这个工具由两部分组成,截图,以及对截图的内容进行文字识别,最后识别的内容保存到剪贴板中。

可行性调查

首先截图,我们可以用mac自带的截图工具。

在swift代码中,新建一个进程然后调用截图工具。

1
2
3
4
5
let task = Process()
task.launchPath = "/usr/sbin/screencapture"
task.arguments = ["-i", "-r", "-c"]
task.launch()
task.waitUntilExit()

使用option -c 可以使截图直接保存到剪贴板中,之后我们的程序可以直接访问剪贴板获取图片。

接着如何实现OCR?

这里自然而然查到可以用apple的Vision Framework。

稍微改编一下这里的例子,就行了。

当然,最关键的,得看一下剪贴板如何调用。

MacOS中,剪贴板是所有程序公用的,编程的时候,可以通过NSPasteboard来访问。

1
2
3
4
5
6
7
//获取剪贴板
let pasteboard = NSPasteboard.general
// 获取剪贴板中的图片
if let readData = pasteboard.data(forType: NSPasteboard.PasteboardType.tiff),
let cbImage = CIImage(data: readData) {
// 执行识别文字
}

那如何把识别出来的问题重新放回剪贴板呢?

1
2
pasteboard.clearContents()
pasteboard.setString('text I want to copy to pasteboard', forType: .string)

好了,关键点都梳理清楚了。

还有一些并不关键的,放到下一节说。

开始编写

首先打开xcode,创建一个mac os 的app。

由于我们需要的是一个status bar only的程序,我们并不需要任何窗口,仅需要一个下拉菜单,于是可以参考
这里
来建一个项目的雏形。

AppDelegate.swift中创建一个菜单。

1
var statusItem: NSStatusItem?

然后override awakeFromNib

1
2
3
4
5
6
override func awakeFromNib() {
super.awakeFromNib()

statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem?.button?.title = "ScreenOCR"
}

在storyboard上
删除不需要的菜单项目。

创建一个

1
@IBOutlet weak var menu: NSMenu?

通过IBOutlet把menu关联到storyboard上的菜单中。

然后删除windowView,这样就没有了讨厌的窗口。不过我们会发现dock上还是会显示app的图标,所以我们在Info.plist中还得添加一行。
在末尾点击加号,然后选择Application is agent (UIElement), 然后在value的地方选择Yes。这样就不会有图标了。

接下来就是把我们上面整理好的逻辑添加上去。

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
58
59
60
61
62
63
64
65
66
67
68
69
import Cocoa
import Vision
import CoreImage

@main
class AppDelegate: NSObject, NSApplicationDelegate {

var statusItem: NSStatusItem?
@IBOutlet weak var menu: NSMenu?
let pasteboard = NSPasteboard.general

func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}

func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}

override func awakeFromNib() {
super.awakeFromNib()

//我们在这里向菜单里添加一个新的项目。当用户点击这个或者使用快捷键,就自动调用takeScreenShot这个function。
menu?.insertItem(withTitle: "Take screenshot", action: #selector(takeScreenShot), keyEquivalent: "4", at: 0)
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem?.button?.title = "OCR"
if let menu = menu {
statusItem?.menu = menu
}

}

@objc func takeScreenShot() {
let task = Process()
task.launchPath = "/usr/sbin/screencapture"
task.arguments = ["-i", "-r", "-c"]
task.launch()
task.waitUntilExit()
if let readData = pasteboard.data(forType: NSPasteboard.PasteboardType.tiff),
let cbImage = CIImage(data: readData)
{
let requestHandler = VNImageRequestHandler(ciImage: cbImage)
let request = VNRecognizeTextRequest(completionHandler: recognizeTextHandler)
do {
// Perform the text-recognition request.
try requestHandler.perform([request])
} catch {
print("Unable to perform the requests: \(error).")
}
}


}

func recognizeTextHandler(request: VNRequest, error: Error?) {
guard let observations =
request.results as? [VNRecognizedTextObservation] else {
return
}
let recognizedStrings = observations.compactMap { observation in
// Return the string of the top VNRecognizedText instance.
return observation.topCandidates(1).first?.string
}

// Process the recognized strings.
pasteboard.clearContents()
pasteboard.setString(recognizedStrings[0], forType: .string)
}
}

总结

能够从解决问题出发进行针对性的学习,还是学习的最大动力。否则看了也忘了,学了也没用。
有了基础的知识能力之后,遇到需要解决的问题,往往需要的只是组合,以及稍微再学习一点额外的东西。
继续加油吧。

Docker环境下的apache+php-fpm

我们需要用到两个container。一个apache的,另一个则是php-fpm。

apache的Docker接受http请求并转给php-fpm处理。php-fpm的Docker默认监听9000端口。

工作目录构成如下。

1
2
3
4
5
6
7
8
.
├── Dockerfile
├── docker-compose.yml
├── extra
│   └── my-php-conf.conf
├── html
│   └── index.php
└── my-httpd.conf

首先我们要加工一下官方的apache2的docker,放入我们自定义的httpd conf文件。

复制一份默认的httpd.conf

1
docker run --rm httpd:2.4 cat /usr/local/apache2/conf/httpd.conf > my-httpd.conf

编辑my-http.conf

1
2
3
4
5
6
# 去掉下面两个注释
#LoadModule proxy_module modules/mod_proxy.so
#LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so

# 增加一行
Include conf/extra/my-php-conf.conf

然后考虑一个重要的问题。我们的php文件到是放哪里?

php-fpm 默认的webroot是/var/www/html,就放这儿能行?

那apache的web server怎么配置?

接下来编辑extra/my-php-conf.conf

1
2
3
4
5
6
7
8
9
10
11
<VirtualHost *:80>
DocumentRoot /usr/local/apache2/htdocs
ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://php:9000/var/www/html/$1
<Directory "/usr/local/apache2/htdocs">
DirectoryIndex index.php
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
DirectoryIndex index.php index.html
</VirtualHost>

最关键的是

1
ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://php:9000/var/www/html/$1

这个配置制定了php文件在php-fpm Docker中的位置。

这不理所当然?有什么问题吗?

问题来自网上充斥着的一种配置。

如下。

1
2
3
<FilesMatch \.php$>
SetHandler "proxy:fcgi://php:9000"
</FilesMatch>

很多人说这个是apache2的推荐做法。但是我们会发现如果我们直接把php文件放入php-fpm的/var/www/html,运行docker的话,返回的结果是404。

原因是,setHandler并没有带任何参数,apache默认的webRoot则是/usr/var/apache2/htdocs,所以用这个handler,php-fpm会直接去找自己docker中的/usr/var/apache2/htdocs,刚刚说过了,php-fpm默认的webroot是/var/www/html,自然只能返回404了。

所以网上很多建议也并不是很负责。

最后我们创建docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
version: '3.7'
services:
web:
build: .
ports:
- "80:80"
volumes:
- ./extra:/usr/local/apache2/conf/extra
php:
image: php:7.4-fpm
volumes:
- ./html:/var/www/html

把一个index.php放入html

内容可以适宜。

1
<?php phpinfo();

最后运行docker-compose up, 打开浏览器localhost,一切顺利。