Cách hiệu quả nhất để lưu trữ hàng nghìn số điện thoại


93

Đây là một câu hỏi phỏng vấn của google:

Có khoảng hàng nghìn số điện thoại được lưu trữ, mỗi số có 10 chữ số. Bạn có thể giả sử 5 chữ số đầu tiên của mỗi chữ số giống nhau trên hàng nghìn số. Bạn phải thực hiện các thao tác sau: a. Tìm kiếm nếu một số nhất định tồn tại. b. In tất cả số

Cách tiết kiệm không gian hiệu quả nhất để làm điều này là gì?

Tôi đã trả lời bảng băm và sau đó viết mã huffman nhưng người phỏng vấn của tôi nói rằng tôi đã không đi đúng hướng. Xin hãy giúp tôi ở đây.

Việc sử dụng trie hậu tố có thể giúp được gì không?

Lý tưởng nhất là lưu trữ 1000 số cần 4 byte mỗi số, vì vậy, tổng cộng sẽ mất 4000 byte để lưu trữ 1000 số. Về mặt định lượng, tôi muốn giảm dung lượng lưu trữ xuống <4000 byte, đây là những gì người phỏng vấn của tôi giải thích cho tôi.


28
Tôi xin trả lời rằng sử dụng cơ sở dữ liệu bình thường, bạn có thể lưu trữ chúng dưới dạng văn bản, thậm chí hàng nghìn / hàng triệu và thao tác tra cứu sẽ vẫn rất nhanh. Tôi sẽ khuyên bạn không nên làm những điều "thông minh" vì toàn bộ hệ thống sẽ phải được làm lại nếu họ muốn trong tương lai hỗ trợ các số điện thoại quốc tế hoặc nếu các số điện thoại bắt đầu bằng "0" bắt đầu xuất hiện hoặc nếu chính phủ quyết định thay đổi định dạng số điện thoại, v.v.
Thomas Bonini

1
@AndreasBonini: Tôi có thể sẽ đưa ra câu trả lời đó, trừ khi tôi đang phỏng vấn tại một công ty như Google hoặc Facebook, các giải pháp không có lợi chỉ không cắt nó. Mặc dù ví dụ như postgres cũng đã thử, nhưng tôi sẽ không chắc rằng những điều này sẽ cắt giảm thông lượng dữ liệu mà google cần thực hiện.
LiKao

1
@LiKao: hãy nhớ rằng OP đã tuyên bố cụ thể "khoảng một nghìn con số"
Thomas Bonini

@AndreasBonini: Đúng, cũng có thể là một bài kiểm tra, rằng người được phỏng vấn biết diễn giải các ràng buộc như vậy một cách chính xác và chọn giải pháp tốt nhất theo điều này.
LiKao

4
"hiệu quả" trong câu hỏi này thực sự cần được xác định - hiệu quả theo những cách nào? không gian, thời gian, cả hai?
matt b

Câu trả lời:


36

Đây là một cải tiến cho câu trả lời của aix . Cân nhắc sử dụng ba "lớp" cho cấu trúc dữ liệu: lớp đầu tiên là hằng số cho năm chữ số đầu tiên (17 bit); như vậy kể từ đây, mỗi số điện thoại chỉ còn lại năm chữ số. Chúng tôi xem năm chữ số còn lại này là số nguyên nhị phân 17 bit và lưu trữ k trong số các bit đó bằng một phương pháp và 17 - k = m bằng một phương pháp khác, xác định k ở cuối để giảm thiểu không gian cần thiết.

Đầu tiên, chúng tôi sắp xếp các số điện thoại (tất cả giảm xuống còn 5 chữ số thập phân). Sau đó ta đếm xem có bao nhiêu số điện thoại mà số nhị phân gồm m bit đầu tiên đều là 0, có bao nhiêu số điện thoại mà m bit đầu tiên có nhiều nhất là 0 ... 01, có bao nhiêu số điện thoại m đầu tiên bit nhiều nhất là 0 ... 10, vân vân, lên đến số lượng số điện thoại mà m bit đầu tiên là 1 ... 11 - số cuối cùng này là 1000 (thập phân). Có 2 ^ m số đếm như vậy và mỗi số đếm nhiều nhất là 1000. Nếu chúng tôi bỏ qua số cuối cùng (vì chúng tôi biết nó là 1000), chúng tôi có thể lưu trữ tất cả các số này trong một khối liền kề của (2 ^ m - 1) * 10 bit. (10 bit là đủ để lưu trữ một số nhỏ hơn 1024.)

K bit cuối cùng của tất cả các số điện thoại (đã giảm) được lưu trữ liên tục trong bộ nhớ; vì vậy, nếu k là 7, thì 7 bit đầu tiên của khối bộ nhớ này (bit 0 đến 6) tương ứng với 7 bit cuối cùng của số điện thoại đầu tiên (giảm), các bit từ 7 đến 13 tương ứng với 7 bit cuối cùng của số điện thoại thứ hai (giảm), vân vân. Điều này yêu cầu 1000 * k bit cho tổng số 17 + (2 ^ (17 - k ) - 1) * 10 + 1000 * k , đạt tối thiểu 11287 cho k = 10. Vì vậy, chúng tôi có thể lưu trữ tất cả các số điện thoại trong ceil ( 11287/8) = 1411 byte.

Không gian bổ sung có thể được tiết kiệm bằng cách quan sát rằng không có số nào của chúng ta có thể bắt đầu bằng ví dụ 1111111 (nhị phân), bởi vì số thấp nhất bắt đầu bằng số đó là 130048 và chúng ta chỉ có năm chữ số thập phân. Điều này cho phép chúng tôi loại bỏ một vài mục nhập khỏi khối bộ nhớ đầu tiên: thay vì 2 ^ m - 1 số đếm, chúng tôi chỉ cần ceil (99999/2 ^ k ). Điều đó có nghĩa là công thức trở thành

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

mà đáng kinh ngạc là đạt được 10997 tối thiểu cho cả k = 9 và k = 10, hoặc ceil (10997/8) = 1375 byte.

Nếu chúng ta muốn biết liệu một số điện thoại nhất định có trong bộ của chúng ta hay không, trước tiên chúng ta kiểm tra xem năm chữ số nhị phân đầu tiên có khớp với năm chữ số mà chúng ta đã lưu trữ hay không. Sau đó, chúng tôi chia năm chữ số còn lại thành m = 7 bit trên cùng của nó (có nghĩa là, m -bit số M ) và k = 10 bit dưới của nó (số K ). Bây giờ chúng ta tìm số a [M-1] của các số điện thoại bị giảm mà m chữ số đầu tiên có nhiều nhất là M - 1 và số a [M] của các số điện thoại bị giảm mà m chữ số đầu tiên có nhiều nhất là M , cả từ khối bit đầu tiên. Bây giờ chúng ta kiểm tra giữa một[M-1] thứ và một [M] thứ tự của k bit trong khối thứ hai của bộ nhớ để xem liệu chúng ta thấy K ; trong trường hợp xấu nhất có 1000 chuỗi như vậy, vì vậy nếu chúng ta sử dụng tìm kiếm nhị phân, chúng ta có thể hoàn thành trong các phép toán O (log 1000).

Giả để in tất cả 1000 số sau, nơi tôi truy cập vào K 'th k -bit xâm nhập của khối đầu tiên của bộ nhớ như một [K] và M ' th m nhập -bit của khối thứ hai của bộ nhớ như b [M] (cả hai điều này sẽ yêu cầu một vài thao tác bit tẻ nhạt để viết ra). Năm chữ số đầu tiên có trong số 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;

Có thể xảy ra sự cố với trường hợp ranh giới cho K = ceil (99999/2 ^ k ), nhưng điều đó đủ dễ dàng để sửa chữa.

Cuối cùng, theo quan điểm entropy, không thể lưu trữ một tập con gồm 10 ^ 3 số nguyên dương, tất cả đều nhỏ hơn 10 ^ 5 với ít hơn ceil (log [2] (nhị thức (10 ^ 5, 10 ^ 3)) ) = 8073. Kể cả 17 ta cần có 5 chữ số đầu tiên vẫn còn khoảng trống là 10997 - 8090 = 2907 bit. Đó là một thách thức thú vị để xem liệu có giải pháp nào tốt hơn mà bạn vẫn có thể truy cập các con số tương đối hiệu quả!


4
Cơ cấu dữ liệu mà bạn đang mô tả ở đây thực ra chỉ là một phiên bản trie rất hiệu quả, chỉ sử dụng ít khi cần thiết cho việc lập chỉ mục và chỉ có hai cấp độ. Trên thực tế, sẽ rất hay nếu điều này có thể đánh bại một bộ ba với nhiều cấp độ hơn, nhưng tôi nghĩ rằng điều này phụ thuộc rất nhiều vào sự phân bố của các con số (Trong thực tế, các số điện thoại thực không hoàn toàn ngẫu nhiên mà chỉ gần như vậy).
LiKao

Xin chào Erik, vì bạn đã nói rằng bạn muốn xem các giải pháp thay thế khác, hãy xem giải pháp của tôi. Nó giải quyết nó trong 8.580 bit, chỉ là 490 bit so với lý thuyết tối thiểu. Sẽ hơi kém hiệu quả khi tra cứu các số riêng lẻ, nhưng dung lượng lưu trữ rất nhỏ gọn.
Briguy37

1
Tôi cho rằng một người phỏng vấn lành mạnh sẽ thích câu trả lời "một trie" thay vì "một cơ sở dữ liệu phức tạp được tạo tùy chỉnh". Nếu bạn muốn thể hiện kỹ năng hack 133t của mình, bạn có thể thêm - "có thể tạo một thuật toán cây cụ thể cho trường hợp đặc biệt này, nếu cần thiết."
KarlP

Xin chào, Bạn có thể vui lòng giải thích cách 5 chữ số có 17 bit được lưu trữ không?
Tushar Banne

@tushar Năm chữ số mã hóa một số từ 00000 đến 99999. Biểu diễn số đó dưới dạng nhị phân. 2 ^ 17 = 131072, vì vậy 17 bit là đủ cho điều đó nhưng 16 thì không.
Erik P.

43

Trong những gì sau đây, tôi coi các số là các biến số nguyên (trái ngược với chuỗi):

  1. Sắp xếp các số.
  2. Chia mỗi số thành năm chữ số đầu tiên và năm chữ số cuối cùng.
  3. Năm chữ số đầu tiên giống nhau trên các số, vì vậy hãy lưu trữ chúng chỉ một lần. Điều này sẽ yêu cầu 17 bit bộ nhớ.
  4. Lưu trữ năm chữ số cuối cùng của mỗi số riêng lẻ. Điều này sẽ yêu cầu 17 bit cho mỗi số.

Tóm lại: 17 bit đầu tiên là tiền tố chung, 1000 nhóm 17 bit tiếp theo là năm chữ số cuối cùng của mỗi số được lưu trữ theo thứ tự tăng dần.

Tổng cộng chúng tôi đang xem xét 2128 byte cho 1000 số hoặc 17,017 bit cho mỗi số điện thoại 10 chữ số.

Tìm kiếm là O(log n)(tìm kiếm nhị phân) và liệt kê đầy đủ là O(n).


Uhm, không gian phức tạp ở đâu?
aioobe

Quá nhiều thời gian để xây dựng (O (log (n) * n k) (k là độ dài) để sắp xếp, so với O (n k) để xây dựng một trie). Ngoài ra, không gian là xa tối ưu, vì các tiền tố chung dài hơn được lưu trữ riêng lẻ. Thời gian tìm kiếm cũng không được tối ưu. Đối với dữ liệu chuỗi như thế này, rất dễ quên độ dài của các con số, điều này chiếm ưu thế trong việc tìm kiếm. Tức là tìm kiếm nhị phân là O (log (n) * k), trong khi một trie chỉ cần O (k). Bạn có thể giảm biểu thức luận đề, khi k là hằng số, nhưng điều này là để chỉ ra một vấn đề chung khi lý luận về cấu trúc dữ liệu lưu trữ chuỗi.
LiKao

@LiKao: Ai nói gì về dây? Tôi đang xử lý riêng với các biến số nguyên vì vậy kkhông liên quan.
NPE

1
Được rồi, tôi đã đọc nhầm câu trả lời. Tuy nhiên, các bộ phận chung không được lưu trữ cùng nhau, do đó, điểm về hiệu quả không gian vẫn còn. Đối với 1000 số có 5 chữ số, sẽ có một lượng lớn các tiền tố phổ biến, vì vậy việc giảm các tiền tố này sẽ giúp ích rất nhiều. Cũng trong trường hợp số, chúng ta có O (log (n)) so với O (k) cho chuỗi, vẫn nhanh hơn.
LiKao

1
@Geek: 1001 nhóm 17 bit là 17017 bit hoặc 2128 byte (với một số thay đổi).
NPE

22

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

Tôi đã từng có một cuộc phỏng vấn nơi họ hỏi về cấu trúc dữ liệu. Tôi quên "Array".


1
+1 đó chắc chắn là con đường để đi. Tôi đã học cái này dưới một cái tên khác, Cây thư viện hoặc cây tìm kiếm từ vựng hoặc một cái gì đó khi tôi còn là sinh viên (nếu ai đó nhớ tên cũ đó, xin vui lòng cho biết).
Valmond

6
Điều này không đáp ứng yêu cầu 4000 byte. Đối với riêng lưu trữ con trỏ, trường hợp xấu nhất là bạn cần 1 con trỏ cho các lá thứ 1-4 ở cấp độ tiếp theo, 10 con trỏ cho các lá thứ 5, 100 cho các cấp độ thứ 6 và 1000 cho các cấp độ thứ 7, 8 và 9 , nâng tổng số con trỏ của chúng tôi lên 3114. Điều đó cung cấp ít nhất 3114 vị trí bộ nhớ riêng biệt cần thiết cho con trỏ trỏ tới, có nghĩa là bạn cần ít nhất 12 bit cho mỗi con trỏ. 12 * 3114 = 37368 bit = 4671 byte> 4000 byte và điều đó thậm chí không thể hiện được cách bạn biểu diễn giá trị của mỗi lá!
Briguy37

16

Tôi có thể muốn xem xét việc sử dụng một số phiên bản nén của một Trie (có thể là một Dawg theo đề nghị của @Misha).

Điều đó sẽ tự động tận dụng lợi thế của thực tế là tất cả chúng đều có một tiền tố chung.

Tìm kiếm sẽ được thực hiện trong thời gian cố định và in sẽ được thực hiện theo thời gian tuyến tính.


Câu hỏi là về cách tiết kiệm không gian nhất để lưu trữ dữ liệu. Bạn có vui lòng cung cấp một số ước tính về dung lượng mà phương pháp này sẽ yêu cầu cho 1000 số điện thoại không? Cảm ơn.
NPE

Không gian cho trie nhiều nhất là O (n * k) với n là số chuỗi và k là độ dài của mỗi chuỗi. Có tính đến việc bạn không cần các ký tự 8 bit để đại diện cho các số, tôi khuyên bạn nên lưu trữ 4 chỉ số thập lục phân hexadeximal và một cho bit còn lại. Theo cách này, bạn cần tối đa 17 bit cho mỗi số. Bởi vì trong mọi trường hợp, bạn sẽ có xung đột ở tất cả các cấp độ với mã hóa này, bạn thực sự có thể nhận được dưới đây. Kỳ vọng rằng chúng tôi lưu trữ 1000 số, chúng tôi đã có thể tiết kiệm tổng cộng 250 bit cho các cuộc đụng độ ở cấp độ đầu tiên. Tốt nhất hãy kiểm tra mã hóa chính xác trên dữ liệu ví dụ.
LiKao

@LiKao, phải, và bằng cách lưu ý rằng, ví dụ: 1000 số không thể có hơn 100 hai chữ số cuối khác nhau, bộ ba có thể bị thu gọn đáng kể ở các cấp cuối cùng.
aioobe

@aioobe: Lá có thể bị xẹp ở tầng cuối vì không có con. Tuy nhiên, các lá ở cấp độ thứ hai đến cấp độ cuối cùng cần có 2 ^ 10 = 1024 trạng thái (mỗi chữ số cuối cùng có thể là bật hoặc tắt), vì vậy nó không thể giảm được trong trường hợp này vì chỉ có 1000 số. Điều này có nghĩa là số lượng con trỏ trong trường hợp xấu nhất vẫn ở mức 3114 (xem nhận xét của tôi về câu trả lời của Misha) trong khi các lá cần thiết là 5 + 10 + 100 + 1000 + 1000 + 10 = 2125, không thay đổi 12 byte cần thiết cho mỗi lá con trỏ. Do đó, điều này vẫn đặt một giải pháp trie ở 4671 byte chỉ xét riêng con trỏ.
Briguy37

@ Briguy37, không chắc tôi hiểu đối số " mỗi chữ số cuối cùng có thể bật hoặc tắt " của bạn. Tất cả các số đều có 10 chữ số phải không?
aioobe

15

Tôi đã nghe nói về vấn đề này trước đây (nhưng không có giả định 5 chữ số đầu tiên giống nhau) và cách đơn giản nhất để làm điều đó là Rice Coding :

1) Vì thứ tự không quan trọng nên chúng ta có thể sắp xếp chúng và chỉ lưu sự khác biệt giữa các giá trị liên tiếp. Trong trường hợp của chúng tôi, chênh lệch trung bình sẽ là 100.000 / 1000 = 100

2) Mã hóa sự khác biệt bằng mã Rice (cơ sở 128 hoặc 64) hoặc thậm chí mã Golomb (cơ sở 100).

CHỈNH SỬA: Một ước tính cho mã hóa Rice với cơ số 128 (không phải vì nó sẽ cho kết quả tốt nhất, mà vì nó dễ tính hơn):

Chúng tôi sẽ lưu giá trị đầu tiên nguyên trạng (32 bit).
Phần còn lại của 999 giá trị là sự khác biệt (chúng tôi mong đợi chúng nhỏ, trung bình là 100) sẽ chứa:

giá trị đơn phân value / 128(số bit thay đổi + 1 bit làm dấu chấm hết)
giá trị nhị phân cho value % 128(7 bit)

Chúng ta phải ước lượng bằng cách nào đó các giới hạn (hãy gọi nó VBL) cho số lượng bit biến đổi:
giới hạn dưới: coi như chúng ta may mắn và không có sự khác biệt nào lớn hơn cơ sở của chúng ta (128 trong trường hợp này). điều này có nghĩa là cung cấp 0 bit bổ sung.
giới hạn cao: vì tất cả các khác biệt nhỏ hơn cơ sở sẽ được mã hóa ở phần nhị phân của số, nên số tối đa mà chúng tôi cần mã hóa theo đơn vị là 100000/128 = 781,25 (thậm chí ít hơn, bởi vì chúng tôi không mong đợi hầu hết các khác biệt bằng 0 ).

Vì vậy, kết quả là 32 + 999 * (1 + 7) + biến (0..782) bit = 1003 + biến (0..98) byte.


Bạn có thể cho biết thêm chi tiết về cách bạn đang mã hóa và về cách tính kích thước cuối cùng. 1101 byte hoặc 8808 bit dường như rất gần với giới hạn lý thuyết là 8091 bit, vì vậy tôi rất ngạc nhiên, rằng có thể đạt được điều gì đó như thế này trong thực tế.
LiKao

Nó sẽ không phải là 32 + 999 * (1 + 7 + variable(0..782))bit? Mỗi số trong số 999 số cần có một đại diện value / 128.
Kirk Broadhurst

1
@Kirk: không, nếu tất cả chúng đều nằm trong khoảng 5 chữ số. Điều này là do chúng tôi hy vọng rằng tổng tất cả những khác biệt này (hãy nhớ, chúng ta mã hóa khác biệt giữa các giá trị liên tục, không lệch giữa giá trị đầu tiên và thứ N) sẽ là dưới 100000 (ngay cả trong trường hợp xấu nhất)
ruslik

Bạn cần 34 bit thay vì 32 bit để đại diện cho giá trị đầu tiên (9,999.999.999> 2 ^ 32 = 4,294,967,296). Ngoài ra, sự khác biệt tối đa sẽ là 00000 đến 99001 vì các số là duy nhất, điều này sẽ thêm 774 1 thay vì 782 cho cơ sở 128. Vì vậy, phạm vi lưu trữ 1.000 số cho cơ sở 128 của bạn là 8026-8800 bit hoặc 1004-1100 byte. Cơ sở 64-bit cho khả năng lưu trữ tốt hơn, với phạm vi từ 879-1072 byte.
Briguy37

1
@raisercostin: đây là những gì Kirk hỏi. Trong ví dụ của bạn, bằng cách mã hóa khi chênh lệch 20k giữa hai giá trị đầu tiên, chỉ 80 nghìn phạm vi tối đa sẽ có thể xảy ra trong tương lai. Điều này sẽ sử dụng hết 20k / 128 = 156 bit đơn phân trong tổng số tối đa 782 (tương ứng với 100k)
ruslik 14/10/11

7

Đây là một vấn đề được biết đến từ Ngọc trai lập trình của Bentley.

Giải pháp: Tách năm chữ số đầu tiên khỏi các số vì chúng giống nhau cho mọi số. Sau đó, sử dụng các phép toán theo chiều bit để biểu diễn giá trị 9999 còn lại có thể. Bạn sẽ chỉ cần 2 ^ 17 Bit để biểu diễn các con số. Mỗi Bit đại diện cho một số. Nếu bit được đặt, số đó nằm trong sổ tele.

Để in tất cả các số, chỉ cần in tất cả các số mà bit được đặt nối với tiền tố. Để tìm kiếm một số nhất định, hãy thực hiện số học bit cần thiết để kiểm tra sự biểu diễn theo từng bit của số đó.

Bạn có thể tìm kiếm một số trong O (1) và hiệu quả về không gian là tối đa do sự tái tạo bit.

HTH Chris.


3
Đây sẽ là một cách tiếp cận tốt cho một bộ số dày đặc. Thật không may, ở đây tập hợp này rất thưa thớt: chỉ có 1.000 số trong số 100.000 có thể. Do đó, cách tiếp cận này sẽ yêu cầu trung bình 100 bit cho mỗi số. Xem câu trả lời của tôi để biết giải pháp thay thế chỉ cần ~ 17 bit.
NPE

1
Thời gian cần thiết để in tất cả các số sẽ tỷ lệ với 100.000 thay vì 1.000?
aioobe

Kết hợp hai ý tưởng về cơ bản bạn sẽ có được trie ngay lập tức. Sử dụng một bitvector với 100.000 mục nhập là cách định vị tổng thể và chiếm nhiều dung lượng. Tuy nhiên tra cứu O (log (n)) thường quá chậm (phụ thuộc vào số lượng truy vấn ở đây). Vì vậy, bằng cách sử dụng tập hợp bit để lập chỉ mục, bạn sẽ lưu trữ tối đa 17 bit cho mỗi số, trong khi vẫn nhận được tra cứu O (1). Đây là cách hoạt động của trie. Ngoài ra, thời gian in được tính bằng O (n) cho trie, nó kế thừa từ trường hợp đã sắp xếp.
LiKao

Đây không phải là "cách tiết kiệm không gian hiệu quả nhất để làm việc này".
Jake Berger

5

Lưu trữ cố định 1073 byte cho 1.000 số:

Định dạng cơ bản của phương pháp lưu trữ này là lưu trữ 5 chữ số đầu tiên, số đếm cho mỗi nhóm và phần bù cho mỗi số trong mỗi nhóm.

Tiền tố: Tiền tố
5 chữ số của chúng tôi chiếm 17 bit đầu tiên .

Phân nhóm:
Tiếp theo, chúng ta cần tìm ra cách phân nhóm có kích thước phù hợp cho các số. Hãy thử có khoảng 1 số cho mỗi nhóm. Vì chúng ta biết có khoảng 1000 số cần lưu trữ, chúng ta chia 99,999 thành khoảng 1000 phần. Nếu chúng ta chọn kích thước nhóm là 100, sẽ có các bit bị lãng phí, vì vậy hãy thử kích thước nhóm là 128, có thể được biểu diễn bằng 7 bit. Điều này mang lại cho chúng tôi 782 nhóm để làm việc.

Đếm:
Tiếp theo, với mỗi nhóm trong số 782 nhóm, chúng ta cần lưu trữ số lượng mục nhập trong mỗi nhóm. Số lượng 7 bit cho mỗi nhóm sẽ mang lại kết quả 7*782=5,474 bits, điều này rất kém hiệu quả vì số trung bình được đại diện là khoảng 1 do cách chúng tôi chọn nhóm của mình.

Do đó, thay vào đó, chúng ta có số đếm có kích thước thay đổi với số 1 đứng đầu cho mỗi số trong một nhóm theo sau là số 0. Vì vậy, nếu chúng ta có các xsố trong một nhóm, chúng ta sẽ x 1'stheo sau là a 0để biểu thị số lượng. Ví dụ: nếu chúng ta có 5 số trong một nhóm thì số đếm sẽ được biểu thị bằng 111110. Với phương pháp này, nếu có 1.000 số chúng ta kết thúc bằng 1000 số 1 và 782 số 0 với tổng số 1000 + 782 = 1.782 bit cho các số đếm .

Phần bù:
Cuối cùng, định dạng của mỗi số sẽ chỉ là phần bù 7 bit cho mỗi nhóm. Ví dụ: nếu 00000 và 00001 là các số duy nhất trong nhóm 0-127, các bit cho nhóm đó sẽ là 110 0000000 0000001. Giả sử 1.000 số, sẽ có 7.000 bit cho các hiệu số .

Do đó, số cuối cùng của chúng tôi giả sử 1.000 số như sau:

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

Bây giờ, hãy kiểm tra xem lựa chọn kích thước nhóm của chúng ta bằng cách làm tròn lên đến 128 bit có phải là lựa chọn tốt nhất cho kích thước nhóm hay không. Chọn xlàm số bit để đại diện cho mỗi nhóm, công thức cho kích thước là:

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

Tối thiểu hóa phương trình này cho các giá trị nguyên của xcho x=6, tạo ra 8.580 bit = 1.073 byte . Do đó, bộ nhớ lý tưởng của chúng tôi như sau:

  • Kích thước nhóm: 2 ^ 6 = 64
  • Số lượng nhóm: 1.562
  • Tổng dung lượng:

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


1

Coi đây là một vấn đề lý thuyết thuần túy và để lại sự hỗ trợ triển khai, cách hiệu quả nhất là chỉ lập chỉ mục tất cả các bộ có thể có 10000 chữ số cuối cùng trong một bảng lập chỉ mục khổng lồ. Giả sử bạn có chính xác 1000 số, bạn sẽ cần hơn 8000 bit một chút để xác định duy nhất tập hợp hiện tại. Không thể nén lớn hơn được, bởi vì khi đó bạn sẽ có hai tập hợp được xác định với cùng một trạng thái.

Các vấn đề với điều này là bạn sẽ phải đại diện cho mỗi bộ trong số 2 ^ 8000 bộ trong chương trình của mình dưới dạng một ổ đĩa và thậm chí google sẽ không có khả năng này từ xa.

Tra cứu sẽ là O (1), in ra tất cả số O (n). Phần chèn sẽ là O (2 ^ 8000) mà theo lý thuyết là O (1), nhưng trong thực tế thì không sử dụng được.

Trong một cuộc phỏng vấn, tôi sẽ chỉ đưa ra câu trả lời này, nếu tôi chắc chắn, rằng công ty đang tìm kiếm một người có khả năng suy nghĩ thấu đáo. Nếu không, điều này có thể khiến bạn trông giống như một nhà lý thuyết không quan tâm đến thế giới thực.

CHỈNH SỬA : Ok, đây là một "triển khai".

Các bước để xây dựng việc triển khai:

  1. Lấy một mảng không đổi có kích thước 100 000 * (1000 chọn 100 000) bit. Vâng, tôi biết thực tế rằng mảng này sẽ cần nhiều không gian hơn các nguyên tử trong vũ trụ vài độ lớn.
  2. Tách mảng lớn này thành các phần 100.000 mỗi phần.
  3. Trong mỗi đoạn lưu trữ một mảng bit cho một tổ hợp cụ thể của năm chữ số cuối cùng.

Đây không phải là chương trình, mà là một loại chương trình meta, sẽ tạo ra một LUT khổng lồ hiện có thể được sử dụng trong một chương trình. Nội dung không đổi của chương trình thường không được tính khi tính toán hiệu quả không gian, vì vậy chúng tôi không quan tâm đến mảng này khi thực hiện các phép tính cuối cùng của chúng tôi.

Đây là cách sử dụng LUT này:

  1. Khi ai đó cung cấp cho bạn 1000 số, bạn sẽ lưu trữ riêng biệt năm chữ số đầu tiên.
  2. Tìm xem phần nào trong mảng của bạn phù hợp với tập hợp này.
  3. Lưu trữ số của tập hợp trong một số 8074 bit duy nhất (gọi đây là c).

Điều này có nghĩa là để lưu trữ, chúng tôi chỉ cần 8091 bit, mà chúng tôi đã chứng minh ở đây là mã hóa tối ưu. Tuy nhiên, việc tìm ra đoạn chính xác lấy O (100 000 * (100 000 chọn 1000)), theo quy tắc toán học là O (1), nhưng trong thực tế sẽ luôn mất nhiều thời gian hơn thời gian của vũ trụ.

Tuy nhiên, việc tra cứu rất đơn giản:

  1. dải năm chữ số đầu tiên (số còn lại sẽ được gọi là n ').
  2. kiểm tra xem chúng có khớp không
  3. Tính i = c * 100000 + n '
  4. Kiểm tra xem bit tại i trong LUT có được đặt thành một hay không

Việc in tất cả các số cũng đơn giản (và thực tế lấy O (100000) = O (1), bởi vì bạn luôn phải kiểm tra tất cả các bit của đoạn hiện tại, vì vậy tôi đã tính sai điều này ở trên).

Tôi sẽ không gọi đây là một "sự thực hiện", bởi vì sự coi thường trắng trợn các giới hạn (kích thước của vũ trụ và thời gian vũ trụ này đã sống hay trái đất này sẽ tồn tại). Tuy nhiên trên lý thuyết đây là giải pháp tối ưu. Đối với các vấn đề nhỏ hơn, điều này thực sự có thể được thực hiện và đôi khi sẽ được thực hiện. Ví dụ, các mạng sắp xếp là một ví dụ cho cách mã hóa này và có thể được sử dụng như một bước cuối cùng trong các thuật toán sắp xếp đệ quy, để có được tốc độ nhanh chóng.


1
Cách tiết kiệm không gian hiệu quả nhất để làm điều này là gì?
Sven

1
Khi thực hiện các phép tính về không gian thời gian chạy, điều này có thể dễ dàng được chứng minh là cách tiết kiệm không gian hiệu quả nhất, bởi vì bạn liệt kê mọi trạng thái có thể có của hệ thống chỉ bằng một số. Không thể có bất kỳ mã hóa nào nhỏ hơn cho vấn đề này. Mẹo cho câu trả lời này là, kích thước chương trình hầu như không bao giờ được xem xét khi thực hiện các phép tính (hãy thử tìm bất kỳ câu trả lời nào có tính đến điều này, và bạn sẽ hiểu ý tôi). Vì vậy, đối với bất kỳ vấn đề nào có giới hạn về kích thước, bạn luôn có thể liệt kê tất cả các trạng thái, để có cách xử lý tiết kiệm không gian nhất.
LiKao

1

Điều này tương đương với việc lưu trữ một nghìn số nguyên không âm, mỗi số nhỏ hơn 100.000. Chúng ta có thể sử dụng một cái gì đó như mã hóa số học để làm điều này.

Cuối cùng, các số sẽ được lưu trữ trong một danh sách được sắp xếp. Tôi lưu ý rằng sự khác biệt dự kiến ​​giữa các số liền kề trong danh sách là 100.000 / 1000 = 100, có thể được biểu diễn bằng 7 bit. Cũng sẽ có nhiều trường hợp cần nhiều hơn 7 bit. Một cách đơn giản để biểu diễn những trường hợp ít phổ biến này là áp dụng lược đồ utf-8 trong đó một byte đại diện cho số nguyên 7 bit trừ khi bit đầu tiên được đặt, trong trường hợp đó byte tiếp theo được đọc để tạo ra số nguyên 14 bit, trừ khi bit đầu tiên của nó được đặt, trong trường hợp này byte tiếp theo được đọc để biểu thị một số nguyên 21 bit.

Vì vậy, ít nhất một nửa sự khác biệt giữa các số nguyên liên tiếp có thể được biểu diễn bằng một byte và hầu hết tất cả phần còn lại yêu cầu hai byte. Một vài số, được phân tách bởi sự khác biệt lớn hơn 16.384, sẽ yêu cầu ba byte, nhưng không thể có nhiều hơn 61 trong số này. Dung lượng lưu trữ trung bình sau đó sẽ là khoảng 12 bit cho mỗi số, hoặc ít hơn một chút, hoặc nhiều nhất là 1500 byte.

Nhược điểm của phương pháp này là việc kiểm tra sự tồn tại của một số bây giờ là O (n). Tuy nhiên, không có yêu cầu phức tạp về thời gian được chỉ định.

Sau khi viết, tôi nhận thấy ruslik đã đề xuất phương pháp khác biệt ở trên, sự khác biệt duy nhất là sơ đồ mã hóa. Của tôi có thể đơn giản hơn nhưng kém hiệu quả hơn.


1

Chỉ để hỏi nhanh bất kỳ lý do nào mà chúng tôi không muốn thay đổi các số thành cơ số 36. Nó có thể không tiết kiệm nhiều dung lượng nhưng chắc chắn sẽ tiết kiệm thời gian tìm kiếm vì bạn sẽ nhìn ít hơn rất nhiều so với 10digts. Hoặc tôi sẽ chia chúng thành các tệp tùy thuộc vào từng nhóm. vì vậy tôi sẽ đặt tên tệp (111) -222.txt và sau đó tôi sẽ chỉ lưu trữ các số phù hợp với nhóm đó trong đó và sau đó có thể tìm kiếm chúng theo thứ tự số theo cách này, tôi luôn có thể theo dõi để xem tệp có thoát hay không. trước khi tôi chạy một tìm kiếm lớn hơn. hoặc chính xác là tôi sẽ chạy đến tìm kiếm nhị phân một cho tệp để xem liệu nó có thoát hay không. và một tìm kiếm bonary khác về nội dung của tệp


0

Tại sao không giữ nó đơn giản? Sử dụng một mảng cấu trúc.

Vì vậy, chúng ta có thể lưu 5 chữ số đầu tiên dưới dạng hằng số, vì vậy hãy quên chúng đi ngay bây giờ.

65535 là số tối đa có thể được lưu trữ dưới dạng số 16 bit và số tối đa chúng ta có thể có là 99999, phù hợp với số bit thứ 17 với số tối đa là 131071.

Sử dụng kiểu dữ liệu 32 bit là một điều lãng phí vì chúng ta chỉ cần 1 bit trong số 16 bit bổ sung đó ... do đó, chúng ta có thể xác định cấu trúc có boolean (hoặc ký tự) và số 16 bit ..

Giả sử C / C ++

typedef struct _number {

    uint16_t number;
    bool overflow;
}Number;

Cấu trúc này chỉ chiếm 3 byte và chúng ta cần một mảng 1000, vì vậy tổng cộng là 3000 byte. Chúng tôi đã giảm tổng dung lượng xuống 25%!

Đối với việc lưu trữ các con số, chúng ta có thể thực hiện phép toán bitwise đơn giản

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

Và nghịch đảo

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

Để in tất cả chúng, chúng ta có thể sử dụng một vòng lặp đơn giản trên mảng. Tất nhiên, việc truy xuất một số cụ thể xảy ra trong thời gian không đổi, vì nó là một mảng.

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

Để tìm kiếm một số, chúng ta cần một mảng được sắp xếp. Vì vậy, khi các số được lưu, hãy sắp xếp mảng (tôi sẽ chọn sắp xếp hợp nhất theo cách cá nhân, O (nlogn)). Bây giờ để tìm kiếm, tôi sẽ sử dụng phương pháp sắp xếp hợp nhất. Tách mảng và xem số của chúng ta nằm giữa mảng nào. Sau đó chỉ gọi hàm trên mảng đó. Làm điều này một cách đệ quy cho đến khi bạn có một kết quả phù hợp và trả về chỉ mục, nếu không, nó không tồn tại và in mã lỗi. Việc tìm kiếm này sẽ khá nhanh và trường hợp xấu nhất vẫn tốt hơn O (nlogn) vì nó sẽ hoàn toàn thực thi trong thời gian ngắn hơn so với sắp xếp hợp nhất (chỉ lặp lại 1 bên của phần tách mỗi lần, thay vì cả hai bên :)), là O (nlogn).


0

Giải pháp của tôi: trường hợp tốt nhất 7,025 bit / số, trường hợp xấu nhất 14,193 bit / số, trung bình thô 8,551 bit / số. Được mã hóa luồng, không có quyền truy cập ngẫu nhiên.

Ngay cả trước khi đọc câu trả lời của ruslik, tôi đã nghĩ ngay đến việc mã hóa sự khác biệt giữa mỗi số, vì nó sẽ nhỏ và phải tương đối nhất quán, nhưng giải pháp cũng phải có khả năng ứng phó với trường hợp xấu nhất. Chúng ta có một không gian 100000 số chỉ chứa 1000 số. Trong một danh bạ điện thoại hoàn toàn đồng nhất, mỗi số sẽ lớn hơn số trước đó 100:

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

Nếu đúng như vậy, nó sẽ yêu cầu bộ nhớ không để mã hóa sự khác biệt giữa các số, vì đó là một hằng số đã biết. Thật không may, các số có thể thay đổi so với các bước lý tưởng là 100. Tôi sẽ mã hóa sự khác biệt so với bước tăng lý tưởng là 100, để nếu hai số liền kề khác nhau 103, tôi sẽ mã hóa số 3 và nếu hai số liền kề khác nhau 92, tôi sẽ mã hóa -8. Tôi gọi delta từ mức tăng lý tưởng 100 là " phương sai ".

Phương sai có thể nằm trong khoảng từ -99 (tức là hai số liên tiếp) đến 99000 (toàn bộ danh bạ bao gồm các số 00000… 00999 và thêm một số xa nhất 99999), là một phạm vi 99100 giá trị có thể có.

Tôi muốn nhắm đến phân bổ một lưu trữ tối thiểu để mã hóa những khác biệt phổ biến nhất và mở rộng lưu trữ nếu tôi gặp phải sự khác biệt lớn hơn (như Protobuf ‘s varint). Tôi sẽ sử dụng các phần bảy bit, sáu để lưu trữ và một bit cờ bổ sung ở cuối để chỉ ra rằng phương sai này được lưu trữ với một phần bổ sung sau phần hiện tại, tối đa là ba phần (sẽ cung cấp tối đa 3 * 6 = 18 bit dung lượng lưu trữ, là 262144 giá trị có thể có, nhiều hơn số phương sai có thể có (99100). Mỗi đoạn bổ sung theo sau cờ tăng có các bit có ý nghĩa cao hơn, do đó, đoạn đầu tiên luôn có các bit 0- 5, phần thứ hai tùy chọn có các bit 6-11 và phần thứ ba tùy chọn có các bit 12-17.

Một đoạn duy nhất cung cấp sáu bit lưu trữ có thể chứa 64 giá trị. Tôi muốn ánh xạ 64 phương sai nhỏ nhất để vừa với một đoạn duy nhất đó (tức là phương sai từ -32 đến +31) vì vậy, tôi sẽ sử dụng mã hóa ProtoBuf ZigZag, lên đến các phương sai từ -99 đến +98 (vì không cần đối với phương sai âm vượt quá -99), lúc đó tôi sẽ chuyển sang mã hóa thông thường, bù bằng 98:  

Phương sai | Giá trị được mã hóa
----------- + ----------------
    0 | 0
   -1 | 1
    1 | 2
   -2 | 3
    2 | 4
   -3 | 5
    3 | 6
   ... | ...
  -31 | 61
   31 | 62
  -32 | 63
----------- | --------------- 6 bit
   32 | 64
  -33 | 65
   33 | 66
   ... | ...
  -98 | 195
   98 | 196
  -99 | 197
----------- | --------------- Hết ZigZag
   100 | 198
   101 | 199
   ... | ...
  3996 | 4094
  3997 | 4095
----------- | --------------- 12 bit
  3998 | 4096
  3999 | 4097
   ... | ...
 262045 | 262143
----------- | --------------- 18 bit

Một số ví dụ về cách các phương sai sẽ được mã hóa dưới dạng bit, bao gồm cờ để chỉ ra một đoạn bổ sung:

Phương sai | Các bit được mã hóa
----------- + ----------------
     0 | 000000 0
     5 | 001010 0
    -8 | 001111 0
   -32 | 111111 0
    32 | 000000 1 000001 0
   -99 | 000101 1 000011 0
   177 | 010011 1 000100 0
 14444 | 001110 1 100011 1 000011 0

Vì vậy, ba số đầu tiên của một danh bạ điện thoại mẫu sẽ được mã hóa thành một dòng bit như sau:

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

Trường hợp tốt nhất , danh bạ điện thoại được phân phối hơi đồng đều và không có hai số điện thoại nào có phương sai lớn hơn 32, vì vậy nó sẽ sử dụng 7 bit cho mỗi số cộng với 32 bit cho số bắt đầu với tổng số 32 + 7 * 999 = 7025 bit .
Một kịch bản hỗn hợp , trong đó phương sai của 800 số điện thoại nằm trong một đoạn (800 * 7 = 5600), 180 số phù hợp với hai đoạn mỗi đoạn (180 * 2 * 7 = 2520) và 19 số vừa với ba đoạn mỗi đoạn (20 * 3 * 7 = 399), cộng với 32 bit ban đầu, tổng cộng là 8551 bit .
Trường hợp xấu nhất , 25 số vừa với ba khối (25 * 3 * 7 = 525 bit) và 974 số còn lại vừa với hai khối (974 * 2 * 7 = 13636 bit), cộng với 32 bit cho số đầu tiên cho một đại Tổng cộng14193 bit .

   Lượng số được mã hóa |
 1-chunk | 2 khối | 3 khúc | Tổng số bit
--------- + ---------- + ---------- + ------------
   999 | 0 | 0 | 7025
   800 | 180 | 19 | 8551
    0 | 974 | 25 | 14193

Tôi có thể thấy bốn tối ưu hóa bổ sung có thể được thực hiện để giảm thêm dung lượng cần thiết:

  1. Đoạn thứ ba không cần đầy đủ bảy bit, nó có thể chỉ là năm bit và không cần một bit cờ.
  2. Có thể có một số lần vượt qua ban đầu để tính toán kích thước tốt nhất cho mỗi đoạn. Có thể đối với một danh bạ nhất định, sẽ là tối ưu nếu đoạn đầu tiên có 5 + 1 bit, 7 + 1 thứ hai và 5 + 1 thứ ba. Điều đó sẽ tiếp tục giảm kích thước xuống tối thiểu 6 * 999 + 32 = 6026 bit, cộng với hai bộ ba bit để lưu trữ kích thước của khối 1 và 2 (kích thước của chunk 3 là phần còn lại của 16 bit được yêu cầu) cho tổng cộng của 6032 bit!
  3. Cùng một lần vượt qua ban đầu có thể tính toán mức tăng dự kiến ​​tốt hơn so với mặc định 100. Có thể có một danh bạ điện thoại bắt đầu từ 55555-50000 và do đó, danh bạ có một nửa dải số nên mức tăng dự kiến ​​phải là 50. Hoặc có thể có một danh bạ phi tuyến tính có thể sử dụng phân phối (độ lệch chuẩn) và một số gia số dự kiến ​​tối ưu khác. Điều này sẽ làm giảm phương sai điển hình và có thể cho phép sử dụng đoạn đầu tiên thậm chí còn nhỏ hơn.
  4. Phân tích sâu hơn có thể được thực hiện trong lần vượt qua đầu tiên để cho phép phân vùng danh bạ điện thoại, với mỗi phân vùng có gia số dự kiến ​​và tối ưu hóa kích thước chunk riêng. Điều này sẽ cho phép kích thước đoạn đầu tiên nhỏ hơn cho một số phần nhất định có tính đồng nhất cao của danh bạ điện thoại (giảm số lượng bit được tiêu thụ) và kích thước khối lớn hơn cho các phần không đồng nhất (giảm số lượng bit bị lãng phí trên cờ tiếp tục).

0

Câu hỏi thực sự là một trong những lưu trữ số điện thoại năm chữ số.

Bí quyết là bạn cần 17 bit để lưu trữ dải số từ 0..99,999. Nhưng lưu trữ 17 bit trên ranh giới từ 8 byte thông thường là một rắc rối. Đó là lý do tại sao họ hỏi liệu bạn có thể làm với ít hơn 4k bằng cách không sử dụng số nguyên 32 bit hay không.

Câu hỏi: Có phải tất cả các kết hợp số đều có thể?

Do bản chất của hệ thống điện thoại, có thể có ít hơn 65 nghìn kết hợp có thể. Tôi sẽ cho rằng bởi vì chúng ta đang nói về năm vị trí sau trong số điện thoại, trái ngược với mã vùng hoặc tiền tố trao đổi.

Câu hỏi: danh sách này sẽ tĩnh hay nó sẽ cần hỗ trợ các bản cập nhật?

Nếu nó là tĩnh , thì khi đến lúc điền cơ sở dữ liệu, hãy đếm số chữ số <50.000 và số chữ số> = 50.000. Phân bổ hai mảnguint16độ dài thích hợp: một mảng cho các số nguyên dưới 50.000 và một mảng cho tập hợp cao hơn. Khi lưu trữ các số nguyên trong mảng cao hơn, hãy trừ đi 50.000 và khi đọc các số nguyên từ mảng đó, hãy cộng 50.000. Bây giờ bạn đã lưu trữ 1.000 số nguyên của mình trong 2.000 từ 8 byte.

Việc xây dựng danh bạ sẽ yêu cầu hai lần truyền dữ liệu đầu vào, nhưng trung bình, việc tra cứu sẽ diễn ra trong một nửa thời gian so với khi thực hiện với một mảng. Nếu thời gian tra cứu là rất quan trọng, bạn có thể sử dụng nhiều mảng hơn cho các phạm vi nhỏ hơn nhưng tôi nghĩ ở các kích thước này, giới hạn hiệu suất của bạn sẽ kéo các mảng khỏi bộ nhớ và 2k có thể sẽ lưu trữ vào bộ nhớ cache của CPU nếu không đăng ký dung lượng trên bất kỳ thứ gì bạn đang sử dụng ngày.

Nếu nó là động , hãy phân bổ một mảng 1000 hoặc hơn uint16, và thêm các số theo thứ tự đã sắp xếp. Đặt byte đầu tiên thành 50,001 và đặt byte thứ hai thành giá trị null thích hợp, như NULL hoặc 65,000. Khi bạn lưu trữ các số, hãy lưu trữ chúng theo thứ tự được sắp xếp. Nếu một số dưới 50.001 thì hãy lưu nó trước điểm đánh dấu 50.001. Nếu một số lớn hơn hoặc bằng 50.001, hãy lưu trữ nó sau điểm đánh dấu 50.001, nhưng trừ đi 50.000 từ giá trị được lưu trữ.

Mảng của bạn sẽ trông giống như sau:

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

Vì vậy, khi bạn tra cứu một số trong danh bạ, bạn sẽ duyệt qua mảng và nếu bạn đạt đến giá trị 50,001, bạn bắt đầu thêm 50.000 vào các giá trị mảng của mình.

Điều này làm cho các phụ trang rất tốn kém, nhưng việc tra cứu rất dễ dàng và bạn sẽ không tốn nhiều hơn 2 nghìn cho bộ nhớ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.