我在思考一个大整数除法的算法:用余数bigint C 除以 bigint D,其中我们知道C在基数b下的表示,而D是形如 b^k-1 的形式。最好通过示例来展示。让我们尝试用D=999将C=21979182173除以。
- 我们将数字写成三个数字的集合:21 979 182 173
- 从左侧开始,我们对连续的集合进行求和(模999):21 001 183 356
- 我们在"超过999"的那些集合之前加1:22 001 183 356
确实,21979182173/999=22001183,余数为356。
我已经计算了复杂度,并且如果我没有弄错的话,该算法应该在O(n)内工作,n是C在基数b表示中的位数。我还编写了一个非常粗糙且未经优化的版本(仅针对b=10)的算法,在C ++中进行了测试,与GMP的普通整数除法算法进行了比较,它确实似乎比GMP更好。我在任何我查找的地方都找不到类似的实现,因此我不得不使用一般除法进行测试。
我找到了几篇涉及到很相似的问题的文章,但是没有一篇集中讨论实际实现,特别是在不同于2的基数下。我想这是因为数字的内部存储方式,尽管即使考虑到这一点,上述算法在b=10的情况下也很有用。我也尝试联系其他人,但是无济于事。
因此,我的问题是:是否有一篇文章或一本书或其他东西描述了上述算法,可能讨论了其实现方式?如果没有,那么对我来说,在C/C ++中尝试实现和测试这样的算法是否有意义?这个算法本质上是否有缺陷?
另外,我不是程序员,虽然我在编程方面还可以,但我承认我对计算机的“内部”知识并不了解。因此,请原谅我的无知-这篇文章中可能存在一个或多个非常愚蠢的问题。再次抱歉。
非常感谢!
对评论/答案中提出的观点进行进一步澄清:
非常感谢大家 - 由于我不想用同样的话在所有很棒的答案和建议上进行评论,所以我只想解决你们讨论的一个问题。
我完全意识到,在2^n进制下工作通常是做事情最有效的方式。几乎所有的大整数库都使用2^32或其他类似的进制。然而,如果(仅仅针对这个特定算法!)我们将大整数实现为进制为b的数字数组,那么怎么办呢?当然,这里要求b是“合理的”:b=10,也就是最自然的情况,似乎足够合理。我知道这在内存和时间上都相对低效,考虑到数字的内部存储方式,但是如果我的(基本且可能存在缺陷的)测试正确,我已经能够比GMP的普通除法更快地产生结果,这就给实现这样一个算法提供了意义。
Ninefingers指出,这种情况下我必须使用一种昂贵的取模运算。我希望不需要:只需通过查看old+new+1的位数就可以看出是否越过了999。如果它有4个数字,我们就搞定了。更重要的是,由于old<999且new<=999,所以我们知道如果old+new+1有4个数字(它不能有更多),那么(old+new)%999等于删除(old+new+1)的最左边的数字,我想我们可以便宜地做到这一点。
当然,我并不否认这种算法明显存在的局限性,也不声称它无法改进 - 它只能除以某些类别的数字,而且我们必须预先知道在基数b中被除数的表示形式。然而,对于b=10,后者似乎很自然。
现在,假设我们已经按照上面的方式实现了大整数。假设C=(a_1a_2...a_n)是在基数b下表示的,并且D=b^k-1。该算法(可能还可以更加优化)将像这样进行。希望没有太多的错别字。
- 如果 k>n,则显然我们已经完成了
- 在 C 的开头添加一个零(即 a_0=0)(以防万一,比如我们试图用 99 去除 9999)
- l=n%k (对于“常规”整数的模运算-不应太费力)
- old=(a_0...a_l) (第一组数字,可能少于 k 个数字)
- for (i=l+1; i < n; i=i+k) (我们将进行 floor(n/k) 次迭代或更多次)
- new=(a_i...a_(i+k-1))
- new=new+old (这是大整数加法,因此 O(k))
- aux=new+1 (同样是大整数加法 - O(k),我对此不太满意)
- 如果 aux 有超过 k 个数字
- 删除 aux 的第一个数字
- old=old+1 (再次进行大整数加法)
- 在开头填充零,使 old 具有应有的位数
- (a_(i-k)...a_(i-1))=old (如果 i=l+1,则为(a _ 0...a _ l)=old)
- new=aux
- 在开头填充零,使 new 具有应有的位数
- (a_i...a_(i+k-1)=new
- quot=(a_0...a_(n-k+1))
- rem=new
好的,感谢与我讨论此事 - 如我所说,如果没有人发现任何致命缺陷,这似乎是一个有趣的“特殊情况”算法可以尝试实现、测试和讨论。如果这是迄今为止不广泛讨论的问题,那就更好了。请告诉我你的想法。对于这篇长文章,我感到抱歉。
还有一些个人评论:
@Ninefingers:我对GMP的工作原理、功能和大数除法算法有一些(非常基础的)了解,所以我能够理解你的很多论点。我也知道GMP高度优化,并且在某种程度上为不同的平台定制自己,因此我肯定不会试图在普遍情况下“战胜”它——这似乎与用尖木棍攻击坦克一样毫无成果。然而,这不是这个算法的想法——它适用于非常特殊的情况(GMP似乎没有涵盖)。另外,您确定常规除法是O(n)吗?我见过的最多只有M(n)。(如果我理解正确),这可以在实践中(例如,Schönhage–Strassen等)达到不到O(n)。 如果我没记错的话,弗雷尔(Fürer's)算法几乎纯属理论。
@Avi Berger:这似乎并不完全与“抛弃九”的概念相同,尽管思路类似。然而,前面提到的算法应该始终有效,如果我没记错的话。
b
的位操作而不总是使用2^32: 如果你需要经常除以某个b
,这是一个有效的选择。例如,在一个代码高尔夫挑战中打印Fibonacci(10^9)的前1000个数字(有性能要求),我使用了半蛮力方法,通过在数字过大时除以10^9来保留最重要的1009个十进制数字。基数为10^9的位(32位元素)使得这种方法非常高效,并且手动进行进位比加法快。[105字节的x86机器码] (https://codegolf.stackexchange.com/a/135618/30206) - Peter Cordes