Подтвердить что ты не робот

Эффективное слияние и пересортировка отсортированных списков

Это не классический вопрос о "слиянии двух отсортированных" списков, который довольно тривиальный в линейном времени.

То, что я пытаюсь сделать, это объединить два списка пар (key, value), уже отсортированных по value, где в обоих списках есть объекты с тем же key: такие объекты должны иметь слияние value (добавлено), что может изменить порядок сортировки. Меня в первую очередь интересует, как сортировка может быть эффективно выполнена с использованием информации из уже отсортированных списков, поскольку сортировка является самой медленной частью этого алгоритма.

Возьмем конкретный пример. Представьте себе List объектов Student:

class Student {
  final String name;
  final int score;
  ...
}

Учитывая, что в качестве входных данных два List<Student> отсортированы по score, я хотел бы создать новый объединенный список студентов, где любой учащийся (идентифицированный Student.name), появляющийся в обоих списках, появляется один раз в конечном списке, с оценка, равная сумме их оценки в обоих списках. Исходные списки должны быть оставлены без изменений.

Например,

List 1:
{"bob", 20}
{"john", 15}
{"mark", 14}

List 2:
{"bill", 11}
{"mark", 9}
{"john", 1}

Result:
{"mark", 23}
{"bob", 20}
{"john", 16}
{"bill", 11}

Слияние (идентификация учеников, отображаемых в обоих списках) может быть выполнено в ожидаемое время O (1) с использованием любой структуры поиска/вставки O (1), такой как HashMap. Меня больше всего интересует шаг сортировки (хотя я не исключаю решения, которые объединяют и сортируют в одно и то же время).

Вопрос, однако, в том, как эффективно пересобирать такой список? Упорядочение существующих списков явно ограничивает конечную позицию элементов в объединенном списке. Например, если студент находится в позиции i в первом списке и j во втором, он должен появиться среди первых учеников i + j в объединенном списке простым аргументом, анализирующим максимальное количество студентов, которые могли бы имеют более высокий балл. Однако не сразу понятно, была ли эта информация полезной при сортировке списка.

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

Кажется, что этот тип операции был бы общим для любого типа распределенной реализации запросов + сортировки. Например, представьте себе "select state, count (*) group by state" тип проблемы запроса для распределенной системы (чтобы подсчитать количество записей в каждом состоянии) - естественно, вы получите отсортированный список (state, count ) возвращает объекты из каждого node, а затем вы хотите объединить и повторно сортировать их во время операции уменьшения. Кажется глупым отбросить всю работу, уже сделанную на распределенных узлах.

Количественные примечания

Меня интересует случай, когда списки, которые должны быть объединены и пересортированы, невелики: обычно около 256 записей. Диапазон баллов варьируется от 0 до 100 в некоторых случаях, до примерно 0 - 10 000 000 в других. Конечно, учитывая небольшое количество элементов, каждая операция будет быстрой в абсолютном времени, даже с наивными алгоритмами - но выполняется в миллиарды раз, она складывается.

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

4b9b3361

Ответ 1

Похоже, вам нужно использовать алгоритм адаптивной сортировки.

"Алгоритм сортировки попадает в адаптивное семейство сортировки, если он использует преимущества существующего порядка в своем вводе. Он извлекает выгоду из предрасположенности во входной последовательности или ограниченного количества беспорядков для различных определений мер беспорядка - и рода быстрее. Адаптивная сортировка обычно выполняется путем изменения существующих алгоритмов сортировки". - Статья Википедии, связанная выше.

Примеры включают сортировку вставки и Timsort; более подробно см. статью выше. Обратите внимание, что в Java 8 в библиотечном методе Arrays.sort(Object[]) используется модифицированный Timsort.


Мне неизвестен какой-либо опубликованный алгоритм, который касается конкретных требований вашего примера, но вот идея:

  • Выполните классическое объединение на двух входных списках L1 и L2:

    • Когда вы объединяете пару объектов и меняете ключи, которые определяют порядок, поместите объединенный объект во временный список A.
    • В противном случае объекты будут помещены во временный список B... который останется упорядоченным.
  • Сортировка временного списка A.

  • Объединить списки A и B.

Предполагая, что:

  • длины исходных списков L1 и L2 являются соответственно M и N, а
  • количество объединенных объектов, чьи ключи изменены, R (меньше макс (M, N)),

тогда общая сложность O (M + N + RlogR). Если R мало относительно M + N, это должно быть улучшением.


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

Сложность похожа на предыдущую, но вы избегаете дополнительного прохода для объединения списков. А также RlogR становится RlogA, где A - средний размер очереди приоритетов.


Имейте в виду, что меня особенно интересует случай, когда R приблизительно равно max (M, N), а также M == N.

(Вы не указали это в своем вопросе! И на самом деле для R не имеет значения > min (M, N)!)

В этом случае, возможно, просто используйте очередь приоритетов в качестве инкрементного сортировщика. Бросьте все объединенные записи и все записи, которые не могут быть объединены в очередь, и потяните наши записи, если у них есть ключ/счет, который меньше, чем текущие главы этих двух списков. Предполагая, что M и N - длины списка, а A - средний размер очереди приоритетов, тогда сложность max (M, N) * log A). Будет ли это улучшение простого повторного сортировки, будет зависеть от того, будет ли среднее значение A значительным (в терминах Big O) меньше, чем max (M, N). Это будет зависеть от входных данных... и функции слияния.


Число (N) меняется, но типично 256-1000. Возможно, целых 10 000.

Для списков этого типичного размера вы находитесь на уровне, где анализ сложности не будет полезен. Но также вы находитесь на том уровне, где оптимизация становится бессмысленной... если вы не выполняете операцию много, много раз или в жестком "бюджете времени".


Все это очень приблизительное, и мои математики в лучшем случае "отрывочны".

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

Ответ 2

Похоже, вы хотите слияние O (n), как и при сортировке слияния. Думаю, у меня могут быть плохие новости. Я собираюсь (надеюсь) доказать, что вы не можете сделать лучше, чем O (nlog (n)) для обобщенной задачи: (поэтому, следовательно, вы должны просто использовать любой из оптимальных решений O (nlog (n)), представленных другими). Во-первых, я начну с интуиции относительно того, почему это так, а затем я напишу неофициальное доказательство.

Интуиция

Идея состоит в том, чтобы превратить проблему сортировки списка в вашу проблему и показать, что если вы сможете решить свою проблему быстрее, чем O (nlog (n)), то я могу сортировать любой список быстрее, чем O (nlog (n)), которые мы знаем как ложные. Мы просто будем работать с целыми числами, чтобы все было просто.

Предположим, что у вас есть какая-то странная последовательность для сортировки: X = 1, 3, 2, -10, 5, 4, 7, 25. Теперь я построю два списка Dec и Inc., начиная с 1 = 1 + 0 (т.е. x_1 = x_1 + 0). Затем после этого, если x_{i-1} -> x_i является увеличением, я вычитаю 1 из своего значения в Dec и вычислим необходимое значение в Inc для суммирования до x_i. Если x_{i-1} -> x_i - уменьшение, то я добавляю 1 к моему значению в Inc и вычисляю необходимое значение в Dec для суммирования до x_i. Мы применяем этот алгоритм к последовательности в следующей таблице:

idx   x     Dec    Inc      
----------------------
 1 |  1  =  1   +  0
 2 |  3  =  0   +  3
 3 |  2  =  -2  +  4
 4 | -10 =  -15 +  5
 5 |  5  =  -16 +  21
 6 |  4  =  -18 +  22
 7 |  7  =  -19 +  23
 8 |  25 =  -20 +  45

Обратите внимание, что я могу преобразовать из сортировки в вашу проблему в O (n) - note: reverse Inc в O (n), чтобы получить две уменьшающиеся последовательности. Затем мы можем ввести вашу проблему

A = {(1, 1), (2, 0), (3, -2), (4, -15), (5, -16), (6, -18), (7, -19), (8, -20)}
B = {(8, 45), (7, 23), (6, 22), (5, 21), (4, 5), (3, 4), (2, 3), (1, 0)}

Теперь, если вы можете комбинировать A и B в отсортированном порядке по сумме их значений (второй элемент в упорядоченных парах) и получить что-то вроде

C = {(8, 25), (7, 7), (5, 5), (6, 4), (2, 3), (3, 2), (1, 1), (4, -10)

то вы по существу сделали argsort (сортировать по индексу) начальной последовательности x_i. Поэтому, если вы решите свою проблему быстрее, чем O (nlog (n)), я могу сортировать быстрее, чем O (nlog (n)), сначала решая проблему, а затем преобразовывая решение в мою проблему сортировки списка. В частности, я бы сортировал со сложностью O (n) + O (сложность для решения вашей проблемы)

Утверждение, подлежащее проверке

Пусть ваши два списка значений ключа

A = [(ka_i, va_i) | i = 1..n]
B = [(kb_i, vb_i) | i = 1..m] 

сортируется в порядке убывания значения. Вы не можете найти объединенный список

C = [(ka_i, va_i + va_j) | ka_i = kb_j]

быстрее, чем время O (nlog (n)).

Доказательство

Единственное предположение, которое делает это доказательство, заключается в том, что вы не можете сортировать список быстрее, чем время O (nlog (n)), и это доказательство будет продолжено путем предоставления сокращения, которое выполняется в O (n) времени, от сортировки любого произвольного списка до вашего проблема.

В сущности, мы покажем, что если мы решим вашу проблему быстрее, чем O (nlog (n)), то мы можем также отсортировать любой произвольный список быстрее, чем O (nlog (n)). И мы уже знаем, что сортировать список невозможно быстрее, чем nlog (n), поэтому ваше желаемое решение также должно быть невозможным.

Сведения о подтверждении

Для простоты мы будем сортировать список целых чисел. Пусть S = x_1, x_2, ..., x_n - любая последовательность целых чисел. Теперь мы построим два списка: Dec и Inc.

У нас есть три ограничения:

  • Inc строго возрастает
  • Dec строго уменьшается
  • На итерации я алгоритма Inc[j] + Dec[j] = x_j for all j = 1..i-1

Как следует из их названий, Dec будет строго снижаться, а Inc будет строго возрастать. Мы сохраним инвариант, что x_i = Dec[i] + Inc[i] for i = 1..n

Вот сокращение:

# (Assume 1-indexed lists)
1. Initialize Inc = [x_1] and Dec = [0]
2. For i = 2..n:
    a. if x[i] > x[i-1] then
          Dec.append(Dec[i-1] - 1)
          Inc.append(x_i - Dec[i])
       else   # We must have x[i] <= x[i-1]
          Inc.append(Inc[i-1] + 1)
          Dec.append(x_i - Inc[i])

3. Create list A and B:
    A = [(i, Dec[i]) | i = 1..n]
    B = [(i, Inc[i]) | i = 1..n]
4. B = reverse(B) # Reverse B because B was in increasing order and we
                  # need both lists to be in decreasing order
5. A and B are inputs to your algorithm.
  If your algorithm can combine A and B into sorted order,
  then we have also sorted S (via argsort on the keys).

Вероятно, вы также голодны за доказательство того, что мой ad hoc метод выбора увеличения Inc на 1 или уменьшение Dec на 1 работает. Ну вот неофициальное "доказательство" (вы можете его формализовать, используя индукцию):

Случай x_ {i} > X_ {я-1}

Напомним, что в этом случае мы выбираем декремент Dec на 1. Нам дано, что x_{i} > x_{i-1} и мы знаем, что Dec_{i-1} + Inc_{i-1} = x_{i-1}. Можно также сказать, что (Dec_{i-1} - 1) + (Inc_{i+1} + 1) = x_{i-1}.

Так как x_{i} > x_{i-1}, мы должны иметь x_{i} >= x_{i-1} + 1. Поэтому x_{i} >= (Dec_{i-1} - 1) + (Inc_{i+1} + 1). Поэтому, если мы уменьшаем Dec на 1, мы будем вынуждены добавить не менее 1 к Inc, поэтому Inc остается строго возрастающим.

Случай x_ {i} & le; X_ {я-1}

Напомним, что в этом случае мы выбираем приращение Inc на 1. Нам дано, что x_{i} <= x_{i-1} и мы знаем, что Dec_{i-1} + Inc_{i-1} = x_{i-1}. Можно также сказать, что (Dec_{i-1} - 1) + (Inc_{i+1} + 1) = x_{i-1}, а так как x_{i} <= x_{i-1}, это должно быть так, что (Dec_{i-1} - 1) + (Inc_{i+1} + 1) <= x_{i}. Поэтому, если мы добавим 1 к Inc, мы уверены, что мы должны вычесть по крайней мере 1 из Dec.

Заключение

Ваша проблема не может быть выполнена быстрее, чем O (nlog (n)). Вам лучше просто объединиться в HashMap, а затем отсортировать его элементы в O (nlog (n)), потому что невозможно найти более быстрое решение.

Не стесняйтесь комментировать, однако, если вы обнаружите проблему с сокращением или имеете вопросы. Я уверен, что это правильно. Конечно, если я ошибаюсь в том, что сортировка не быстрее O (nlog (n)), все это доказательство разваливается, но в последний раз я проверял, что кто-то уже доказал, что O (nlog (n)) - самая быстрая сложность сортировки, Комментарий, если вы предпочитаете формальное сокращение. Мне стало поздно, и я пропустил некоторые "формализации", но я могу изменить их, когда получаю шанс.

Если вы кодируете алгоритм для создания сокращения, вы можете получить лучшее понимание.

Кроме того: см. этот пост, если вы хотите объяснение для O (nlog (n)), связанного при сортировке Каковы правила для барьера Ω (n log n) для сортировки алгоритмов?

Ответ 3

(Отклонение для первого слияния, а затем повторного сортировки). Мой первый удар будет состоять в том, чтобы объявить отсортированные входные списки (полустатические) очереди приоритетов и действовать в два этапа. Чтобы избежать двусмысленности в терминах слияния, я буду называть создание/изменение объекта для представления значений "общих объектов" comb/combination; чтобы уменьшить беспорядок, я буду обозначать приоритетную очередь PQ.

  • идентифицировать объекты, которые появляются в обеих/более одной "очереди ввода"
    (в качестве второстепенного интереса здесь)
    • объединить (возможно, недействительность позиции в любом списке),
    • поместите их в другой (динамический) PQ (если необходимо)
    • удалить из /invalidate в очереди (вводах), где они больше не будут.
  • Слияние PQ обычным способом

Это должно работать в линейном времени в числе n объектов, плюс O (c log c) для c "общих" объектов, где объединенный объект будет вне последовательности вместо любого объединенного объекта. (... учитывая ожидаемое постоянное время (идентифицировать и) объединить один (набор общих) объектов (см. Примечание о ожидаемом O (1) в вопросе))
Затем я боюсь, что это не соответствует основному пункту:

Есть ли способ извлечь выгоду из конечного ключа как (линейного, монотонного)
комбинация по крайней мере одной упорядоченной последовательности и "других значений"
(С большим количеством общих записей - все думают.)

Если комбинация уменьшает приоритет монотонно (в примере добавление (положительных) значений баллов увеличивает приоритет), обойтись без фазы комбинирования и объединить объекты при слиянии PQ, что потенциально уменьшит память и время. В противном случае выберите один PQ, чтобы принимать объекты (уменьшая приоритет), чтобы потенциально комбинировать с другими объектами.
"Наихудший случай" может показаться приоритетом комбинированных объектов, не показывающих корреляции: я боюсь, что ответ будет обычно. (см. user2570465 ответ для явного аргумента)
(как указывает BeeOnRope, выбранные объекты (последовательность), которые доминируют в комбинации (невыгодный выбор), могут фактически превратиться в хороший случай, если это можно обнаружить и использовать. )
С другой стороны, можно ожидать, что (линейная, монотонная) комбинация будет искажать распределение ключей даже без (положительной) корреляции (предполагается в вопросе): обязательно использовать (динамическую) реализацию PQ, где наилучшим вариантом является вставка в порядке а не худшее:
Во-первых, возьмите неявную кучу в массиве (дети элемента с индексом я находятся в 2i и 2i + 1 (или 2i + 1 & 2i + 2 "не растрачивая элемент 0", но немного больше манипуляции с индексами):
просто добавьте элементы (с распределением с уменьшением приоритета) до конца:
ожидаемое количество обменов с родителем ниже 1 (будет почти 1 без перекоса).

Ответ 4

  • Поддерживайте карту, которая отображает что-то уникальное для фактической информации о студенте.

    Map<String, Student> scores = new HashMap<>();
    
  • Перебирайте все списки и помещайте их в карту оценок

    for (Student s : list1) {
        if (scores.containsKey(s.name)) {
            scores.put(s.name, s.score + scores.get(s.name));
        } else {
            scores.put(s.name, s.score); 
        } 
    }
    
  • Сортировка набора записей с использованием потоков Java 8

    scores.entrySet()
      .stream()
      .sorted((s1, s2) -> (s2.getValue().score - s1.getValue().score)
      .map(s1 -> s1.getValue())
      .collect(Collectos.toList());
    

Это все еще O(N Log N)

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

Ответ 5

Мне кажется, что любое решение, как правило, относится к категории сложности O (n * log (n)) (с n = длина (L1) + длина (L2) или n = max (длина (L1), длина (L2))).

Мой основной алгоритм был бы следующим:

  Let use two intermediate structures:
  - a TreeSet R, which guarantees ordering by rank, 
  - an HashMap M, which guarantees constant time insertion and retrieve 
  Call R size n

  1 for each student in each list
      1.1 find the student in M by name (O(1)).
      1.2 if the student is found          
         1.2.1 find the student in R by its rank (O(log(n)).  
         1.2.2 remove the student from R (O(log(n))
         1.2.3 update the student rank 
      1.3 else 
        1.3.1. put the student in M O(1)
      1.4 put the student in R (O(log(n))
  2 At the end (if needed) transform the TreeSet in a list

Общая сложность O - это O (n * log (n)),

Предполагая, что L1 является самым длинным из 2 списков, небольшая оптимизация будет заключаться в том, чтобы найти ученика при обходе L1, в этом случае сложность O одинакова, но вы будете иметь меньше операций в абсолютном. Лучший случай - конечно, когда Len (L1) → Len (L2).

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

1 - упорядочивание списка результатов, поэтому списки сканирования, поиск совпадений и повторное присвоение позиции каждый раз

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

Обе возможности обычно вычисляются в O (n * log (n))

Ответ 6

Как я вижу, тот факт, что список уже отсортирован по счету, не помогает, так как сначала нам нужно объединить оценки.

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

Подход будет следующим:

  • Примените inplace-binary-most-significant-bit-radix-sort в List-1 и List-2 вместе.
  • Студенты, чей результат дважды появится, будут смежными, объедините такие записи.
  • Наконец, используйте набор inplace-binary-most-important-bit-radix-sort (как указано выше) для множества студентов в объединенном списке (так, чтобы пар очков и студент был перестроен по мере необходимости).

Обновление # 1: Сорт на шаге 1 находится на имени студента.

Ответ 7

Попробуйте:

//Изменен класс Student.

public class Student {

        String name = "";
        int score = 0;

        public Student(String name, int score) {
            this.name = name;
            this.score = score;
        }

        @Override
        public boolean equals(Object v) {
            if (v instanceof Student) {
                return this.name.equals(((Student) v).name);
            } else if (v instanceof String) {
                return this.name.equals(String.valueOf(v));
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 67 * hash + Objects.hashCode(this.name);
            return hash;
        }
    }

//Класс CustomComparator для сортировки списка по объекту или stri

public class CustomComparator implements Comparator<Object> {

        public int orderby = 0;

        @Override
        public int compare(Object o1, Object o2) {
            Student st1 = (Student)o1;
            Student st2 = (Student)o2;
            if (orderby==0){
                //order by name.
                return st1.name.compareTo(st2.name);
            }else{
                //order by score.
                Integer a=st1.score;
                Integer b = st2.score;
                return a.compareTo(b);
            }

        }
    }

//Пример

List<Student> A = new ArrayList<Student>();
A.add(new Student("bob", 20));
A.add(new Student("john", 15));
A.add(new Student("mark", 14));

List<Student> B = new ArrayList<Student>();
B.add(new Student("bill", 11));
B.add(new Student("mark", 9));
B.add(new Student("john", 1));

List<Student> merge = new ArrayList<Student>();
merge.addAll(A);
merge.addAll(B);

//Copy.
List<Student> result = new ArrayList<Student>();
for (Student st : merge) {
    if (result.contains(st)) {
        for (Student r : result) {
            if (r.equals(st)) {
                System.out.println(st.score + " > " +r.score);
                //Se the best score
                if (st.score > r.score) {
                    r.score = st.score;
                    break;
                }
            }
        }
    } else {
        result.add(st);
    }
}

//Sort result by name.
CustomComparator comparator = new CustomComparator();
comparator.orderby=0; //1 sort by score.
Collections.sort(result, comparator);
for (Student r : result) {
    System.out.println(r.name + " = " + r.score);
}

//Пример результата:

bill = 11 | bob = 20 | john = 15 | mark = 14