[python] 백만 개의 문자열이 주어지면 반복되는 세 자리 숫자를 모두 반환하십시오.

몇 달 전에 뉴욕의 헤지 펀드 회사와 인터뷰를했지만 불행히도 데이터 / 소프트웨어 엔지니어로서 인턴쉽을받지 못했습니다. (또한 파이썬으로 솔루션을 요청했습니다.)

나는 첫 인터뷰 문제에 거의 망 쳤어.

질문 : 백만 개의 문자열 (예 : Pi)이 주어지면 반복되는 3 자리 숫자와 1보다 큰 반복 횟수를 모두 반환하는 함수 / 프로그램을 작성하십시오.

예를 들어 문자열이 123412345123456다음과 같으면 함수 / 프로그램은 다음을 반환합니다.

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

그들은 인터뷰에 실패한 후에 해결책을주지 않았지만 가능한 모든 결과가 다음과 같기 때문에 해결책의 시간 복잡성이 1000이라고 일정하다고 말했습니다.

000-> 999

나는 그것에 대해 생각하고 있기 때문에 일정한 시간 알고리즘을 생각해내는 것이 불가능하다고 생각합니다. 그렇습니까?



답변

당신은 당신이 아마 가볍게 내려서 하지 quants 기본 알고리즘을 이해하지 못하는 헤지 펀드에 대한 작동 할 🙂

이 경우처럼 모든 요소를 ​​한 번 이상 방문해야하는 경우 임의 크기의 데이터 구조를 처리 할 수있는 방법 이 없습니다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 초가 걸리고 100,000,000 자 문자열은 36 초가 걸리기 때문에 선형 적으로도 확장되는 것으로 보입니다.

그리고 그것들 그보다 더 필요 하다면 , 속도를 크게 높일 수있는 이런 종류의 것들을 병렬화하는 방법이 있습니다.

물론 GIL로 인해 단일 파이썬 인터프리터 내에는 아니지만 문자열을 다음과 같이 나눌 수 있습니다 ( vv경계 영역을 올바르게 처리하려면 겹침이 표시 되어야 함).

    vv
123412  vv
    123451
        5123456

이를 분리하여 작업자를 분리하고 나중에 결과를 결합 할 수 있습니다.

입력 분할과 출력 결합은 작은 문자열 (및 수백만 자릿수의 문자열)로 절약 할 수 있지만 훨씬 더 큰 데이터 세트의 경우 큰 차이가있을 수 있습니다. 물론 “측정, 추측하지 말아”라는 나의 평범한 만트라가 여기에 적용됩니다.


이 만트라는 파이썬을 우회하고 더 빠른 다른 언어를 사용하는 것과 같은 다른 가능성 에도 적용됩니다 .

예를 들어, 다음의 코드 C는 이전 파이썬 코드와 동일한 하드웨어에서 실행하는 처리 0.6 초 만 자리 파이썬 코드 처리 시간으로 대략 동일한 양의 하나 백만. 다시 말해, 훨씬 빠릅니다.

#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 (n)의 시간 복잡성입니다.이 경우 n = 백만입니다.

간단한 O (n) 솔루션의 경우 가능한 각 3 자리 숫자의 발생 횟수를 나타내는 1000 크기의 배열을 작성하십시오. 한 번에 1 자리 씩 이동하고 첫 번째 인덱스 == 0, 마지막 인덱스 == 999997 및 증분 배열 [3 자리 숫자]을 사용하여 히스토그램 (가능한 3 자리 숫자마다 발생 횟수)을 만듭니다. 그런 다음 개수가 1보다 큰 배열의 내용을 출력하십시오.


답변

아래 답변에 백만이 작습니다. 인터뷰에서 솔루션을 일시 정지없이 실행할 수 있어야한다고 예상하면 다음은 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)

면접관이 표준 라이브러리 컬렉션을 사용할 수 있기를 바랍니다.

병렬 실행 버전

자세한 설명과 함께 블로그 게시물 을 작성했습니다 .


답변

간단한 O (n) 솔루션은 각 3 자리 숫자를 세는 것입니다.

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

이것은 백만 자릿수를 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.


답변

“consensus”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


답변

다음과 같이 문제를 해결할 것입니다.

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}

이 솔루션은 제공된 문자열의 길이가 n 인 O (n)에서 실행되며 가장 좋은 방법이라고 생각합니다.


답변

내 이해에 따라, 당신은 일정한 시간에 솔루션을 가질 수 없습니다. 문자열을 가정하면 백만 자리수 이상의 패스를 한 번 이상 받아야합니다. 백만 길이의 숫자에 대해 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보다 큰 키로 필터링 할 수 있습니다.