수천 개의 전화 번호를 저장하는 가장 효율적인 방법


94

이것은 Google 인터뷰 질문입니다.

각각 10 자리 숫자로 된 약 천개의 전화 번호가 저장됩니다. 각각의 처음 5 자리 숫자가 천 개의 숫자에서 동일하다고 가정 할 수 있습니다. 다음 작업을 수행해야합니다. 주어진 번호가 있는지 검색합니다. 비. 모든 번호를 인쇄

이를 수행하는 가장 효율적인 공간 절약 방법은 무엇입니까?

나는 해시 테이블과 나중에 허프만 코딩에 대답했지만 면접관은 내가 올바른 방향으로 가고 있지 않다고 말했습니다. 여기서 도와주세요.

접미사 트라이를 사용하면 도움이 될 수 있습니까?

이상적으로 1000 개의 숫자를 저장하는 데는 숫자 당 4 바이트가 필요하므로 1000 개의 숫자를 저장하는 데 4000 바이트가 걸립니다. 양적으로 저는 스토리지를 4000 바이트 미만으로 줄이고 싶습니다. 인터뷰 담당자가 설명해주었습니다.


28
일반 데이터베이스를 사용하면 수천 / 백만 개까지도 텍스트로 저장할 수 있으며 조회 작업은 여전히 ​​매우 빠릅니다. 앞으로 국제 전화 번호를 지원하기를 원하거나 "0"으로 시작하는 전화 번호가 나타나기 시작하거나 정부에서 전화 번호 형식 등을 변경합니다.
Thomas Bonini 2011 년

1
@AndreasBonini : 구글이나 페이스 북과 같은 회사에서 인터뷰를하지 않는 한 그 대답을 할 것입니다. 예를 들어 postgres에도 시도가 있지만 이것이 Google이 가져야하는 데이터 처리량을 줄인다는 것은 확신하지 못합니다.
LiKao 2011 년

1
@LiKao : OP에서 "약 천 개의 숫자"를 구체적으로 언급했음을 명심하십시오.
Thomas Bonini 2011 년

@AndreasBonini : 사실, 인터뷰 대상자가 이러한 제약 조건을 올바르게 해석하고 이에 따라 최상의 솔루션을 선택하는 것을 알고있는 테스트 일 수도 있습니다.
LiKao 2011 년

4
이 질문에서 "효율적"은 정말로 정의되어야합니다. 어떤면에서 효율적입니까? 공간, 시간, 둘 다?
matt b

답변:


36

다음은 aix의 답변에 대한 개선 사항 입니다. 데이터 구조에 세 개의 "계층"을 사용하는 것을 고려하십시오. 첫 번째는 처음 5 자리 (17 비트)에 대한 상수입니다. 이제부터 각 전화 번호는 나머지 5 자리 만 남습니다. 나머지 5 자리를 17 비트 이진 정수로보고 한 가지 방법을 사용하여 해당 비트 중 k 를 저장 하고 다른 방법으로 17- k = m 을 저장 하여 필요한 공간을 최소화하기 위해 끝에 k 를 결정 합니다.

먼저 전화 번호를 정렬합니다 (모두 10 진수 5 자리로 축소됨). 그런 다음 우리는 첫 번째로 구성된 이진수하는 얼마나 많은 전화 번호 계산 m의 비트는 첫번째 얼마나 많은 전화 번호를 모두 0 m의 얼마나 많은 전화 번호를 처음 들어 ... 0 대부분에 비트가 01 m 비트는 최대 0 ... 10 등이며, 첫 번째 m 비트가 1 ... 11 인 전화 번호 수까지입니다. 이 마지막 수는 1000 (십진수)입니다. 이러한 개수 는 2 ^ m 개이고 각 개수는 최대 1000 개입니다. 마지막 숫자를 생략하면 (어쨌든 1000 개라는 것을 알기 때문에)이 모든 숫자를 (2 ^ m -1) 의 연속 블록에 저장할 수 있습니다. * 10 비트. (1024 미만의 숫자를 저장하려면 10 비트이면 충분합니다.)

모든 (축소 된) 전화 번호 의 마지막 k 비트는 메모리에 연속적으로 저장됩니다. 그래서 만약 K가 인 마지막 7 비트의 메모리의 블록의 다음 제 7 비트 (비트 0-6을 통해) 제 (감소) 전화 번호의 최종 7 비트에 대응하는 비트 7 내지 13에 대응 통해, 7 말 두 번째 (축소 된) 전화 번호 등. 이를 위해서는 총 17 + (2 ^ (17- k )-1) * 10 + 1000 * k에 대해 1000 * k 비트 가 필요합니다 . 이는 k = 10에 대해 최소 11287에 도달합니다 . 따라서 모든 전화 번호를 ceil (에 저장할 수 있습니다. 11287/8) = 1411 바이트.

추가 공간은 1111111 (이진수)로 시작할 수있는 숫자가 없다는 것을 관찰함으로써 절약 할 수 있습니다. 그로 시작하는 가장 낮은 숫자는 130048이고 십진수 5 자리 만 있기 때문입니다. 이를 통해 첫 번째 메모리 블록에서 몇 개의 항목을 줄일 수 있습니다. 2 ^ m -1 카운트 대신 ceil (99999 / 2 ^ k ) 만 필요합니다 . 즉 공식이

17 + ceil (99999 / 2 ^ k ) * 10 + 1000 * k

놀랍게도 k = 9 및 k = 10 또는 ceil (10997/8) = 1375 바이트에 대해 최소 10997을 얻습니다 .

특정 전화 번호가 세트에 있는지 알고 싶다면 먼저 처음 5 자리 이진수가 저장 한 5 자리 숫자와 일치하는지 확인합니다. 그런 다음 나머지 5 자리를 상위 m = 7 비트 (즉, m 비트 수 M )와 하위 k = 10 비트 (수 K ) 로 나눕니다 . 우리는 지금 번호를 찾을 수 [M-1] 처음있는 감소 된 전화 번호 m의 숫자가 가장에있는 M 1, 및 수 - [M] 처음있는 감소 된 전화 번호 m의 숫자가 가장에있는 M을 , 둘 다 첫 번째 비트 블록에서 가져온 것입니다. 이제 우리 [M-1] 번째 및 A는 [M]가의 번째 시퀀스 유전율 우리가 발견되면 메모리의 제 2 블록 내의 비트보고 K를 ; 최악의 경우 1000 개의 시퀀스가 ​​있으므로 이진 검색을 사용하면 O (log 1000) 연산을 완료 할 수 있습니다.

1000 개의 숫자를 모두 인쇄하기위한 의사 코드는 다음과 같습니다. 여기서 첫 번째 메모리 블록의 K 번째 k 비트 항목을 a [K] 로 액세스하고 두 번째 메모리 블록의 M '번째 m 비트 항목을 b [M]으로 액세스합니다. (둘 다 작성하는 데 지루한 몇 가지 작업이 필요합니다). 처음 5 자리 숫자는 c 입니다.

i := 0;
for K from 0 to ceil(99999 / 2^k) do
  while i < a[K] do
    print(c * 10^5 + K * 2^k + b[i]);
    i := i + 1;
  end do;
end do;

K = ceil (99999 / 2 ^ k ) 의 경계 케이스에 문제가있을 수 있지만 수정하기에는 쉽습니다.

마지막으로, 엔트로피 관점에서 10 ^ 5보다 작은 10 ^ 3 양의 정수의 하위 집합을 ceil (log [2] (binomial (10 ^ 5, 10 ^ 3)) 미만으로 저장할 수 없습니다. ) = 8073. 처음 5 자리에 필요한 17 개를 포함하여 여전히 10997-8090 = 2907 비트의 간격이 있습니다. 상대적으로 효율적으로 숫자에 액세스 할 수있는 더 나은 솔루션이 있는지 확인하는 것은 흥미로운 도전입니다!


4
여기서 설명하는 데이터 구조는 실제로 인덱싱에 필요한만큼만 사용하고 두 수준 만 사용하는 매우 효율적인 trie 버전입니다. 실제로 이것이 더 많은 레벨의 트라이를 이길 수 있는지 확인하는 것이 좋을 것입니다. 그러나 이것은 번호의 분포에 크게 좌우된다고 생각합니다 (실제 전화 번호는 완전히 무작위가 아니라 거의).
LiKao 2011 년

Erik 님, 다른 대안을보고 싶다고 하셨으니 제 솔루션을 확인해보세요. 이론적 최소값에서 490 비트에 불과한 8,580 비트로 해결됩니다. 개별 숫자를 찾는 것은 약간 비효율적이지만 저장 공간은 매우 작습니다.
Briguy37 2011 년

1
건전한 면접관이 "복잡한 맞춤형 데이터베이스"대신 "트리"라는 대답을 선호한다고 생각합니다. 133t의 해킹 기술을 보여주고 싶다면 "필요하다면이 특별한 경우에 대한 특정 트리 알고리즘을 만드는 것이 가능할 것"이라고 덧붙일 수 있습니다.
KarlP 2011 년

안녕하세요, 5 자리 숫자로 17 비트를 저장하는 방법을 설명해 주시겠습니까?
Tushar Banne

@tushar 5 자리 숫자는 00000에서 99999 사이의 숫자를 인코딩합니다. 그 숫자를 이진수로 나타냅니다. 2 ^ 17 = 131072이므로 17 비트이면 충분하지만 16 비트는 충분하지 않습니다.
Erik P.

43

다음에서는 숫자를 문자열이 아닌 정수 변수로 취급합니다.

  1. 숫자를 정렬하십시오.
  2. 각 숫자를 처음 5 자리와 마지막 5 자리로 나눕니다.
  3. 처음 다섯 자리는 숫자간에 동일하므로 한 번만 저장하십시오. 이를 위해서는 17 비트의 저장 공간이 필요합니다.
  4. 각 숫자의 마지막 5 자리를 개별적으로 저장합니다. 숫자 당 17 비트가 필요합니다.

요약하자면, 처음 17 비트는 공통 접두사이고, 이후 1000 개의 17 비트 그룹은 오름차순으로 저장된 각 숫자의 마지막 5 자리입니다.

총 1000 개의 숫자에 대해 2128 바이트 또는 10 자리 전화 번호 당 17.017 비트를 찾습니다.

검색은 O(log n)(이진 검색)이고 전체 열거는 O(n)입니다.


음, 공간 복잡성은 어디에 있습니까?
aioobe 2010 년

빌드하는 데 너무 많은 시간이 소요됩니다 (O (log (n) * n k) (k는 길이), 정렬을 위해 O (n k)를 빌드하는 데 비해 ). 또한 더 긴 공통 접두사가 개별적으로 저장되기 때문에 공간은 최적이 아닙니다. 검색 시간도 최적이 아닙니다. 이와 같은 문자열 데이터의 경우 검색을 지배하는 숫자의 길이를 잊어 버리기 쉽습니다. 즉 이진 검색은 O (log (n) * k) 인 반면 트라이에는 O (k) 만 필요합니다. k가 일정 할 때 이러한 표현을 줄일 수 있지만 이것은 문자열을 저장하는 데이터 구조에 대해 추론 할 때 일반적인 문제를 보여주기위한 것입니다.
LiKao 2011 년

@LiKao : 문자열에 대해 누가 말했습니까? 나는 정수 변수를 독점적으로 다루고 있으므로 k관련이 없습니다.
NPE 2011 년

1
좋아, 나는 대답을 잘못 읽었습니다. 그래도 공통 부품은 함께 보관되지 않기 때문에 공간 효율성에 대한 요점이 남아 있습니다. 5 자리 숫자 중 1000 개의 공통 접두사가 많으므로이를 줄이는 것이 많은 도움이됩니다. 또한 숫자의 경우 문자열에 대해 O (log (n)) 대 O (k)가 있는데, 여전히 더 빠릅니다.
LiKao 2011 년

1
@Geek : 17 비트 1001 개 그룹은 17017 비트 또는 2128 바이트입니다 (일부 변경 있음).
NPE

22

http://en.wikipedia.org/wiki/Acyclic_deterministic_finite_automaton

나는 그들이 데이터 구조에 대해 물었던 인터뷰를 한 적이있다. "배열"을 잊었습니다.


1
+1은 확실히 갈 길입니다. 나는 이것을 다른 이름, 도서관 나무 또는 어휘 검색 나무 또는 내가 학생이었을 때 배웠습니다 (누군가가 그 옛날 이름을 기억한다면 알려주십시오).
Valmond 2011 년

6
4000 바이트 요구 사항을 충족하지 않습니다. 포인터 저장의 경우 최악의 시나리오는 다음 수준으로 1-4 번째 잎에 1 개의 포인터, 5 번째에 10 개의 포인터, 6 번째에 100 개, 7 번째, 8 번째 및 9 번째 수준에 1000 개가 필요하다는 것입니다. , 이는 총 포인터를 3114로 가져옵니다. 이는 포인터가 가리키는 데 필요한 최소 3114 개의 개별 메모리 위치를 제공합니다. 즉, 각 포인터에 대해 최소 12 비트가 필요합니다. 12 * 3114 = 37368 비트 = 4671 바이트> 4000 바이트이며 각 리프의 값을 나타내는 방법도 알 수 없습니다!
Briguy37 2011 년

16

나는 아마 일부 압축 된 버전 사용을 고려할 것 트리는 (아마도 임마 @Misha에 의해 제안을).

그것은 그들이 모두 공통 접두사를 가지고 있다는 사실을 자동으로 이용할 것입니다.

검색은 일정한 시간으로 수행되고 인쇄는 선형 시간으로 수행됩니다.


문제는 데이터를 저장하는 가장 공간 효율적인 방법에 관한 것입니다. 이 방법이 1000 개의 전화 번호에 필요한 공간을 추정 해 주시겠습니까? 감사.
NPE

트라이에 대한 공간은 최대 O (n * k)이며 여기서 n은 문자열의 수이고 k는 각 문자열의 길이입니다. 숫자를 나타내는 데 8 비트 문자가 필요하지 않다는 점을 고려하여 4 개의 16 진수 인덱스를 16 진수로 저장하고 나머지 비트에 대해 1 개를 저장하는 것이 좋습니다. 이렇게하면 숫자 당 최대 17 비트가 필요합니다. 모든 경우에이 코딩으로 모든 수준에서 충돌이 발생하기 때문에 실제로 이보다 낮을 수 있습니다. 1000 개의 숫자를 저장할 것으로 예상하면 첫 번째 수준의 충돌에 대해 총 250 비트를 이미 저장할 수 있습니다. 예제 데이터에서 올바른 코딩을 가장 잘 테스트하십시오.
LiKao 2011 년

@LiKao, 맞습니다. 예를 들어 1000 개의 숫자가 100 개 이상의 다른 마지막 두 자리를 가질 수 없다는 점에 주목하면 트라이가 마지막 수준에서 크게 축소 될 수 있습니다.
aioobe

@aioobe : 자식이 없기 때문에 마지막 단계에서 잎이 무너질 수 있습니다. 그러나 두 번째에서 마지막 수준까지의 잎은 2 ^ 10 = 1024 상태 (각 마지막 숫자가 켜짐 또는 꺼짐 일 수 있음)가 필요하므로이 경우에는 숫자가 1000 개뿐이므로 축소 할 수 없습니다. 이것은 최악의 경우 포인터의 수가 3114 (Misha의 답변에 대한 내 의견 참조)에 머무르는 반면 필요한 잎은 5 + 10 + 100 + 1000 + 1000 + 10 = 2125로 이동하여 각각에 필요한 12 바이트를 변경하지 않음을 의미합니다. 바늘. 따라서 이것은 포인터만을 고려하여 4671 바이트에 트라이 솔루션을 배치합니다.
Briguy37 2011 년

@ Briguy37, " 각 마지막 숫자가 설정 또는 해제 될 수 있음 "인수를 받는지 확실하지 않습니다 . 모든 숫자는 10 자리입니다. 그렇죠?
aioobe 2011 년

15

이전에이 문제에 대해 들어 본 적이 있습니다 (하지만 처음 5 자리 숫자는 같은 가정이 아님).이를 수행하는 가장 간단한 방법은 Rice Coding 이었습니다 .

1) 순서는 중요하지 않으므로 정렬하고 연속 된 값 간의 차이 만 저장할 수 있습니다. 우리의 경우 평균 차이는 100.000 / 1000 = 100입니다.

2) Rice 코드 (base 128 또는 64) 또는 Golomb 코드 (base 100)를 사용하여 차이점을 인코딩합니다.

편집 : 기본 128을 사용하는 Rice 코딩에 대한 추정 (최상의 결과를 제공하기 때문이 아니라 계산하기 쉽기 때문에) :

첫 번째 값을있는 그대로 (32 비트) 저장합니다.
나머지 999 개 값은 차이입니다 (작게 예상되며 평균적으로 100 개).

단항 값 value / 128(가변 비트 수 + 종결 자로 1 비트) (7 비트)에
대한 이진 값value % 128

우리는 어떻게 든 VBL가변 비트 수에 대한 한계를 추정해야합니다 (
하한 한계 : 우리가 운이 좋다고 생각하고이 경우에는 128). 이것은 0 개의 추가 비트를 제공한다는 것을 의미합니다.
상한 : 기본보다 작은 모든 차이는 숫자의 이진 부분으로 인코딩되므로 단항으로 인코딩해야하는 최대 수는 100000/128 = 781.25입니다 (대부분의 차이가 0이 될 것으로 예상하지 않기 때문에 더 적습니다. ).

따라서 결과는 32 + 999 * (1 + 7) + 변수 (0..782) 비트 = 1003 + 변수 (0..98) 바이트입니다.


인코딩 방식과 최종 크기 계산에 대해 자세히 설명해 주시겠습니까? 1101 바이트 또는 8808 비트는 이론적 한계 인 8091 비트에 매우 근접해 보이므로 실제로 이와 같은 것을 달성 할 수 있다는 사실에 매우 놀랐습니다.
LiKao 2011 년

그것은하지 않을까요 32 + 999 * (1 + 7 + variable(0..782))비트? 999 개의 숫자는 각각 value / 128.
Kirk Broadhurst 2011 년

1
@Kirk : 아니요, 모두 5 자리 범위에있는 경우입니다. 이는 이러한 모든 차이의 합계 (첫 번째 값과 N 번째 값이 아닌 연속 값 간의 차이를 인코딩 함)가 100000 미만 (최악의 시나리오에서도)이 될 것으로 예상하기 때문입니다
ruslik

첫 번째 값을 나타내려면 32 비트 대신 34 비트가 필요합니다 (9,999,999,999> 2 ^ 32 = 4,294,967,296). 또한 숫자가 고유하기 때문에 최대 차이는 00000에서 99001이 될 것입니다. 이는 기본 128의 경우 782 대신 774 1이 추가됩니다. 따라서 기본 128에 대한 1,000 개의 숫자 저장 범위는 8026-8800 비트 또는 1004-1100 바이트입니다. 64 비트 기반은 879-1072 바이트 범위로 더 나은 스토리지를 제공합니다.
Briguy37

1
@raisercostin : 이것이 Kirk가 물은 것입니다. 귀하의 예에서 처음 두 값 간의 20k 차이를 한 번 인코딩하면 향후 최대 범위의 80k 만 발생할 수 있습니다. 이것은 최대 782 개 (100k에 해당) 중 20k / 128 = 156 개의 단항 비트를 사용합니다.
ruslik

7

이것은 Bentley의 Programming Pearls에서 잘 알려진 문제입니다.

해결 방법 : 모든 숫자에 대해 동일하므로 숫자에서 처음 다섯 자리를 제거하십시오. 그런 다음 비트 연산을 사용하여 나머지 9999 가능한 값을 나타냅니다. 숫자를 표현하려면 2 ^ 17 비트 만 필요합니다. 각 비트는 숫자를 나타냅니다. 비트가 설정된 경우 번호는 전화 번호부에 있습니다.

모든 숫자를 인쇄하려면 접두사와 연결된 비트가 설정된 모든 숫자를 인쇄하면됩니다. 주어진 숫자를 검색하려면 숫자의 비트 표현을 확인하는 데 필요한 비트 산술을 수행하십시오.

O (1)에서 숫자를 검색 할 수 있으며 비트 표현으로 인해 공간 효율성이 극대화됩니다.

HTH 크리스.


3
이것은 조밀 한 숫자 집합에 대한 좋은 접근 방식입니다. 안타깝게도 여기에서 집합은 매우 드물다. 가능한 100,000 개 중 1,000 개의 숫자 만 있습니다. 따라서이 접근 방식은 평균적으로 숫자 당 100 비트가 필요합니다. ~ 17 비트 만 필요한 대안에 대한 내 대답을 참조하십시오.
NPE 2011 년

1
모든 숫자를 인쇄하는 데 걸리는 시간이 1,000 대신 100,000에 비례하지 않습니까?
aioobe 2010 년

두 가지 아이디어를 결합하면 기본적으로 즉시 시도 할 수 있습니다. 100,000 개의 항목이있는 비트 벡터를 사용하는 것은 오버로드 방식이며 많은 공간을 차지합니다. 그러나 O (log (n)) 조회는 종종 너무 느립니다 (여기의 쿼리 수에 따라 다름). 따라서 인덱싱을 위해 계층적인 비트 세트를 사용하면 O (1) 조회를 얻는 동안 숫자 당 최대 17 비트를 저장할 수 있습니다. 이것이 트라이가 작동하는 방식입니다. 또한 인쇄 시간은 trie의 경우 O (n)이며, 정렬 된 케이스에서 상속됩니다.
LiKao 2011 년

이것은 "이를 수행하는 가장 효율적인 공간 절약 방법"이 아닙니다.
Jake Berger 2011 년

5

1,000 개의 숫자에 대해 1073 바이트의 고정 스토리지 :

이 저장 방법의 기본 형식은 처음 5 자리, 각 그룹의 개수 및 각 그룹의 각 숫자에 대한 오프셋을 저장하는 것입니다.

접두사 :
5 자리 접두사는 처음 17 비트를 차지합니다 .

그룹화 :
다음으로 숫자에 적합한 크기의 그룹을 찾아야합니다. 그룹당 1 개 정도의 숫자를 만들어 봅시다. 저장할 숫자가 약 1000 개라는 것을 알기 때문에 99,999 개를 약 1000 개의 부분으로 나눕니다. 그룹 크기를 100으로 선택하면 낭비되는 비트가 있으므로 7 비트로 표현할 수있는 그룹 크기 128을 시도해 보겠습니다. 이를 통해 782 개의 그룹과 함께 작업 할 수 있습니다.


개수 : 다음으로 782 개 그룹 각각에 대해 각 그룹의 항목 개수를 저장해야합니다. 각 그룹에 대한 7 비트 카운트는을 산출합니다 7*782=5,474 bits. 이는 우리가 그룹을 선택한 방식으로 인해 표시된 평균 수가 약 1이기 때문에 매우 비효율적입니다.

따라서 대신 그룹의 각 숫자에 대해 선행 1과 0이 뒤 따르는 가변 크기의 개수가 있습니다. 따라서 x그룹에 숫자가있는 경우 개수를 나타내는 x 1'sa 가 뒤 따랐을 것 0입니다. 예를 들어, 한 그룹에 5 개의 숫자가있는 경우 개수는로 표시됩니다 111110. 이 방법을 사용하면 1000 개의 숫자가 있는 경우 카운트 에 대해 총 1000 + 782 = 1,782 비트에 대해 1000 개의 1과 782 0으로 끝납니다 .

오프셋 :
마지막으로 각 숫자의 형식은 각 그룹에 대한 7 비트 오프셋입니다. 예를 들어 00000과 00001이 0-127 그룹의 유일한 숫자 인 경우 해당 그룹의 비트는입니다 110 0000000 0000001. 1,000 개의 숫자를 가정하면 오프셋에 7,000 비트 가 있습니다 .

따라서 1,000 개의 숫자를 가정 한 최종 개수는 다음과 같습니다.

17 (prefix) + 1,782 (counts) + 7,000 (offsets) = 8,799 bits = 1100 bytes

이제 128 비트로 반올림하여 그룹 크기 선택이 그룹 크기에 가장 적합한 선택인지 확인하겠습니다. x각 그룹을 나타내는 비트 수를 선택하면 크기에 대한 공식은 다음과 같습니다.

Size in bits = 17 (prefix) + 1,000 + 99,999/2^x + x * 1,000

정수 값에 대한이 식을 최소화하는 것은 x제공 x=68,580 비트 = 수득되는, 1,073 바이트 . 따라서 이상적인 스토리지는 다음과 같습니다.

  • 그룹 규모 : 2 ^ 6 = 64
  • 그룹 수 : 1,562
  • 총 저장 용량 :

    1017 (prefix plus 1's) + 1563 (0's in count) + 6*1000 (offsets) = 8,580 bits = 1,073 bytes


1

이것을 순전히 이론적 인 문제로 받아들이고 구현을 제쳐두고 가장 효율적인 방법은 거대한 인덱싱 테이블에서 가능한 모든 마지막 자릿수 10000 세트를 인덱싱하는 것입니다. 정확히 1000 개의 숫자가 있다고 가정하면 현재 세트를 고유하게 식별하려면 8000 비트보다 약간 더 많은 비트가 필요합니다. 더 큰 압축은 가능하지 않습니다. 그러면 동일한 상태로 식별되는 두 세트가 있기 때문입니다.

이것의 문제는 프로그램의 각 2 ^ 8000 세트를 lut로 표현해야한다는 것입니다. 그리고 구글조차도 이것을 원격으로 할 수 없습니다.

조회는 O (1)이고 모든 숫자 O (n)을 인쇄합니다. 삽입은 이론상 O (1) 인 O (2 ^ 8000)이지만 실제로는 사용할 수 없습니다.

인터뷰에서 나는 회사가 틀에서 벗어난 생각을 많이 할 수있는 사람을 찾고 있다고 확신하는 경우에만이 대답을 줄 것입니다. 그렇지 않으면 현실 세계에 대한 우려가없는 이론가처럼 보일 수 있습니다.

편집 : 좋아, 여기에 하나의 "구현"이 있습니다.

구현을 구성하는 단계 :

  1. 100,000 * (1000은 100000을 선택) 비트 크기의 상수 배열을 가져옵니다. 예, 저는이 배열이 우주의 원자보다 몇 가지 크기로 더 많은 공간을 필요로한다는 사실을 알고 있습니다.
  2. 이 큰 배열을 각각 100,000 개의 청크로 분리합니다.
  3. 각 청크에는 마지막 다섯 자리의 특정 조합에 대한 비트 배열이 저장됩니다.

이것은 프로그램이 아니라 일종의 메타 프로그램으로, 이제 프로그램에서 사용할 수있는 거대한 LUT를 구성합니다. 프로그램의 상수는 공간 효율성을 계산할 때 일반적으로 계산되지 않으므로 최종 계산을 수행 할 때이 배열에 대해 신경 쓰지 않습니다.

이 LUT를 사용하는 방법은 다음과 같습니다.

  1. 누군가 1000 개의 숫자를 주면 처음 다섯 자리를 따로 저장합니다.
  2. 이 세트와 일치하는 배열의 청크를 찾으십시오.
  3. 세트 번호를 단일 8074 비트 번호로 저장합니다 (이를 c라고 함).

이것은 저장을 위해 우리가 여기서 최적의 인코딩으로 입증 된 8091 비트 만 필요함을 의미합니다. 그러나 올바른 청크를 찾는 데에는 O (100 000 * (100 000 choose 1000))가 필요합니다. 수학 규칙에 따르면 O (1)이지만 실제로는 항상 우주의 시간보다 오래 걸립니다.

하지만 조회는 간단합니다.

  1. 처음 다섯 자리의 스트립 (나머지 번호는 n '이라고 함).
  2. 일치하는지 테스트
  3. i = c * 100000 + n '계산
  4. LUT의 i에서 비트가 1로 설정되어 있는지 확인하십시오.

모든 숫자를 인쇄하는 것도 간단합니다 (실제로 O (100000) = O (1)를 사용합니다. 항상 현재 청크의 모든 비트를 확인해야하기 때문에 위에서 잘못 계산했습니다).

나는 이것을 "구현"이라고 부르지 않을 것이다. 왜냐하면 한계 (우주의 크기와이 우주가 살았거나 지구가 존재할 시간)를 노골적으로 무시하기 때문이다. 그러나 이론적으로 이것은 최적의 솔루션입니다. 작은 문제의 경우 실제로 수행 할 수 있으며 때로는 수행 될 수 있습니다. 예를 들어 정렬 네트워크 는 이러한 코딩 방식의 예이며 재귀 정렬 알고리즘의 마지막 단계로 사용하여 속도를 크게 높일 수 있습니다.


1
이를 수행 하는 가장 효율적인 공간 절약 방법 은 무엇입니까 ?
Sven

1
런타임 공간을 계산할 때 하나의 숫자로만 시스템의 가능한 상태를 열거하기 때문에 이것이 가장 효율적인 공간 절약 방법임을 쉽게 입증 할 수 있습니다. 이 문제에는 더 작은 인코딩이있을 수 없습니다. 이 대답의 비결은 계산을 할 때 프로그램 크기가 거의 고려되지 않는다는 것입니다 (이를 고려한 대답을 찾으면 내가 의미하는 바를 알 수 있습니다). 따라서 크기 제한이있는 문제에 대해 항상 모든 상태를 열거하여이를 처리하는 가장 공간 절약 방법을 얻을 수 있습니다.
LiKao 2011 년

1

이는 각각 100,000보다 작은 음이 아닌 정수 1,000 개를 저장하는 것과 같습니다. 이를 위해 산술 인코딩과 같은 것을 사용할 수 있습니다.

궁극적으로 숫자는 정렬 된 목록에 저장됩니다. 목록에서 인접한 숫자 간의 예상 차이는 100,000 / 1000 = 100이며 7 비트로 표현할 수 있습니다. 7 비트 이상이 필요한 경우도 많이 있습니다. 이러한 덜 일반적인 경우를 나타내는 간단한 방법은 첫 번째 비트가 설정되지 않은 경우 한 바이트가 7 비트 정수를 나타내는 utf-8 체계를 채택하는 것입니다. 최초의 비트가있는 경우에는 다음 바이트가 21 비트 정수를 나타내는 판독되어 설정된다.

따라서 연속적인 정수 간의 차이의 절반 이상 을 1 바이트로 나타낼 수 있으며 나머지는 거의 모두 2 바이트가 필요합니다. 16,384보다 큰 차이로 구분 된 몇 개의 숫자에는 3 바이트가 필요하지만이 중 61 바이트를 초과 할 수 없습니다. 그러면 평균 저장 공간은 숫자 당 약 12 ​​비트, 또는 약간 적거나 최대 1500 바이트입니다.

이 접근법의 단점은 숫자의 존재를 확인하는 것이 이제 O (n)이라는 것입니다. 그러나 시간 복잡성 요구 사항은 지정되지 않았습니다.

글을 쓴 후 ruslik이 이미 위의 차이 방법을 제안했음을 알았습니다. 유일한 차이점은 인코딩 체계입니다. 광산은 더 간단하지만 효율성이 떨어집니다.


1

숫자를 기본 36으로 변경하고 싶지 않은 이유를 신속하게 물어보십시오. 공간을 많이 절약하지는 않지만 10digts보다 훨씬 적게 검색하므로 검색 시간을 확실히 절약 할 수 있습니다. 또는 각 그룹에 따라 파일로 분할합니다. 그래서 파일 이름을 (111) -222.txt로 지정한 다음 해당 그룹에 맞는 숫자 만 저장 한 다음 숫자 순서로 볼 수 있도록합니다. 이렇게하면 파일이 종료되는지 항상 확인할 수 있습니다. 더 큰 검색을 실행하기 전에. 또는 정확하려면 파일이 종료되는지 확인하기 위해 이진 검색을 실행합니다. 파일의 내용에 대한 또 다른 검색


0

단순하게 유지하지 않는 이유는 무엇입니까? 구조체 배열을 사용합니다.

따라서 처음 5 자리를 상수로 저장할 수 있으므로 지금은 잊어 버리십시오.

65535는 16 비트 숫자에 저장할 수있는 최대 숫자이고, 우리가 가질 수있는 최대 숫자는 99999이며, 이는 최대 131071의 17 번째 비트 숫자에 적합합니다.

추가 16 비트 중 1 비트 만 필요하기 때문에 32 비트 데이터 유형을 사용하는 것은 낭비입니다. 따라서 부울 (또는 문자)과 16 비트 숫자가있는 구조를 정의 할 수 있습니다.

C / C ++ 가정

typedef struct _number {

    uint16_t number;
    bool overflow;
}Number;

이 구조체는 3 바이트 만 차지하며 1000 개의 배열이 필요하므로 총 3000 바이트가 필요합니다. 총 공간을 25 % 줄였습니다!

숫자를 저장하는 한 간단한 비트 수학을 할 수 있습니다.

overflow = (number5digits & 0x10000) >> 4;
number = number5digits & 0x1111;

그리고 그 반대

//Something like this should work
number5digits = number | (overflow << 4);

모두 인쇄하기 위해 배열에 대한 간단한 루프를 사용할 수 있습니다. 특정 숫자를 검색하는 것은 물론 배열이기 때문에 일정한 시간에 발생합니다.

for(int i=0;i<1000;i++) cout << const5digits << number5digits << endl;

숫자를 검색하려면 정렬 된 배열이 필요합니다. 따라서 숫자가 저장되면 배열을 정렬합니다 (개인적으로 병합 정렬을 선택합니다. O (nlogn)). 이제 검색하려면 병합 정렬 방식을 사용합니다. 배열을 분할하고 어떤 숫자가 사이에 있는지 확인하십시오. 그런 다음 해당 배열에서만 함수를 호출하십시오. 일치 항목이 있고 인덱스를 반환 할 때까지 재귀 적으로 수행합니다. 그렇지 않으면 존재하지 않고 오류 코드를 인쇄합니다. 이 검색은 매우 빠르며, 최악의 경우는 병합 정렬보다 더 적은 시간에 절대적으로 실행되기 때문에 O (nlogn)보다 낫습니다 (양쪽 대신 매번 분할의 한쪽 만 반복 :)). O (nlogn)입니다.


0

내 솔루션 : 최상의 경우 7.025 비트 / 숫자, 최악의 경우 14.193 비트 / 숫자, 대략적인 평균 8.551 비트 / 숫자. 스트림 인코딩, 임의 액세스 없음.

ruslik의 답변을 읽기 전에도 각 숫자의 차이를 인코딩하는 것을 즉시 생각했습니다. 작은 숫자이고 상대적으로 일관성이 있어야하기 때문입니다.하지만 솔루션은 최악의 시나리오도 수용 할 수 있어야합니다. 1000 개의 숫자 만 포함 된 100000 개의 숫자 공간이 있습니다. 완벽하게 균일 한 전화 번호부에서 각 번호는 이전 번호보다 100 씩 커집니다.

55555-12 3 45
55555-12 4 45
55555-12 5 45

이 경우 알려진 상수이기 때문에 숫자 간의 차이를 인코딩하기 위해 저장 공간이 필요하지 않습니다. 안타깝게도 숫자는 이상적인 단계 인 100과 다를 수 있습니다. 이상적인 증분 100과의 차이를 인코딩하여 두 개의 인접한 숫자가 103만큼 다르면 숫자 3을 인코딩하고 두 개의 인접한 숫자가 92만큼 다르면 I -8을 인코딩합니다. 이상적인 증분 인 100의 델타를“ 분산 ” 이라고 부릅니다 .

분산의 범위는 -99 (즉, 두 개의 연속 된 숫자)에서 99000 (전체 전화 번호부는 숫자 00000… 00999 및 추가적으로 가장 먼 번호 99999로 구성됨) 범위이며 99100 개의 가능한 값 범위입니다.

나는 (같은 더 큰 차이가 발생하면 가장 일반적인 차이를 인코딩 및 스토리지를 확장 할 수있는 최소한의 스토리지를 할당하는 것을 목표 것 ProtoBufvarint). 저는 7 비트 청크, 저장을 위해 6 개, 마지막에 추가 플래그 비트를 사용하여이 분산이 현재 청크 이후에 추가 청크와 함께 최대 3 개의 청크 (최대 3 * 6 = 가능한 분산 수 (99100)보다 많은 262144 개의 가능한 값인 18 비트 저장 공간. 올린 플래그 뒤에 오는 각 추가 청크에는 더 높은 의미의 비트가 있으므로 첫 번째 청크에는 항상 비트 0- 도 5에서, 선택적 두 번째 청크에는 비트 6-11이 있고 선택적 세 번째 청크에는 비트 12-17이 있습니다.

단일 청크는 64 개의 값을 수용 할 수있는 6 비트 스토리지를 제공합니다. 단일 청크 (즉, -32에서 +31의 분산)에 맞게 64 개의 가장 작은 분산을 매핑하고 싶습니다. 따라서 ProtoBuf ZigZag 인코딩을 사용하여 최대 -99에서 +98까지 분산 할 것입니다 (필요가 없기 때문에 -99를 초과하는 음의 편차),이 시점에서 98만큼 오프셋 된 일반 인코딩으로 전환합니다.  

차이 | 인코딩 된 값
----------- + ----------------
    0 | 0
   -1 | 1
    1 | 2
   -2 | 삼
    2 | 4
   -3 | 5
    3 | 6
   ... | ...
  -31 | 61
   31 | 62
  -32 | 63
----------- | --------------- 6 비트
   32 | 64
  -33 | 65
   33 | 66
   ... | ...
  -98 | 195
   98 | 196
  -99 | 197
----------- | --------------- ZigZag 끝
   100 | 198
   101 | 199
   ... | ...
  3996 | 4094
  3997 | 4095
----------- | --------------- 12 비트
  3998 | 4096
  3999 | 4097
   ... | ...
 262045 | 262143
----------- | --------------- 18 비트

추가 청크를 나타내는 플래그를 포함하여 분산이 비트로 인코딩되는 방법에 대한 몇 가지 예 :

차이 | 인코딩 된 비트
----------- + ----------------
     0 | 000000 0
     5 | 001010 0
    -8 | 001111 0
   -32 | 111111 0
    32 | 000000 1 000001 0
   -99 | 000101 1 0000110
   177 | 010011 1 000100 0
 14444 | 001110 1 100011 1 0000110

따라서 샘플 전화 번호부의 처음 세 숫자는 다음과 같이 비트 스트림으로 인코딩됩니다.

BIN 000101001011001000100110010000011001 000110 1 010110 1 000010
PH # 55555-12345 55555-12448 55555-12491
POS 1 2 3

가장 좋은 시나리오 는 전화 번호부가 다소 균일하게 분포되어 있고 분산이 32보다 큰 두 개의 전화 번호가 없기 때문에 번호 당 7 비트에 시작 번호로 32 비트를 사용하여 총 32 + 7 * 999가됩니다. = 7025 비트 . 800 개의 전화 번호 차이가 하나의 청크 (800 * 7 = 5600)에 들어가고 180 개의 번호가 각각 두 개의 청크 (180 * 2 * 7 = 2520)에 들어가고 19 개의 번호가 각각 3 개의 청크에 들어가는
혼합 시나리오 (20 * 3 * 7 = 399)이고 초기 32 비트를 더하면8551 비트가 됩니다.
최악의 경우 25 개의 숫자는 3 개의 청크 (25 * 3 * 7 = 525 비트)에 적합하고 나머지 974 개의 숫자는 2 개의 청크 (974 * 2 * 7 = 13636 비트)에 적합합니다. 총14193 비트 .

   인코딩 된 숫자의 양 |
 1 청크 | 2- 청크 | 3 청크 | 총 비트
--------- + ---------- + ---------- + ------------
   999 | 0 | 0 | 7025
   800 | 180 | 19 | 8551
    0 | 974 | 25 | 14193

필요한 공간을 더욱 줄이기 위해 수행 할 수있는 네 가지 추가 최적화를 볼 수 있습니다.

  1. 세 번째 청크에는 전체 7 비트가 필요하지 않으며 플래그 비트없이 5 비트 만 가능합니다.
  2. 각 청크에 가장 적합한 크기를 계산하기 위해 숫자의 초기 단계가있을 수 있습니다. 특정 전화 번호부의 경우 첫 번째 청크에 5 + 1 비트, 두 번째 7 + 1 및 세 번째 5 + 1 비트를 사용하는 것이 가장 좋습니다. 그러면 크기가 최소 6 * 999 + 32 = 6026 비트로 줄어들고, 총 3 개의 비트로 구성된 2 세트가 추가되어 청크 1과 2의 크기를 저장합니다 (청크 3의 크기는 필요한 16 비트의 나머지입니다). 6032 비트의!
  3. 동일한 초기 패스가 기본 100보다 더 나은 예상 증분을 계산할 수 있습니다. 55555-50000에서 시작하는 전화 번호부가있을 수 있으므로 번호 범위가 절반이므로 예상 증분은 50이어야합니다. 또는 비선형 일 수도 있습니다. 분포 (표준 편차) 및 기타 최적의 예상 증분을 사용할 수 있습니다. 이렇게하면 일반적인 분산이 줄어들고 더 작은 첫 번째 청크를 사용할 수 있습니다.
  4. 첫 번째 단계에서 추가 분석을 수행하여 전화 번호부를 분할 할 수 있으며 각 파티션은 자체 예상 증분 및 청크 크기 최적화를 갖습니다. 이렇게하면 전화 번호부의 매우 균일 한 특정 부분에 대해 더 작은 첫 번째 청크 크기 (소비되는 비트 수 감소)를 허용하고 균일하지 않은 부분에 대해 더 큰 청크 크기 (연속 플래그에 낭비되는 비트 수 감소)를 허용합니다.

0

진짜 문제는 5 자리 전화 번호를 저장하는 것입니다.

트릭은 0..99,999의 숫자 범위를 저장하려면 17 비트가 필요하다는 것입니다. 그러나 기존의 8 바이트 워드 경계에 17 비트를 저장하는 것은 번거 롭습니다. 이것이 32 비트 정수를 사용하지 않고 4k 미만으로 할 수 있는지 묻는 이유입니다.

질문 : 모든 숫자 조합이 가능합니까?

전화 시스템의 특성으로 인해 가능한 조합이 65k 미만일 수 있습니다. 지역 번호 또는 교환 번호와는 반대로 전화 번호의 후자 5 개 위치에 대해 이야기하고 있기 때문에 그렇다고 가정 하겠습니다.

질문 :이 목록은 정적입니까, 아니면 업데이트를 지원해야합니까?

이 경우 정적 , 그것은 숫자의 수 <50,000 자리의 수> = 50,000, 데이터베이스를 채우는 계산 시간을 때. 할당 이 개 배열uint1650,000 아래의 정수의 하나는 높은 세트 하나를 적절한 길이를. 상위 배열에 정수를 저장할 때 50,000을 빼고 해당 배열에서 정수를 읽을 때 50,000을 더합니다. 이제 1,000 개의 정수를 2,000 개의 8 바이트 단어로 저장했습니다.

전화 번호부를 작성하려면 두 번의 입력 순회가 필요하지만 조회는 단일 어레이를 사용할 때보 다 평균적으로 절반의 시간에 이루어집니다. 조회 시간이 매우 중요하다면 더 작은 범위에 더 많은 어레이를 사용할 수 있지만 이러한 크기에서 성능 한계는 메모리에서 어레이를 가져오고 2k는 아마도 CPU 캐시에 공간을 등록하지 않으면 CPU 캐시에 숨겨 질 것이라고 생각합니다. 일.

동적 인 경우 1000 정도의 배열 하나를 할당 uint16하고 정렬 된 순서로 숫자를 추가합니다. 첫 번째 바이트를 50,001로 설정하고 두 번째 바이트를 NULL 또는 65,000과 같은 적절한 null 값으로 설정합니다. 번호를 저장할 때 정렬 된 순서로 저장하십시오. 숫자가 50,001 미만 이면 50,001 마커 앞에 저장합니다 . 숫자가 50,001 이상 이면 50,001 마커 뒤에 저장 하되 저장된 값에서 50,000을 뺍니다.

배열은 다음과 같습니다.

00001 = 00001
12345 = 12345
50001 = reserved
00001 = 50001
12345 = 62345
65000 = end-of-list

따라서 전화 번호부에서 번호를 찾을 때 배열을 순회하고 50,001 값에 도달하면 배열 값에 50,000을 더하기 시작합니다.

이로 인해 삽입은 매우 비싸지 만 조회가 쉽고 스토리지에 2k 이상을 소비하지 않을 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.