Алгоритм объединения нескольких отсортированных последовательностей в одну отсортированную последовательность в C++

Я ищу алгоритм для объединения нескольких отсортированных последовательностей, скажем, X отсортированных последовательностей с n элементами , в одну отсортированную последовательность на C++, можете ли вы привести некоторые примеры?

примечание: Я не хочу использовать любые библиотеки

5 ответов


есть три метода, которые делают слияние : -

Предположим, вы объединяете m lists с n elements each

алгоритм 1 :-

объединить списки по два за раз. Используйте сортировку слияния как процедуру слияния для слияния при сортировке списков. Это очень просто реализовать без каких-либо библиотек. Но требуется время O(m^2*n) который достаточно мал, если m не большой.

алгоритм 2:-

это улучшение по сравнению с 1. где мы всегда объединяем список, которые являются самыми маленькими двумя в оставшемся списке. Используйте priority queue для этого выберите наименьший список из двух и объедините их и добавьте новый список в очередь. Этого пока только 1 Список, что бы ваш ответ. Эта техника используется в huffman coding производит optimal merge pattern. Это занимает O(m*n*logm). Более того, для списков подобного размера это может быть сделано parallel как мы можем выбрать пару из списка и соединяются в параллель. Если у вас есть m processors тогда алгоритм может в идеале работать в O(n*logm) вместо O(m*n*logm)

алгоритм 3:-

это наиболее эффективный алгоритм, где вы поддерживаете priority queue для первых элементов всех списков и извлечь min, чтобы получить новый элемент также поддерживать индекс списка min элемент принадлежит, так что вы можете добавить следующий элемент из этого списка. Это взять O(s*logm) где s-общее количество элементов во всех списках.


предположения

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

предположим, что у нас есть m сортировка списков, которые мы хотим объединить.

пусть n обозначает общее количество элементов во всех списках.

идея

первый элемент в результирующем списке должен быть наименьшим элементом в наборе всех глав списков.

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

если m мало

A линейного сканирования через головы составляет O(m) в результате O(m * n) общее время, которое прекрасно, если m - небольшая константа.

если m не так уж и мало

тогда мы можем сделать лучше, используя приоритет в очереди, например a кучу. Инвариант здесь заключается в том, что наименьший элемент в куче всегда является наименьшим элементом из текущих головок.

поиск минимального элемента-это куча O(1), исключив минимум O(log m) если есть m элементы в куче, и вставка элемента в куче тоже O(log m).

в целом, для каждого из n элементы, мы вставляем его в кучу один раз и удаляем его оттуда также один раз. Общая сложность с кучей -O(n log m) что значительно быстрее, что O(n * m) если m не является малой константой.

резюме

какой метод быстрее зависит от того, сколько списков мы хотим объединить. Если m небольшой выбор линейного сканирования, в другом случае реализуйте его с помощью приоритет в очереди. Иногда трудно судить, m маленькая или нет, и в этом случае некоторые эксперименты будут полезны.


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

#include <list>
#include <iostream>

using namespace std;

int main(void)
 {
  list<int> a = { 1, 3, 5, 7, 9}, b = { 2, 4 , 6, 8, 9, 10}, c; //c is out
  for(auto it1 = begin(a), it2 = begin(b); it1 != end(a) || it2 != end(b);)
   if(it1 != end(a) && (it2 == end(b) || *it1 < *it2)) {
      c.push_back(*it1);
      ++it1;
    }
   else {
     c.push_back(*it2);
     ++it2;
    }
  for(auto x : c)
   cout<<x<<' ';
  cout<<'\n';
 }

результат:

1 2 3 4 5 6 7 8 9 9 10

Внимание! Вы должны скомпилировать с флагом-std=c++11 (или другим для c++11). Например:

г++ -с std=с++11 -стены -педантичный - Векстра-О2 d.cpp - o программа.вон!--9-->

сложность: Θ (n)

память: Θ (n)

нетрудно увидеть, что каждый элемент оценивается ровно один раз в O (1), у нас есть n элементов, так это Θ (n).

сложность памяти очевидна. Стоит отметить, что если два списка больше не нужны, это можно сделать без дополнительных ассигнований (const память.)

сам алгоритм был описал так много раз, что нет смысла писать еще раз.

в основной задаче у нас много последовательностей, но идея та же. Вот вам обогащенный пример:

int main(void)
 {
  vector<vector<int> > in{{ 1, 3, 5, 7, 9}, { 2, 4 , 6, 8, 9, 10}, {2,5,7,12,10,11,18}};
  vector<int> out;
  typedef tuple<int, vector<int>::iterator, vector<int>::iterator> element;
  priority_queue<element, vector<element>, greater<element> >  least;
  for(auto& x : in) //Adding iterators to the beginning of (no empty) lists
   if(!x.empty())   //and other parts of the element (value and end of vector)
    least.emplace(x.front(),begin(x),end(x));

  while(!least.empty()) {            //Solving
    auto temp = least.top(); least.pop();
    out.push_back(get<0>(temp));     //Add the smallest at the end of out
    ++get<1>(temp);
    if(get<1>(temp) != get<2>(temp)){//If this is not the end
      get<0>(temp) = *get<1>(temp);
      least.push(temp);              //Update queue
     }
   }

  for(const auto& x : out) //Print solution
   cout<<x<<' ';
  cout<<'\n';
 }

сложность: Θ (N log k)

память: Θ (n)

операции Pop и insert находятся в O (log k), мы выполняем их n раз, так что это O(n log k).

память по-прежнему очевидна, у нас всегда есть k элементов в priority_queue и O (n) в последовательности out.


код для этого может быть похож на сортировку слияния на основе указателя и подсчета, начиная с создания "исходного" массива указателей и подсчетов для каждой последовательности и выделения второго массива "назначения" для слияния "исходного" массива указателей и подсчетов. Каждый проход этого алгоритма объединяет пары указателей и подсчетов на основе последовательностей из массива "источник" в массив "назначение", уменьшая количество записей в массиве примерно на 1/2. Затем указывает на " source" и массивы "назначения" меняются местами, и процесс слияния повторяется до тех пор, пока массив указателей и счетчиков не будет иметь только одну запись.


стандартная библиотека C++ содержит std::merge

std::vector<int> v1 { 1,2,5,7 }, 
                 v2 { 3,6,9 }, 
                 out;

std::merge(v1.begin(), v1.end(), 
           v2.begin(), v2.end(), 
           std::back_inserter(out));

http://en.cppreference.com/w/cpp/algorithm/merge