【问题标题】:Given a string of a million numbers, return all repeating 3 digit numbers给定一百万个数字的字符串,返回所有重复的 3 位数字
【发布时间】:2018-05-14 20:19:30
【问题描述】:

几个月前,我在纽约接受了一家对冲基金公司的面试,不幸的是,我没有得到数据/软件工程师的实习机会。 (他们还要求在 Python 中提供解决方案。)

我在第一个面试问题上几乎搞砸了......

问题:给定一百万个数字的字符串(例如 Pi),写 一个函数/程序,返回所有重复的 3 位数字和 重复大于 1

例如:如果字符串是:123412345123456,那么函数/程序将返回:

123 - 3 times
234 - 3 times
345 - 2 times

在我面试失败后他们没有给我解决方案,但他们确实告诉我,解决方案的时间复杂度恒定为 1000,因为所有可能的结果都在:

000 --> 999

现在我正在考虑它,我认为不可能提出一个恒定时间算法。是吗?

【问题讨论】:

  • 如果他们认为解决方案是 1000 的常数,那么这让我认为他们会构建所有三位数,然后正则表达式搜索它们。人们通常认为他们实际上没有编写/看到的操作是“免费的”。我很确定这与字符串的长度成线性关系。
  • 挑剔的是,如果输入大小是常数,则每个算法都是常数时间;-)
  • 1000 的常数什么? (添加?大象?)
  • 好吧,如果字符串长度是恒定的(1M)并且子字符串/数字长度是恒定的(3),那么从技术上讲,每个解决方案都是恒定的时间......
  • They did not give me the solution after I failed the interview, but they did tell me that the time complexity for the solution was constant of 1000 since all the possible outcomes are between: 000 --> 999 这可能是实际测试。看看你是否可以向他们证明为什么这是不可能的,并向他们展示正确的最小时间复杂度。

标签: python algorithm data-structures number-theory


【解决方案1】:

固定时间是不可能的。所有 100 万位数字至少需要查看一次,因此时间复杂度为 O(n),在这种情况下 n = 100 万。

对于一个简单的 O(n) 解决方案,创建一个大小为 1000 的数组,该数组表示每个可能的 3 位数字的出现次数。一次前进 1 位,第一个索引 == 0,最后一个索引 == 999997,并递增数组 [3 位数字] 以创建直方图(每个可能的 3 位数字的出现计数)。然后输出counts > 1的数组内容。

【讨论】:

  • @ezzzCash - 是的,字典可以工作,但它不是必需的。所有可能的“密钥”都是预先知道的,限制在 0 到 999 的范围内。开销的差异是使用 3 个字符串作为密钥进行基于密钥的访问所需的时间,与转换 3数字字符串到索引,然后使用索引访问数组。
  • 如果您想要数字技巧,您还可以决定使用 BCD 并将三个数字存储在 12 位中。并通过屏蔽低 4 位来解码 ASCII 数字。但是x-'0' 模式在 Python 中无效,它是 C-ism(其中字符是整数)。
  • @LorenPechtel:Python 中的字典查找速度非常快。当然,数组访问速度更快,所以如果我们从一开始就处理整数,那你是对的。但是,在这种情况下,我们有 3 长度的字符串,如果我们想将它们与数组一起使用,我们首先必须将其转换为整数。事实证明,与人们最初预期的相反,字典查找实际上比整数转换 + 数组访问要快。在这种情况下,阵列解决方案实际上慢了 50%。
  • 我想有人可能会争辩说,如果输入数字总是精确 100万位,那么算法 O(1),具有100 万的常数因子。
  • @AleksiTorhamo - 如果目标是比较算法实现的相对速度,我更喜欢 C 或 C++ 之类的传统语言,因为 Python 速度明显较慢,而且与 Python 相比似乎具有独特的开销到其他语言。
【解决方案2】:

简单的 O(n) 解决方案是计算每个 3 位数字:

for nr in range(1000):
    cnt = text.count('%03d' % nr)
    if cnt > 1:
        print '%03d is found %d times' % (nr, cnt)

这将搜索所有 100 万个数字 1000 次。

只遍历数字一次:

counts = [0] * 1000
for idx in range(len(text)-2):
    counts[int(text[idx:idx+3])] += 1

for nr, cnt in enumerate(counts):
    if cnt > 1:
        print '%03d is found %d times' % (nr, cnt)

时间表明,仅在索引上迭代一次的速度是使用 count 的两倍。

【讨论】:

  • text.count()有黑色星期五折扣吗?
  • @EricDuminil 你说得有道理,但是因为text.count 是用高速编译语言(例如C)完成的,而不是慢的python级解释循环,是的,有折扣.
  • 单独计算每个数字的效率非常低,但它是一个恒定的时间,因此仍然是 O(n)。
  • 您提出的使用count 的选项不正确,因为它不会计算重叠模式。请注意,'111'.count('11') == 1 当我们期望它是 2 时。
  • 另外,您的“简单O(n) 解决方案”实际上是O(10**d * n),其中d 是搜索的位数,n 是字符串的总长度。第二个是O(n)时间和O(10**d + n)空间。
【解决方案3】:

根据我的理解,您无法在恒定时间内获得解决方案。它将至少通过一百万位数字(假设它是一个字符串)。您可以对百万长度数字的数字进行 3 位滚动迭代,如果哈希键已经存在,则将其值增加 1;如果哈希键不存在,则创建一个新的哈希键(由值 1 初始化)字典。

代码将如下所示:

def calc_repeating_digits(number):

    hash = {}

    for i in range(len(str(number))-2):

        current_three_digits = number[i:i+3]
        if current_three_digits in hash.keys():
            hash[current_three_digits] += 1

        else:
            hash[current_three_digits] = 1

    return hash

您可以过滤到项目值大于 1 的键。

【讨论】:

    【解决方案4】:

    我会这样解决问题:

    def find_numbers(str_num):
        final_dict = {}
        buffer = {}
        for idx in range(len(str_num) - 3):
            num = int(str_num[idx:idx + 3])
            if num not in buffer:
                buffer[num] = 0
            buffer[num] += 1
            if buffer[num] > 1:
                final_dict[num] = buffer[num]
        return final_dict
    

    应用于您的示例字符串,这会产生:

    >>> find_numbers("123412345123456")
    {345: 2, 234: 3, 123: 3}
    

    这个解决方案在 O(n) 中运行,因为 n 是提供的字符串的长度,我猜是你能得到的最好的。

    【讨论】:

    • 您可以简单地使用Counter。您不需要final_dict,也不必在每次迭代时更新它。
    【解决方案5】:

    这是“共识”O(n) 算法的 NumPy 实现:遍历所有三元组和 bin。装箱是在遇到“385”时完成的,在 bin[3, 8, 5] 上加一,这是一个 O(1) 操作。垃圾箱排列在10x10x10 立方体中。由于分箱是完全矢量化的,因此代码中没有循环。

    def setup_data(n):
        import random
        digits = "0123456789"
        return dict(text = ''.join(random.choice(digits) for i in range(n)))
    
    def f_np(text):
        # Get the data into NumPy
        import numpy as np
        a = np.frombuffer(bytes(text, 'utf8'), dtype=np.uint8) - ord('0')
        # Rolling triplets
        a3 = np.lib.stride_tricks.as_strided(a, (3, a.size-2), 2*a.strides)
    
        bins = np.zeros((10, 10, 10), dtype=int)
        # Next line performs O(n) binning
        np.add.at(bins, tuple(a3), 1)
        # Filtering is left as an exercise
        return bins.ravel()
    
    def f_py(text):
        counts = [0] * 1000
        for idx in range(len(text)-2):
            counts[int(text[idx:idx+3])] += 1
        return counts
    
    import numpy as np
    import types
    from timeit import timeit
    for n in (10, 1000, 1000000):
        data = setup_data(n)
        ref = f_np(**data)
        print(f'n = {n}')
        for name, func in list(globals().items()):
            if not name.startswith('f_') or not isinstance(func, types.FunctionType):
                continue
            try:
                assert np.all(ref == func(**data))
                print("{:16s}{:16.8f} ms".format(name[2:], timeit(
                    'f(**data)', globals={'f':func, 'data':data}, number=10)*100))
            except:
                print("{:16s} apparently crashed".format(name[2:]))
    

    不出所料,NumPy 比 @Daniel 在大型数据集上的纯 Python 解决方案要快一些。示例输出:

    # n = 10
    # np                    0.03481400 ms
    # py                    0.00669330 ms
    # n = 1000
    # np                    0.11215360 ms
    # py                    0.34836530 ms
    # n = 1000000
    # np                   82.46765980 ms
    # py                  360.51235450 ms
    

    【讨论】:

    • 展平数字字符串而不是嵌套 bin 可能要快得多,除非 NumPy 最终将其实现为具有高效索引的 3D 矩阵。你反对哪个版本的@Daniel;对每个整数运行字符串搜索的那个,还是带有直方图的那个?
    • @PeterCordes 我对此表示怀疑。 ndarrays,核心 numpy 类型,都是关于多维数字数组的高效存储、操作和索引。有时你可以通过展平来减少几个 %,但在这种情况下,手动执行 100 x[0] + 10 x[1] + x[2] 不会给你带来太多好处。我用了@Daniel说的更快,你可以自己检查基准代码。
    • 我不太了解 NumPy(或一般的 Python;我主要为 x86 进行 C 和汇编性能调整),但我认为您只有一个 3D 数组,对吧?我从您的英文文本(显然我什至没有仔细阅读)中认为您有实际的嵌套 Python 对象并且正在分别索引它们。但事实并非如此,所以 nvm 我的第一条评论。
    • 我认为您使用的纯 Python 版本与投票率更高的答案所使用的直方图实现几乎相同,但是如果用 Python 编写它的不同方式对速度的影响很大。
    【解决方案6】:

    这是我的答案:

    from timeit import timeit
    from collections import Counter
    import types
    import random
    
    def setup_data(n):
        digits = "0123456789"
        return dict(text = ''.join(random.choice(digits) for i in range(n)))
    
    
    def f_counter(text):
        c = Counter()
        for i in range(len(text)-2):
            ss = text[i:i+3]
            c.update([ss])
        return (i for i in c.items() if i[1] > 1)
    
    def f_dict(text):
        d = {}
        for i in range(len(text)-2):
            ss = text[i:i+3]
            if ss not in d:
                d[ss] = 0
            d[ss] += 1
        return ((i, d[i]) for i in d if d[i] > 1)
    
    def f_array(text):
        a = [[[0 for _ in range(10)] for _ in range(10)] for _ in range(10)]
        for n in range(len(text)-2):
            i, j, k = (int(ss) for ss in text[n:n+3])
            a[i][j][k] += 1
        for i, b in enumerate(a):
            for j, c in enumerate(b):
                for k, d in enumerate(c):
                    if d > 1: yield (f'{i}{j}{k}', d)
    
    
    for n in (1E1, 1E3, 1E6):
        n = int(n)
        data = setup_data(n)
        print(f'n = {n}')
        results = {}
        for name, func in list(globals().items()):
            if not name.startswith('f_') or not isinstance(func, types.FunctionType):
                continue
            print("{:16s}{:16.8f} ms".format(name[2:], timeit(
                'results[name] = f(**data)', globals={'f':func, 'data':data, 'results':results, 'name':name}, number=10)*100))
        for r in results:
            print('{:10}: {}'.format(r, sorted(list(results[r]))[:5]))
    

    数组查找方法非常快(甚至比@paul-panzer 的 numpy 方法还要快!)。当然,它会作弊,因为它在完成后并没有在技术上完成,因为它正在返回一个生成器。如果值已经存在,它也不必检查每次迭代,这可能会有很大帮助。

    n = 10
    counter               0.10595780 ms
    dict                  0.01070654 ms
    array                 0.00135370 ms
    f_counter : []
    f_dict    : []
    f_array   : []
    n = 1000
    counter               2.89462101 ms
    dict                  0.40434612 ms
    array                 0.00073838 ms
    f_counter : [('008', 2), ('009', 3), ('010', 2), ('016', 2), ('017', 2)]
    f_dict    : [('008', 2), ('009', 3), ('010', 2), ('016', 2), ('017', 2)]
    f_array   : [('008', 2), ('009', 3), ('010', 2), ('016', 2), ('017', 2)]
    n = 1000000
    counter            2849.00500992 ms
    dict                438.44007806 ms
    array                 0.00135370 ms
    f_counter : [('000', 1058), ('001', 943), ('002', 1030), ('003', 982), ('004', 1042)]
    f_dict    : [('000', 1058), ('001', 943), ('002', 1030), ('003', 982), ('004', 1042)]
    f_array   : [('000', 1058), ('001', 943), ('002', 1030), ('003', 982), ('004', 1042)]
    

    【讨论】:

    • 那么你到底在比较什么?你不应该返回列表而不是未使用的生成器吗?
    • Counters 不是这样使用的。如果使用得当,它们将成为您示例中最快的选择。如果您将timeit 与插入生成器的列表一起使用,您的方法将比Counterdict 慢。见here
    • 最后,如果您首先将每个 char 转换为 int :ints = [int(c) for c in text] 然后使用 i, j, k = ints[n:n+3],您的 f_array 可能会更快。
    【解决方案7】:

    你很轻松,你可能想为一个量化分析师不懂基本算法的对冲基金工作:-)

    没有方法可以在O(1) 中处理任意大小的数据结构,如果在这种情况下,您需要至少访问每个元素一次。在这种情况下,您可以希望的最佳O(n),其中n 是字符串的长度。

    尽管顺便说一句,名义上的O(n) 算法对于固定的输入大小为O(1),所以从技术上讲,它们在这里可能是正确的。然而,这通常不是人们使用复杂性分析的方式。

    在我看来,您可以通过多种方式给他们留下深刻印象。

    首先,通知他们在O(1)不可能这样做,除非你使用上面给出的“可疑”推理。

    其次,通过提供 Pythonic 代码展示您的精英技能,例如:

    inpStr = '123412345123456'
    
    # O(1) array creation.
    freq = [0] * 1000
    
    # O(n) string processing.
    for val in [int(inpStr[pos:pos+3]) for pos in range(len(inpStr) - 2)]:
        freq[val] += 1
    
    # O(1) output of relevant array values.
    print ([(num, freq[num]) for num in range(1000) if freq[num] > 1])
    

    这个输出:

    [(123, 3), (234, 3), (345, 2)]
    

    当然,您可以将输出格式修改为任何您想要的格式。

    最后,通过告诉他们O(n) 解决方案几乎肯定没有问题,因为上面的代码在不到半秒的时间内为一百万位数的字符串提供了结果。它的扩展似乎也相当线性,因为 10,000,000 个字符的字符串需要 3.5 秒,而 100,000,000 个字符的字符串需要 36 秒。

    而且,如果他们需要比这更好,有办法将这类东西并行化,可以大大加快速度。

    当然,由于 GIL,不在一个 单个 Python 解释器中,但是您可以将字符串拆分为类似的内容(vv 指示的重叠是允许正确处理边界区域所必需的):

        vv
    123412  vv
        123451
            5123456
    

    您可以将这些分包给单独的工作人员,然后再合并结果。

    输入的拆分和输出的组合可能会淹没任何小字符串(甚至可能是百万位数的字符串)的节省,但对于更大的数据集,它可能会产生影响。我常用的“衡量,不要猜测”的口头禅当然适用于此。


    这句话也适用于其他可能性,例如完全绕过 Python 并使用可能更快的不同语言。

    例如,下面的 C 代码与早期的 Python 代码在相同的硬件上运行,在 0.6 秒内处理 亿 百万位数字,与 Python 代码处理的时间大致相同 一百万。换句话说,快得多

    #include <stdio.h>
    #include <string.h>
    
    int main(void) {
        static char inpStr[100000000+1];
        static int freq[1000];
    
        // Set up test data.
    
        memset(inpStr, '1', sizeof(inpStr));
        inpStr[sizeof(inpStr)-1] = '\0';
    
        // Need at least three digits to do anything useful.
    
        if (strlen(inpStr) <= 2) return 0;
    
        // Get initial feed from first two digits, process others.
    
        int val = (inpStr[0] - '0') * 10 + inpStr[1] - '0';
        char *inpPtr = &(inpStr[2]);
        while (*inpPtr != '\0') {
            // Remove hundreds, add next digit as units, adjust table.
    
            val = (val % 100) * 10 + *inpPtr++ - '0';
            freq[val]++;
        }
    
        // Output (relevant part of) table.
    
        for (int i = 0; i < 1000; ++i)
            if (freq[i] > 1)
                printf("%3d -> %d\n", i, freq[i]);
    
        return 0;
    }
    

    【讨论】:

    • 这个“固定输入大小”听起来真的是一个糟糕的笑话,无论是面试官还是被面试者都没有得到。每个算法都变成O(1)n 是固定的或有界的。
    • 如果他们需要比这更好的,也许他们不应该使用 Python,至少对于特定的算法。
    • @ezzzCash 因为在尝试并行方法时字符串被“分解”的点可能存在重叠。由于您要查找 3 位数组,因此 -2 允许检查两个并行分组以错过潜在的有效匹配。
    • @ezzzCash 并不是缺乏并行编程知识。考虑一个长度为N 的字符串。如果您在位置N/2 将其分成两部分,您仍然需要考虑这样一个事实,即您可能会在string1 末尾和@987654338 开头的“边界”处错过有效的3 位数匹配@。因此,您需要检查 string1[N/2-2]string2[2] 之间的匹配(使用从零开始的索引)等。这就是想法。
    • 对于更长的数字序列,通过滑动窗口优化转换为整数会有所收获,该滑动窗口可让您删除最高数字并添加新数字。 (Python 开销可能会杀死这个,所以它只适用于 C 或其他低级实现)。 val -= 100 * (d[i]-'0'); 删除前导数字。 val = 10*val + d[i+2]-'0' 累积一个新的最低有效数字(普通字符串->整数解析)。 val % 100 可能并不可怕,但前提是 100 是编译时常量,因此它不使用真正的硬件除法。
    【解决方案8】:

    正如另一个答案中提到的,您不能在恒定时间内执行此算法,因为您必须查看至少 n 位数字。线性时间是您可以获得的最快时间。

    但是,该算法可以在 O(1) 空间中完成。您只需要存储每个 3 位数字的计数,因此您需要一个包含 1000 个条目的数组。然后,您可以将号码流式传输。

    我的猜测是面试官在给你解决方案时说错了,或者当他们说“恒定空间”时你听错了“恒定时间”。

    【讨论】:

    • 正如其他人指出的那样,直方图方法是O(10**d) 额外空间,其中d 是您要查找的小数位数。
    • 对于 n 位,字典方法将是 O (min (10^d, n))。例如,如果您有 n = 10^9 位,并且想要查找出现不止一次的罕见 15 位序列。
    【解决方案9】:

    对于我在下面给出的答案来说,一百万是很小的。只期望你必须能够在面试中运行解决方案,没有停顿,那么下面的工作将在不到两秒的时间内给出所需的结果:

    from collections import Counter
    
    def triple_counter(s):
        c = Counter(s[n-3: n] for n in range(3, len(s)))
        for tri, n in c.most_common():
            if n > 1:
                print('%s - %i times.' % (tri, n))
            else:
                break
    
    if __name__ == '__main__':
        import random
    
        s = ''.join(random.choice('0123456789') for _ in range(1_000_000))
        triple_counter(s)
    

    希望面试官会考虑使用标准库 collections.Counter 类。

    并行执行版本

    我为此写了blog post 并提供了更多解释。

    【讨论】:

    • 它工作正常,似乎是最快的非 numpy 解决方案。
    • @EricDuminil,我认为您不必担心这里的时间安排太快,因为大多数给出的解决方案都不会耽误您太多时间。更好地表明你对 Python 标准库有很好的掌握,并且我认为可以在面试情况下编写可维护的代码。 (除非面试官强调时间紧迫,你应该在评估下一步之前询问实际时间)。
    • 我们同意 100%。虽然我不确定如果面试官真的认为可以在O(1) 做任何答案是相关的。
    • 如果面试官强调时间紧迫,那么, profiling 确认这是极限之后,可能是时候编写一个C 模块来解决这个瓶颈了。在我们切换到使用 c 模块后,我有一个脚本比 python 代码改进了 84 倍。
    • 嗨@TemporalWolf,我读了你所说的然后认为另一种更快且可扩展的解决方案可能是将其更改为并行算法,以便它可以在计算场/云上的许多进程上运行.您必须将字符串拆分为 n 个部分;将每个部分的最后 3 个字符与其下一个部分重叠。然后可以独立地扫描每个部分的三元组,将三元组相加,并在除最后一个部分之外的所有部分结束时减去三个字符三元组,因为它会被重复计算。我有代码,可能会把它变成一篇博文……
    【解决方案10】:
    inputStr = '123456123138276237284287434628736482376487234682734682736487263482736487236482634'
    
    count = {}
    for i in range(len(inputStr) - 2):
        subNum = int(inputStr[i:i+3])
        if subNum not in count:
            count[subNum] = 1
        else:
            count[subNum] += 1
    
    print count
    

    【讨论】:

    • 感谢您的回答,但它与@abhishek arora 5-6 天前给出的算法太相似了。此外,最初的问题不是要求算法,而是一个不同的问题(已经回答过多次)
    【解决方案11】:

    图片作为答案:

    看起来像一个滑动窗口。

    【讨论】:

      【解决方案12】:

      这是我的解决方案:

      from collections import defaultdict
      string = "103264685134845354863"
      d = defaultdict(int)
      for elt in range(len(string)-2):
          d[string[elt:elt+3]] += 1
      d = {key: d[key] for key in d.keys() if d[key] > 1}
      

      在 for 循环中有一点创意(以及额外的查找列表,例如 True/False/None),您应该能够摆脱最后一行,因为您只想在我们访问过一次的 dict 中创建键到那时。 希望对你有帮助:)

      【讨论】:

      • pho7's answer。和厘米。尝试找出为什么它没有获得大量选票。
      【解决方案13】:

      -从 C 的角度讲。 - 你可以有一个 int 3-d 数组 results[10][10][10]; - 从第 0 个位置到第 n-4 个位置,其中 n 是字符串数组的大小。 - 在每个位置,检查当前、下一个和下一个。 -将cntr递增为resutls[current][next][next's next]++; - 打印

      的值
      results[1][2][3]
      results[2][3][4]
      results[3][4][5]
      results[4][5][6]
      results[5][6][7]
      results[6][7][8]
      results[7][8][9]
      

      -这是 O(n) 时间,不涉及比较。 - 你可以通过对数组进行分区并计算分区周围的匹配来运行一些并行的东西。

      【讨论】:

        猜你喜欢
        • 2012-01-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-06-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多