Сортировка Radix для отрицательных целых чисел

Я пытаюсь реализовать сортировку radix для целых чисел, включая отрицательные целые числа. Для неотрицательных ints я планировал создать очередь из 10 очередей соответственно для цифр 0-9 и реализовать алгоритм LSD. Но меня как-то путали с отрицательными целыми числами. То, что я думаю сейчас, - это пойти вперед и создать еще одну очередь из 10 очередей для них и отдельно отсортировать их, а затем в конце я дам 2 списка, один из которых содержит сортированные отрицательные Инты, а другой содержит неотрицательных целых чисел. И, наконец, я солью их.

Что вы думаете об этом? Есть ли более эффективный способ обработки с отрицательными целыми числами?

спасибо!

8 ответов


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


обратите внимание, что бит знака является самым верхним битом в знаковом целом, но по умолчанию все числа обрабатываются сортировкой radix как целые числа без знака. Поэтому вам нужно сказать алгоритму, что отрицательные числа меньше положительных. В случае 32-разрядных целых чисел со знаком вы можете сначала отсортировать три нижних байта, затем отсортировать четвертый (верхний) байт со знаком, перевернутым так, что 0 будет использоваться для отрицательных чисел вместо 1, и, следовательно, они будут идти первыми.

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


еще одно решение состоит в том, чтобы отделить отрицательные целые числа от массива, сделать их положительными, отсортировать как положительные значения с помощью radix, затем отменить его и добавить с отсортированным неотрицательным массивом.


абсолютно! Конечно, вам нужно позаботиться о разделении негативов от позитивов, но, к счастью, это легко. В начале ваш алгоритм сортировки все, что вам нужно сделать, это разбить массив вокруг значения 0. После этого radix сортируется ниже и выше раздела.

вот алгоритм на практике. Я получил это от сорта MSD radix Кевина Уэйна и Боба Седжвика: http://algs4.cs.princeton.edu/51radix/MSD.java.html

private static final int CUTOFF = 15;
private static final int BITS_PER_INT = 32;
private static final int BITS_PER_BYTE = 8;
private static final int R = 256;

public void sort(int[] a){
    int firstPositiveIndex = partition(0, a, 0, a.length-1);
    int[] aux =new int[a.length];
    if(firstPositiveIndex>0){
        recSort(a, firstPositiveIndex, a.length-1, 0,aux);
        recSort(a, 0, firstPositiveIndex-1, 0,aux);
    }else{//all positive
        recSort(a, 0, a.length-1, 0, aux);
    }
}

private void recSort(int[] a, int lo, int hi, int d, int[] aux){
    if(d>4)return;
    if(hi-lo<CUTOFF){
        insertionSort(a,lo, hi);
        return;
    }

    int[] count = new int[R+1];

    //compute counts
    int bitsToShift = BITS_PER_INT-BITS_PER_BYTE*d-BITS_PER_BYTE;
    int mask = 0b1111_1111;
    for(int i = lo; i<=hi; i++){
        int c = (a[i]>>bitsToShift) & mask;
        count[c+1]++;
    }

    //compute indices
    for(int i = 0; i<R; i++){
        count[i+1]=count[i]+count[i+1];
    }

    //distribute
    for(int i = lo; i<=hi; i++){
        int c = (a[i]>>bitsToShift) & mask;
        aux[count[c]+lo] = a[i];
        count[c]++;
    }
    //copy back
    for(int i = lo; i<=hi; i++){
        a[i]=aux[i];
    }

    if(count[0]>0)
        recSort(a, lo, lo+count[0]-1, d+1, aux);
    for(int i = 1; i<R; i++){
        if(count[i]>0)
            recSort(a, lo+count[i-1], lo+count[i]-1, d+1, aux);
    }
}

// insertion sort a[lo..hi], starting at dth character
private void insertionSort(int[] a, int lo, int hi) {
    for (int i = lo; i <= hi; i++)
        for (int j = i; j > lo && a[j] < a[j-1]; j--)
            swap(a, j, j-1);
}


//returns the index of the partition or to the right of where it should be if the pivot is not in the array 
public int partition(int pivot, int[] a, int lo, int hi){
    int curLo = lo;
    int curHi = hi;
    while(curLo<curHi){
        while(a[curLo]<pivot){
            if((curLo+1)>hi)return hi+1;
            curLo++;
        }

        while(a[curHi]>pivot){
            if((curHi-1)<lo)return lo-1;
            curHi--;
        }
        if(curLo<curHi){
            swap(a, curLo, curHi);
            if(a[curLo]!=pivot)curLo++;
            if(a[curHi]!=pivot)curHi--;             
        }
    }
    return curLo;
}


private void swap(int[] a, int i1, int i2){
    int t = a[i1];
    a[i1]=a[i2];
    a[i2]=t;
}

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

Это использует первый метод, а также байт-размера цифры (доступ к байтам обычно более эффективен):

void lsdradixsort(int* a, size_t n)
{
    // isolate integer byte by index.
    auto bmask = [](int x, size_t i)
    {
        return (static_cast<unsigned int>(x) >> i*8) & 0xFF;
    };

    // allocate temporary buffer.
    auto m = std::make_unique<int[]>(n);
    int* b = m.get();

    // for each byte in integer (assuming 4-byte int).
    for ( size_t i, j = 0; j < 4; j++ ) {
        // initialize counter to zero;
        size_t h[256] = {}, start;

        // histogram.
        // count each occurrence of indexed-byte value.
        for ( i = 0; i < n; i++ )
            h[bmask(a[i], j)]++;

        // accumulate.
        // generate positional offsets. adjust starting point
        // if most significant digit.
        start = (j != 3) ? 0 : 128;
        for ( i = 1+start; i < 256+start; i++ )
            h[i % 256] += h[(i-1) % 256];

        // distribute.
        // stable reordering of elements. backward to avoid shifting
        // the counter array.
        for ( i = n; i > 0; i-- )
            b[--h[bmask(a[i-1], j)]] = a[i-1];
        std::swap(a, b);
    }
}

Примечание: код не тестировался. Извинения за любые ошибки/опечатки.


ваша сортировка radix не будет быстрее, чем знаменитые сравнения, если вы не используете "bitshift" и "bitwise AND" для вычисления radix.

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

например
436163157 (как 32-разрядное число) = 00011001 01010010 11111111 01010101
-436163157 (как 32-разрядное число) = 11100110 00000000 10101101 10101011

1 (Как 32-битное число) = 00000000 00000000 00000000 00000001
-1 (как 32-битное число) = 11111111 1111111 1111111 11111111

0 представлено = 00000000 00000000 00000000 00000000
Наибольшее отрицательное значение а = 10000000 00000000 00000000 00000000

Итак, вы видите, чем более отрицательным становится число, оно теряет, что много 1, небольшое отрицательное число имеет много 1, Если вы установите только знак-бит в 0, он станет очень большим положительным числом. Наоборот малое положительное число становится большим отрицательным числом.

в сортировке radix ключ к сортировке отрицательных чисел-это то, как вы обрабатываете последние 8 бит, для отрицательных чисел по крайней мере последний бит должен быть 1, в 32-битной схеме он должен быть от
10000000 00000000 00000000 00000000, которое является самым отрицательным значением от нуля до 11111111 11111111 11111111 11111111 которого равен -1. Если вы посмотрите на самые левые 8 бит, величина колеблется от 10000000 до 11111111, то есть от 128 до 255.

эти значения могут быть получены с помощью этого фрагмента кода

V = ( A[i] >> 24 ) & 255

для отрицательных чисел V всегда будет лежать от 128 до 255. Для положительных чисел это будет от 0 до 127. Как было сказано ранее, значение M будет 255 для -1 и 128 для наибольшего отрицательного числа в 32-битной схеме. Строить гистограмму как обычно. Затем от индекса 128 до 255 выполните кумулятивная сумма, затем добавьте частоту 255 к 0 и продолжите кумулятивную сумму от 0 до индекса 127. Выполните сортировку как обычно. Эта техника оптимальна, быстра, элегантна и аккуратна как в теории, так и на практике. Нет необходимости в каких-либо отдельных списках или развороте ордеров после сортировки или преобразования всех входных данных в положительные, которые делают сортировку медленной и беспорядочной.

код см. В разделе Оптимизация Сортировки Radix
64-разрядную версию можно построить используя такое же понятия

Далее read:
http://codercorner.com/RadixSortRevisited.htm
http://stereopsis.com/radix.html


Это можно сделать без необходимости разбиения или практически инвертировать MSB. Вот рабочее решение на Java:

public class RadixSortsInterviewQuestions {
    private static final int MSB = 64;

    static Map.Entry<Integer, Integer> twoSum(long[] a, long sum) {
        int n = a.length - 1;
        sort(a, MSB, 0, n);

        for (int i = 0, j = n; i < j; ) {
            long t = a[i] + a[j];
            if (t == sum) {
                return new SimpleImmutableEntry<>(i, j);
            } else if (t < sum) {
                i++;
            } else {
                j--;
            }
        }
        return null;
    }

    // Binary MSD radix sort: https://en.wikipedia.org/wiki/Radix_sort#In-place_MSD_radix_sort_implementations
    private static void sort(long[] a, int d, int lo, int hi) {
        if (hi < lo || d < 1) return;

        int left = lo - 1;
        int right = hi + 1;

        for (int i = left + 1; i < right; ) {
            if (isBitSet(a[i], d)) {
                swap(a, i, --right);
            } else {
                left++;
                i++;
            }
        }
        sort(a, d - 1, lo, left);
        sort(a, d - 1, right, hi);
    }

    private static boolean isBitSet(long x, int k) {
        boolean set = (x & 1L << (k - 1)) != 0;

        // invert signed bit so that all positive integers come after negative ones
        return (k == MSB) != set;
    }

    private static void swap(long[] a, int i, int j) {
        long tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
    }
}

принятый ответ требует еще одного прохода, чем это необходимо.

просто переверните знак бит.

это, по сути, ответ, опубликованный punpcklbw, но есть крошечная оговорка, которая должна быть решена. В частности, это предполагает, что вы работаете с представлением двух дополнений, что верно для 99,999% из нас. Например, Java и Rust указывают, что целые числа со знаком используют дополнение two. Спецификации C и C++ не требуют каких-либо конкретных формат, но ни MSVC, GCC, ни LLVM не поддерживают другие представления. В сборке почти любой процессор, с которым вы будете иметь дело, является дополнением двух, и вы наверняка уже знаете иначе.

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

Binary    | 2s-comp  | Flip sign
----------+----------+----------
0000      | 00       | -8
0001      | +1       | -7
0010      | +2       | -6
0011      | +3       | -5
0100      | +4       | -4
0101      | +5       | -3
0110      | +6       | -2
0111      | +7       | -1
1000      | -8       | 00
1001      | -7       | +1
1010      | -6       | +2
1011      | -5       | +3
1100      | -4       | +4
1101      | -3       | +5
1110      | -2       | +6
1111      | -1       | +7

ответ, данный punpcklbw, рекомендует только переворачивать бит, когда вы смотрите на самый высокий байт, но моя интуиция говорит мне, что было бы быстрее просто переворачивать верхний бит каждый раз, прежде чем вытаскивать байт, на который вы смотрите. Это потому, что делать один xor каждый раз, чтобы перевернуть бит будет быстрее, чем делать ветку каждый раз, чтобы решить, следует ли вам перевернуть или нет.

[An важная деталь, которую некоторые учебники не учитывают должным образом, заключается в том, что реальная реализация должна сортировать по байтам, а не по десятичным цифрам. Это, очевидно, все еще правильно, потому что вы просто сортируете по радиусу 256 вместо 10, но думать об этом таким образом приведет к лучшим реализациям.]