Сортировка массива Java: быстрый способ получить отсортированный список индексов массива

Проблема: рассмотрим следующие числа с плавающей запятой[]:

d[i] =     1.7 -0.3  2.1  0.5

Мне нужен массив int[], который представляет порядок исходного массива с индексами.

s[i] =       1    3    0    2
d[s[i]] = -0.3  0.5  1.7  2.1

Конечно, это можно сделать с помощью собственного компаратора, отсортированного набора пользовательских объектов или просто отсортировав массив, а затем отыскав индексы в исходном массиве (содрогаясь).

На самом деле я ищу эквивалент второго возвращаемого аргумента функция сортировки Matlab.

Есть ли простой способ сделать это (‹5 LOC)? Может ли быть решение, при котором не нужно выделять новый объект для каждого элемента?


Обновлять:

Спасибо за ваши ответы. К сожалению, ничто из того, что было предложено до сих пор, не похоже на простое и эффективное решение, на которое я надеялся. Поэтому я открыл ветку на форуме обратной связи JDK, предлагая добавить новую функцию библиотеки классов для решения этой проблемы. Посмотрим, что Sun/Oracle думает по этому поводу.

http://forums.java.net/jive/thread.jspa?threadID=62657&tstart=0


person edgar.holleis    schedule 04.06.2009    source источник
comment
даже если бы это было помещено в JDK, что-то, что я действительно сомневаюсь, когда-либо произойдет, это закончилось бы статическим служебным методом в классе Arrays (или чем-то подобным) и в конечном итоге было бы реализовано очень похоже на что-то ниже. Так почему вы не можете просто написать функцию?   -  person Jherico    schedule 12.06.2009
comment
В чем проблема с кастомным компаратором в качестве решения? Я могу неправильно понять подход, который это подразумевает в вашем уме.   -  person jerryjvl    schedule 12.06.2009
comment
Может быть, я недостаточно усердно ищу, но насколько я знаю, нет способа использовать компаратор без упаковки каждого элемента при каждом вызове компаратора. Для массива из n поплавков это будет означать 2*n*log(n) поплавков для сборщика мусора. При n=10000 это означает 80000 единиц мусора. Я бы предпочел статический служебный метод в классе Arrays. Если бы меня не волновал мусор, я мог бы использовать TreeMap или что-то в этом роде.   -  person edgar.holleis    schedule 17.06.2009
comment
Это так полезно, и я бы хотел, чтобы вы могли сделать это на Java. В R вы можете просто использовать функцию rank().   -  person twolfe18    schedule 30.09.2010
comment
Если у вас меньше 5000 номеров, просто используйте пузырьковую сортировку. Читается легко, коротко и достаточно быстро.   -  person Thomas Ahle    schedule 17.11.2011
comment
Это можно сделать с помощью ‹5 LOC с Java 8, stackoverflow.com/questions/951848/   -  person Pratap Koritala    schedule 29.02.2016


Ответы (14)


Я бы адаптировал алгоритм быстрой сортировки для одновременного выполнения операции обмена с несколькими массивами: массивом индексов и массивом значений. Например (на основе этой быстрой сортировки):

public static void quicksort(float[] main, int[] index) {
    quicksort(main, index, 0, index.length - 1);
}

// quicksort a[left] to a[right]
public static void quicksort(float[] a, int[] index, int left, int right) {
    if (right <= left) return;
    int i = partition(a, index, left, right);
    quicksort(a, index, left, i-1);
    quicksort(a, index, i+1, right);
}

// partition a[left] to a[right], assumes left < right
private static int partition(float[] a, int[] index, 
int left, int right) {
    int i = left - 1;
    int j = right;
    while (true) {
        while (less(a[++i], a[right]))      // find item on left to swap
            ;                               // a[right] acts as sentinel
        while (less(a[right], a[--j]))      // find item on right to swap
            if (j == left) break;           // don't go out-of-bounds
        if (i >= j) break;                  // check if pointers cross
        exch(a, index, i, j);               // swap two elements into place
    }
    exch(a, index, i, right);               // swap with partition element
    return i;
}

// is x < y ?
private static boolean less(float x, float y) {
    return (x < y);
}

// exchange a[i] and a[j]
private static void exch(float[] a, int[] index, int i, int j) {
    float swap = a[i];
    a[i] = a[j];
    a[j] = swap;
    int b = index[i];
    index[i] = index[j];
    index[j] = b;
}
person akarnokd    schedule 24.06.2009
comment
Несмотря на то, что это далеко не желаемое требование ‹= 5 LOC, это единственное предлагаемое на данный момент решение, которое поддерживает приемлемые характеристики производительности. Простите народ :-( - person edgar.holleis; 03.07.2009
comment
Это очень печально. У меня та же проблема, и это единственное приемлемое решение для больших наборов данных. - person ansgri; 14.01.2010
comment
Но это решение изменяет массив данных. Обычно цель сортировки индекса состоит в том, чтобы избежать изменения порядка данных. Чтобы сделать его доступным только для чтения, не меняйте местами данные в exch() и замените каждый a[x] на a[index[x]]. - person xan; 02.01.2011
comment
Наконец-то кто-то понял! Спасибо! - person stolsvik; 18.02.2017

Простое решение для создания массива индексаторов: отсортируйте индексатор, сравнивая значения данных:

final Integer[] idx = { 0, 1, 2, 3 };
final float[] data = { 1.7f, -0.3f,  2.1f,  0.5f };

Arrays.sort(idx, new Comparator<Integer>() {
    @Override public int compare(final Integer o1, final Integer o2) {
        return Float.compare(data[o1], data[o2]);
    }
});
person carloscs    schedule 06.04.2010
comment
Довольно элегантный, но использует досадное количество автобоксов. Поэтому я подозреваю, что это не так эффективно, как ответ kd304. Хотя это проще. - person edgar.holleis; 23.04.2010
comment
Попробуйте проверить скорость. Если целые числа малы, автоупаковка не будет использоваться. И сортировка java более оптимизирована, чем у kd304. - person Thomas Ahle; 17.11.2011
comment
Определенно то, что я искал. final это небольшой недостаток - person keyser; 08.03.2014
comment
@keyser, почему final является недостатком? - person Neo M Hacker; 05.06.2015
comment
@NeoMHacker Это требование, поскольку мы используем его в анонимном внутреннем классе, в этом случае особых проблем не возникает, поскольку мы все равно не изменяем массив данных. См. stackoverflow.com/q/4162531/209882. - person Bar; 12.04.2017

Создайте TreeMap значений для индексов

    float[] array = new float[]{};
    Map<Float, Integer> map = new TreeMap<Float, Integer>();
    for (int i = 0; i < array.length; ++i) {
        map.put(array[i], i);
    }
    Collection<Integer> indices = map.values();

индексы будут отсортированы по числам с плавающей запятой, на которые они указывают, исходный массив останется нетронутым. Преобразование Collection<Integer> в int[] оставлено в качестве упражнения, если это действительно необходимо.

РЕДАКТИРОВАТЬ: Как отмечено в комментариях, этот подход не работает, если в массиве с плавающей запятой есть повторяющиеся значения. Это можно решить, превратив Map<Float, Integer> в Map<Float, List<Integer>>, хотя это немного усложнит внутреннюю часть цикла for и генерацию окончательной коллекции.

person Jherico    schedule 04.06.2009
comment
именно то отображение, которое я собирал. - person Tetsujin no Oni; 04.06.2009
comment
Использует неудачное количество автобоксов, но делает свое дело. +1 - person Michael Myers; 04.06.2009
comment
Недостаток этого подхода заключается в том, что он не обрабатывает повторяющиеся значения с плавающей запятой. - person Mark; 05.06.2009

Использование функций Java 8 (без дополнительной библиотеки), краткий способ достижения этого.

int[] a = {1,6,2,7,8}
int[] sortedIndices = IntStream.range(0, a.length)
                .boxed().sorted((i, j) -> a[i] - a[j])
                .mapToInt(ele -> ele).toArray();
person Pratap Koritala    schedule 29.02.2016
comment
отличный. что, если у меня есть список или массив элементов, для которых a[i] - a[j] не разрешено? - person törzsmókus; 23.06.2016
comment
Вы можете выполнить сравнение с docs.oracle .com/javase/7/docs/api/java/lang/ - person Pratap Koritala; 25.08.2016

С Функциональной Java:

import static fj.data.Array.array;
import static fj.pre.Ord.*;
import fj.P2;

array(d).toStream().zipIndex().sort(p2Ord(doubleOrd, intOrd))
  .map(P2.<Double, Integer>__2()).toArray();
person Apocalisp    schedule 04.06.2009
comment
Не поймите неправильно... но - вау! Совершенно нечитаемый код! Извините - просто не мог удержаться :-) Хотя он соответствует требованию ‹5 LOC - включая импорт - очень впечатляет! - person Kevin Day; 05.06.2009
comment
Нечитабельно как? Давайте прочитаем это. Перенесите свой массив в поток, соедините (заархивируйте) каждый элемент с его индексом, быстро отсортируйте их по порядку пар (сначала по двойному, затем по int), получите вторую половину каждой пары, а затем перенесите все это в массив. Легкий! - person Apocalisp; 05.06.2009
comment
Это нечитаемо, только если вы новичок в функциональном программировании. Такого рода вещи совершенно нормальны в функциональных языках. Можно ли это сделать в Java 8 без сторонней библиотеки? - person SigmaX; 13.02.2015

Более общий случай ответ Джерико, допускающий дублирование значений, будет таким:

// Assuming you've got: float[] array; defined already

TreeMap<Float, List<Integer>> map = new TreeMap<Float, List<Integer>>();
for(int i = 0; i < array.length; i++) {
    List<Integer> ind = map.get(array[i]);
    if(ind == null){
        ind = new ArrayList<Integer>();
        map.put(array[i], ind);
    }
    ind.add(i);
}

// Now flatten the list
List<Integer> indices = new ArrayList<Integer>();
for(List<Integer> arr : map.values()) {
    indices.addAll(arr);
}
person Mark Elliot    schedule 14.11.2009
comment
Я думаю, что map.put(array[i], ind); должен быть помещен после ind.add(i); - person lizzie; 07.02.2014
comment
@lizzie работает как есть, потому что ind содержит местоположение списка, и вещь, обновляемая ind.add, будет такой же, как вещь на карте. - person Mark Elliot; 08.02.2014

Лучшее решение было бы похоже на qsort C, который позволяет вам указывать функции для сравнения и замены, поэтому qsort не нужно знать о типе или организации сортируемых данных. Вот один из них, который вы можете попробовать. Поскольку в Java нет функций, используйте внутренний класс Array для переноса массива или коллекции, подлежащих сортировке. Затем оберните это в IndexArray и отсортируйте. Результатом getIndex() для IndexArray будет массив индексов, как описано в JavaDoc.

public class QuickSortArray {

public interface Array {
    int cmp(int aindex, int bindex);
    void swap(int aindex, int bindex);
    int length();
}

public static void quicksort(Array a) {
    quicksort(a, 0, a.length() - 1);
}

public static void quicksort(Array a, int left, int right) {
    if (right <= left) return;
    int i = partition(a, left, right);
    quicksort(a, left, i-1);
    quicksort(a, i+1, right);
}

public static boolean isSorted(Array a) {
    for (int i = 1, n = a.length(); i < n; i++) {
        if (a.cmp(i-1, i) > 0)
            return false;
    }
    return true;
}

private static int mid(Array a, int left, int right) {
    // "sort" three elements and take the middle one
    int i = left;
    int j = (left + right) / 2;
    int k = right;
    // order the first two
    int cmp = a.cmp(i, j);
    if (cmp > 0) {
        int tmp = j;
        j = i;
        i = tmp;
    }
    // bubble the third down
    cmp = a.cmp(j, k);
    if (cmp > 0) {
        cmp = a.cmp(i, k);
        if (cmp > 0)
            return i;
        return k;
    }
    return j;
}

private static int partition(Array a, int left, int right) {
    int mid = mid(a, left, right);
    a.swap(right, mid);
    int i = left - 1;
    int j = right;

    while (true) {
        while (a.cmp(++i, right) < 0)
            ;
        while (a.cmp(right, --j) < 0)
            if (j == left) break;
        if (i >= j) break;
        a.swap(i, j);
    }
    a.swap(i, right);
    return i;
}

public static class IndexArray implements Array {
    int[] index;
    Array a;

    public IndexArray(Array a) {
        this.a = a;
        index = new int[a.length()];
        for (int i = 0; i < a.length(); i++)
            index[i] = i;
    }

    /**
     * Return the index after the IndexArray is sorted.
     * The nested Array is unsorted. Assume the name of
     * its underlying array is a. The returned index array
     * is such that a[index[i-1]] <= a[index[i]] for all i
     * in 1..a.length-1.
     */
    public int[] index() {
        int i = 0;
        int j = index.length - 1;
        while (i < j) {
            int tmp = index[i];
            index[i++] = index[j];
            index[j--] = tmp;
        }
        int[] tmp = index;
        index = null;
        return tmp;
    }

    @Override
    public int cmp(int aindex, int bindex) {
        return a.cmp(index[aindex], index[bindex]);
    }

    @Override
    public void swap(int aindex, int bindex) {
        int tmp = index[aindex];
        index[aindex] = index[bindex];
        index[bindex] = tmp;
    }

    @Override
    public int length() {
        return a.length();
    }

}
person bobfoster    schedule 25.09.2011

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

class SortPair implements Comparable<SortPair>
{
  private int originalIndex;
  private double value;

  public SortPair(double value, int originalIndex)
  {
    this.value = value;
    this.originalIndex = originalIndex;
  }

  @Override public int compareTo(SortPair o)
  {
    return Double.compare(value, o.getValue());
  }

  public int getOriginalIndex()
  {
    return originalIndex;
  }

  public double getValue()
  {
    return value;
  }

}

person Mark    schedule 05.06.2009

Еще одно не простое решение. Вот версия сортировки слиянием, которая стабильна и не изменяет исходный массив, хотя слияние требует дополнительной памяти.

public static int[] sortedIndices(double[] x) {
    int[] ix = new int[x.length];
    int[] scratch = new int[x.length];
    for (int i = 0; i < ix.length; i++) {
        ix[i] = i;
    }
    mergeSortIndexed(x, ix, scratch, 0, x.length - 1);
    return ix;
}

private static void mergeSortIndexed(double[] x, int[] ix, int[] scratch, int lo, int hi) {
    if (lo == hi)
        return;
    int mid = (lo + hi + 1) / 2;
    mergeSortIndexed(x, ix, scratch, lo, mid - 1);
    mergeSortIndexed(x, ix, scratch, mid, hi);
    mergeIndexed(x, ix, scratch, lo, mid - 1, mid, hi);
}

private static void mergeIndexed(double[] x, int[] ix, int[] scratch, int lo1, int hi1, int lo2, int hi2) {
    int i = 0;
    int i1 = lo1;
    int i2 = lo2;
    int n1 = hi1 - lo1 + 1;
    while (i1 <= hi1 && i2 <= hi2) {
        if (x[ix[i1]] <= x[ix[i2]])
            scratch[i++] = ix[i1++];
        else
            scratch[i++] = ix[i2++];
    }
    while (i1 <= hi1)
        scratch[i++] = ix[i1++];
    while (i2 <= hi2)
        scratch[i++] = ix[i2++];
    for (int j = lo1; j <= hi1; j++)
        ix[j] = scratch[j - lo1];
    for (int j = lo2; j <= hi2; j++)
        ix[j] = scratch[(j - lo2 + n1)];
}
person xan    schedule 02.01.2011

Я бы сделал что-то вроде этого:

public class SortedArray<T extends Comparable<T>> {
    private final T[] tArray;
    private final ArrayList<Entry> entries;

    public class Entry implements Comparable<Entry> {
        public int index;

        public Entry(int index) {
            super();
            this.index = index;
        }

        @Override
        public int compareTo(Entry o) {
            return tArray[index].compareTo(tArray[o.index]);
        }
    }

    public SortedArray(T[] array) {
        tArray = array;
        entries = new ArrayList<Entry>(array.length);
        for (int i = 0; i < array.length; i++) {
            entries.add(new Entry(i));
        }
        Collections.sort(entries);
    }

    public T getSorted(int i) {
        return tArray[entries.get(i).index];

    }

    public T get(int i) {
        return tArray[i];
    }
}
person Ofek Ron    schedule 22.05.2014

Ниже приведен метод, основанный на сортировке вставками.

public static int[] insertionSort(float[] arr){
    int[] indices = new int[arr.length];
        indices[0] = 0;
        for(int i=1;i<arr.length;i++){
            int j=i;
            for(;j>=1 && arr[j]<arr[j-1];j--){
                    float temp = arr[j];
                    arr[j] = arr[j-1];
                    indices[j]=indices[j-1];
                    arr[j-1] = temp;
            }
            indices[j]=i;
        }
        return indices;//indices of sorted elements
 }
person Shravan Kumar    schedule 19.11.2014

Я хотел бы использовать это, потому что это очень быстро. Но я использую его для int, вы можете изменить его на float.

private static void mergeSort(int[]array,int[] indexes,int start,int end){
    if(start>=end)return;
    int middle = (end-start)/2+start;
    mergeSort(array,indexes,start,middle);
    mergeSort(array,indexes,middle+1,end);
    merge(array,indexes,start,middle,end);
}
private static void merge(int[]array,int[] indexes,int start,int middle,int end){
    int len1 = middle-start+1;
    int len2 = end - middle;
    int leftArray[] = new int[len1];
    int leftIndex[] = new int[len1];
    int rightArray[] = new int[len2];
    int rightIndex[] = new int[len2];
    for(int i=0;i<len1;++i)leftArray[i] = array[i+start];
    for(int i=0;i<len1;++i)leftIndex[i] = indexes[i+start];
    for(int i=0;i<len2;++i)rightArray[i] = array[i+middle+1];
    for(int i=0;i<len2;++i)rightIndex[i] = indexes[i+middle+1];
    //merge
    int i=0,j=0,k=start;
    while(i<len1&&j<len2){
        if(leftArray[i]<rightArray[j]){
            array[k] = leftArray[i];
            indexes[k] = leftIndex[i];
            ++i;
        }
        else{
            array[k] = rightArray[j];
            indexes[k] = rightIndex[j];
            ++j;
        }
        ++k;
    }
    while(i<len1){
        array[k] = leftArray[i];
        indexes[k] = leftIndex[i];
        ++i;++k;
    }
    while(j<len2){
        array[k] = rightArray[j];
        indexes[k] = rightIndex[j];
        ++j;++k;
    }
}
person Smart Du    schedule 26.05.2016

Я думаю, самый простой способ сделать это - индексировать массив по мере его создания. Вам понадобятся пары ключ, значение. Если индекс представляет собой отдельную структуру, то я не понимаю, как вы могли бы сделать это без других объектов (хотя интересно это увидеть)

person Tom    schedule 04.06.2009

person    schedule
comment
Было бы лучше, если бы вы добавили небольшое пояснение к этому коду. - person vefthym; 06.05.2014
comment
Ницца! мне понравился этот! - person Ofek Ron; 22.05.2014