100万の数字の文字列が与えられた場合、繰り返されるすべての3桁の数字を返します


137

数か月前にニューヨークのヘッジファンド会社にインタビューをしましたが、残念ながら、データ/ソフトウェアエンジニアとしてのインターンシップのオファーはありませんでした。(彼らはまた、ソリューションをPythonにすることを求めました。)

私は最初のインタビューの問題でかなり失敗しました...

質問:100万の数字(Piなど)の文字列が与えられた場合、3桁の数字の繰り返しと1より大きい繰り返しの数をすべて返す関数/プログラムを作成します。

たとえば、文字列が:の123412345123456場合、関数/プログラムは次を返します。

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

私がインタビューに失敗した後、彼らは私に解決策を与えませんでしたが、考えられるすべての結果が次の間にあるため、解決策の時間の複雑さは1000で一定であると教えてくれました:

000-> 999

私はそれについて考えているので、一定の時間のアルゴリズムを思いつくことは可能ではないと思います。それは...ですか?


68
彼らが解が1000の定数であると考える場合、それは彼らがすべての3桁の数字を構築し、正規表現でそれらを検索したと思います。実際に書き込んだり参照したりしなかった操作は「無料」だと考えるのはよくあることです。これは文字列の長さに線形であると確信しています。
mypetlion 2017年

54
ちなみに、入力サイズが一定の場合、すべてのアルゴリズムは一定時間です;-)
–PaŭloEbermann 2017

34
1000の定数はですか?(追加?象?)
ilkkachu 2017年

31
さて、文字列の長さが一定(1M)で、サブ/番号の長さが一定の(3)であれば、技術的にすべてのソリューションは、一定の時間です...
ケビン

8
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 これはおそらく実際のテストでした。これが不可能な理由を彼らに証明できるかどうかを確認し、正しい最小時間の複雑さを彼らに示すため。
James

回答:


168

あなたは軽く降りました、あなたはおそらくクオンツが基本的なアルゴリズムを理解していないヘッジファンドのために働きたくないでしょう:-)

この場合のように、すべての要素に少なくとも1回アクセスする必要がある場合、任意のサイズのデータ​​構造を処理する方法はありません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)]

もちろん、出力形式を好きなように変更することもできます。

そして最後に、上記のコードは0.5秒未満で100万桁の文字列の結果を提供するので、ソリューションに問題がないことをほぼ確実に伝えますO(n)。10,000,000文字の文字列は3.5秒かかり、1億文字の文字列は36秒かかるため、これも非常に直線的にスケーリングされているようです。

そして、彼らがそれよりも良いものを必要するなら、それを大幅にスピードアップできるこの種のものを並列化する方法があります。

もちろん、GILのため、単一の Pythonインタープリター内ではありませんが、文字列を次のようなものに分割できます(vv境界領域を適切に処理するには、で示されるオーバーラップが必要です)。

    vv
123412  vv
    123451
        5123456

これらを別々のワーカーにファームし、後で結果を組み合わせることができます。

入力の分割と出力の結合は、小さな文字列(場合によっては数百万桁の文字列)の節約を損なう可能性がありますが、はるかに大きなデータセットの場合は、違いが生じる可能性があります。もちろん、いつもの「対策、推測しないでください」というスローガンが当てはまります。


このマントラは、他の可能性にも適用されます。たとえば、Pythonを完全にバイパスして、より高速な別の言語を使用するなどです。

例えば、以下のCコードは、以前のPythonコードと同じハードウェア上で実行されている、ハンドル 0.6秒で百万の桁、Pythonコードが処理され、時間のほぼ同じ量の1つの百万。言い換えれば、はるかに高速です:

#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;
}

19
この「固定入力サイズ」は、面接担当者も面接担当者も受け取れなかった冗談のように聞こえます。すべてのアルゴリズムはなっO(1)ているn固定または制限されています。
Eric Duminil 2017

5
それ以上のものが必要な場合は、少なくとも特定のアルゴリズムについては、Pythonを使用すべきではありません。
Sebastian Redl 2017

3
@ezzzCash並列アプローチを試行するときに文字列が「分割」されているポイントで重複がある可能性があるためです。あなたはしているが、3桁のグループを探しているので、-2、両方の並列グループのチェックをすることができない潜在的に有効な試合を欠場。
code_dredd 2017

5
@ezzzCash並列プログラミングの知識が不足しているわけではありません。長さの文字列を考えますN。あなたが位置に二つの部分にそれを破る場合N/2、あなたはまだあなたがの終わりに、「国境」で有効な3桁の試合を欠場する可能性があるという事実を考慮するために必要string1との始まりstring2。したがって、(ゼロベースのインデックスを使用して)などstring1[N/2-2]との間の一致をチェックする必要がありますstring2[2]。それがアイデアです。
code_dredd 2017

1
より長い数字列を使用すると、最大の数字を削除して新しい数字を追加できるスライディングウィンドウを使用して整数への変換を最適化することで何かが得られます。(Pythonオーバーヘッドはおそらくこれを無効にするため、Cまたは他の低レベルの実装にのみ適用されます)。 val -= 100 * (d[i]-'0');先頭の桁を削除します。 val = 10*val + d[i+2]-'0'新しい最下位桁を蓄積します(通常の文字列->整数解析)。 val % 100恐ろしいことではないかもしれ100ませんが、コンパイル時の定数であり、実際のHW除算を使用しない場合のみです。
Peter Cordes

78

一定の時間は不可能です。100万桁すべてを少なくとも1回見る必要があるため、これはO(n)の時間の複雑さです。この場合、n = 100万です。

単純なO(n)ソリューションの場合、考えられる3桁の数値それぞれの出現回数を表すサイズ1000の配列を作成します。一度に1桁ずつ進み、最初のインデックス== 0、最後のインデックス== 999997、増分配列[3桁の番号]で、ヒストグラム(可能な3桁の番号ごとの出現回数)を作成します。次に、カウントが1より大きい配列の内容を出力します。


26
@ezzzCash-はい、辞書は機能しますが、必要ありません。可能なすべての「キー」は事前にわかっており、0〜999の範囲に制限されています。オーバーヘッドの違いは、3つの文字列をキーとして使用してキーベースのアクセスを行うのにかかる時間と、3を変換するのにかかる時間です。数字列をインデックスに変換し、インデックスを使用して配列にアクセスします。
rcgldr 2017年

4
数値トリックが必要な場合は、BCDを実行して、3桁を12ビットに格納することもできます。そして、下位4ビットをマスクしてASCII桁をデコードします。しかし、そのx-'0'パターンはPythonでは有効ではなく、C-ismです(文字は整数です)。
Yann Vernier 2017年

5
@LorenPechtel:Pythonでの辞書検索は本当に高速です。確かに、配列へのアクセスはさらに高速なので、最初から整数を扱っていれば、それは正しいでしょう。ただし、この場合、3つの長さの文字列があり、配列で使用する場合は最初に整数に変換する必要があります。最初に予想されることとは反対に、実際の辞書検索は整数変換+配列アクセスよりも高速であることがわかります。この場合、アレイソリューションは実際には50%遅くなります。
Aleksi Torhamo 2017

2
入力数が常に正確に 100万桁である場合、そのアルゴリズム O(1)であり、定数が100万であると私は主張するかもしれません。
tobias_k 2017

2
@AleksiTorhamo-目標がアルゴリズムの実装の相対的な速度を比較することである場合、Pythonは他の言語に比べて非常に遅く、Pythonに固有のオーバーヘッドがあるように見えるため、CやC ++などの従来の言語を使用します。
rcgldr 2017

14

100万は私が下に与える答えのために小さいです。面接で一時停止せずにソリューションを実行できる必要があることだけを期待している場合、以下は2秒未満で機能し、必要な結果が得られます。

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クラスの使用を探しているでしょう。

並列実行バージョン

私はこれについてブログ記事を書きました。


それは正常に動作し、最速で非派手なソリューションのようです。
Eric Duminil 2017

3
@EricDuminil、私はあなたが与えられたほとんどの解決策があなたをそれほど遅らせないとき、ここでより速いタイミングを持っていることについて心配するべきではないと思います。あなたがPython標準ライブラリをよく理解していて、私が思うインタビューの状況で保守可能なコードを書くことができることを示す方がはるかに良いでしょう。(面接担当が時間の重要性を強調した場合を除いて、次に何が起こるかを評価する前に実際のタイミングを尋ねる必要があります)。
Paddy3118 2017

1
100%同意します。面接担当者がで実行できると本当に思っている場合、どの回答も関連があるとは思えませんO(1)
Eric Duminil 2017

1
インタビュアーがタイムクリティカルであると強調した場合、プロファイリングしてこれが限界であることを確認した、このボトルネックに対処するためのCモジュールを作成するときがきたかもしれません。ACモジュールの使用に切り替えた後、Pythonコードよりも84倍向上したスクリプトがあります。
TemporalWolf 2017

こんにちは@TemporalWolfです。別のより高速でスケーラブルなソリューションが並列アルゴリズムに変更して、コンピューティングファーム/クラウドの多くのプロセスで実行できるようになるとあなたが言ったことを読みました。文字列をn個のセクションに分割する必要があります。各セクションの最後の3文字を次のセクションと重ねます。次に、各セクションを個別にスキャンしてトリプルを探し、トリプルを合計し、最後のセクションを除いて3文字のトリプルをダブルカウントしたため、最後のセクションを差し引くことができます。私はコードを持っているので、おそらくそれをブログ投稿に変えます...
Paddy3118

13

単純な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回検索されます。

数字のトラバースは1回だけです。

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)

タイミングは、インデックスに対して1回だけ繰り返すと、を使用する場合の2倍の速さであることを示していますcount


37
黒い金曜日の割引はありtext.count()ますか?
Eric Duminil 2017年

3
@EricDuminil良い点はありますが、text.countPythonレベルで解釈されるループが遅いのではなく、高速コンパイルされた言語(Cなど)で行われるため、割引があります。
John1024

各数値を個別にカウントすることは非常に非効率的ですが、一定の時間であるため、依然としてO(n)です。
Loren Pechtel 2017年

11
使用する提案したオプションcountは、重複するパターンをカウントしないため、正しくありません。'111'.count('11') == 1それが期待されるときは注意してください2
Cireo

2
また、「簡単なO(n)解決策」は、実際O(10**d * n)dは、検索された桁数とn文字列の全長です。二つ目はO(n)時間とO(10**d + n)空間です。
Eric Duminil 2017

10

以下は、「コンセンサス」O(n)アルゴリズムのNumPy実装です。移動しながらすべてのトリプレットとビンを調べます。ビニングは、たとえば「385」に遭遇したときにO(1)操作であるbin [3、8、5]に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

NumPyが効率的にインデックス付けされた3Dマトリックスとして実装しない限り、ネストされたビンを使用する代わりに、数字列をフラット化する方がおそらくかなり高速です。@Danielのどのバージョンに対抗しましたか。各整数の文字列検索を実行するもの、またはヒストグラム付きのもの?
Peter Cordes

2
@PeterCordes疑わしい。ndarrays、核となるnumpy型はすべて、数値の多次元配列の効率的な格納、操作、およびインデックス付けに関するものです。場合によっては、平坦化によって数%を削ることができますが、この場合、100 x [0] + 10 x [1] + x [2]を手動で実行してもあまり効果がありません。@Danielの方が速い方を使用しました。自分でベンチマークコードを確認できます。
ポールパンツァー2017

私はNumPy(または一般的にPython;ほとんどがx86のCおよびアセンブリパフォーマンスチューニングを行います)を本当に知りませんが、単一の3D配列があると思いますよね?私はあなたが実際にネストされたPythonオブジェクトがあり、それらを別々にインデックス付けしているとあなたの英語のテキスト(どうやら私は注意深く読んでもいなかった)から考えていました。しかし、そうではないので、最初のコメントはnvmです。
Peter Cordes 2017

あなたが使用した純粋なPythonバージョンは、投票数の多い回答が使用したヒストグラム実装とほぼ同じだと思いますが、Pythonでの書き込み方法が異なると速度に大きく影響します。
Peter Cordes

3

私は次のように問題を解決します:

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。また、反復ごとにを更新する必要はありません。
Eric Duminil 2017年

2

私の理解によると、あなたは一定の時間内に解決策を持つことはできません。100万桁の数字を少なくとも1回パスします(文字列を想定)。百万長の数字の桁で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より大きいキーに絞り込むことができます。


2

別の答えで述べたように、少なくともn桁を見なければならないため、このアルゴリズムを一定の時間で実行することはできません。線形時間は、取得できる最速です。

ただし、アルゴリズムはO(1)空間で実行できます。各3桁の数値のカウントを保存するだけでよいので、1000エントリの配列が必要です。その後、その番号をストリーミングできます。

おそらく、インタビュアーがあなたに解決策を与えたときに間違って話したか、または「一定の空間」と言ったときに「一定の時間」を誤って聞いたと思います。


他の人が指摘したように、ヒストグラムアプローチはO(10**d)余分なスペースです。ここdで、は探している10進数の桁数です。
Peter Cordes

1
辞書のアプローチは、n桁でO(min(10 ^ d、n))になります。たとえば、n = 10 ^ 9桁で、2回以上発生するまれな15桁のシーケンスを見つけたい場合などです。
gnasher729

1

これが私の答えです:

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)]

1
それであなたは正確に何を比較していますか?未使用のジェネレータの代わりにリストを返すべきではありませんか?
Eric Duminil 2017

Countersそのように使用されていません。適切に使用すると、これらはあなたの例で最も速いオプションになります。timeitジェネレーターが組み込まれたリストで使用すると、メソッドはCounterまたはより遅くなりますdict。こちらをご覧ください
Eric Duminil 2017

最後に、f_array最初にすべての文字をintに変換しints = [int(c) for c in text]、次にを使用すると、処理が速くなる可能性がありますi, j, k = ints[n:n+3]
Eric Duminil 2017


1

これが私の解決策です:

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を含む追加のルックアップリストなど)を使用すると、最後の行を取り除くことができるはずです。 。それが役に立てば幸い :)


pho7の回答を参照してください。そしてコメント。それが大量の投票を取得しない理由を理解してみてください。
greybeard 2017

0

-Cの観点から説明します。-int3次元配列の結果を得ることができます[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)時間です。比較は行われません。-ここでは、配列をパーティション分割し、パーティションの周囲で一致を計算することにより、並列処理を実行できます。


-1
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

回答ありがとうございます。5〜6日前に@abhishek aroraによって提供されたアルゴリズムと非常に似ています。また、元の質問は、アルゴリズムを求めていなかったのではなく(すでに複数回に答えた)別の質問は
its.david
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.