В моем коде я должен сравнивать ключи из структур, которые возвращаются в случайном порядке в виде списка. Мне нужно проверить, имеют ли две структуры одинаковые ключевые элементы, игнорируя порядок и сравнивая только уникальные элементы.
На данный момент я использую код, подобный показанному в следующем примере:
#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::list
сам по себе обычно не считается эффективным.
@max66 Тем не менее, хороший намек. Создание неупорядоченного набора может быть быстрее. Вопрос в том, уравновешивает ли более медленное сравнение выигрыш.
@HolyBlackCat Действительно, но это то, что я получаю и с чем мне приходится работать.
В случае, если размер наборов различен, сравнение осуществляется за постоянное время в обоих случаях. Таким образом, неупорядоченное сравнение наборов может быть наихудшим, когда наборы равны или очень похожи (когда ключи действительно разные, я полагаю, что сравнение прекращается почти сразу в обоих случаях), поэтому я полагаю, что более медленное сравнение может быть проблемой, только когда наборы когда-либо равные или похожие.
вы можете избежать создания новых контейнеров, отсортировав исходные, но не уверен, что это действительно лучше с точки зрения сложности. std::list
довольно плох с точки зрения производительности для начала
Мое мышление больше связано с оптимизацией пространства. Но один список может «включить» ключ в чем-то вроде unordered_map. Затем другой список может «отключить» ключи, если они найдены, иначе произойдет преждевременный возврат. Наконец, просмотрите unordered_map на наличие ключей, которые все еще остаются включенными.
В ответе должно быть большое «это зависит»: можно ли увеличить сложность пространства, используя дополнительные контейнеры? Можете ли вы изменить ввод? Как уже упоминалось, хорошим подходом может быть сортировка списка, а затем его обход.
Вот другой вариант. Требуется только один новый контейнер, и копируется только один список.
#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
, иначе оба списка не равны.
@JorgeLópez JorgeLópez Задают не этот вопрос. OP заботится только об одних и тех же уникальных значениях, существующих в обоих списках. Посмотрите еще раз на их пример кода, особенно на функцию main()
. И чтобы забить точку домой std::set может содержать только уникальные значения.
упс, вы правы, и я сделал бу-бу.
Действительно, я предлагаю вам обновить свой ответ, чтобы он действительно отражал цель исходного вопроса. Я также предлагаю использовать неупорядоченный набор.
Ваше первое предложение не нужно. Я отвечаю на вопрос прямо, тут нечего уточнять. В «исходном» вопросе был только один дополнительный тег по сравнению с тем, что видно сейчас. Ваше непонимание не было ошибкой ОП или меня. Ссылка на «исходный вопрос» в данном случае интересная формулировка. Что касается вашего второго предложения; Я обдумал это. Я остановился на unordered_map по субъективным причинам, но, прочитав немного о том, что удаление unordered_set является (в среднем) операцией O (1), я могу отредактировать альтернативную версию. Это может сократить время выполнения до O (2N).
Это кажется интересным подходом. Я должен буду проверить это.
Из двух ответов ваш пока самый эффективный. Он использует только примерно 53% ресурсов ЦП и более эффективно использует память, чем исходный метод.
Небольшое обновление: я не буду редактировать решение с помощью std::unordered_set
. Приблизительное время выполнения эквивалентно, потому что вы запустили std::unique
во втором списке, и в целом это немного сложнее.
@sweenish Смотрите мой ответ для решения, основанного на вашем принципе, но без копирования ключей. В моем случае я получил ускорение почти в десять раз по сравнению с моим первоначальным методом. Тем не менее, с большими наборами коротких ключей ваше решение лучше.
Интересно, смогу ли я сэкономить немного времени, специализировав функцию для строк, чтобы вместо этого сделать ключ string_view
. Однако недостаточно сделать достаточное влияние по сравнению с вашим алгоритмом.
Для полноты: вы также можете попробовать использовать стандартные алгоритмы. Предполагая, что вы не хотите изменять свои входные данные, вам понадобятся копии. (Решение только с одной копией см. в ответе @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». Но он использует последовательные контейнеры в качестве промежуточного хранилища, что может быть более эффективным с точки зрения кэширования на современном оборудовании. Как всегда с оптимизацией в наши дни, вы должны измерять с репрезентативными данными.
У вас есть хорошая точка там. Я должен буду проверить это.
По сравнению с исходным методом, ваш требует только примерно 76% ресурсов ЦП.
Для полноты этого вопроса самый быстрый алгоритм примерно основан на ответе от 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 сравнений наборов с небольшими мутациями, я получаю такие результаты:
Алгоритм также имеет наименьшее использование памяти.
Я полагаю, что вы можете использовать
std::unordered_set
вместоstd::set
, но я не вижу лучшего способа.