Как вы конвертируете длинный Java в *unsigned* base-X String (и обратно)?

[РЕДАКТИРОВАТЬ] Я НЕ принимаю никакого ответа, который включает BigInteger, или другой столь же неэффективный метод. Пожалуйста, прочитайте вопрос, прежде чем ответить!

К сожалению, Java не поддерживает типы чисел без знака. Вы можете преобразовать byte, short или int в unsigned, используя следующий больший тип, например:

short s = -10;
int unsigned_short = s & 0xFFFF;

Но вы не можете делать это долго, так как нет более крупного типа.

Итак, как преобразовать long со знаком в "unsigned" base-X, в моем случае base-36, и обратно? Класс Long имеет эти методы, но обрабатывает длинные как подписанные просто потому, что они есть.

Я мог бы сделать это, используя некоторые манипуляции и BigInteger, но BigInteger невероятно медленный и создает мусор посредством временного создания BigInteger. И я собираюсь сделать много этих преобразований (я думаю). Мне нужен алгоритм, который так же эффективен, как стандартная реализация Long.toString(long i, int radix).

Пытаясь адаптировать код Long.toString(), я прихожу к:

final int RADIX = 36;
final char[] DIGITS = { '0', ... , 'Z' };
long value = 100;
if (value == 0) {
    return "0";
} else {
    char[] buf = new char[13];
    int charPos = 12;
    long i = value;
    while (i != 0) {
        buf[charPos--] = DIGITS[Math.abs((int) (i % RADIX))];
        i /= RADIX;
    }
    return new String(buf, charPos + 1, (12 - charPos));
}

Но он не обрабатывает отрицательные значения правильно, несмотря на Math.abs().

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

[EDIT] На самом деле, я только что посмотрел код для Long.parseLong(String s, int radix), и он выглядит сложнее, чем Long.toString(long i, int radix).

5 ответов

Решение
    long l = 0xffffffffffffffffL; // any long, e.g. -1

    // to string
    BigInteger bi = new BigInteger(Long.toString(l & ~(1L << 63)));
    if (l < 0) bi = bi.setBit(64);
    final String b36 = bi.toString(36);
    System.out.println("original long:" + l);
    System.out.println("result 36: " + b36);

    // parse
    final BigInteger parsedBi = new BigInteger(b36, 36);

    l = parsedBi.longValue();
    if (parsedBi.testBit(64)) l = l | (1L << 63);
    System.out.println("parsed long = " + l);

Бенчмаркинг (один миллион операций):

    // toString
    long l = 0x0ffffffffffffeffL;
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) toStringBi(l);
        System.out.println("BigInteger time = " + 
            (System.currentTimeMillis() - start) + " ms.");
    }
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) Long.toString(l, 36);
        System.out.println("Long.toString time = " + 
           (System.currentTimeMillis() - start) + "ms.");
    }
    // Parsing
    final String b36 = toStringBi(l);
    final String long36 = Long.toString(l, 36);
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            final BigInteger parsedBi = new BigInteger(b36, 36);
            l = parsedBi.longValue();
            if (parsedBi.testBit(64)) l = l | (1L << 63);
        }
        System.out.println("BigInteger.parse time = " 
            + (System.currentTimeMillis() - start) + " ms.");
    }
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) Long.parseLong(long36, 36);
        System.out.println("Long.parseLong time = " 
            + (System.currentTimeMillis() - start) + "ms.");
    }
  • Время BigInteger = 1027 мс
  • Long.toString time = 244 мс.
  • BigInteger.parse время = 297 мс.
  • Long.parseLong time = 132ms.

Другой вариант - использовать UnsignedLongs из guava-библиотек Google (которые также имеют много других полезностей):

String s = UnsignedLongs.toString( -1L, Character.MAX_RADIX );

а также

long l = UnsignedLongs.parseUnsignedLong( "2jsu3j", 36 );

Добавленный к тесту от +EugeneRetunsky (см. Ниже), это дает следующие времена на моей машине:

  • Время BigInteger (1-й прогон) = 1306 мс.
  • Время BigInteger (второй прогон) = 1075 мс.
  • Long.toString time = 422 мс.
  • UnsignedLongs.toString time = 445мс.
  • BigInteger.parse время = 298 мс.
  • Long.parseLong time = 164 мс.
  • UnsignedLongs.parseUnsignedLong time = 107мс.

Из любопытства я позволил первому тесту пройти дважды, чтобы проверить, не улучшит ли это время. Это последовательно (до ~400 мс на моей машине), также для случая UnsignedLongs. Другие опции, кажется, больше не выигрывают от компилятора горячей точки.

public class UnsignedLongsTest {
private static String toStringBi( long l ) {
    BigInteger bi = new BigInteger(Long.toString(l & ~(1L << 63)));
    if (l < 0) {
        bi = bi.setBit(64);
    }
    final String b36 = bi.toString(36);
    return b36;
}

public static void main( String[] args ) {
    // toString
    long l = 0x0ffffffffffffeffL;
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            toStringBi(l);
        }
        System.out.println("BigInteger time (1st run) = " +
                (System.currentTimeMillis() - start) + " ms.");
    }
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            toStringBi(l);
        }
        System.out.println("BigInteger time (2nd run) = " +
                (System.currentTimeMillis() - start) + " ms.");
    }
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            Long.toString(l, 36);
        }
        System.out.println("Long.toString time = " +
           (System.currentTimeMillis() - start) + "ms.");
    }
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            UnsignedLongs.toString(l, 36);
        }
        System.out.println("UnsignedLongs.toString time = " +
                (System.currentTimeMillis() - start) + "ms.");
    }
    // Parsing
    final String b36 = toStringBi(l);
    final String long36 = Long.toString(l, 36);
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            final BigInteger parsedBi = new BigInteger(b36, 36);
            l = parsedBi.longValue();
            if (parsedBi.testBit(64)) {
                l = l | (1L << 63);
            }
        }
        System.out.println("BigInteger.parse time = "
            + (System.currentTimeMillis() - start) + " ms.");
    }
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            Long.parseLong(long36, 36);
        }
        System.out.println("Long.parseLong time = "
            + (System.currentTimeMillis() - start) + "ms.");
    }
    {
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            UnsignedLongs.parseUnsignedLong( long36, 36 );
        }
        System.out.println("UnsignedLongs.parseUnsignedLong time = "
                + (System.currentTimeMillis() - start) + "ms.");
    }
}

Проблема в том, что вы ищете быстрый беззнаковый 64-битный divmod, имеющий только 64-битный divmod со знаком. Поиск udivmoddi3 должен дать вам несколько реализаций в C - они обычно используются для выполнения 64-битного divmod на архитектурах, которые поддерживают только 32-битный divmod в аппаратном обеспечении.

Обратите внимание, что вам нужно только захватить нижнюю цифру - как только вы это сделаете, частное будет положительным, и вы можете использовать Long.toString().

Если основание четное (вы указываете базу 36), вы можете получить нижнюю цифру без особых хлопот (моя математика может быть неправильной):

int bottomDigit = ((value>>>1)%(radix/2))<<1)|((int)value&1);
long rest = (value>>>1)/(radix/2);
if (rest == 0)
{
  return Integer.toString(bottomDigit,radix);
}
return Long.toString(rest,radix) + Integer.toString(bottomDigit,radix);

Очевидной дальнейшей оптимизацией является вызов Long.toString() напрямую, если значение положительное.

Поскольку, несмотря на то, что "НЕ принимаем никаких ответов, связанных с BigInteger", вы приняли решение BigInteger, здесь есть альтернативное решение BigInteger. Вместо того, чтобы маскировать знак, вы можете заставить знак всегда быть положительным:

long input = 0xffffffffffffffffL; // any long, e.g. -1
byte[] bytes = ByteBuffer.allocate(8).putLong(input).array();

String base36 = new BigInteger(1, bytes).toString(36);

Кроме того, если вы работаете с длинным байтовым массивом, @JonnyDee имеет алгоритм (в Python, но он короткий) для преобразования между любыми двумя базами, который применим здесь, если вы считаете, что байтовый массив является числом с Base-256 цифры. Преобразование обратно в байты - это просто преобразование base-36 в base-256.

/questions/25580761/raspechatat-bolshoj-massiv-256-v-baze-10-v-s/25580775#25580775

И его соответствующий пост в блоге:

https://jonnydee.wordpress.com/2011/05/01/convert-a-block-of-digits-from-base-x-to-base-y/

Другие вопросы по тегам