https://leetcode.com/problems/find-resultant-array-after-removing-anagrams/
Дан массив слов words. Слово содержит латинские буквы в нижнем регистре a-z. Проверить пары смежных слов и удалить
, когда
и
- анаграммы.
Строки a и b - анаграммы, когда b получается из a перестановкой символов и наоборот.
a.length() == b.length() && a.sort() == b.sort()
Ищем анаграммы в массиве
Заметим, если a, b - анаграммы, b, c - анаграммы, то a, c - анаграммы. Математики называют такие отношения транзитивными.

Найдем группы смежных анаграмм и оставим в массиве первую строку каждой группы.

w3 и w6 - не анаграммы, когда w3 и w5 - анаграммы, а w5 и w6 - нет.

Предлагаю два способа:
-
Ищем первую и последнюю анаграмму группы

Начало и конец группы -
Ищем границы между группами - пары смежных "не анаграмм"

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

vector<string> removeAnagrams(vector<string>& words) {
int write = 0;
int begin = 0;
int end = begin + 1;
while (begin < words.size()) {
while (end < words.size() && areAnagrams(words[begin], words[end])) {
++end;
}
std::swap(words[write++], words[begin]);
begin = end;
end = begin + 1;
}
words.resize(write);
return words;
}
Цикл обходит группы анаграмм и пишет первую строку группы по указателю write.
Цикл выбирает первую строку каждой группы, пишет по указателю writeи переходит к следующей группе.
begin указывает на начало группы анаграмм. Вложенный цикл сдвигает end так, чтобы end - 1 указал последнюю анаграмму группы.
Почему я вызвал swap, а не присвоил =
Инструкция words[w++] = words[r] копирует строки. Вызываем std::swap, чтобы обменять строки без копирования.
words[w++] = std::move(words[r]) уничтожит строку, когда w == r, а std::swap нет. Напишите в комментариях, почему так. Пример:
vector<string> words{"Hello"s, "World"s};
for (int i = 0; i < words.size(); ++i) {
words[i] = std::move(words[i]);
}
for (const auto &w: words) {
// пустые строки
cout << w << endl;
}
Пишем функцию areAnagrams, что узнает анаграммы
C++ предлагает такие способы узнать анаграммы:
Посчитаем буквы с помощью массива
Латинский алфавит содержит 26 букв. Объявим массив размера 26 и посчитаем, сколько раз слово содержит каждую букву.
Код
bool areAnagrams(const string &lhs, const string &rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
vector<int> stat(26);
for (char c: lhs) {
++stat[c - 'a'];
}
for (char c: rhs) {
--stat[c - 'a'];
}
for (int num: stat) {
if (0 != num) {
return false;
}
}
return true;
}
Время:
O(n)Память:
26 * sizeof(int)байтов
C++предлагает иstd::array, но не обнуляет массив. Элементыvector<int> stat(26)равны нулю, аarray<int, 26> stat- нет.arrayбольше похож на массив языка Си, чемvector.
Посчитаем буквы с помощью std::map
Код
bool areAnagrams(const string &lhs, const string &rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
map<char, int> stat;
for (char c: lhs) {
++stat[c];
}
for (char c: rhs) {
--stat[с];
}
for (const auto&[c, num]: stat) {
if (0 != num) {
return false;
}
}
return true;
}
Время:
O(n * log(n)), потому чтоmap::operator[]работает заO(log(n))Память:
mapтребует память только под те буквы, что слово содержит
Посчитаем буквы с помощью std::multiset
Код
bool areAnagrams(const string &lhs, const string &rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
multiset<char> stat;
for (char c: lhs) {
++stat.insert(c);
}
for (char c: rhs) {
auto iter = stat.find(c);
if (stat.end() == iter) {
return false;
}
stat.erase(iter);
}
return stat.empty();
}
Время:
O(n * log(n)), потому чтоmultiset::insertиmultiset::findработают заlog(n)Память:
multisetтребует память только под те буквы, что слово содержит
Упорядочим буквы слов по возрастанию и сравним
Код
bool areAnagrams(string lhs, string rhs) {
if (lhs.size() == rhs.size()) {
sort(lhs.begin(), lhs.end());
sort(rhs.begin(), rhs.end());
return lhs == rhs;
}
return false;
}
Время:
O(n * log(n)), потому чтоstd::sortработает заO(n * log(n))Память:
O(n), потому что копирует строки
Ищем границы групп - пары "не анаграмм"
vector<string> removeAnagrams(vector<string>& words) {
int write = 1;
for (int prev = 0, next = 1; next < words.size(); ++prev, ++next) {
if (!areAnagrams(words[prev], words[next])) {
words[write++] = words[next];
}
}
words.resize(write);
return words;
}
Теперь не могу вызвать std::swap, потому что prev укажет на слово next на следующем шаге цикла.
Выбираем лучший алгоритм
Говорят "Алгоритм работает за время ". Звучит, словно мы знаем время работы алгоритма, но это не так.
означает размер входных данных алгоритма, например, размер массива
words в задаче. Время работы алгоритма растет не быстрее, чем функция , когда мы увеличиваем
. Алгоритм
справится с ростом
лучше, чем алгоритм
.
Leetcode сообщает "твое решение работает за 1 мс - это быстрее 90% других". Это разовое измерение. Это значит, что твой алгоритм - лучший по сложности , но в этот раз не повезло. Запусти еще раз, и тот же код способен отработать за 0 мс.
Выбирайте лучший по сложности
алгоритм и заставьте работать. Не думайте о времени работы программы, пока не написали программу верно.
Говорим "Нет!" преждевременной оптимизации
Я оптимизировал вызов areAnagrams. Цикл снова и снова сравнивает words[begin] с другим словом, поэтому я сортировал words[begin] однажды, а в areAnagrams сортировал только второе слово:
while (begin < words.size()) {
string sample = words[begin];
sort(sample.begin(), sample.end());
while (end < words.size() && isAnagram(sample, words[end])) {
++end;
}
//...
}
//...
bool isAnagram(const string &sample, string test) {
if (sample.size() == test.size()) {
sort(test.begin(), test.end());
return sample == test;
}
return false;
}
Оказалось, что решение без оптимизации
bool areAnagrams(string lhs, string rhs) {
if (lhs.size() == rhs.size()) {
sort(lhs.begin(), lhs.end());
sort(rhs.begin(), rhs.end());
return lhs == rhs;
}
return false;
}
все равно работает за 0 мс и бьет других решений.
Комментарии (5)

KukarekusUltra
19.10.2025 14:23Для map вы неправильно посчитали асимптотику.
Там будет О(n * log (m)) где m количество букв, в данном случае 26. То есть асимптотика будет O(n)

sa2304 Автор
19.10.2025 14:23https://en.cppreference.com/w/cpp/container/map/operator_at.html
Complexity
Logarithmic in the size of the container.Спасибо. Верно ли я понял?
m- размер контейнера, сложностьmap::operator[]-log(m).n- размер массива - вход алгоритма.-
Считаем сложность
map::operator[]константойO(1), когдаmне зависит отn. Тогда сложность алгоритма -O(n * log(m))все равно чтоO(n). Например:латинский алфавит из 26 букв
map<char, ...>,m = 26любой символ
charот0до0xFF,m = sizeof(char) = 255
Сложность
map::operator[]-log(n), если быmзависел отn. Тогда сложность алгоритма -O(n * log(n))
-

Jijiki
19.10.2025 14:23зашел на литкод и посмотрел задачку, и подумал щас
а что если проходиться по вектору, входному, и каждое слово тоесть побуквено кидать в своё дерево(бинарное), которое при добавлении сортирует буквы, когда дошли до конца вектора, имеем отсортированные слова, ну а там сравниваем и скипаем повторы как я понял
cpud47
Вы кажется очень лихо оценили сложность сортировки.
Просто нужно использовать words[write-1] вместо prev.
sa2304 Автор
Упс, ошибся, когда написал
:)
Видимо, слова
wordsкороткие, поэтому решение соstd::sortработает не хуже подсчета букв в массиве.