Сжатие больших целых чисел в наименьшую возможную строку - PullRequest
17 голосов
/ 05 мая 2011

У меня есть набор из 10 цифр, которые я передаю в URL. Что-то вроде: "4294965286", "2292964213". Они всегда будут положительными и всегда будут 10 цифрами.

Я бы хотел сжать эти целые числа в наименьшую возможную форму, которую все еще можно использовать в URL-адресе (иначе букв и цифр вполне нормально), а затем распаковать их позже. Я смотрел на использование gzipstream, но он создает большие строки, а не короче.

В настоящее время я использую asp.net, поэтому было бы лучше использовать vb.net или c #.

Спасибо

Ответы [ 5 ]

27 голосов
/ 05 мая 2011

Да. GZIP - это алгоритм сжатия , который требует сжимаемых данных и накладных расходов (кадрирование, словари и т. Д.). Вместо этого следует использовать алгоритм кодирования .

«Простой» метод заключается в использовании base-64 кодировки .

То есть, преобразуйте число (которое представлено как основание 10 в строке) в фактическую серию байтов, которые представляют число (5 байтов будут покрывать десятичное число из 10 цифр), а затем в результате получится основание-64. Каждый символ base-64 хранит 6 бит информации (до десятичных чисел ~ 3.3 бит / символ) и, таким образом, приводит к размеру примерно чуть более половины (в этом случае требуется 6 * выходных символов base-64).

Кроме того, поскольку длины ввода / вывода можно получить из самих данных, «123» может быть первоначально (до кодирования base-64) преобразовано в 1 байт, «30000» - в 2 байта и т. Д. Это будет полезно если не все числа примерно одинаковой длины.

Удачного кодирования.


* Для использования base-64 требуется 6 выходных символов .

Редактировать: Первоначально я ошибся , где я сказал «2,3 бит / символ» для десятичного числа и предложил, чтобы требовалось менее половины символов. Я обновил ответ выше и покажу здесь (должно быть правильно) математику, где lg(n) - это лог для базы 2.

Количество входных битов, необходимых для представления входного номера, составляет bits/char * chars -> lg(10) * 10 (или просто lg(9999999999)) -> ~33.2 bits. Используя манипуляции jball, чтобы сначала сдвинуть число, требуемое количество битов составляет lg(8999999999) -> ~33.06 bits. Однако это преобразование не может увеличить эффективность в данном конкретном случае (количество входных битов должно быть уменьшено до 30 или ниже, чтобы изменить ситуацию).

Поэтому мы пытаемся найти x (количество символов в кодировке base-64), такое что:

lg(64) * x = 33.2 -> 6 * x = 33.2 -> x ~ 5.53. Конечно, пять с половиной символов не имеют смысла, поэтому мы выбираем 6 как число максимум символов, необходимое для кодирования значения до 999999999 в кодировке Base-64. Это чуть больше половины оригинальных 10 символов.

Однако следует отметить, что для получения только 6 символов в выводе base-64 требуется нестандартный кодировщик base-64 или немного манипуляций (большинство кодировщиков base-64 работают только с целыми байтами). Это работает, потому что из первоначальных 5 «требуемых байтов» используются только 34 из 40 битов (первые 6 битов всегда равны 0). Для кодирования всех 40 битов потребуется 7 символов base-64.

Вот модификация кода, который Гуффа разместил в своем ответе (если вам это нравится, иди и проголосуйте), который требует только 6 символов base-64. Пожалуйста, смотрите другие примечания в ответе Guffa и Base64 для приложений URL , так как в приведенном ниже методе не используется сопоставление с URL-адресами.

byte[] data = BitConverter.GetBytes(value);
// make data big-endian if needed
if (BitConverter.IsLittleEndian) {
   Array.Reverse(data);
}
// first 5 base-64 character always "A" (as first 30 bits always zero)
// only need to keep the 6 characters (36 bits) at the end 
string base64 = Convert.ToBase64String(data, 0, 8).Substring(5,6);

byte[] data2 = new byte[8];
// add back in all the characters removed during encoding
Convert.FromBase64String("AAAAA" + base64 + "=").CopyTo(data2, 0);
// reverse again from big to little-endian
if (BitConverter.IsLittleEndian) {
   Array.Reverse(data2);
}
long decoded = BitConverter.ToInt64(data2, 0);

Делаем его "красивее"

Поскольку в base-64 определено использование 6 символов, любой вариант кодирования, который все еще кодирует входные биты в 6 символов, будет создавать такой же маленький выходной сигнал. Использование кодировки base-32 не совсем удастся, поскольку в кодировке base-32 6 символов могут хранить только 30 бит информации (lg(32) * 6).

Однако тот же размер вывода может быть достигнут с помощью пользовательской кодировки base-48 (или 52/62). (Преимущество базы 48-62 состоит в том, что они требуют только подмножества буквенно-цифровых символов и не нуждаются в символах; при желании можно избежать «неоднозначных» символов, таких как 1 и «I», для вариантов). В системе base-48 6 символов могут кодировать ~ 33,5 бит (lg(48) * 6) информации, что чуть выше требуемых ~ 33,2 (или ~ 33,06) бит (lg(10) * 10).

Вот подтверждение концепции:

// This does not "pad" values
string Encode(long inp, IEnumerable<char> map) {
    Debug.Assert(inp >= 0, "not implemented for negative numbers");

    var b = map.Count();
    // value -> character
    var toChar = map.Select((v, i) => new {Value = v, Index = i}).ToDictionary(i => i.Index, i => i.Value);
    var res = "";
    if (inp == 0) {
      return "" + toChar[0];
    }
    while (inp > 0) {
      // encoded least-to-most significant
      var val = (int)(inp % b);
      inp = inp / b;
      res += toChar[val];
    }
    return res;
}

long Decode(string encoded, IEnumerable<char> map) {
    var b = map.Count();
    // character -> value
    var toVal = map.Select((v, i) => new {Value = v, Index = i}).ToDictionary(i => i.Value, i => i.Index);      
    long res = 0;
    // go in reverse to mirror encoding
    for (var i = encoded.Length - 1; i >= 0; i--) {
      var ch = encoded[i];
      var val = toVal[ch];
      res = (res * b) + val;
    }
    return res;
}

void Main()
{
    // for a 48-bit base, omits l/L, 1, i/I, o/O, 0
    var map = new char [] {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K',
        'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
        'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
        'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't',
        'u', 'v', 'x', 'y', 'z', '2', '3', '4',
    };
    var test = new long[] {0, 1, 9999999999, 4294965286, 2292964213, 1000000000};
    foreach (var t in test) {
        var encoded = Encode(t, map);
        var decoded = Decode(encoded, map);
        Console.WriteLine(string.Format("value: {0} encoded: {1}", t, encoded));
        if (t != decoded) {
            throw new Exception("failed for " + t);
        }
    }
}

Результат:

value: 0 encoded: A
value: 1 encoded: B
value: 9999999999 encoded: SrYsNt
value: 4294965286 encoded: ZNGEvT
value: 2292964213 encoded: rHd24J
value: 1000000000 encoded: TrNVzD

Выше рассмотрен случай, когда числа являются «случайными и непрозрачными»; то есть ничего не может быть определено с внутренностями числа. Однако, если определенная структура существует (например, 7-й, 8-й и 9-й биты всегда равны нулю, а 2-й и 15-й биты всегда одинаковы), тогда и только тогда, когда 4 или более бит информации могут быть исключены от ввода - потребуется всего 5 символов base-64. Дополнительные сложности и опора на структуру, скорее всего, перевесят любое предельное усиление.

4 голосов
/ 03 ноября 2016

Я думаю, что вы ищете хеш-идентификаторы: http://hashids.org/

Они имеют реализации на многих языках, хотя, похоже, C # не один из них.

Я сделалпример для вас на JavaScript: http://codepen.io/codycraven/pen/MbWwQm

var hashids = new Hashids('my salt', 1, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890');
var input = 4294965286;
var hex = input.toString(16); // 8 characters: fffff826
var hashid = hashids.encode(input); // 7 characters: 0LzaR1Y
var base64 = window.btoa(input).replace(/=+/, ''); // 14 characters: NDI5NDk2NTI4Ng

Обратите внимание, что библиотеки HashIDs защищают ваши хэши от нецензурной лексики.

4 голосов
/ 05 мая 2011

Вы можете использовать кодировку base64, чтобы уменьшить данные до семи символов.Вам нужно пять байтов для представления числа, и они могут быть закодированы в восемь символов с использованием base64, но этот последний символ всегда является заполнителем =, поэтому его можно удалить:

long value = 4294965286;

// get the value as an eight byte array (where the last three are zero)
byte[] data = BitConverter.GetBytes(value);
// encode the first five bytes
string base64 = Convert.ToBase64String(data, 0, 5).Substring(0, 7);
Console.WriteLine(base64);

Вывод:

Jvj//wA

Чтобы декодировать текст, вы снова добавляете =, декодируете его и читаете его как число:

// create an eight byte array
byte[] data = new byte[8];
// decode the text info five bytes and put in the array
Convert.FromBase64String(base64 + "=").CopyTo(data, 0);
// get the value from the array
long value = BitConverter.ToInt64(data, 0);

Console.WriteLine(value);

Вывод:

4294965286

Два символа, которые использует base64, не подходят для использования в URL, поэтому вы можете заменить их другими символами, а затем заменить их обратно.Например, символы + и / можно заменить на - и _.

.
3 голосов
/ 05 мая 2011

В дополнение к изменению базы кодировки ( pst и у меня были одинаковые мысли примерно в одно и то же время), так как все ваши числа - 10 десятичных цифр, вы можете вычесть самое маленькое 10-значное число 10E9) от каждого номера до его кодирования, а затем добавить его обратно после декодирования. Это сместит ваши закодированные числа в диапазон от 0 до 8999999999, что позволит использовать меньшие строки после изменения базы.

2 голосов
/ 05 мая 2011

Как насчет преобразования большого числа в формулу: вместо 21312312312 я мог бы использовать 4 ^ 34. http://mathforum.org/library/drmath/view/65726.html

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...