从网上下一个 950M 的 txt 文件,里面保存的是圆周率小数点后的 10 亿位数字。想使用 python 查找某个指定的 6 位或 8 位数字在其中的位置,现在直接读文件后用 str.find()查找实在太慢了,请教各位有什么比较快的办法吗?
文件下载地址: https://stuff.mit.edu/afs/sipb/contrib/pi/pi-billion.txt
1 renmu123 2021-11-10 17:45:01 +08:00 via Android 用滑动窗口应该能稍微快一点 |
2 Ediacaran 2021-11-10 17:46:44 +08:00 via iPhone ![]() 000 ~ 999 所在位置索引一个表 |
![]() | 3 Junzhou 2021-11-10 17:48:48 +08:00 ![]() kmp |
4 vvhhaaattt 2021-11-10 17:49:53 +08:00 via Android ![]() 没有限制的话,空间换时间,类似 ngram 索引 |
![]() | 5 hahasong 2021-11-10 17:51:47 +08:00 读取文件了,直接操作内存,分片多线程查找 |
6 cclin 2021-11-10 17:52:25 +08:00 via Android KMP 么 |
![]() | 7 radiocontroller 2021-11-10 17:53:43 +08:00 字符串查找子串,kmp 算法 |
8 aircjm 2021-11-10 17:54:20 +08:00 via Android 盲猜从圆周率里面取日期 |
![]() | 9 SmiteChow 2021-11-10 17:54:43 +08:00 倒排 |
10 cclin 2021-11-10 18:12:52 +08:00 我把文件下载下来了,用 str.find() 挺快的呀,读取文件 3.66s ,查找 0.007s |
![]() | 11 Vegetable 2021-11-10 18:24:49 +08:00 都是什么宰牛刀啊都,直接全量塞数据库啊!才多大点啊 |
![]() | 12 3dwelcome 2021-11-10 18:26:06 +08:00 PI 属于随机数那种,索引都不好建。 怕是没什么好办法。只能挨个查找。 |
14 xx6412223 2021-11-10 18:30:31 +08:00 都是 O(n), 事先加载文件并事先分段并多线程 |
![]() | 15 hidemyself 2021-11-10 18:31:41 +08:00 楼上说 kmp 的有没有看过 python 对于 find 的实现哇。。 |
![]() | 17 nazor 2021-11-10 18:35:06 +08:00 有个数据结构叫后缀数组,特别适合你提出的这种文本不变,模式串不同的查询需求。 |
![]() | 18 oOoOoOoOoOo 2021-11-10 18:39:10 +08:00 via Android |
![]() | 19 分片 线程查找 |
![]() | 20 3dwelcome 2021-11-10 18:44:04 +08:00 “这是题吗,没看出来啊,这种问题最优解就是空间换时间,再怎么做不还是索引吗” 问题的关键,是如何去建索引。完全乱序的数字,没办法建立有效的索引结构。 |
![]() | 21 datou 2021-11-10 18:50:05 +08:00 两秒出结果,很慢么? |
22 djFFFFF 2021-11-10 18:50:29 +08:00 预处理,用空间换时间是最优解法。只是六位到八位(而且盲猜是出生日期?那更简单了)的话存一张表轻松解决。 @hidemyself cpython 我印象里 str.find() 是用的 BMH 算法?反正虽然这个题面是个标准的 KMP 算法的场景,现实生产环境谁用谁是傻子。 |
![]() | 23 546L5LiK6ZOt 2021-11-10 18:59:37 +08:00 via iPhone ![]() https://nullprogram.com/blog/2014/09/18/ 这个老外尝试了多种方法,可以参考下 |
![]() | 24 lonenol 2021-11-10 20:21:46 +08:00 最粗暴的就是 hash 呗,key 是数字,value 是位置,第一次构建比较慢,剩余的查询就都是 O(1)的了 |
![]() | 25 lonenol 2021-11-10 20:22:30 +08:00 不好意思,python 里叫字典,我习惯用 hash 指代 Java 里的 HashMap 了 |
![]() | 26 yianing 2021-11-10 20:28:46 +08:00 trie 就行了吧,只是加载需要点时间,搞个常驻进程就行,我用 go 试了下内存大约 1G ,加载不到 10 分钟  |
27 GrayXu 2021-11-10 20:42:48 +08:00 这如果是个题,考的自然是子串匹配,Boyer-Moore 等。 就算建索引,也是用 trie 树系列,用 hash 有点太异想天开。。。 |
28 tianq 2021-11-10 20:57:58 +08:00 via iPhone ![]() 好久以前研究过在 pi 里找生日: https://lil-q.github.io/blog/pi/ |
![]() | 29 searene 2021-11-10 21:08:07 +08:00 我也把文件下下来了,1.6 秒左右就找到了。如果是题目的话,这道题目是不合格的,因为现实情况就是用 find 就可以了,建索引还更慢 |
30 Jelebi 2021-11-10 22:32:26 +08:00 Ctrl + F |
![]() | 31 vanton 2021-11-10 22:36:15 +08:00 本地跑 str.find() 最多几秒种,速度足够了。 如果你有特别的需求,比如高并发服务,那就索引,数据库或者 hash 都行,不要读文本。 |
![]() | 32 lesismal 2021-11-10 23:46:44 +08:00 用数据库存上也是慢,内存里缓存起来性能最好了,下面代码大概意思是 converter 先统计好索引到数组,然后把数组写入到文件,finder 读入文件初始化数组,然后再查找。没仔细调试,因为太烧机器了,有兴趣的同学可以完善下: 1. converter.py ```python # -*- coding:utf-8 -*- #!/usr/bin/python3 import datetime class PIConverter: def __init__(self, minNum=100000, maxNum=99999999): self.minNum = minNum self.maxNum = maxNum self.positiOns= [0]*(self.maxNum+1-self.minNum) def convert(self, srcFile, dstFile): fsrc = open(srcFile,'r') fsrc.read(2) try: lastStr = "" readSize = 1024*8 currPos = 0 readed = 0 starttime = datetime.datetime.now() offset = len(str(self.minNum)) - 1 while True: s = fsrc.read(readSize) s = lastStr + s # 这里可以再优化下 currPos -= len(lastStr) for i in range(len(s)-8): strLen = len(str(self.minNum)) while strLen <= len(str(self.maxNum)): subs = s[i:i+strLen] strLen += 1 num = int(subs) index = num - self.minNum if self.positions[index] == 0: self.positions[index] = currPos + i if len(s) == 0: break lastStr = s[len(s)-5:] currPos += readSize readed += readSize if readed % (1024*1024*8) == 0: print("total read: {}, time used: {}s".format(readed, (datetime.datetime.now() - starttime).seconds)) print("total read: {}, time used: {}s".format(readed, (datetime.datetime.now() - starttime).seconds)) print("done") try: fdst = open(dstFile,'rw+') for index in range(self.positions): fdst.write(str(index)+"\n") finally: fdst.close() finally: fsrc.close() def find(self, n): if n < self.minNum or n > 99999999: return -1 return self.positions[n - self.minNum] piCOnverter= PIConverter() # 把已经统计出来的生成更小的文件 piConverter.convert("./pi-billion.txt", "./pi-position.txt") # converter 初始化太慢了,所以最好还是先 piConverter.convert 把已经统计出来的生成更小的文件,finder.py 用该文件初始化和做查找 # print("141592:", piConverter.find(141592)) # print("415926:", piConverter.find(415926)) ``` 2. finder.py ```python # -*- coding:utf-8 -*- #!/usr/bin/python3 class PIFinder: def __init__(self, fname, minNum=100000, maxNum=99999999): self.minNum = minNum self.maxNum = maxNum self.positiOns= [0]*(self.maxNum+1-self.minNum) f = open(fname,'r') try: i = 0 for line in f: num = int(line) self.positions[i] = num finally: f.close() def find(self, n): if n < self.minNum or n > 99999999: return -1 return self.positions[n - self.minNum] piFinder = PIFinder("./pi-position.txt") print("141592:", piFinder.find(141592)) print("415926:", piFinder.find(415926)) ``` |
![]() | 33 lesismal 2021-11-10 23:53:39 +08:00 #32 文件尾、打开写文件的好像都有问题,平时不写 py ,实在不熟悉,v 站发代码也确实难受,对齐好像都没了 |
![]() | 34 lesismal 2021-11-11 00:22:06 +08:00 ![]() 算了,忍不住还是调试了下,完整版的: https://gist.github.com/lesismal/c4528eacc35db33f754ac2f8eb9e7634 |
![]() | 35 c0xt30a 2021-11-11 02:03:53 +08:00 我提一个用素数来 Hash 查找的方法,大致如下: 1. 将 0-9 映射为 前 10 个素数 [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] 2. 用一个定长为 6/8 的滑动窗口遍历这个 pi 的字符串,每次增长的时候,当前的 hash 先除以最后一位数字对应的素数再乘以新增数字对应的素数,可以得到最新的 hash 数值 3. 如果当前 hash 数值与要寻找的数字的 hash 相等,则停下来进一步比对字符串 |
![]() | 36 c0xt30a 2021-11-11 05:45:27 +08:00 当然 直接乘以 10 加上新来的数字再对 10^7 取 mode 以更新 hash 也行 |
![]() | 37 kuangwinnie 2021-11-11 05:51:31 +08:00 950MB 塞内存里也没多大啊。 |
![]() | 38 murmur 2021-11-11 08:28:30 +08:00 950m 进内存配合现在的处理器可能有发帖时间都做出来了吧,这是跑 leetcode 限制内存了? |
![]() | 39 gulugu 2021-11-11 08:42:44 +08:00 分割了,然后分布式查询 |
![]() | 40 ihainan 2021-11-11 08:52:56 +08:00 固定 6 位和 8 位的话或许可以考虑 Rabin-Karp 算法求哈希值。 |
![]() | 41 rrfeng 2021-11-11 09:12:37 +08:00 via Android 那要查的数字范围 6/8 的前 N 位枚举出来遍历一下做位置索引,N 取值可以做个测试找到空间和时间的平衡点。 盲猜你要查生日,那查询目标才没几个,全量索引都不为过。 |
42 xiao109 2021-11-11 09:30:00 +08:00 重点是查,所以建立索引结构的时间应该不会纳入耗时的计算。 按 6 或 8 位截取数字映射到索引中,然后再搜。 |
43 arthurire 2021-11-11 09:36:19 +08:00 这是啥算法题啊... 算法不就是 KMP 之类 你还能突破理论极限不成? 要是比速度就建立各种索引,然后 O(1) 别侮辱算法题啊 |
44 xz410236056 2021-11-11 10:34:23 +08:00 str.find() 是 Boyer-Moore 和 Horspool 算法的结合,这都慢用 KMP 能快吗? |
![]() | 45 lizytalk 2021-11-11 10:35:47 +08:00 如果是查多次的话, 可以把整个文档处理成后缀数组 (只需要常数空间), 然后每次查询可以做到对数时间 O(P log (T)), T 是整个文档的长度, P 是查询的长度. 至于建索引, 时间倒是 O(1)的, 但是索引的空间可是指数级别的. |
![]() | 46 lesismal 2021-11-11 10:37:33 +08:00 |
![]() | 47 irobbin 2021-11-11 10:40:03 +08:00 ![]() 如果算生日的话,365*100= 36500 ,总共就这么点数据,找个时间整体遍历一遍,查完存起来搞定。 |
![]() | 48 neptuno 2021-11-11 11:18:18 +08:00 遍历所有数字排列,存数据库 |
![]() | 49 asmoker 2021-11-11 11:48:47 +08:00 sublime 或者 vim |
50 MegrezZhu 2021-11-11 11:50:58 +08:00 才 6 到 8 位…就算用上 O(n+m)的 kmp 也撑死优化几倍的时间,有这个工夫用 C++写个暴力就得了 |
![]() | 51 trcnkq 2021-11-11 15:48:31 +08:00 |
52 wnma3mz 2021-11-12 09:36:49 +08:00 盲猜是查找生日字符串之类的,之前实现过,供参考 https://github.com/wnma3mz/birthday_in_pi/blob/master/read_large_pi.py#L20-L30 主要问题还是放到服务器里面内存问题啥的。 暴力的方法,还可以建表。。 |