私は数ヶ月前にニューヨークのヘッジファンド会社にインタビューをしましたが、残念ながらデータ/ソフトウェアエンジニアとしてのインターンシップの申し出を受けませんでした。 (彼らはまた、解決策をPythonにするよう求めた。)
私は最初のインタビューの問題にほとんど失敗しました...
質問:百万の数の文字列(例えばPi)を考えて、すべての繰り返し3桁の数字と1より大きい繰り返しの数を返す関数/プログラムを書いてください。
たとえば、文字列が123412345123456
の場合、関数/プログラムは次のように戻ります。
123 - 3 times
234 - 3 times
345 - 2 times
私がインタビューに失敗した後、彼らは私に解決策を与えませんでした、しかし彼らは私がすべての可能な結果がその間にあるので解決策のための時間複雑さが1000で一定であると言っていました
000 - > 999
私はそれについて考えている今、私はそれが一定時間のアルゴリズムを考え出すことは可能だとは思わない。それは...ですか?
あなたは軽く降りた、あなたはおそらくdo n'tクオンツが基本的なアルゴリズムを理解していないヘッジファンドで働きたい:-)
noO(1)
の任意のサイズのデータ構造を処理する方法があります。この場合、少なくとも一度はすべての要素を訪れる必要があります。 bestは、この場合O(n)
です。ここで、n
は文字列の長さです。
余談ですが、固定入力サイズの場合、名目上の
O(n)
アルゴリズムwillはO(1)
になるため、技術的にはここで正しいかもしれません。しかし、それは通常、人々が複雑性分析を使用する方法ではありません。
いろいろな方法で彼らに感銘を与えることができたようです。
最初に、上記の「疑わしい」推論を使用しない限り、O(1)
でnotを実行できることを通知します。
次に、次のようなPythonコードを提供して、エリートのスキルを示します。
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)]
もちろん、出力形式を必要に応じて変更できます。
そして最後に、上記のコードは100万桁の文字列の結果を0.5秒未満で提供するため、O(n)
ソリューションにno問題があることをほぼ確実に伝えることにより、 10,000,000文字の文字列には3.5秒かかり、100,000,000文字の文字列には36秒かかるため、非常に直線的にスケーリングするようです。
そして、もしneedがそれよりも優れていれば、この種のものを並列化する方法があります。
シングル Pythonインタープリター内ではなく、GILのため、もちろん、文字列を次のようなものに分割することもできます(境界を適切に処理するには、vv
で示されるオーバーラップが必要です)エリア):
vv
123412 vv
123451
5123456
これらを別々のワーカーにファームアウトし、後で結果を組み合わせることができます。
入力の分割と出力の結合は、小さな文字列(および場合によっては百万桁の文字列)を節約する可能性がありますが、より大きなデータセットの場合は、違いが生じる可能性があります。 "メジャー、推測しない"の私の通常のマントラはもちろんここに適用されます。
このマントラは、Pythonを完全にバイパスし、より高速な別の言語を使用するなど、otherの可能性にも適用されます。
たとえば、以前のPythonコードと同じハードウェアで実行されている次のCコードは、0.6秒で百百万桁を処理します。これは、_とほぼ同じ時間です。Pythonコード処理one million。言い換えれば、much fast:
#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;
}
一定の時間は不可能です。 100万桁すべてを少なくとも一度調べる必要があるので、これはO(n)の時間の複雑さです。ここではn = 100万です。
単純なO(n)ソリューションでは、考えられる3桁の数字それぞれの出現回数を表すサイズ1000の配列を作成します。一度に1桁ずつ進み、最初のインデックス== 0、最後のインデックス== 999997、および増加する配列[3桁の番号]でヒストグラムを作成します(各可能な3桁の番号の発生数)。次に、count> 1の配列の内容を出力します。
単純な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回だけ反復することがcount
を使用するより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 __== '__main__':
import random
s = ''.join(random.choice('0123456789') for _ in range(1_000_000))
triple_counter(s)
うまくいけば、インタビュアーは標準ライブラリcollections.Counterクラスの使用を探しているでしょう。
私はこれについて ブログ記事 を書きました。
これが "コンセンサス" O(n)アルゴリズムのNumPy実装です。すべてのトリプレットとbinを見ながら進みます。ビニングは、例えば "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
私はこの問題を次のように解決します。
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は与えられた文字列の長さです。
私の理解によれば、あなたは解決策を一定の時間内に持つことはできません。 100万桁の数字を1回以上通過します(文字列を想定)。 100万の長さの数字を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より大きい項目値を持つキーまで絞り込むことができます。
別の答えで述べたように、少なくともn桁を見なければならないので、このアルゴリズムを一定時間で実行することはできません。線形時間はあなたが得ることができる最も速いです。
ただし、アルゴリズムはO(1) spaceで実行できます。各3桁の数字のカウントを保存するだけでよいので、1000エントリの配列が必要です。あなたはそれから数を流すことができる。
インタビュアーが解決策を提示したときにインタビュアーが誤って入力したか、または「一定のスペース」と言ったときに「一定の時間」を聞き間違えたのでしょうか。
これが私の答えです:
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)]
答えとしての画像:
スライド式の窓のように見えます。
これが私の解決策です:
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などの追加のルックアップリスト)に少し創造性を持たせることで、最後の行を削除することができるはずです。 。それが役に立てば幸い :)
- Cから見た場合 - int 3-d配列を持つことができます[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)時間です。比較は必要ありません。 - ここでいくつかの並列処理を実行することができます。ここでは配列を分割し、分割周辺の一致を計算します。