Это вопрос для интервью в Google:
Можно сохранить около тысячи телефонных номеров, каждый из которых состоит из 10 цифр. Вы можете предположить, что первые 5 цифр каждой из тысяч номеров одинаковы. Вам необходимо выполнить следующие операции: a. Найдите, существует ли данный номер. б. Распечатать все числа
Каков наиболее эффективный способ сэкономить место?
Я ответил на хэш-таблицу, а затем на кодирование Хаффмана, но мой интервьюер сказал, что я иду не в правильном направлении. Пожалуйста, помогите мне здесь.
Может ли помочь использование суффикса trie?
В идеале для хранения 1000 номеров требуется 4 байта на номер, поэтому для хранения 1000 номеров потребуется 4000 байтов. Количественно я хочу уменьшить объем хранилища до <4000 байт, как мне объяснил мой интервьюер.
источник
Ответы:
Вот улучшение ответа 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 .
Возможно, что-то пойдет не так с граничным случаем для K = ceil (99999/2 ^ k ), но это достаточно легко исправить.
Наконец, с точки зрения энтропии, невозможно сохранить подмножество из 10 ^ 3 натуральных чисел меньше 10 ^ 5 в меньшем количестве, чем ceil (log [2] (binomial (10 ^ 5, 10 ^ 3)) ) = 8073. Включая 17 нужных нам для первых 5 цифр, все равно остается пробел в 10997 - 8090 = 2907 бит. Это интересная задача - увидеть, есть ли лучшие решения, позволяющие относительно эффективно получать доступ к номерам!
источник
В дальнейшем я рассматриваю числа как целые переменные (в отличие от строк):
Напомним: первые 17 бит - это общий префикс, последующие 1000 групп по 17 бит - это последние пять цифр каждого числа, хранящегося в порядке возрастания.
Всего мы смотрим на 2128 байт на 1000 номеров или 17,017 бит на 10-значный телефонный номер.
Поиск есть
O(log n)
(бинарный поиск), а полное перечисление -O(n)
.источник
k
это не имеет значения.http://en.wikipedia.org/wiki/Acyclic_deterministic_finite_automaton
Однажды у меня было интервью, где меня спросили о структурах данных. Я забыл "Массив".
источник
Я бы, вероятно, рассмотрел возможность использования некоторой сжатой версии Trie (возможно, DAWG, предложенной @Misha).
Это автоматически использовало бы тот факт, что все они имеют общий префикс.
Поиск будет выполняться за постоянное время, а печать - за линейное время.
источник
Я слышал об этой проблеме раньше (но без предположения о том, что первые 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) байта.
источник
32 + 999 * (1 + 7 + variable(0..782))
биты? Каждое из 999 чисел требует представленияvalue / 128
.Это хорошо известная проблема из журнала Bentley Programming Pearls.
Решение: уберите первые пять цифр из чисел, поскольку они одинаковы для всех чисел. Затем используйте побитовые операции для представления оставшихся 9999 возможных значений. Вам понадобится всего 2 ^ 17 бит для представления чисел. Каждый бит представляет собой число. Если бит установлен, номер находится в телефонной книге.
Чтобы напечатать все числа, просто напечатайте все числа, в которых установлен бит, вместе с префиксом. Для поиска заданного числа выполните необходимую битовую арифметику, чтобы проверить поразрядное представление числа.
Вы можете искать число в O (1), и эффективность использования пространства максимальна из-за битового представления.
HTH Крис.
источник
Фиксированное хранилище 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 чисел, выглядит следующим образом:
Теперь давайте проверим, был ли наш выбор размера группы путем округления до 128 бит лучшим выбором для размера группы. При выборе
x
количества битов для представления каждой группы формула размера будет следующей:Минимизация этого уравнения для целых значений
x
даетx=6
, что дает 8 580 бит = 1073 байта . Таким образом, наше идеальное хранилище выглядит следующим образом:Общий объем памяти:
1017 (prefix plus 1's) + 1563 (0's in count) + 6*1000 (offsets) = 8,580 bits = 1,073 bytes
источник
Принимая это как чисто теоретическую проблему и оставляя реализацию без помощи, единственный наиболее эффективный способ - просто проиндексировать все возможные наборы из 10000 последних цифр в гигантской таблице индексации. Предполагая, что у вас точно 1000 чисел, вам понадобится чуть больше 8000 бит, чтобы однозначно идентифицировать текущий набор. Невозможно большее сжатие, потому что тогда у вас будет два набора, которые идентифицируются с одним и тем же состоянием.
Проблема в том, что вам придется представлять каждый из 2 ^ 8000 наборов в вашей программе как lut, и даже Google не сможет удаленно на это.
Поиск будет O (1), все число будет O (n). Вставка будет O (2 ^ 8000), что теоретически равно O (1), но на практике невозможно.
В интервью я бы дал такой ответ только в том случае, если бы был уверен, что компания ищет человека, способного много мыслить нестандартно. В противном случае вы можете выглядеть теоретиком, не заботящимся о реальном мире.
РЕДАКТИРОВАТЬ : Хорошо, вот одна «реализация».
Шаги по созданию реализации:
Это не программа, а своего рода метапрограмма, которая построит гигантскую LUT, которую теперь можно использовать в программе. Постоянные данные программы обычно не учитываются при вычислении эффективности использования пространства, поэтому мы не заботимся об этом массиве при выполнении наших окончательных вычислений.
Вот как использовать эту LUT:
Это означает, что для хранения нам нужно только 8091 бит, что, как мы доказали, является оптимальным кодированием. Однако поиск правильного фрагмента занимает O (100 000 * (100 000 выбирают 1000)), что, согласно математическим правилам, равно O (1), но на практике всегда занимает больше времени, чем время Вселенной.
Однако поиск прост:
Распечатать все числа также просто (и на самом деле требуется O (100000) = O (1), потому что вам всегда нужно проверять все биты текущего фрагмента, поэтому я просчитал это выше).
Я бы не назвал это «реализацией» из-за вопиющего игнорирования ограничений (размер Вселенной и время, в течение которого эта Вселенная жила или эта Земля будет существовать). Однако теоретически это оптимальное решение. Для небольших проблем это действительно возможно, а иногда и будет. Например, сети сортировки являются примером этого способа кодирования и могут использоваться в качестве последнего шага в алгоритмах рекурсивной сортировки для получения значительного ускорения.
источник
Это эквивалентно хранению одной тысячи неотрицательных целых чисел, каждое из которых меньше 100000. Для этого мы можем использовать что-то вроде арифметического кодирования.
В конечном итоге числа будут храниться в отсортированном списке. Отмечу, что ожидаемая разница между соседними числами в списке составляет 100000/1000 = 100, что может быть представлено 7 битами. Также будет много случаев, когда необходимо более 7 бит. Простой способ представить эти менее распространенные случаи - принять схему utf-8, где один байт представляет 7-битное целое число, если не установлен первый бит, и в этом случае следующий байт считывается для получения 14-битного целого числа, если только его первый бит установлен, и в этом случае считывается следующий байт, представляющий 21-битовое целое число.
Таким образом, по крайней мере половина различий между последовательными целыми числами может быть представлена одним байтом, а почти все остальные требуют двух байтов. Для нескольких чисел, разделенных большей разницей, чем 16 384, потребуется три байта, но их не может быть больше 61. В этом случае средняя память будет составлять около 12 бит на число, или немного меньше, или самое большее 1500 байт.
Обратной стороной этого подхода является то, что проверка существования числа теперь выполняется O (n). Однако требования к временной сложности не указаны.
После написания я заметил, что руслик уже предлагал метод разницы выше, разница только в схеме кодирования. Мой, вероятно, проще, но менее эффективен.
источник
Просто чтобы быстро спросить причину, по которой мы не хотели бы преобразовывать числа в основание 36. Это может не сэкономить столько места, но наверняка сэкономит время на поиске, поскольку вы будете смотреть намного меньше, чем на 10 цифр. Или я бы разделил их на файлы в зависимости от каждой группы. поэтому я бы назвал файл (111) -222.txt, а затем я бы сохранял там только числа, которые вписываются в эту группу, а затем их можно было искать в числовом порядке, таким образом, я всегда могу проверить, выходит ли файл. прежде чем я начну более крупный поиск. или, чтобы быть правильным, я бы запустил двоичный поиск файла, чтобы увидеть, выходит ли он. и еще один поиск бонусов по содержимому файла
источник
Почему бы не сделать это простым? Используйте массив структур.
Итак, мы можем сохранить первые 5 цифр как константу, так что пока забудьте о них.
65535 - это максимальное значение, которое может быть сохранено в 16-битном числе, а максимальное число, которое мы можем иметь, составляет 99999, что соответствует 17-битному числу с максимальным значением 131071.
Использование 32-битных типов данных бесполезно, потому что нам нужен только 1 бит из этих дополнительных 16 бит ... поэтому мы можем определить структуру, которая имеет логическое (или символьное) и 16-битное число ...
Предполагая C / C ++
Эта структура занимает всего 3 байта, а нам нужен массив из 1000, итого 3000 байтов. Мы уменьшили общую площадь на 25%!
Что касается хранения чисел, мы можем выполнять простую побитовую математику
И обратное
Чтобы распечатать их все, мы можем использовать простой цикл по массиву. Получение определенного числа, конечно, происходит в постоянное время, поскольку это массив.
Для поиска числа нам нужен отсортированный массив. Поэтому, когда числа сохранены, отсортируйте массив (я бы лично выбрал сортировку слиянием, O (nlogn)). Теперь для поиска я бы выбрал метод сортировки слиянием. Разделите массив и посмотрите, между каким из них находится наш номер. Затем вызовите функцию только для этого массива. Рекурсивно делайте это, пока не найдете совпадение и не вернете индекс, в противном случае он не существует и напечатайте код ошибки. Этот поиск будет довольно быстрым, и в худшем случае все же лучше, чем O (nlogn), поскольку он будет выполняться за меньшее время, чем сортировка слиянием (каждый раз рекурсивно только 1 сторону разделения, а не обе стороны :)), что равно O (nlogn).
источник
Мое решение: в лучшем случае 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 возможных значений.
Я бы стремился выделить минимальное хранилище для кодирования наиболее распространенных различий и расширить хранилище, если я столкнусь с большими различиями (например, ProtoBuf
varint
). Я буду использовать блоки по семь бит, шесть для хранения и дополнительный бит флага в конце, чтобы указать, что это отклонение сохраняется с дополнительным блоком после текущего, максимум до трех блоков (что обеспечит максимум 3 * 6 = 18 бит памяти, что на 262144 возможных значения больше, чем количество возможных отклонений (99100). Каждый дополнительный фрагмент, следующий за поднятым флагом, имеет биты более высокого значения, поэтому первый фрагмент всегда имеет биты 0- 5 необязательный второй фрагмент имеет биты 6-11, а необязательный третий фрагмент имеет биты 12-17.Один фрагмент обеспечивает шесть бит памяти, в которых можно разместить 64 значения. Я хотел бы сопоставить 64 наименьших отклонения, которые поместились бы в этот единственный фрагмент (т.е. отклонения от -32 до +31), поэтому я буду использовать кодировку ProtoBuf ZigZag с отклонениями от -99 до +98 (поскольку нет необходимости для отрицательной дисперсии выше -99), после чего я переключусь на обычную кодировку со смещением на 98:
Некоторые примеры того, как отклонения будут закодированы как биты, включая флаг для указания дополнительного фрагмента:
Таким образом, первые три номера телефонной книги-примера будут закодированы как поток битов следующим образом:
В лучшем случае телефонная книга распределена равномерно, и нет двух телефонных номеров с дисперсией более 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 бит .
Я вижу четыре дополнительных оптимизации, которые можно выполнить, чтобы еще больше уменьшить необходимое пространство:
источник
Настоящий вопрос заключается в хранении пятизначных телефонных номеров.
Хитрость в том, что вам понадобится 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 из сохраненного значения.Ваш массив будет выглядеть примерно так:
Итак, когда вы найдете номер в телефонной книге, вы пройдете по массиву, и, если вы достигнете значения 50 001, вы начнете добавлять 50 000 к значениям массива.
Это делает вставки очень дорогими, но поиск выполняется легко, и вы не собираетесь тратить больше 2 КБ на хранилище.
источник