Самый эффективный способ хранить тысячи телефонных номеров

94

Это вопрос для интервью в Google:

Можно сохранить около тысячи телефонных номеров, каждый из которых состоит из 10 цифр. Вы можете предположить, что первые 5 цифр каждой из тысяч номеров одинаковы. Вам необходимо выполнить следующие операции: a. Найдите, существует ли данный номер. б. Распечатать все числа

Каков наиболее эффективный способ сэкономить место?

Я ответил на хэш-таблицу, а затем на кодирование Хаффмана, но мой интервьюер сказал, что я иду не в правильном направлении. Пожалуйста, помогите мне здесь.

Может ли помочь использование суффикса trie?

В идеале для хранения 1000 номеров требуется 4 байта на номер, поэтому для хранения 1000 номеров потребуется 4000 байтов. Количественно я хочу уменьшить объем хранилища до <4000 байт, как мне объяснил мой интервьюер.

принцесса персии
источник
28
Я бы ответил, что, используя обычную базу данных, вы можете хранить их как текст, даже тысячи / миллионы, и операции поиска по-прежнему будут очень быстрыми. Я не рекомендую делать «умные» вещи, поскольку всю систему придется переделывать, если они захотят в будущем поддерживать международные номера, или если начнут появляться телефонные номера, начинающиеся с «0», или если правительство решит изменить формат номера телефона и т. д.
Томас Бонини
1
@AndreasBonini: Я бы, наверное, ответил так, если бы я не проходил собеседование в такой компании, как Google или Facebook, стандартные решения просто не годятся. Хотя у postgres, например, тоже есть попытки, я не был бы уверен, что они сокращают пропускную способность данных, которую необходимо использовать Google.
LiKao 08
1
@LiKao: имейте в виду, что в ОП конкретно указано «около тысячи цифр»
Томас Бонини
@AndreasBonini: Да, это тоже могло быть тестом, который знает собеседник, чтобы правильно интерпретировать такие ограничения и выбрать лучшее решение в соответствии с этим.
LiKao 08
4
«Эффективный» в этом вопросе действительно нужно определить - каким образом? пространство, время, оба?
matt b

Ответы:

36

Вот улучшение ответа aix . Рассмотрите возможность использования трех «слоев» для структуры данных: первый - это константа для первых пяти цифр (17 бит); Таким образом, с этого момента для каждого телефонного номера остались только оставшиеся пять цифр. Мы рассматриваем эти оставшиеся пять цифр как 17-битные двоичные целые числа и сохраняем k этих битов одним методом и 17 - k = m другим методом, определяя k в конце, чтобы минимизировать требуемое пространство.

Сначала мы сортируем телефонные номера (все сокращаются до 5 десятичных знаков). Затем мы подсчитываем, сколько телефонных номеров у которых двоичное число, состоящее из первых m битов, все равно 0, для скольких телефонных номеров первые m битов не более 0 ... 01, для скольких телефонных номеров первые m биты составляют не более 0 ... 10 и так далее, вплоть до количества телефонных номеров, для которых первые m битов равны 1 ... 11 - последнее количество равно 1000 (десятичное). Таких счетчиков 2 ^ m , и каждое из них не превышает 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 ^ m - 1 counts нам нужен только ceil (99999/2 ^ k ). Это означает, что формула становится

17 + ceil (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 , оба из первого блока бит. Теперь мы проверяем между a[М-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] (binomial (10 ^ 5, 10 ^ 3)) ) = 8073. Включая 17 нужных нам для первых 5 цифр, все равно остается пробел в 10997 - 8090 = 2907 бит. Это интересная задача - увидеть, есть ли лучшие решения, позволяющие относительно эффективно получать доступ к номерам!

Эрик П.
источник
4
Структура данных, которую вы здесь описываете, на самом деле представляет собой только очень эффективную версию trie, которая использует только минимальное количество необходимых для индексации и только два уровня. На практике было бы неплохо посмотреть, сможет ли это превзойти дерево с большим количеством уровней, но я думаю, что это во многом зависит от распределения номеров (в реальном времени номера телефонов не являются полностью случайными, а лишь почти).
LiKao 08
Привет, Эрик, раз уж вы сказали, что вам будет интересно увидеть другие альтернативы, ознакомьтесь с моим решением. Он решает это за 8 580 бит, что всего на 490 бит меньше теоретического минимума. Искать отдельные числа немного неэффективно, но хранилище очень компактное.
Briguy37,
1
Я полагаю, что здравомыслящий интервьюер предпочел бы ответ «дерево» вместо «сложная база данных, сделанная на заказ». Если вы хотите продемонстрировать свои 133 хакерские навыки, вы можете добавить - «при необходимости можно было бы создать конкретный древовидный алгоритм для этого особого случая».
KarlP
Привет, не могли бы вы объяснить, как для хранения 5 цифр требуется 17 бит?
Tushar Banne
@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).

NPE
источник
Гм, а где космическая сложность?
aioobe
Слишком много времени для создания (O (log (n) * n k) (k - длина) для сортировки, по сравнению с O (n k) для создания дерева). Также пространство далеко не оптимально, потому что более длинные общие префиксы хранятся индивидуально. Время поиска тоже не оптимальное. Для таких строковых данных легко забыть о длине чисел, которая доминирует при поиске. Т.е. двоичный поиск - это O (log (n) * k), тогда как для дерева требуется только O (k). Вы можете сократить эти выражения, когда k является постоянным, но это должно показать общую проблему при рассуждении о структурах данных, хранящих строки.
LiKao
@LiKao: Кто что сказал о струнах? Я имею дело исключительно с целочисленными переменными, поэтому kэто не имеет значения.
NPE
1
Хорошо, тогда я неправильно понял ответ. Тем не менее, общие части не хранятся вместе, поэтому остается вопрос экономии места. Для 1000 5-значных номеров будет довольно много общих префиксов, поэтому их сокращение очень поможет. Также в случае чисел у нас есть 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 это определенно путь. Я выучил это под другим именем, деревом библиотеки или деревом лексического поиска или чем-то еще, когда был студентом (если кто-то помнит это старое имя, пожалуйста, сообщите).
Valmond
6
Это не соответствует требованию в 4000 байт. Для одного только хранилища указателей в худшем случае вам понадобится 1 указатель для 1-4-го перехода на следующий уровень, 10 указателей для 5-го, 100 для 6-го и 1000 для 7-го, 8-го и 9-го уровней. , что доводит общее количество указателей до 3114. Это дает по крайней мере 3114 различных ячеек памяти, необходимых для указателей, что означает, что вам потребуется не менее 12 бит для каждого указателя. 12 * 3114 = 37368 бит = 4671 байт> 4000 байт, и это даже не влияет на то, как вы представляете значение каждого листа!
Briguy37 07
15

Я бы, вероятно, рассмотрел возможность использования некоторой сжатой версии Trie (возможно, DAWG, предложенной @Misha).

Это автоматически использовало бы тот факт, что все они имеют общий префикс.

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

aioobe
источник
Вопрос в том, как лучше всего использовать пространство для хранения данных. Не могли бы вы дать приблизительную оценку того, сколько места потребуется для этого метода для 1000 телефонных номеров? Спасибо.
NPE
Пространство для дерева не превышает O (n * k), где n - количество строк, а k - длина каждой строки. Принимая во внимание, что вам не нужны 8-битные символы для представления чисел, я бы предложил хранить 4 шестнадцатеричных индекса и один для оставшегося бита. Таким образом, вам нужно максимум 17 бит на число. Поскольку во всех случаях у вас будут конфликты на всех уровнях с этим кодированием, вы действительно можете опуститься ниже этого. Ожидая, что мы сохраним 1000 чисел, мы уже можем сэкономить 250 бит для конфликтов на первом уровне. Лучше всего проверить правильность кодирования на примере данных.
LiKao 07
@LiKao, верно, и, отметив, что, например, 1000 чисел не могут иметь более 100 различных последних двух цифр, дерево может значительно свернуться на последних уровнях.
aioobe
@aioobe: Листья могут быть свернуты на последнем уровне, потому что там нет детей. Однако листья на предпоследнем уровне нуждаются в 2 ^ 10 = 1024 состояниях (каждая последняя цифра может быть включена или выключена), поэтому в этом случае ее нельзя уменьшить, поскольку имеется только 1000 чисел. Это означает, что количество указателей наихудшего случая остается на уровне 3114 (см. Мой комментарий к ответу Миши), в то время как количество необходимых листьев идет на 5 + 10 + 100 + 1000 + 1000 + 10 = 2125, что не меняет необходимых 12 байтов для каждого указатель. Таким образом, это по-прежнему ставит trie-решение на 4671 байт, учитывая только указатели.
Briguy37
@ Briguy37, я не уверена, что понимаю твой аргумент " каждая последняя цифра может быть включена или выключена ". Все числа состоят из 10 цифр, верно?
aioobe
15

Я слышал об этой проблеме раньше (но без предположения о том, что первые 5 цифр совпадают), и самым простым способом решения этой проблемы было Rice Coding :

1) Поскольку порядок не имеет значения, мы можем отсортировать их и сохранить только различия между последовательными значениями. В нашем случае средняя разница будет 100000/1000 = 100.

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

РЕДАКТИРОВАТЬ: оценка кодирования Райса с базой 128 (не потому, что это даст лучшие результаты, а потому, что ее легче вычислить):

Мы сохраним первое значение как есть (32 бита).
Остальные 999 значений являются различиями (мы ожидаем, что они будут небольшими, в среднем 100) будут содержать:

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

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

Итак, результат: 32 + 999 * (1 + 7) + переменная (0..782) бит = 1003 + переменная (0..98) байта.

руслик
источник
Не могли бы вы подробнее рассказать о способе кодирования и окончательном расчете размера. 1101 байт или 8808 битов кажутся очень близкими к теоретическому пределу в 8091 бит, поэтому я очень удивлен, что на практике можно добиться чего-то подобного.
LiKao 08
Разве не 32 + 999 * (1 + 7 + variable(0..782))биты? Каждое из 999 чисел требует представления value / 128.
Кирк Бродхерст,
1
@ Кирк: нет, если все они в диапазоне 5 цифр. Это потому, что мы ожидаем, что сумма всех этих различий (помните, мы кодируем различия между последовательными значениями, а не между первым и N-м значением) будет ниже 100000 (даже в худшем случае)
ruslik
Для представления первого значения вам потребуется 34 бита вместо 32 (9 999 999 999> 2 ^ 32 = 4 294 967 296). Кроме того, максимальная разница будет от 00000 до 99001, поскольку числа уникальны, что добавит 774 единиц вместо 782 для базы 128. Таким образом, ваш диапазон для хранения 1000 чисел для базы 128 составляет 8026-8800 бит или 1004-1100 байтов. 64-битная база дает лучшее хранилище с диапазоном от 879 до 1072 байт.
Briguy37
1
@raisercostin: это то, что спросил Кирк. В вашем примере, при кодировании разницы в 20k между первыми двумя значениями, в будущем будет возможно только 80k максимального диапазона. Это будет использовать до 20k / 128 = 156 унарных битов из максимальных 782 (что соответствует 100k)
ruslik
7

Это хорошо известная проблема из журнала Bentley Programming Pearls.

Решение: уберите первые пять цифр из чисел, поскольку они одинаковы для всех чисел. Затем используйте побитовые операции для представления оставшихся 9999 возможных значений. Вам понадобится всего 2 ^ 17 бит для представления чисел. Каждый бит представляет собой число. Если бит установлен, номер находится в телефонной книге.

Чтобы напечатать все числа, просто напечатайте все числа, в которых установлен бит, вместе с префиксом. Для поиска заданного числа выполните необходимую битовую арифметику, чтобы проверить поразрядное представление числа.

Вы можете искать число в O (1), и эффективность использования пространства максимальна из-за битового представления.

HTH Крис.

Крис
источник
3
Это был бы хороший подход для плотного набора чисел. К сожалению, здесь набор очень скудный: из 100000 возможных номеров всего 1000. Следовательно, этот подход потребует в среднем 100 бит на номер. См. Мой ответ для альтернативы, для которой требуется всего ~ 17 бит.
NPE
1
Разве время, необходимое для печати всех чисел, не будет пропорционально 100000 вместо 1000?
aioobe 07
Комбинируя две идеи, вы в основном сразу получаете дерево. Использование битового вектора со 100000 записей - это слишком много места и занимает много места. Однако поиск 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 из-за того, как мы выбрали наши группы.

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

Смещение:
наконец, формат каждого числа будет просто 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 бит = 1073 байта . Таким образом, наше идеальное хранилище выглядит следующим образом:

  • Размер группы: 2 ^ 6 = 64
  • Количество групп: 1562
  • Общий объем памяти:

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

Бригуй37
источник
1

Принимая это как чисто теоретическую проблему и оставляя реализацию без помощи, единственный наиболее эффективный способ - просто проиндексировать все возможные наборы из 10000 последних цифр в гигантской таблице индексации. Предполагая, что у вас точно 1000 чисел, вам понадобится чуть больше 8000 бит, чтобы однозначно идентифицировать текущий набор. Невозможно большее сжатие, потому что тогда у вас будет два набора, которые идентифицируются с одним и тем же состоянием.

Проблема в том, что вам придется представлять каждый из 2 ^ 8000 наборов в вашей программе как lut, и даже Google не сможет удаленно на это.

Поиск будет O (1), все число будет O (n). Вставка будет O (2 ^ 8000), что теоретически равно O (1), но на практике невозможно.

В интервью я бы дал такой ответ только в том случае, если бы был уверен, что компания ищет человека, способного много мыслить нестандартно. В противном случае вы можете выглядеть теоретиком, не заботящимся о реальном мире.

РЕДАКТИРОВАТЬ : Хорошо, вот одна «реализация».

Шаги по созданию реализации:

  1. Возьмите постоянный массив размером 100 000 * (1000 выбирает 100 000) бит. Да, я знаю, что этому массиву потребуется больше места, чем атомам во Вселенной, на несколько порядков.
  2. Разделите этот большой массив на куски по 100 000 каждый.
  3. В каждом блоке хранится битовый массив для одной конкретной комбинации последних пяти цифр.

Это не программа, а своего рода метапрограмма, которая построит гигантскую LUT, которую теперь можно использовать в программе. Постоянные данные программы обычно не учитываются при вычислении эффективности использования пространства, поэтому мы не заботимся об этом массиве при выполнении наших окончательных вычислений.

Вот как использовать эту LUT:

  1. Когда кто-то дает вам 1000 номеров, вы сохраняете по отдельности первые пять цифр.
  2. Узнайте, какой из фрагментов вашего массива соответствует этому набору.
  3. Сохраните номер набора в одном 8074-битном числе (назовите это c).

Это означает, что для хранения нам нужно только 8091 бит, что, как мы доказали, является оптимальным кодированием. Однако поиск правильного фрагмента занимает O (100 000 * (100 000 выбирают 1000)), что, согласно математическим правилам, равно O (1), но на практике всегда занимает больше времени, чем время Вселенной.

Однако поиск прост:

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

Распечатать все числа также просто (и на самом деле требуется O (100000) = O (1), потому что вам всегда нужно проверять все биты текущего фрагмента, поэтому я просчитал это выше).

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

LiKao
источник
1
Каков наиболее эффективный способ сэкономить место?
Sven
1
При выполнении расчетов пространства времени выполнения можно легко доказать, что это наиболее эффективный способ экономии пространства, поскольку вы перечисляете любое возможное состояние системы только одним числом. Для этой проблемы не может быть кодировки меньшего размера. Уловка этого ответа заключается в том, что размер программы почти никогда не учитывается при выполнении вычислений (попробуйте найти любой ответ, который учитывает это, и вы поймете, что я имею в виду). Таким образом, для любой проблемы, имеющей ограниченный размер, вы всегда можете перечислить все состояния, чтобы получить наиболее экономичный способ ее решения.
LiKao 08
1

Это эквивалентно хранению одной тысячи неотрицательных целых чисел, каждое из которых меньше 100000. Для этого мы можем использовать что-то вроде арифметического кодирования.

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

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

Обратной стороной этого подхода является то, что проверка существования числа теперь выполняется O (n). Однако требования к временной сложности не указаны.

После написания я заметил, что руслик уже предлагал метод разницы выше, разница только в схеме кодирования. Мой, вероятно, проще, но менее эффективен.

Кросби
источник
1

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

WojonsTech
источник
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).

jyore
источник
0

Мое решение: в лучшем случае 7,025 бит / число, в худшем случае 14,193 бит / число, приблизительное среднее значение 8,551 бит / число. Потоковое кодирование, без произвольного доступа.

Еще до того, как прочитать ответ ruslik, я сразу подумал о кодировании разницы между каждым числом, поскольку он будет небольшим и должен быть относительно согласованным, но решение также должно быть способно учесть наихудший сценарий. У нас есть пространство из 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 возможных значений.

Я бы стремился выделить минимальное хранилище для кодирования наиболее распространенных различий и расширить хранилище, если я столкнусь с большими различиями (например, ProtoBufvarint ). Я буду использовать блоки по семь бит, шесть для хранения и дополнительный бит флага в конце, чтобы указать, что это отклонение сохраняется с дополнительным блоком после текущего, максимум до трех блоков (что обеспечит максимум 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
----------- | --------------- Конец зигзага
   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

Таким образом, первые три номера телефонной книги-примера будут закодированы как поток битов следующим образом:

БИН 000101001011001000100110010000011001 000110 1 010110 1 00001 0
PH # 55555-12345 55555-12448 55555-12491
ПОС 1 2 3

В лучшем случае телефонная книга распределена равномерно, и нет двух телефонных номеров с дисперсией более 32, поэтому будет использоваться 7 бит на номер плюс 32 бита для начального номера, всего 32 + 7 * 999. = 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 до 999999. Но сохранить 17-битные границы на обычных 8-байтовых границах слова - проблема. Вот почему они спрашивают, можете ли вы сделать меньше 4 КБ, не используя 32-битные целые числа.

Вопрос: возможны ли все комбинации цифр?

Из-за особенностей телефонной системы может быть менее 65 тысяч возможных комбинаций. Я предполагаю, что да, потому что мы говорим о последних пяти позициях в номере телефона, а не о коде города или обменных префиксах.

Вопрос: будет ли этот список статическим или потребуется поддержка обновлений?

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

Для построения телефонной книги потребуется два обхода ввода, но в среднем поиск должен происходить в два раза быстрее, чем при использовании одного массива. Если бы время поиска было очень важным, вы могли бы использовать больше массивов для меньших диапазонов, но я думаю, что при этих размерах ваша оценка производительности будет вытягивать массивы из памяти, и 2k, вероятно, будет храниться в кеше ЦП, если не зарегистрируйте пространство для чего-либо, что вы бы использовали дней.

Если он динамический , выделите один массив из 1000 или около того uint16и добавьте числа в отсортированном порядке. Установите для первого байта значение 50 001, а для второго байта установите соответствующее значение NULL, например 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 КБ на хранилище.

данниман
источник