Каков наиболее ресурсоэффективный способ проверить, содержат ли два списка std::list одинаковые уникальные элементы?

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

На данный момент я использую код, подобный показанному в следующем примере:

#include <list>
#include <set>
#include <string>


template<typename T>
auto areListsAsSetsEqual(const std::list<T> &a, const std::list<T> &b) -> bool {
    auto aSet = std::set<T>{a.begin(), a.end()};
    auto bSet = std::set<T>{b.begin(), b.end()};
    return aSet == bSet;
}


auto main() -> int {
    auto x = std::list<std::string>{"red", "blue", "yellow", "green", "green"};
    auto y = std::list<std::string>{"blue", "green", "yellow", "red", "red"};
    auto z = std::list<std::string>{"green", "red", "yellow"};

    auto xyEqual = areListsAsSetsEqual(x, y);
    assert(xyEqual == true);
    auto xzEqual = areListsAsSetsEqual(x, z);
    assert(xzEqual == false);
    return 0;
}

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

Есть ли более эффективный и элегантный способ сравнения двух списков на наличие одних и тех же уникальных элементов с меньшим использованием ЦП и/или памяти?

Я полагаю, что вы можете использовать std::unordered_set вместо std::set, но я не вижу лучшего способа.

max66 15.02.2023 14:46
std::list сам по себе обычно не считается эффективным.
HolyBlackCat 15.02.2023 14:54

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

Flovdis 15.02.2023 14:56

@HolyBlackCat Действительно, но это то, что я получаю и с чем мне приходится работать.

Flovdis 15.02.2023 14:58

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

max66 15.02.2023 15:13

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

463035818_is_not_a_number 15.02.2023 15:29

Мое мышление больше связано с оптимизацией пространства. Но один список может «включить» ключ в чем-то вроде unordered_map. Затем другой список может «отключить» ключи, если они найдены, иначе произойдет преждевременный возврат. Наконец, просмотрите unordered_map на наличие ключей, которые все еще остаются включенными.

sweenish 15.02.2023 17:15

В ответе должно быть большое «это зависит»: можно ли увеличить сложность пространства, используя дополнительные контейнеры? Можете ли вы изменить ввод? Как уже упоминалось, хорошим подходом может быть сортировка списка, а затем его обход.

Jorge López 15.02.2023 19:35
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
8
80
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вот другой вариант. Требуется только один новый контейнер, и копируется только один список.

#include <cassert>
#include <list>
#include <string>
#include <unordered_map>

template <typename T>
auto areListsAsSetsEqual(const std::list<T>& a, const std::list<T>& b) -> bool {
  std::unordered_map<T, bool> firstListKeys;
  for (const auto& i : a) {
    firstListKeys[i] = true;
  }

  for (const auto& i : b) {
    if (firstListKeys.find(i) != firstListKeys.end()) {
      firstListKeys[i] = false;
    } else {
      return false;
    }
  }

  for (const auto& p : firstListKeys) {
    if (p.second == true) {
      return false;
    }
  }

  return true;
}

auto main() -> int {
  auto x = std::list<std::string>{"red", "blue", "yellow", "green", "green"};
  auto y = std::list<std::string>{"blue", "green", "yellow", "red", "red"};
  auto z = std::list<std::string>{"green", "red", "yellow"};

  auto xyEqual = areListsAsSetsEqual(x, y);
  assert(xyEqual == true);
  auto xzEqual = areListsAsSetsEqual(x, z);
  assert(xzEqual == false);
  return 0;
}

Первый список копируется в std::unordered_map, и каждому ключу присваивается значение true. Второй список повторяется и ищет карту (O (1)). Если не нашли, можем сразу вернуть. Если найдено, ключ устанавливается на false. Затем нам нужно найти unordered_map, чтобы увидеть, остались ли какие-либо элементы в состоянии true.

Я не проводил никаких тестов, поэтому потребуется тестирование, чтобы увидеть, является ли это решение более эффективным. На бумаге оба работают со скоростью O(3N), но необходимо проверить среднее время выполнения.

Но хотя прирост эффективности во время выполнения невелик, этот метод дает явное преимущество в эффективности использования пространства. Ваш текущий алгоритм в настоящее время требует ~ 2N пространства, где мой ближе к N.

Я предлагаю добавить std::unordered_map<T, unsigned int> вместо std::unordered_map<T, bool> и использовать значение в качестве счетчика. В противном случае вы не сможете учитывать повторяющиеся элементы. Все еще нужно три прохода, список a увеличивает счетчик, затем обход списка b уменьшает счетчик и, наконец, проверяет, что все значения на карте равны 0, иначе оба списка не равны.

Jorge López 15.02.2023 19:27

@JorgeLópez JorgeLópez Задают не этот вопрос. OP заботится только об одних и тех же уникальных значениях, существующих в обоих списках. Посмотрите еще раз на их пример кода, особенно на функцию main(). И чтобы забить точку домой std::set может содержать только уникальные значения.

sweenish 15.02.2023 19:33

упс, вы правы, и я сделал бу-бу.

Jorge López 15.02.2023 19:38

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

Jorge López 15.02.2023 20:28

Ваше первое предложение не нужно. Я отвечаю на вопрос прямо, тут нечего уточнять. В «исходном» вопросе был только один дополнительный тег по сравнению с тем, что видно сейчас. Ваше непонимание не было ошибкой ОП или меня. Ссылка на «исходный вопрос» в данном случае интересная формулировка. Что касается вашего второго предложения; Я обдумал это. Я остановился на unordered_map по субъективным причинам, но, прочитав немного о том, что удаление unordered_set является (в среднем) операцией O (1), я могу отредактировать альтернативную версию. Это может сократить время выполнения до O (2N).

sweenish 15.02.2023 20:50

Это кажется интересным подходом. Я должен буду проверить это.

Flovdis 16.02.2023 00:15

Из двух ответов ваш пока самый эффективный. Он использует только примерно 53% ресурсов ЦП и более эффективно использует память, чем исходный метод.

Flovdis 16.02.2023 14:44

Небольшое обновление: я не буду редактировать решение с помощью std::unordered_set. Приблизительное время выполнения эквивалентно, потому что вы запустили std::unique во втором списке, и в целом это немного сложнее.

sweenish 16.02.2023 16:18

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

Flovdis 16.02.2023 17:58

Интересно, смогу ли я сэкономить немного времени, специализировав функцию для строк, чтобы вместо этого сделать ключ string_view. Однако недостаточно сделать достаточное влияние по сравнению с вашим алгоритмом.

sweenish 16.02.2023 22:08

Для полноты: вы также можете попробовать использовать стандартные алгоритмы. Предполагая, что вы не хотите изменять свои входные данные, вам понадобятся копии. (Решение только с одной копией см. в ответе @sweenish.)

#include <list>
#include <string>
#include <cassert>
#include <algorithm>
#include <iterator>
#include <vector>

template<typename T>
auto areListsEqual(std::list<T> const &a, std::list<T> const &b) -> bool {
    std::vector<T> aVector(a.size());
    std::partial_sort_copy(a.cbegin(), a.cend(), aVector.begin(), aVector.end());
    auto aEnd = std::unique(aVector.begin(), aVector.end());
    
    std::vector<T> bVector(b.size());
    std::partial_sort_copy(b.cbegin(), b.cend(), bVector.begin(), bVector.end());
    auto bEnd = std::unique(bVector.begin(), bVector.end());

    return std::distance(aVector.begin(),aEnd) == std::distance(bVector.begin(),bEnd)
         ? std::equal(aVector.begin(), aEnd, bVector.begin())
         : false;
}

auto main() -> int {
    auto x = std::list<std::string>{"red", "blue", "yellow", "green", "green"};
    auto y = std::list<std::string>{"blue", "green", "yellow", "red", "red"};
    auto z = std::list<std::string>{"green", "red", "yellow"};
    auto w = std::list<std::string>{"green", "red", "yellow", "black"};

    auto xyEqual = areListsEqual(x, y);
    assert(xyEqual == true);
    auto xzEqual = areListsEqual(x, z);
    assert(xzEqual == false);
    auto xwEqual = areListsEqual(x, w);
    assert(xwEqual == false);
    return 0;
}

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

У вас есть хорошая точка там. Я должен буду проверить это.

Flovdis 16.02.2023 00:14

По сравнению с исходным методом, ваш требует только примерно 76% ресурсов ЦП.

Flovdis 16.02.2023 14:46

Для полноты этого вопроса самый быстрый алгоритм примерно основан на ответе от sweenish, но работает без копий исходных данных. Следовательно, следующий алгоритм лучше при следующих двух условиях:

  • Создание копии/хэша элементов в списке дорого (например, для std::string или строк в целом.
  • Последовательный поиск элементов в контейнере выполняется быстро (короткие списки, быстрое сравнение).
#include <vector>
#include <list>
#include <algorithm>
#include <string>

template<typename T>
auto areListsAsSetsEqual(const std::list<T> &a, const std::list<T> &b) -> bool {
    auto keyMap = std::vector<bool>(a.size(), false);
    const auto srcEndA = a.end();
    const auto srcEndB = b.end();
    std::size_t keyIndex = 0;
    for (auto it = a.begin(); it != srcEndA; ++it, ++keyIndex) {
        if (std::find(a.begin(), it, *it) == it) {
            keyMap[keyIndex] = true;
        }
    }
    for (const auto &element : b) {
        auto foundIt = std::find(a.begin(), a.end(), element);
        if (foundIt == a.end()) {
            return false;
        }
        keyMap[std::distance(a.begin(), foundIt)] = false;
    }
    return std::all_of(keyMap.begin(), keyMap.end(), [](bool flag) -> bool { return !flag; });
}

auto main() -> int {
    auto x = std::list<std::string>{"red", "blue", "yellow", "green", "green"};
    auto y = std::list<std::string>{"blue", "green", "yellow", "red", "red"};
    auto z = std::list<std::string>{"green", "red", "yellow"};

    auto xyEqual = areListsAsSetsEqual(x, y);
    assert(xyEqual == true);
    auto xzEqual = areListsAsSetsEqual(x, z);
    assert(xzEqual == false);
    return 0;
}

Используя мои обширные тестовые данные, в которых используются ключи std::string размером от 4 до 128 символов и наборы ключей от нуля до 32 элементов. Сделав 200 000 сравнений наборов с небольшими мутациями, я получаю такие результаты:

Выполнение Среднее время на вызов Среднее время, если равно Увеличение скорости оригинальный 0,005232 мс 0,005444 мс 1× каба 0,004337 мс 0,004275 мс 1,21× шведский 0,002796 мс 0,003919 мс 1,87× это решение 0,000566 мс 0,001305 мс 9,24×

Алгоритм также имеет наименьшее использование памяти.

Другие вопросы по теме