Ежедневная часть (e) C++ # 16, Общий вопрос интервью: Объединение отсортированных списков с поворотом C++

Сегодня мы рассмотрим еще один распространенный вопрос на собеседовании — объединение отсортированных списков.

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

Постановка проблемы

Учитывая отсортированные списки как std::vector<std::forward_list<>>, создайте объединенные отсортированные std::forward_list<>.

Ваша реализация должна заботиться о сложности big-O и избегать ненужных копий.

Для этого ввод задается по значению и может быть изменен.

Прежде чем читать решение, попробуйте решить его самостоятельно: https://compiler-explorer.com/z/5raraWG5f.

Решение

У нас есть два варианта решения этой проблемы.

Мы можем либо повторно выбрать список с наименьшим ведущим элементом, что приведет к n (количество элементов) повторений log(k) (выбор наименьшего элемента).

Или мы можем многократно объединять пары списков, что приводит к log(k) (поскольку мы делим количество списков на два в каждой итерации) повторений n операций во время слияния.

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

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

Решение A (повторно выберите список с наименьшим ведущим элементом):

std::forward_list<int> merge(std::vector<std::forward_list<int>> in) {
    using fwlst = std::forward_list<int>;

    // Custom comparator, compare based on the first element
    auto cmp = [](const fwlst& l, const fwlst& r) {
        return l.front() < r.front();
    };
    // View of non-empty lists, if we filter here, 
    // we don't have to check later.
    auto non_empty = in |
      std::views::filter(std::not_fn(&fwlst::empty)) |
      std::views::common;
    // Consume the input using std::move_iterator, 
    // avoids making copies of the lists.
    std::multiset<fwlst,decltype(cmp)> q(
        std::move_iterator(non_empty.begin()), 
        std::move_iterator(non_empty.end()), 
        cmp);

    fwlst result;
    fwlst::iterator tail = result.before_begin();
    while (!q.empty()) {
        // Extract the node that holds the element, 
        // without making a copy
        auto top = q.extract(q.begin());

        // Splice the first element of the top list to the result
        result.splice_after(tail,
                            top.value(), top.value().before_begin());
        ++tail;

        if (!top.value().empty())
           q.insert(std::move(top)); // put back
    }
    // Each element is the minimum once, therefore the main loop
    // will loop O(n) times, where n is the total number of elements.
    // The insert is our only non-constant operation, meaning that
    // we end up with O(n*logk), where k is the number of lists.
    return result;
}

Нажмите здесь, чтобы открыть решение в Compiler Explorer.

Решение B (многократное объединение пар списков):

std::forward_list<int> merge_binary(
  std::vector<std::forward_list<int>> in) {
    auto tail = in.begin();
    auto end = in.end();
    // While we have more than one list
    while (std::distance(in.begin(), end) > 1) {
        auto it = in.begin();
        // Take a pair of lists
        while (it != end && std::next(it) != end) {
            // Merge
            it->merge(*std::next(it));
            // and shift to the begining of the array
            tail->swap(*it);
            ++tail;
            std::advance(it, 2);
        }
        // If we have odd number of lists
        if (it != end) {
            tail->swap(*it);
            ++tail;
        }
        // The new end is one behind the last moved list
        end = tail;
        tail = in.begin();
    }
    // The content of the main loop is O(n), as each merge is O(m), 
    // where m the total number of elements in both lists.
    // We will repeat the main loop O(logk) times, since 
    // we divide the number of lists by two each iteration.
    // Hence, again O(n*logk).
    return in[0];
}

Нажмите здесь, чтобы открыть решение в Compiler Explorer.