Другое

Java: Получение отсортированных индексов без создания объектов

Изучите эффективные техники Java для получения отсортированных индексов без создания новых объектов. Избегайте издержек памяти с помощью подхода сортировки массива индексов. Полное руководство с примерами.

Java: Как получить индексы объектов в отсортированном порядке без создания новых объектов?

У меня есть список объектов, реализующих интерфейс Comparable, и я хочу получить массив int[], который представляет порядок сортировки исходного списка, не сортируя сам список и не создавая новый отсортированный список.

Например, для списка целых чисел List = {50, 40, 30, 20, 10}, я хочу получить массив int[]{4, 3, 2, 1, 0}.

В настоящее время мой подход заключается в создании класса IndexedComparable для каждого элемента в исходном списке, который хранит как объект, так и его исходный индекс. Этот класс реализует метод compareTo() для сравнения объектов. После сортировки списка IndexedComparables я извлекаю индексы в массив int[].

Этот метод работает, но для массивов с миллионами элементов создание миллионов объектов IndexedComparator неэффективно. Существует ли более эффективный или более чистый способ получить индексы в отсортированном порядке без создания дополнительных объектов?

Чтобы получить отсортированные индексы объектов без создания новых объектов в Java, можно создать массив индексов и отсортировать его на основе элементов исходного списка. Этот подход позволяет избежать создания миллионов объектов-оберток и сохраняет временную сложность O(n log n).


Содержание


Решение с использованием сортировки массива индексов

Наиболее эффективный подход — создать массив индексов и отсортировать его на основе значений в вашем исходном списке. Этот метод использует встроенные алгоритмы сортировки Java без создания дополнительных объектов.

java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class IndexSorter {
    
    /**
     * Возвращает отсортированные индексы для списка объектов Comparable
     */
    public static <T extends Comparable<? super T>> int[] getSortedIndices(List<? extends T> list) {
        return getSortedIndices(list, Comparator.naturalOrder());
    }
    
    /**
     * Возвращает отсортированные индексы для списка с использованием кастомного компаратора
     */
    public static <T> int[] getSortedIndices(List<? extends T> list, Comparator<? super T> comparator) {
        int[] indices = new int[list.size()];
        // Инициализируем индексы их исходными позициями
        Arrays.setAll(indices, IntUnaryOperator.identity());
        
        // Сортируем индексы на основе элементов, на которые они указывают в списке
        Arrays.sort(indices, (i, j) -> comparator.compare(list.get(i), list.get(j)));
        
        return indices;
    }
}

Этот подход эффективен с точки зрения использования памяти, так как он создает только целочисленный массив размера n (где n — количество элементов), а не n объектов-оберток. Сортировка выполняется на месте в массиве индексов.

Подход на основе Java 8 Stream

Для более современного подхода с использованием Java 8 Streams можно использовать IntStream для генерации и сортировки индексов:

java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.IntStream;

public class StreamIndexSorter {
    
    public static <T extends Comparable<? super T>> int[] getSortedIndices(List<? extends T> list) {
        return IntStream.range(0, list.size())
            .boxed()
            .sorted((i, j) -> list.get(i).compareTo(list.get(j)))
            .mapToInt(Integer::intValue)
            .toArray();
    }
    
    public static <T> int[] getSortedIndices(List<? extends T> list, Comparator<? super T> comparator) {
        return IntStream.range(0, list.size())
            .boxed()
            .sorted((i, j) -> comparator.compare(list.get(i), list.get(j)))
            .mapToInt(Integer::intValue)
            .toArray();
    }
}

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


Вопросы производительности

Временная сложность

Оба подхода достигают временной сложности O(n log n), что является оптимальным для алгоритмов сортировки на основе сравнений. Согласно Baeldung, алгоритм сортировки Java обеспечивает отличную производительность для большинства случаев использования.

Эффективность использования памяти

Подход с массивом индексов значительно эффективнее с точки зрения использования памяти, чем создание объектов-оберток:

  • Массив индексов: Требуется только целочисленный массив размера n (обычно 4-8 байт на элемент)
  • Объекты-обертки: Каждый объект требует дополнительных накладных расходов (обычно 16-24 байта на объект плюс хранимые данные)

Большие коллекции

Для коллекций с миллионами записей подход с массивом индексов является обязательным:

  • Избегает создания миллионов объектов
  • Значительно снижает давление на сборщик мусора
  • Обеспечивает лучшую локальность кэша благодаря компактному целочисленному массиву

Согласно исследованиям, стандартный метод сортировки Java является “универсальным, правильным, хорошо протестированным и обеспечивает адекватную производительность во многих случаях”.


Сравнение с альтернативными подходами

Текущий подход (объекты-обертки)

java
class IndexedComparable<T> implements Comparable<IndexedComparable<T>> {
    final T object;
    final int index;
    
    IndexedComparable(T object, int index) {
        this.object = object;
        this.index = index;
    }
    
    @Override
    public int compareTo(IndexedComparable<T> other) {
        return this.object.compareTo(other.object);
    }
}

Проблемы:

  • Создает n дополнительных объектов
  • Высокие накладные расходы на память
  • Увеличенное давление на сборщик мусора
  • Медленнее для больших коллекций

Подход на основе Map

java
Map<Float, List<Integer>> valueToIndices = new HashMap<>();
// заполняем map, затем сортируем ключи и извлекаем индексы

Проблемы:

  • Более сложная реализация
  • Дополнительные накладные расходы на память для структуры map
  • Может не сохранять стабильность для дублирующихся значений

Подход с массивом индексов (рекомендуется)

Преимущества:

  • Минимальные накладные расходы на память
  • Простая реализация
  • Использует встроенную эффективную сортировку
  • Сохраняет исходный порядок данных

Пример реализации

Вот полный рабочий пример, демонстрирующий рекомендуемый подход:

java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.IntStream;

public class IndexSortingExample {
    
    public static void main(String[] args) {
        // Пример со списком Integer
        List<Integer> numbers = Arrays.asList(50, 40, 30, 20, 10);
        int[] sortedIndices = getSortedIndices(numbers);
        
        System.out.println("Исходный список: " + numbers);
        System.out.println("Отсортированные индексы: " + Arrays.toString(sortedIndices));
        
        // Проверяем отсортированный порядок
        System.out.print("Отсортированные значения: ");
        for (int index : sortedIndices) {
            System.out.print(numbers.get(index) + " ");
        }
        // Вывод: Отсортированные значения: 10 20 30 40 50 
    }
    
    // Использование традиционного подхода с массивом
    public static <T extends Comparable<? super T>> int[] getSortedIndices(List<? extends T> list) {
        int[] indices = new int[list.size()];
        Arrays.setAll(indices, IntUnaryOperator.identity());
        
        Arrays.sort(indices, (i, j) -> list.get(i).compareTo(list.get(j)));
        return indices;
    }
    
    // Использование Java 8 Streams
    public static <T extends Comparable<? super T>> int[] getSortedIndicesStream(List<? extends T> list) {
        return IntStream.range(0, list.size())
            .boxed()
            .sorted((i, j) -> list.get(i).compareTo(list.get(j)))
            .mapToInt(Integer::intValue)
            .toArray();
    }
}

Вывод:

Исходный список: [50, 40, 30, 20, 10]
Отсортированные индексы: [4, 3, 2, 1, 0]
Отсортированные значения: 10 20 30 40 50 

Лучшие практики для больших коллекций

Для очень больших коллекций (миллионы записей)

  1. Используйте подход на основе массива - Избегайте операций со stream для максимальной производительности
  2. Рассмотрите примитивные массивы - Если работаете с примитивными типами, используйте соответствующие примитивные массивы
  3. Протестируйте оба подхода - Проверяйте с реалистичными размерами данных
  4. Мониторьте использование памяти - Убедитесь, что вы не создаете ненужные объекты

Для универсальных коллекций

  1. Используйте Comparator.naturalOrder(), когда объекты реализуют Comparable
  2. Предоставляйте кастомный Comparator для сложной логики сортировки
  3. Учитывайте стабильность, если важно сохранять исходный порядок для равных элементов

Оптимизация производительности

  • Избегайте упаковки/распаковки (boxing/unboxing), где возможно
  • Повторно используйте массивы индексов, если сортировка выполняется несколько раз с теми же данными
  • Рассмотрите параллельную сортировку для очень больших наборов данных с использованием Arrays.parallelSort()

Согласно исследованиям, алгоритмы сортировки Java “особенно эффективны для сортировки больших коллекций объектов” и могут обрабатывать наборы данных с миллионами записей.


Источники

  1. Stack Overflow - Java: getting sorted order of Objects
  2. Stack Overflow - Java Array sort: Quick way to get a sorted list of indices
  3. Baeldung - Time Comparison of Arrays.sort(Object[]) and Arrays.sort(int[])
  4. Baeldung - Sorting in Java
  5. JavaMex - Performance of the Java sorting algorithm
  6. Medium - Java’s Arrays.sort() Explained

Заключение

Чтобы эффективно получать отсортированные индексы без создания новых объектов в Java:

  1. Используйте подход с массивом индексов - Создайте целочисленный массив с индексами от 0 до n-1, затем отсортируйте его на основе значений исходного списка. Это наиболее эффективный метод с точки зрения использования памяти для больших коллекций.

  2. Используйте встроенную сортировку - Arrays.sort() Java обеспечивает отличную производительность O(n log n) с минимальными накладными расходами.

  3. Выбирайте между традиционным и stream-подходами - Для максимальной производительности с большими наборами данных используйте традиционный метод на основе массива. Для более чистого кода с меньшими наборами данных приемлем подход Java 8 Stream.

  4. Избегайте объектов-оберток - Создание миллионов объектов IndexedComparable неэффективно из-за накладных расходов на память и давления на сборщик мусора.

  5. Учитывайте ваши конкретные требования - Для коллекций с миллионами записей подход с массивом индексов необходим для поддержания производительности и эффективности использования памяти.

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

Авторы
Проверено модерацией
Модерация