Найефективніший спосіб зберігання тисяч телефонних номерів


94

Це питання про інтерв'ю в Google:

Є близько тисячі телефонних номерів, які потрібно зберігати, кожен з яких має 10 цифр. Ви можете вважати, що перші 5 цифр кожної з них будуть однаковими на тисячі чисел. Ви повинні виконати такі операції: a. Шукайте, чи існує вказане число. б. Роздрукуйте весь номер

Який найефективніший спосіб заощадити місце?

Я відповів на хеш-таблицю та пізніше кодування Хаффмана, але мій інтерв'ю сказав, що я не йду в правильному напрямку. Будь ласка, допоможіть мені тут.

Чи можна використовувати суфікс-трійку?

В ідеалі 1000 зберігання чисел займає 4 байти на число, тому для зберігання 1000 числа потрібно всього 4000 байт. Кількісно я хочу зменшити обсяг пам’яті до <4000 байт, саме це мені пояснив мій інтерв'юер.


28
Я відповів би, що за допомогою звичайної бази даних ви можете зберігати їх як текст, навіть тисячі / мільйони, а операції пошуку все ще будуть дуже швидкими. Я раджу не робити «розумних» речей, оскільки всю систему доведеться переробляти, якщо вони хочуть надалі підтримувати міжнародні номери, або якщо почнуть з’являтися телефонні номери, що починаються з «0», або якщо уряд вирішить змінити формат телефонного номера тощо.
Thomas Bonini

1
@AndreasBonini: Я, мабуть, дав би цю відповідь, якщо б я не брав інтерв'ю в компанії типу Google або Facebook, не було б справжніх рішень просто не вирізати. Хоча postgres, наприклад, теж намагається, я не був би впевнений, що це скорочує пропускну здатність даних, яку повинен взяти Google.
LiKao

1
@LiKao: майте на увазі, що в ОП було зазначено "близько тисячі цифр"
Томас Боніні,

@AndreasBonini: Правда, може бути і тестом, що опитуваний знає правильно інтерпретувати такі обмеження та вибирати найкраще рішення відповідно до цього.
LiKao

4
"ефективний" у цьому питанні дійсно повинен бути визначений - ефективним у який спосіб? простір, час, і те, і інше?
мат б

Відповіді:


36

Ось покращення відповіді Екса . Розглянемо використання трьох "шарів" для структури даних: перший - константа для перших п'яти цифр (17 біт); тож звідси у кожного номера телефону залишилося лише п’ять цифр, що залишилися. Ми розглядаємо ці п'ять цифр як 17-бітні двійкові цілі числа і зберігаємо k цих бітів одним методом, а 17 - k = m іншим методом, визначаючи k в кінці для мінімізації необхідного простору.

Спочатку ми сортуємо номери телефонів (усі зменшуються до 5 десяткових цифр). Тоді ми підраховуємо, скільки є телефонних номерів, для яких двійкове число, що складається з перших m бітів, дорівнює 0, на скільки телефонних номерів перші m біти становлять максимум 0 ... 01, на скільки телефонних номерів перший m біт - максимум 0 ... 10, і т.д., до числа телефонних номерів, для яких перші m бітів дорівнюють 1 ... 11 - останній підрахунок 1000 (десятковий). Таких підрахунків є 2 ^ м , і кожен рахунок становить максимум 1000. Якщо опустити останній (оскільки ми все-таки знаємо, що це 1000), ми можемо зберігати всі ці числа у суміжному блоці (2 ^ m - 1) * 10 біт. (10 біт достатньо для зберігання числа менше 1024.)

Останні k біт усіх (зменшених) номерів телефонів постійно зберігаються в пам'яті; тому якщо k , скажімо, 7, то перші 7 біт цього блоку пам'яті (біти 0 до 6) відповідають останнім 7 бітам першого (зменшеного) номера телефону, біти 7 через 13 відповідають останнім 7 бітам другого (зменшеного) номера телефону, тощо. Для цього потрібно 1000 * k біт на загальну кількість 17 + (2 ^ (17 - k ) - 1) * 10 + 1000 * k , що досягає свого мінімуму 11287 для k = 10. Тому ми можемо зберігати всі телефонні номери в ceil ( 11287/8) = 1411 байт.

Додатковий простір можна заощадити, спостерігаючи, що жодне з наших чисел не може починатися, наприклад, з 1111111 (двійкове), оскільки найменше число, яке починається з цього, - 130048, і у нас є лише п'ять десяткових цифр. Це дозволяє нам відрізати кілька записів першого блоку пам’яті: замість 2 ^ м - 1 підрахунок, нам потрібен лише ceil (99999/2 ^ k ). Це означає, що формула стає

17 + стеля (99999/2 ^ k ) * 10 + 1000 * k

що досить дивовижно досягає свого мінімуму 10997 для k = 9 і k = 10, або ceil (10997/8) = 1375 байт.

Якщо ми хочемо знати, чи є в наборі певний номер телефону, ми спочатку перевіряємо, чи відповідають перші п'ять двійкових цифр п’яти цифрам, які ми зберегли. Потім розділимо решту п’яти цифр на її верхню m = 7 біт (це, скажімо, m -бітове число M ) та її нижню k = 10 біт (число K ). Тепер ми знаходимо число a [M-1] зменшених номерів телефонів, для яких перші m цифр становлять максимум M - 1, і число a [M] скорочених телефонних номерів, для яких перші m цифр - максимум M , як з першого блоку бітів. Тепер перевіряємо між а[М-1] й і [М] е послідовність K біт в другому блоці пам'яті , щоб побачити , якщо ми знаходимо K ; в гіршому випадку є 1000 таких послідовностей, тож якщо ми використовуємо двійковий пошук, ми можемо закінчити операції O (log 1000).

Псевдокод для друку всіх 1000 номерів слід, де доступ до До -му до -бітний введення першого блоку пам'яті , як в [K] і Мм записи бітової другого блоку пам'яті в якості Ь [М] (і те й інше потребує декількох бітних операцій, які нудно виписувати). Перші п’ять цифр знаходяться в числі 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 ^ 3 додатних цілих чисел, усіх менше 10 ^ 5, менше ніж ceil (log [2] (двочлен (10 ^ 5, 10 ^ 3)) ) = 8073. Включаючи 17, які нам потрібні для перших 5 цифр, все ще залишається пробіл 10997 - 8090 = 2907 біт. Цікавим завданням є те, чи є кращі рішення, де ти можеш відносно ефективно отримувати доступ до номерів!


4
Структура даних, яку ви описуєте тут, насправді є дуже ефективною версією трие, яка використовує лише так мало, як потрібно для індексації, і лише два рівні. На практиці було б непогано побачити, чи може це перемогти трійку з більшою кількістю рівнів, але я думаю, що це багато що залежить від розподілу чисел (Насправді номери телефонів у реальному часі не є повністю випадковими, а лише майже).
LiKao

Привіт Еріку, оскільки ти сказав, що тобі буде цікаво побачити інші варіанти, ознайомтеся з моїм рішенням. Він вирішує це у 88080 біт, що становить всього 490 біт від теоретичного мінімуму. Шукати окремі номери трохи неефективно, але зберігання дуже компактне.
Briguy37

1
Я б припустив, що розумний інтерв'юєр віддасть перевагу відповіді "трійка" замість "складної бази даних на замовлення". Якщо ви хочете продемонструвати свої 133-х хакерські навички, можете додати - "можна було б скласти конкретний алгоритм дерева для цього особливого випадку, якщо це необхідно".
KarlP

Привіт, Ви можете пояснити, як 5 цифр потребує 17 біт для зберігання?
Тушар Банне

@tushar П'ять цифр кодують число від 00000 до 99999 включно. Представити це число у двійковій формі. 2 ^ 17 = 131072, тому для цього достатньо 17 біт, але 16 - ні.
Ерік П.

43

Далі я розглядаю числа як цілі змінні (на відміну від рядків):

  1. Сортуйте числа.
  2. Розділіть кожне число на перші п'ять цифр і останні п'ять цифр.
  3. Перші п’ять цифр однакові в цифрах, тому зберігайте їх лише один раз. Для цього буде потрібно 17 біт пам’яті.
  4. Зберігайте останні п'ять цифр кожного числа окремо. Для цього знадобиться 17 біт на число.

Для повторного підключення: перші 17 біт є загальним префіксом, наступні 1000 груп з 17 біт - це останні п'ять цифр кожного числа, що зберігаються у порядку зростання.

Загалом ми бачимо 2128 байт на 1000 номерів або 17.017 біт на 10-значний номер телефону.

Пошук є O(log n)(двійковий пошук) і повне перерахування O(n).


Гм, де складність космосу?
aioobe

Занадто багато часу для побудови (O (log (n) * n k) (k - довжина) для сортування, порівняно з O (n k) для побудови трійника). Також простір далеко не оптимальний, оскільки довші загальні префікси зберігаються окремо. Час пошуку також не є оптимальним. Для таких рядкових даних легко забути довжину чисел, яка домінує в пошуку. Тобто бінарний пошук - O (log (n) * k), тоді як трійці потрібен лише O (k). Ви можете зменшити вирази тез, коли k є постійним, але це має показати загальну проблему при міркуванні про структури даних, що зберігають рядки.
LiKao

@LiKao: Хто щось сказав про струни? Я маю справу виключно із цілими змінними, тому kце не має значення.
NPE

1
Гаразд, тоді я неправильно прочитав відповідь. Проте загальні частини не зберігаються разом, тому питання про космічну ефективність залишається. Для 1000 п'ятизначних чисел буде досить велика кількість загальних префіксів, тож їх зменшення допоможе дуже багато. Також у випадку чисел у нас є O (log (n)) проти O (k) для рядків, що все ж швидше.
LiKao

1
@Geek: 1001 група з 17 біт - 17017 біт або 2128 байт (з деякою зміною).
NPE

22

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

Я колись провів інтерв'ю, де вони запитували про структури даних. Я забув "масив".


1
+1, це безумовно шлях. Я дізнався це під іншим ім’ям, бібліотечним деревом або лексичним деревом пошуку, або ще щось, коли я був студентом (якщо хтось пам’ятає це старе ім’я, будь ласка, скажіть).
Валмонд

6
Це не відповідає вимозі 4000 байт. Лише для зберігання вказівника найгірший сценарій полягає в тому, що вам знадобиться 1 покажчик на 1-4-е листя на наступний рівень, 10 покажчиків на 5-й, 100 на 6-й і 1000 на 7-й, 8-й та 9-й рівні , що приводить наш покажчик до 3114. Це дає щонайменше 3114 чітких місць пам'яті, необхідних для вказівників, а це означає, що вам знадобиться щонайменше 12 біт для кожного вказівника. 12 * 3114 = 37368 біт = 4671 байт> 4000 байт, і це навіть не означає, як ви представляєте значення кожного аркуша!
Briguy37

16

Можливо, я б подумав про використання компресованої версії Trie (можливо, DAWG, як запропонував @Misha).

Це автоматично використовувало б той факт, що всі вони мають спільний префікс.

Пошук здійснюватиметься у постійному часі, а друк - у лінійному часі.


Питання полягає в найбільш просторовому способі зберігання даних. Не хотіли б ви дати деяку оцінку, скільки місця знадобиться цьому методу для 1000 телефонних номерів? Дякую.
NPE

Простір для трійки - це не більше O (n * k), де n - кількість рядків, а k - довжина кожного рядка. Беручи до уваги, що для представлення чисел вам не потрібні 8-бітові символи, я б запропонував зберегти 4 шістнадцяткових індекси в шістнадцяткових і один для решти бітів. Таким чином вам потрібно максимум 17 біт на число. Оскільки у всіх випадках у вас будуть зіткнення на всіх рівнях з цим кодуванням, ви насправді можете опуститися нижче цього. Очікуючи, що ми збережемо 1000 номерів, ми можемо зберегти в цілому 250 біт для сутичок на першому рівні. Найкраще перевірити правильність кодування на прикладах даних.
LiKao

@LiKao, правильно, і зазначивши, що, наприклад, 1000 чисел не можуть мати більше 100 різних останніх двох цифр, трійка може бути значно згорнута на останніх рівнях.
aioobe

@aioobe: Листя можна було згортати на останньому рівні, оскільки дітей немає. Однак для листя другого до останнього рівня потрібно 2 ^ 10 = 1024 стани (кожна остання цифра може бути увімкнена або вимкнена), тому це не можна зменшити в цьому випадку, оскільки є лише 1000 чисел. Це означає, що кількість найгірших покажчиків залишається на рівні 3114 (див. Мій коментар до відповіді Міші), тоді як потрібне листя йде до 5 + 10 + 100 + 1000 + 1000 + 10 = 2125, що не змінює необхідні 12 байт для кожного покажчик. Таким чином, це все ще ставить рішення трие в 4671 байт, враховуючи лише вказівники.
Briguy37

@ Briguy37, не впевнений, що я отримую аргумент " кожна остання цифра може бути включена чи вимкнена ". Усі цифри мають 10 цифр, правда?
aioobe

15

Я чув про цю проблему раніше (але без перших п'ятизначних припущень - це одне і те ж припущення), і найпростішим способом це було кодування Райса :

1) Оскільки порядок не має значення, ми можемо їх сортувати, і зберегти просто різниці між послідовними значеннями. У нашому випадку середня різниця становила б 100 000/1000 = 100

2) Кодуйте відмінності, використовуючи коди Райсу (база 128 або 64) або навіть коди Голомба (база 100).

EDIT: Оцінка кодування Rice з базою 128 (не тому, що це дало б найкращі результати, а тому, що простіше обчислити):

Ми збережемо перше значення як є (32 біта).
Решта 999 значень - це різниці (ми очікуємо, що вони будуть невеликими, в середньому 100) міститимуть:

унарне значення value / 128(змінна кількість біт + 1 біт як термінатор)
двійкове значення для value % 128(7 біт)

Ми повинні якось оцінити межі (назвемо це VBL) для кількості змінних бітів:
нижня межа: вважайте, що нам пощастило, і різниця не більша за нашу базу (128 у цьому випадку). це означатиме дати 0 додаткових біт.
високий межа: оскільки всі відмінності, менші за базові, будуть закодовані у двійковій частині числа, максимальне число, яке нам потрібно буде кодувати в одинарному, - 100000/128 = 781,25 (навіть менше, тому що ми не очікуємо, що більшість різниць буде нульовим ).

Отже, результат 32 + 999 * (1 + 7) + змінна (0..782) біт = 1003 + змінна (0..98) байт.


Чи можете ви надати більш детальну інформацію про спосіб кодування та про остаточний розрахунок розміру. 1101 байт або 8808 біт здається дуже близьким до теоретичної межі 8091 біт, тому я дуже здивований, що чогось подібного можна досягти на практиці.
LiKao

Хіба це не 32 + 999 * (1 + 7 + variable(0..782))бити? Кожен з 999 номерів потребує представлення value / 128.
Кірк Бродхерст

1
@Kirk: ні, якщо всі вони знаходяться в межах 5 цифр. Це тому, що ми очікували, що сума цих різниць (пам’ятайте, що ми кодуємо різниці між послідовними значеннями, а не між першим та N-м значенням) буде нижче 100000 (навіть у найгіршому випадку)
ruslik

Вам потрібно 34 біта замість 32 біта, щоб представити перше значення (9,999,999,999> 2 ^ 32 = 4,294,967,296). Крім того, максимальна різниця складе від 00000 до 99001, оскільки цифри є унікальними, що додасть 774 1 замість 782 для базових 128. Таким чином, ваш діапазон для зберігання 1000 чисел для базової 128 становить 8026-8800 біт або 1004-1100 байт. 64-бітна база забезпечує кращу пам’ять, коливається в межах 879-1072 байт.
Briguy37

1
@raisercostin: це запитав Кірк. У вашому прикладі, кодуючи раз 20k різницю між першими двома значеннями, в майбутньому може виникнути лише 80k максимального діапазону. Це
витратить

7

Це добре відома проблема з програмування перлів програмування Bentley.

Рішення: Перші п'ять цифр зніміть із чисел, оскільки вони однакові для кожного числа. Потім використовуйте побітові операції, щоб представити решту 9999 можливого значення. Вам потрібно буде лише 2 ^ 17 біт для представлення чисел. Кожен Біт являє собою число. Якщо біт встановлений, номер знаходиться в телефонній книзі.

Щоб надрукувати всі числа, просто надрукуйте всі числа, де встановлено біт, поєднаний з префіксом. Для пошуку заданого числа виконайте необхідну арифметику бітів, щоб перевірити наявність бітового представлення числа.

Ви можете шукати число в O (1), а ефективність простору максимальна завдяки репрезентації бітів.

HTH Chris.


3
Це був би вдалий підхід для щільного набору чисел. На жаль, тут набір дуже рідкісний: із 100 000 можливих лише 1000 номерів. Тому такий підхід вимагає в середньому 100 біт на число. Дивіться мою відповідь щодо альтернативи, яка потребує лише 17 біт.
NPE

1
Невже час, необхідний для друку всіх номерів, не буде пропорційним 100 000 замість 1000?
aioobe

Поєднуючи дві ідеї, ви в основному отримуєте трійку негайно. Використання бітрейктора зі 100 000 записами є способом перерозподілу і займає багато місця. Однак пошук O (log (n)) часто занадто повільний (залежить від кількості запитів тут). Таким чином, використовуючи ієрахію наборів бітів для індексації, ви зберігатимете максимум 17 біт на число, при цьому все одно отримуєте пошук O (1). Так працює трійка. Також час для друку є в O (n) для трійника, який він успадковує від відсортованого випадку.
LiKao

Це не "найефективніший спосіб економії місця".
Джейк Бергер

5

Фіксоване зберігання 1073 байт на 1000 номерів:

Основний формат цього способу зберігання - це збереження перших 5 цифр, підрахунку для кожної групи та зміщення для кожного числа в кожній групі.

Приставка:
наш 5-значний префікс займає перші 17 біт .

Групування:
Далі нам потрібно визначити групування за розмірами за розміром. Спробуємо мати приблизно 1 число на групу. Оскільки ми знаємо, що є близько 1000 номерів для зберігання, ми ділимо 99,999 на приблизно 1000 частин. Якщо ми вибрали розмір групи 100, то були би марні біти, тож давайте спробуємо групу розміром 128, яка може бути представлена ​​7 бітами. Це дає нам 782 групи для роботи.

Підрахунки:
Далі для кожної з 782 груп нам потрібно зберігати кількість записів у кожній групі. 7-бітове підрахунок для кожної групи дасть результат 7*782=5,474 bits, що є дуже неефективним, оскільки середнє представлене число становить приблизно 1 через те, як ми обрали наші групи.

Таким чином, замість цього ми маємо підрахунки за розмірами з провідними значеннями 1 для кожного числа групи з наступним числом 0. Таким чином, якби у нас були xцифри в групі, ми мали би x 1'sслідувати а, 0щоб представляти кількість. Наприклад, якби у нас було 5 чисел у групі, підрахунок був би представлений 111110. За допомогою цього методу, якщо є 1000 чисел, ми закінчуємо 1000 1 і 782 0 усього 1000 + 782 = 1,782 біт для підрахунків .

Зсув:
Останнє, формат кожного числа буде просто 7-бітовим зміщенням для кожної групи. Наприклад, якщо 00000 та 00001 є єдиними числами у групі 0-127, біти для цієї групи будуть 110 0000000 0000001. Якщо припустити 1000 чисел, для компенсацій буде 7000 біт .

Таким чином, наш підсумковий підрахунок, припускаючи 1000 чисел, такий:

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=6, що дає 8,580 біт = 1,073 байти . Таким чином, наше ідеальне сховище таке:

  • Розмір групи: 2 ^ 6 = 64
  • Кількість груп: 1562
  • Загальний обсяг пам’яті:

    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 наборів у вашій програмі як лут, і навіть Google не може бути віддалений на це.

Пошук буде O (1), друкуючи все число O (n). Вставкою буде O (2 ^ 8000), що в теорії є O (1), але на практиці є непридатним.

В інтерв'ю я би дав цю відповідь, лише якщо був би впевнений, що компанія шукає того, хто вміє багато думати з коробки. Інакше це може змусити вас виглядати теоретиком, який не має жодних реальних проблем.

EDIT : Гаразд, ось одна "реалізація".

Кроки до побудови реалізації:

  1. Візьміть постійний масив розміром 100 000 * (1000 виберіть 100 000) біт. Так, я усвідомлюю той факт, що цьому масиву буде потрібно більше простору, ніж атомам у Всесвіті на кілька величин.
  2. Розділіть цей великий масив на шматки по 100 000 кожен.
  3. У кожному фрагменті зберігайте невеликий масив для однієї конкретної комбінації останніх п’яти цифр.

Це не програма, а своєрідна метапрограма, яка створить гігантський LUT, який тепер можна використовувати в програмі. Постійні матеріали програми зазвичай не враховуються при обчисленні космічної ефективності, тому ми не дбаємо про цей масив, коли робимо наші остаточні розрахунки.

Ось як використовувати цей LUT:

  1. Коли хтось дає вам 1000 номерів, ви зберігаєте перші п'ять цифр окремо.
  2. Дізнайтеся, який з фрагментів вашого масиву відповідає цьому набору.
  3. Збережіть номер набору в одному 8074-бітовому номері (називайте це с).

Це означає, що для зберігання нам потрібні лише 8091 біт, який ми довели, що це оптимальне кодування. Визначення правильного шматка вимагає O (100 000 * (100 000 вибирати 1000)), що відповідно до математичних правил є O (1), але на практиці завжди буде тривати більше часу, ніж час Всесвіту.

Пошук простий, хоча:

  1. смужка перших п'яти цифр (решта числа буде називатися n ').
  2. перевірити, чи відповідають вони
  3. Обчисліть i = c * 100000 + n '
  4. Перевірте, чи встановлено біт на i в LUT на один

Друк усіх чисел також простий (і займає O (100000) = O (1) насправді, тому що ви завжди повинні перевірити всі біти поточного фрагмента, тому я прорахував це вище).

Я б не назвав це «реалізацією» через кричуще зневага до обмежень (розмір Всесвіту та час, коли ця Всесвіт прожила або ця земля існуватиме). Однак теоретично це оптимальне рішення. Для менших проблем це насправді можна зробити, а іноді і робити. Наприклад, мережі сортування є прикладом такого способу кодування і можуть бути використані як завершальний крок в алгоритмах рекурсивного сортування, щоб отримати велику швидкість.


1
Який найефективніший спосіб заощадити місце?
Свен

1
Виконуючи обчислення простору часу роботи, це може бути доведено як найефективніший спосіб економії місця, оскільки ви перераховуєте будь-який можливий стан системи лише одним числом. Не може бути меншої кодування для цієї проблеми. Хитрість цієї відповіді полягає в тому, що розмір програми майже ніколи не враховується під час обчислень (спробуйте знайти будь-яку відповідь, яка враховує це, і ви побачите, що я маю на увазі). Отже, для будь-якої проблеми, що має обмежений розмір, ви завжди можете перераховувати всі стани, щоб отримати найбільш економічний простір для її вирішення.
LiKao

1

Це еквівалентно збереженню однієї тисячі невід’ємних цілих чисел, менших ніж 100 000. Для цього ми можемо використовувати щось на зразок арифметичного кодування.

Зрештою, номери будуть зберігатися у відсортованому списку. Зауважу, що очікувана різниця між сусідніми числами у списку становить 100 000/1000 = 100, які можна представити у 7 біт. Також буде багато випадків, коли необхідно більше 7 біт. Простий спосіб представити ці менш поширені випадки - це прийняти схему utf-8, де один байт представляє 7-бітове ціле число, якщо не встановлений перший біт, і в цьому випадку наступний байт зчитується для отримання 14-бітного цілого числа, якщо тільки його перший біт встановлюється, і в цьому випадку наступний байт зчитується, щоб представляти 21-бітове ціле число.

Отже, принаймні половина відмінностей між послідовними цілими числами може бути представлена ​​одним байтом, а майже всі інші потребують двох байтів. Для кількох чисел, розділених більшими різницями, ніж 16 384, знадобиться три байти, але не може бути більше 61 з них. Тоді середнє сховище становитиме близько 12 біт на число, або трохи менше, або не більше 1500 байт.

Мінусом цього підходу є те, що перевірка існування числа зараз є O (n). Однак не було визначено жодної вимоги до складності часу.

Після написання я помітив, що руслік вже запропонував метод різниці вище, єдиною відмінністю є схема кодування. Шахта, ймовірно, простіша, але менш ефективна.


1

Просто швидко запитати будь-яку причину того, що ми не хотіли б змінювати числа на базові 36. Це може не заощадити стільки місця, але це точно заощадить час на пошук, оскільки ви будете дивитись набагато менше 10 дбит. Або я би розділив їх на файли залежно від кожної групи. тож я б назвав файл (111) -222.txt, і тоді я б зберігав лише номери, які входять до цієї групи там, а потім їх можна шукати в цифровому порядку таким чином, я завжди можу перевірити, чи не виходить файл. перш ніж я запустити більш великий пошук. або щоб бути правильним, я би побіг до двійкового пошуку одного файлу, щоб побачити, чи він виходить. і ще один добросовісний пошук вмісту файлу


0

Чому б не зробити це просто? Використовуйте масив структур.

Таким чином, ми можемо зберегти перші 5 цифр як постійні, тому забудьте їх поки.

65535 - це найбільше, що можна зберегти у 16-бітному числі, а максимальне число, яке ми можемо мати, - це 99999, що відповідає розміру 17-го бітового числа та максимумом 131071.

Використання 32-бітових типів даних є марною, тому що нам потрібен лише 1 біт цих додаткових 16-бітних ... тому ми можемо визначити структуру, яка має булевий (або символьний) і 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), оскільки він буде абсолютно виконаний за менший час, ніж сортування злиття (тільки повторюється 1 сторона розбиття кожного разу замість обох сторін :)), яка є O (nlogn).


0

Моє рішення: найкращий випадок 7.025 біт / кількість, найгірший випадок 14.193 біт / число, приблизний середній 8.551 біт / кількість. Потокове кодування, без випадкового доступу.

Ще до того, як прочитати відповідь Русліка, я одразу подумав про кодування різниці між кожним числом, оскільки він буде невеликим і повинен бути відносно послідовним, але рішення також має вміти вміщувати найгірший сценарій. У нас є простір у 100000 номерів, що містять лише 1000 чисел. У абсолютно рівномірній телефонній книзі кожне число було б більше, ніж попереднє число на 100:

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

Якщо це було так, то для кодування відмінностей між числами знадобиться нульове зберігання, оскільки це відома константа. На жаль, числа можуть відрізнятися від ідеальних кроків 100. Я б кодував різницю від ідеального приросту 100, так що якщо два сусідніх числа відрізняються на 103, я б кодував число 3 і якщо два сусідніх числа відрізняються на 92, я буде кодувати -8. Я називаю дельту з ідеального приросту 100 " дисперсією ".

Варіант може варіюватися від -99 (тобто два числа підряд) до 99000 (вся телефонна книга складається з номерів 00000… 00999 та додаткового дальшого номера 99999), що становить діапазон 99100 можливих значень.

Я б прагнути виділити мінімальні зберігання для кодування найбільш поширених відмінностей і розширення пам'яті , якщо я стикаюся великі різниці (як Protobuf «s varint). Я використовуватиму шматки з семи біт, шість для зберігання та додатковий біт прапора в кінці, щоб вказати, що ця дисперсія зберігається з додатковою частиною після поточного, максимум до трьох фрагментів (що забезпечить максимум 3 * 6 = 18 біт пам’яті, що становить 262144 можливе значення, більше, ніж кількість можливих дисперсій (99100). Кожен додатковий фрагмент, що йде за піднятим прапором, має біти більш високої значущості, тому перший фрагмент завжди має біти 0- 5, необов'язковий другий фрагмент має біти 6-11, а необов'язковий третій фрагмент має біти 12-17.

Один фрагмент забезпечує шість біт зберігання, який може вмістити 64 значення. Я хотів би зіставити 64 найменші дисперсії, щоб вони помістилися в цьому одному фрагменті (тобто дисперсії від -32 до +31), тому я буду використовувати кодування ProtoBuf ZigZag, аж до дисперсій від -99 до +98 (оскільки немає потреби для негативної дисперсії понад -99), після чого я перейду до звичайного кодування, компенсованого на 98:  

Варіантність | Зашифроване значення
----------- + ----------------
    0 | 0
   -1 | 1
    1 | 2
   -2 | 3
    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 000011 0
   177 | 010011 1 000100 0
 14444 | 001110 1 100011 1 000011 0

Таким чином, перші три номери зразка телефонної книги кодуються як потік бітів наступним чином:

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

Найкращий сценарій випадку , що телефонна книга дещо рівномірно розподілена, і немає двох телефонних номерів, які мають дисперсію, більше 32, тому для загального 32 + 7 * 999 використовуються 7 біт на число плюс 32 біти для стартового номера = 7025 біт .
Змішаний сценарій , коли дисперсія 800 номерів телефонів відповідає одному шматку (800 * 7 = 5600), 180 номерів вміщуються в два шматки (180 * 2 * 7 = 2520), а 19 номерів вміщуються в три шматки (20 * 3 * 7 = 399) плюс початкові 32 біти, становить 8551 біт .
Найгірший сценарій : 25 номерів поміщаються в три шматки (25 * 3 * 7 = 525 біт), а решта 974 числа вміщуються в два шматки (974 * 2 * 7 = 13636 біт), плюс 32 біти для першого числа для великого всього14193 біт .

   Кількість закодованих чисел |
 1-шматок | 2-шматки | 3-шматки | Всього біт
--------- + ---------- + ---------- + ------------
   999 | 0 | 0 | 7025
   800 | 180 | 19 | 8551
    0 | 974 | 25 | 14193

Я бачу чотири додаткові оптимізації, які можна виконати для подальшого зменшення необхідного місця:

  1. Третьому фрагменту не потрібно повних семи біт, це може бути всього п'ять біт і без біта прапора.
  2. Може бути початковий пропуск чисел, щоб обчислити найкращі розміри для кожного шматка. Можливо, для певної телефонної книги було б оптимальним, щоб перший фрагмент мав 5 + 1 біт, другий 7 + 1 і третій 5 + 1. Це ще більше зменшить розмір до мінімуму 6 * 999 + 32 = 6026 біт, плюс два набори з трьох біт для зберігання розмірів шматок 1 і 2 (розмір фрагмента 3 - це залишок необхідних 16 біт) на загальну кількість з 6032 біт!
  3. Цей же початковий пропуск може обчислити кращий очікуваний приріст, ніж 100 за замовчуванням. Можливо, є телефонна книга, яка починається з 55555-50000, і тому вона має половину діапазону чисел, тому очікуваний приріст повинен бути 50. Або, можливо, є нелінійний розподіл (можливо, стандартне відхилення) та деякі інші оптимальні очікувані збільшення можуть бути використані. Це зменшило б типову дисперсію та могло б дозволити використовувати ще менший перший шматок.
  4. Подальший аналіз може бути зроблений у першому проході, щоб дозволити розділити телефонну книгу, при цьому кожен розділ має власний очікуваний приріст та розмір шматка. Це дозволить отримати менший розмір першого фрагмента для деяких дуже рівномірних частин телефонної книги (зменшення кількості споживаних біт) та більший розмір шматок для нерівномірних деталей (зменшення кількості бітів, витрачених на прапорці продовження).

0

Справжнє питання - це збереження п'ятизначних телефонних номерів.

Хитрість полягає в тому, що вам знадобиться 17 біт, щоб зберігати діапазон чисел від 0..99,999. Але зберігати 17-бітові межі на звичайних 8-байтових словах - це клопот. Ось чому вони запитують, чи можна робити менше ніж 4 к, не використовуючи 32-бітні цілі числа.

Питання: чи можливі всі комбінації чисел?

Через характер телефонної системи може бути менше 65 к. Можливих комбінацій. Я вважаю, що так, оскільки ми говоримо про п'ять останніх позицій у номері телефону, на відміну від коду району або обмінних префіксів.

Запитання: чи буде цей список статичним чи потрібно буде підтримувати оновлення?

Якщо вона статична , то, коли настане час заповнити базу даних, підрахуйте кількість цифр <50 000 та кількість цифр> = 50 000. Виділяють два масиви з uint16відповідної довжини: один для цілих чисел нижче 50000 і один для більш високого безлічі. Під час зберігання цілих чисел у вищому масиві віднімайте 50 000, а при читанні цілих чисел із цього масиву додайте 50 000. Тепер ви зберегли свої 1000 цілих чисел у 2000 8-байтних словах.

Побудова телефонної книги потребуватиме двох вхідних обхідних даних, але пошук в середньому має відбутися в середньому за половину часу, ніж вони з одним масивом. Якщо час пошуку був дуже важливим, ви могли б використовувати більше масивів для менших діапазонів, але я думаю, що при цих розмірах ваше обмеження продуктивності буде витягувати масиви з пам'яті, і 2k, ймовірно, зберігається в кеш-процесорі, якщо не зареєструвати простір для всього, що ви використовуєте ці днів.

Якщо він динамічний , виділіть один масив з 1000 або близько того uint16і додайте числа в упорядкованому порядку. Встановіть перший байт на 50,001, а другий байт встановіть відповідне нульове значення, наприклад NULL або 65,000. Коли ви зберігаєте номери, зберігайте їх у відсортованому порядку. Якщо число нижче 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 до значень масиву.

Це робить вставки дуже дорогими, але пошук у них простий, і ви не збираєтеся витрачати набагато більше 2 кб на зберігання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.