Я написал несколько функций для сжатия и распаковки списка целых чисел, используя Гамма-кодирование Элиаса:
def _reverseBits(number: int) -> int:
rev = 0
while number:
rev = rev << 1
if number & 1:
rev = rev ^ 1 # bitwise ^ 1 is faster than arithmetic + 1
number = number >> 1
return rev
def compress(numbers: List[int]) -> int:
r'''Calculate the elias gamma encoded bitstream representation of the input list of numbers.
>>> compress([5]) == 0b10100
True
>>> compress([15]) == 0b1111000
True
>>> compress([5, 15]) == 0b10100010100
True
'''
bitstream = 0
bitstream_length = 0
previous_number = 0
for number in numbers:
delta = number - previous_number # store deltas, much smaller numbers
N = delta.bit_length() - 1
encoded_number = _reverseBits(delta) << N # reversed bitstream won't have leading zeroes problem in int container
bitstream += encoded_number << bitstream_length
bitstream_length += (N * 2 + 1)
previous_number = number
return bitstream
def decompress(bitstream: int) -> List[int]:
r'''Convert the elias-encoded bitstream into a list of numbers.
>>> decompress(0b10100010100)
[5, 15]
'''
numbers = []
cumulative_sum = 0
while bitstream:
N = 1
while bitstream & 1 == 0: # count Elias leading zeroes
bitstream = bitstream >> 1
N += 1
mask = (1 << N) - 1
number = _reverseBits(bitstream & mask)
number = number << N - number.bit_length()
cumulative_sum += number
numbers.append(cumulative_sum)
bitstream = bitstream >> N
return numbers
Но это очень медленно для огромных списков ввода
start = 5000
size = int(4e6)
numbers = [i for i in range(start, start + size + 1)]
Я начал тест с входным списком размером четыре миллиона, все элементы были последовательными, на кодирование и декодирование на моем ноутбуке ушло 13 минут.
Я думал о распараллеливании того, что у меня уже есть, но я не думаю, что есть где-нибудь, в чем я действительно смогу работать.
Не могли бы вы помочь мне просмотреть мой алгоритм и посмотреть, есть ли способы улучшить или переработать его, чтобы ускорить мои методы сжатия и распаковки?
Или, возможно, это может быть предел гамма-кодирования Элиаса в Python …
Я также заметил проблему с моей: кажется, что память не освобождается после ее завершения. tracemalloc.get_traced_memory()
после запуска он показывает использование памяти выше, чем закодированное int.
Гамма-кодирование Элиаса кодирует такие числа
+--------+---------------+
| Number | γ encoding |
+--------+---------------+
| 1 | 1 |
| 2 | 0 10 |
| 3 | 0 11 |
| 4 | 00 100 |
| 5 | 00 101 |
| 6 | 00 110 |
| 7 | 00 111 |
| ... | |
| 15 | 000 1111 |
| 16 | 0000 1 0000 |
| ... | |
| 31 | 0000 1 1111 |
| 32 | 00000 10 0000 |
Начальные нули сообщают декодеру, сколько из следующих битов являются частью текущего числа. Таким образом, числа входного списка объединяются в поток битов и могут быть однозначно декодированы обратно в тот же список чисел. [1, 4, 3]
кодирует в 1 00100 011
.
Я храню битовый поток в обратном порядке внутри питона int
, обратное, чтобы не было проблем с начальными нулями контейнера int.
Несмотря на то, что битовые последовательности в таблице выглядят большими из-за ведущих нулей, мое сжатие кодирует только разницу между текущим элементом списка и предыдущим (дельта-кодирование), так что в большинстве случаев число для кодирования и объединения намного меньше, чем фактический элемент списка.
Мои методы намеренно работают только с отсортированными списками.
1 ответ
Довольно странно кодировать как число вместо (битовой) строки. И то, как вы это делаете, требует квадратичного времени в размере вашего списка (потому что оба +
и <<
в bitstream += encoded_number << bitstream_length
линейны). Вы можете сделать это за линейное время, построив строку, а затем превратив ее в число в конце.
Это займет всего несколько секунд для вашего большого теста (здесь для краткости удалите строку документации):
def compress(numbers: List[int]) -> int:
bitstream = []
previous_number = 0
for number in numbers:
delta = number - previous_number # store deltas, much smaller numbers
N = delta.bit_length() - 1
encoded_number = bin(delta)[:1:-1] + '0' * N
bitstream.append(encoded_number)
previous_number = number
bitstream.reverse()
return int(''.join(bitstream), 2)
Соответственно, для декомпрессии я бы сначала превратил число в строку, а затем работал бы с этим.
Оптимизированная версия должна занять меньше секунды:
def compress(numbers: List[int]) -> int:
bitstream = []
append = bitstream.append
memo = {}
lookup = memo.get
previous_number = 0
for number in numbers:
delta = number - previous_number # store deltas, much smaller numbers
if not (encoded_number := lookup(delta)):
N = delta.bit_length() - 1
memo[delta] = encoded_number = bin(delta)[:1:-1] + '0' * N
append(encoded_number)
previous_number = number
bitstream.reverse()
return int(''.join(bitstream), 2)
Насколько «маленький», и какое время у вас получилось для обоих? В моем собственном тестировании мой всегда быстрее, даже когда в списке есть только один номер.
— Мануэль
Когда я раньше пробовал со строками, я использовал конкатенацию строк. Я понятия не имел, что создание такого списка строк будет настолько быстрым.
— theonlygusti
«На 100 мс быстрее» мало что говорит, а сколько было раз? Кстати, я немного улучшил его сейчас. Конкатенация строк, как я думаю, вы пытались стать квадратичным, тоже.
— Мануэль
Время было 360 для int, 270 для str для моего набора элементов 469
— theonlygusti
Теперь я переворачиваю список вместо того, чтобы использовать обратный итератор.
— Мануэль
Показать 8 больше комментариев