Эквивалентность и порядок: классы, разбиения и что можно сравнивать
Рефлексивность + симметричность + транзитивность = отношение эквивалентности, а оно разбивает множество на непересекающиеся классы — математика хеш-корзин и дедупликации. Порядки ранжируют; частичный порядок допускает несравнимые пары — так 1.2 и 1.10 сортируются неверно.
Скрипт деплоя выбирал «последний» релиз, сортируя теги версий и беря последний элемент. Месяцами работало. Потом вышла версия 1.10.0 — и скрипт уверенно откатил продакшен на 1.9.0, потому что как строки «1.10.0» сортируется раньше «1.2.3»: строковое сравнение читает символ за символом, а символ «1» меньше «2». Никто не задал тихий вопрос, лежащий под любой сортировкой: каким порядком мы пользуемся и тот ли это порядок, который мы имеем в виду? Этот урок — о двух самых полезных видах отношений, собранных из свойств прошлого урока. Отношения эквивалентности отвечают на вопрос «что считать одинаковым?» — благодаря им работают хеш-корзины, а дедупликация email требует сперва привести их к нижнему регистру. Порядки отвечают на вопрос «что идёт раньше чего?» — и прячут сюрприз: некоторые совершенно честные порядки не могут сравнить каждую пару.
После этого урока ты можешь определить отношение эквивалентности через три его свойства, объяснить, почему каждая эквивалентность разбивает множество на непересекающиеся классы, написать дедупликацию через канонического представителя, определить частичный порядок и отличить его от полного, и найти несравнимые пары в конкретном отношении.
Отношение эквивалентности обладает всеми тремя свойствами: рефлексивностью, симметричностью, транзитивностью. Вместе они формализуют «считается одинаковым для моих целей»: всё одинаково с собой; если a одинаково с b, то b одинаково с a; если a ~ b и b ~ c, то a ~ c. Канонический пример — одинаковый остаток от деления на 3 на целых числах: 7 и 4 оба дают остаток 1, значит, эквивалентны. Другие будничные эквивалентности: одинаковая длина строки, «анаграмма» («listen» ~ «silent»), один email после приведения к нижнему регистру и обрезки пробелов.
Каждое отношение эквивалентности разбивает множество на непересекающиеся классы эквивалентности. Каждый элемент попадает ровно в один класс, а два элемента связаны в точности тогда, когда делят класс. Остаток mod 3 режет целые на три класса: {0, 3, 6, 9, …}, {1, 4, 7, 10, …}, {2, 5, 8, 11, …}. Ни одно число не лежит в двух классах, ни одно не осталось снаружи — это гарантируют три свойства. Симметричность с транзитивностью заставляют классы заглатывать группы целиком; рефлексивность следит, чтобы никто не сбежал.
// "Один и тот же человек" на email = одинаковое значение после нормализации.
const canon = (e) => e.trim().toLowerCase();
const emails = ["Ana@Example.com ", "ana@example.com", "BOB@x.dev"];
const classes = new Map();
for (const e of emails) {
const rep = canon(e);
(classes.get(rep) ?? classes.set(rep, []).get(rep)).push(e);
}
classes.size; // 2 — двое людей, три написания
[...classes.keys()]; // ["ana@example.com", "bob@x.dev"]Частичный порядок — рефлексивный, транзитивный и антисимметричный: взаимная связь влечёт равенство, поэтому стрелки ранжируют, а не группируют. Числа под ≤ — привычный случай. Слово частичный — сюрприз: полный (линейный) порядок сравнивает каждую пару (возьмите любые два числа — одно ≤ другого), а частичный порядок законно оставляет некоторые пары несравнимыми. Под ⊆: {1, 2} и {2, 3} — не равны, и ни одно не содержит другое. Они несравнимы, и ничего не сломано. Порядок просто частичен.
Сортировка не тем полным порядком — это и есть баг версий. Лексикографический порядок и порядок semver — оба полные порядки на строках версий, но они расходятся: как строки «1.10.0» < «1.2.3», потому что посимвольно «1» < «2»; semver же сравнивает компоненты численно. Скрипт деплоя отсортировал не тем полным порядком и откатил продакшен. Сортировка всегда присягает какому-то отношению — класс багов: присяга не тому.
const tags = ["1.9.0", "1.10.0", "1.2.3"];
tags.sort(); // → ["1.10.0", "1.2.3", "1.9.0"] ← неверно
const byVersion = (a, b) => {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
return (pa[0] - pb[0]) || (pa[1] - pb[1]) || (pa[2] - pb[2]);
};
tags.sort(byVersion); // → ["1.2.3", "1.9.0", "1.10.0"] ← верноКлассифицируйте каждое отношение: эквивалентность, частичный порядок, полный порядок или ничто из перечисленного.
- «Одинаковая длина» на строках — рефлексивно («ab» той же длины, что «ab»), симметрично, транзитивно. Эквивалентность. Режет строки на классы по длинам: 0, 1, 2, …
- ⊆ на множествах — рефлексивно, транзитивно, антисимметрично (A ⊆ B и B ⊆ A влечёт A = B). Но {1,2} и {2,3} несравнимы → частичный порядок, не полный.
- ≤ на целых — рефлексивно, транзитивно, антисимметрично, и любые два числа сравнимы. Полный порядок.
- «Является префиксом» на строках — рефлексивно, транзитивно, но не симметрично. Антисимметрично? Взаимная вложенность префиксов возможна только между одинаковыми строками → частичный порядок.
- «Имеет хотя бы один общий символ» — симметрично, но не транзитивно: «ab» и «bc» имеют общий «b», «bc» и «cd» имеют «c», а у «ab» и «cd» общего ничего. Ни то, ни другое.
▸Почему это работает
Почему хеш-корзины считаются классами эквивалентности? Хеш-таблица кладёт x и y в одну корзину, когда hash(x) === hash(y), — а «одинаковый хеш» рефлексивен, симметричен и транзитивен, значит, корзины — классы настоящего отношения эквивалентности. Тонкость: эта эквивалентность грубее равенства — равные значения всегда делят корзину, но коллизии могут класть в неё и неравные. Поэтому поиск двухступенчатый: хеш находит класс, затем внутри него работает честная проверка равенства. Разбиение делает быстрое сужение; равенство говорит последнее слово.
Назовите три свойства, делающих отношение эквивалентностью (по одному слову через запятую).
Остаток mod 3 делит целые числа на сколько классов? Напишите число.
Сравнимы ли {1,2} и {2,3} по ⊆? Ответьте «да» или «нет».
Является ли «является префиксом» симметричным? Ответьте «да» или «нет».
Скрипт сортирует теги версий стандартным JS sort(). Что стоит ПОСЛЕДНИМ: '1.9.0' или '1.10.0'? Напишите точно.
Какое отношение на строках является отношением эквивалентности?
Сохраните все три попарных свойства — рефлексивность, симметричность, транзитивность — и получится отношение эквивалентности, точная форма «считается одинаковым»: одинаковый остаток mod 3, одинаковая длина строки, анаграмма, один email после нормализации. Каждая эквивалентность разбивает множество на непересекающиеся классы с каждым элементом ровно в одном; groupBy разбивает по ключу, хеш-корзины — классы «одинакового хеша», дедупликация хранит по одному каноническому представителю на класс. Замените симметричность антисимметричностью — взаимная связь влечёт равенство — и получите порядки, которые ранжируют, а не группируют. Полный порядок сравнивает каждую пару (числа под ≤); частичный законно оставляет пары несравнимыми: ⊆ на множествах и «должно выполниться раньше» на задачах, где несравнимо значит параллелизуемо, а цикл нарушает антисимметричность так, что корректной последовательности не существует. Баг сортировки версий дистиллирует дисциплину: лексикографический порядок и порядок semver оба полные, но смыслу отвечает один, и «1.10.0», проигрывающее «1.9.0», — вот как выглядит сортировка не тем отношением. Прежде чем сравнивать — назовите порядок; прежде чем группировать — назовите эквивалентность.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.