Answers:
让我们重述一下问题:输出从1到无穷大的每个数字,以便该数字除2和5外没有其他因子。
以下是一个简单的C#代码段:
for (int i = 1;;++i)
{
int num = i;
while(num%2 == 0) num/=2;
while(num%5 == 0) num/=5;
if(num == 1) Console.WriteLine(i);
}
基利安的 / QuestionC的方法是很多更好的性能。使用此方法的C#代码段:
var itms = new SortedSet<int>();
itms.Add(1);
while(true)
{
int cur = itms.Min;
itms.Remove(itms.Min);
itms.Add(cur*2);
itms.Add(cur*5);
Console.WriteLine(cur);
}
SortedSet
防止重复插入。
基本上,它通过确保序列中的下一个数字在中来工作itms
。
证明此方法有效:
所描述的算法可确保在以表格形式输出的任何数字之后2^i*5^j
,集合现在包含2^(i+1)*5^j
和2^i*5^(j+1)
。假设序列中的下一个数字是2^p*5^q
。必须存在形式为2^(p-1)*5^(q)
或的先前输出数字2^p*5^(q-1)
(或如果p和q都不等于0,则两者都存在)。如果不是,则2^p*5^q
不是下一个数字,因为2^(p-1)*5^(q)
和2^p*5^(q-1)
都较小。
第二个代码段使用O(n)
内存(其中n是已输出的数字的数量),因为O(i+j) = O(n)
(因为i和j都小于n),并且会O(n log n)
及时找到n个数字。第一个代码段以指数时间查找数字。
1 = 2^0*5^0, 2 = 2^1*5^0, 4 = 2^2*5^0, 5 = 2^0*5^1, 8 = 2^3*5^0, 10 = 2^1*5^1
。
.Remove()
并.Add()
触发垃圾收集器的不良行为,还是会解决问题?
这是一个足够常见的面试问题,知道答案很有用。这是我的婴儿床单中的相关条目:
- 要按顺序生成形式为3 a 5 b 7 c 的数字,请从1开始,将所有三个可能的后继对象(3,5,7)放入辅助结构,然后将其中的最小数字添加到列表中。
换句话说,您需要采用两步方法以及附加的排序缓冲区来有效解决此问题。(更长的描述是在Gayle McDowell撰写的《破解编码访谈》中。
这是一个以恒定内存运行的答案,但会浪费CPU。在原始问题(即面试中的答案)的背景下,这不是一个很好的答案。但是,如果面试时间是24小时,那还不错。;)
这个想法是,如果我有n个有效答案,那么序列中的下一个将是n的2的幂乘以5的幂,或者n乘以5的幂除以a。二的力量。只要它平均分配。(...或除数可以是1;)在这种情况下,您只需乘以2或5)
例如,要从625变为640,请乘以5 ** 4/2 **7。或者,更一般地说,乘以2 ** m * 5 ** n
某个m 的某个值,n为n,其中一个为正,一个为负或零,并且乘数平均除数。
现在,棘手的部分是找到乘数。但是我们知道a)除数必须平均除数; b)乘数必须大于1(数字不断增加); c)如果我们选择大于1 的最低乘数(即1 <f <所有其他f ),那么这肯定是我们的下一步。之后的步骤将是最低的步骤。
令人讨厌的部分是找到m,n的值。只有log(n)的可能性,因为只有太多的2或5可以放弃,但是我不得不添加-1到+1的系数作为处理四舍五入的草率方法。因此,我们只需要在每个步骤中迭代O(log(n))。因此,总体上为O(n log(n))。
好消息是,因为它需要一个值并找到下一个值,所以您可以在序列中的任何位置开始。因此,如果您想要10亿之后的下一个,它可以通过迭代2/5或5/2并选择大于1的最小乘数来找到它。
(蟒蛇)
MAX = 30
F = - math.log(2) / math.log(5)
def val(i, j):
return 2 ** i * 5 ** j
def best(i, j):
f = 100
m = 0
n = 0
max_i = (int)(math.log(val(i, j)) / math.log(2) + 1) if i + j else 1
#print((val(i, j), max_i, x))
for mm in range(-i, max_i + 1):
for rr in {-1, 0, 1}:
nn = (int)(mm * F + rr)
if nn < -j: continue
ff = val(mm, nn)
#print(' ' + str((ff, mm, nn, rr)))
if ff > 1 and ff < f:
f = ff
m = mm
n = nn
return m, n
def detSeq():
i = 0
j = 0
got = [val(i, j)]
while len(got) < MAX:
m, n = best(i, j)
i += m
j += n
got.append(val(i, j))
#print('* ' + str((val(i, j), m, n)))
#print('- ' + str((v, i, j)))
return got
我对排序列表解决方案生成的前10,000个数字进行了验证,从而验证了生成的前10,000个数字,并且它至少可以工作到现在。
顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句:顺便说一句,顺便说一句,顺便说一句:顺便说一句,顺便说一句。
...
嗯 通过将best()
我当作增量扩展的查找表,我可以获得O(n)性能-每个值(!)O(1)-和O(log n)内存使用率。现在,它通过每次迭代来节省内存,但是它进行了大量的冗余计算。通过保留这些中间值以及最小值列表,我可以避免重复的工作并将其大大提高。但是,中间值的列表将随n增长,因此O(log n)内存。
n
,m
并且到目前为止已经使用了整个序列中的数字。在每次迭代中,n
或m
可能会或可能不会上去。我们创建一个新数字,2^(max_n+1)*5^(max_m+1)
然后以穷举递归的方式减少该数字,每次调用将指数减小1,直到得到的最小数字大于当前数字。我们更新max_n
,max_m
根据需要。这是不变的记忆。O(log^2(n))
如果在还原调用中使用了DP缓存,可能会成为记忆
Brian绝对正确-我的其他答案太过复杂了。这是一种更简单,更快捷的方法。
想象一下欧几里德平面的象限I,限于整数。将一个轴称为i轴,将另一个轴称为j轴。
显然,靠近原点的点将在远离原点的点之前被拾取。另请注意,活动区域将在离开j轴之前离开i轴。
一旦使用了一个点,就不会再使用它。并且只有在直接位于其下方或左侧的点已被使用时,才能使用该点。
将它们放在一起,您可以想象一个围绕原点开始的“边界”或“前沿”,它们向上和向右扩展,沿着i轴的传播比在j轴上的传播更远。
实际上,我们可以发现更多的东西:对于任何给定的i值,在边界/边缘最多会有一个点。(必须将i递增2倍以上才能等于j的递增。)因此,我们可以将边界表示为一个列表,其中每个i坐标包含一个元素,仅随j坐标和函数值而变化。
每次通过时,我们在前缘选择最小的元素,然后在j方向上移动一次。如果碰巧要提高最后一个元素,则添加一个新的最后一个more元素,该元素的i值和j值均为0。
using System;
using System.Collections.Generic;
using System.Text;
namespace TwosFives
{
class LatticePoint : IComparable<LatticePoint>
{
public int i;
public int j;
public double value;
public LatticePoint(int ii, int jj, double vvalue)
{
i = ii;
j = jj;
value = vvalue;
}
public int CompareTo(LatticePoint rhs)
{
return value.CompareTo(rhs.value);
}
}
class Program
{
static void Main(string[] args)
{
LatticePoint startPoint = new LatticePoint(0, 0, 1);
var leadingEdge = new List<LatticePoint> { startPoint } ;
while (true)
{
LatticePoint min = leadingEdge.Min();
Console.WriteLine(min.value);
if (min.j + 1 == leadingEdge.Count)
{
leadingEdge.Add(new LatticePoint(0, min.j + 1, min.value * 2));
}
min.i++;
min.value *= 5;
}
}
}
}
空格:到目前为止已打印的元素数为O(n)。
速度:O(1)插入,但并非每次都插入。(当List<>
必须增长时,有时会更长,但仍要摊销O(1))。最大的耗时是要搜索到目前为止已打印的元素数量中的最小值O(n)。
Does anyone have advice on how to solve such a problem?
试图加深对基本问题的理解。代码转储不能很好地回答该问题。
基于集合的解决方案可能是您的面试官想要的,但是,不幸的结果是,要对元素进行排序会O(n)
占用内存和O(n lg n)
总时间n
。
一点数学可以帮助我们找到一个O(1)
时空O(n sqrt(n))
解决方案。注意2^i * 5^j = 2^(i + j lg 5)
。找到第一个n
元素{i,j > 0 | 2^(i + j lg 5)}
减少到找到第一个n
要素{i,j > 0 | i + j lg 5}
,因为函数(x -> 2^x)
是严格单调递增的,所以对于一些的唯一途径a,b
即2^a < 2^b
是如果a < b
。
现在,我们只需要一种算法即可找到的序列i + j lg 5
,其中i,j
是自然数。换句话说,在给定当前值的情况下i, j
,将下一个移动减至最小(即,给我们序列中的下一个数字)的是,其中一个值(例如j += 1
)有所增加,而另一个值()有所减少i -= 2
。唯一限制我们的是i,j > 0
。
只有两种情况需要考虑- i
增加或j
增加。其中之一必须增加,因为我们的序列在增加,而两者都不会增加,因为否则我们将跳过只有一个i,j
增加的术语。因此,一个增加,另一个保持不变或减少。在C ++ 11中表示的整个算法及其与集合解决方案的比较在此处可用。
这实现了恒定的内存,因为除了输出数组(参见链接)之外,该方法中仅分配了恒定数量的对象。该方法每次迭代都达到对数时间,因为对于任何给定而言(i,j)
,它遍历最佳对(a, b)
,使得(i + a, j + b)
值的增加最小i + j lg 5
。遍历为O(i + j)
:
Attempt to increase i:
++i
current difference in value CD = 1
while (j > 0)
--j
mark difference in value for
current (i,j) as CD -= lg 5
while (CD < 0) // Have to increase the sequence
++i // This while will end in three loops at most.
CD += 1
find minimum among each marked difference ((i,j) -> CD)
Attempt to increase j:
++j
current difference in value CD = lg 5
while (j > 0)
--i
mark difference in value for
current (i,j) as CD -= 1
while (CD < 0) // have to increase the sequence
++j // This while will end in one loop at most.
CD += lg 5
find minimum among each marked difference ((i,j) -> CD)
每次迭代都会尝试更新i
,然后更新,然后j
进行两者中较小的更新。
由于i
和j
最多O(sqrt(n))
,我们有总O(n sqrt(n))
时间。i
而j
生长在广场的速度n
,因为对于任何最大valiues imax
和jmax
存在O(i j)
从中使我们的序列,如果我们的序列是唯一对n
条款,i
以及j
彼此(的一些常数因子中生长,因为该指数是由线性的组合FO两个),我们知道,i
和j
是O(sqrt(n))
。
不必担心浮点错误-由于术语呈指数增长,因此在翻牌错误赶上我们之前,我们必须处理溢出问题。如果有时间,我将对此进行更多讨论。