Стабилно сортиране и вграденият sort
Накратко: Стабилното сортиране пази реда на елементите с равни ключове; Array.prototype.sort в JavaScript е стабилен, затова подреждането по няколко ключа става на няколко минавания.
Проблем
Датите, които се разбъркват
В админ панела на магазина поръчките стоят по дата: най-старите отгоре. Служителят натиска заглавието „Клиент“, за да види поръчките на всеки клиент заедно. Очаква поръчките на Иван да са една под друга, и пак по дата.
Ето какво дава подреждане по клиент със selection sort:
function selectionSort(orders) { for (let i = 0; i < orders.length - 1; i++) { let smallest = i; for (let j = i + 1; j < orders.length; j++) { if (orders[j].customer < orders[smallest].customer) { smallest = j; } } const tmp = orders[i]; orders[i] = orders[smallest]; orders[smallest] = tmp; }}const orders = [ { date: "2026-10-03", customer: "Иван", amount: 42.50 }, { date: "2026-10-05", customer: "Мария", amount: 18.00 }, { date: "2026-10-07", customer: "Иван", amount: 120.00 }, { date: "2026-10-09", customer: "Петър", amount: 64.90 }, { date: "2026-10-12", customer: "Мария", amount: 35.20 }, { date: "2026-10-15", customer: "Иван", amount: 9.99 },];selectionSort(orders);for (const o of orders) { console.log(o.customer, o.date);}Иван 2026-10-03Иван 2026-10-07Иван 2026-10-15Мария 2026-10-12Мария 2026-10-05Петър 2026-10-09Клиентите са подредени. Но при Мария поръчката от 12 октомври е преди тази от 5 октомври. Служителят вижда последната поръчка на Мария като първа и ѝ звъни за грешната.
Идеята
Стабилност: равните пазят реда си
Подреждане е стабилно, ако елементите с равен ключ остават в реда, в който са били. Двете поръчки на Мария са с равен ключ (клиентът). Стабилното подреждане ги оставя както са дошли: първо 5 октомври, после 12.
Кои алгоритми са стабилни:
- insertion sort: мести елемент само покрай строго по-големи, затова никога не прескача равен;
- merge sort: при равни взима от лявата половина (merge sort);
- counting sort: поставя отляво надясно (counting sort).
Кои не са:
- selection sort, quick sort и heap sort. И трите разменят елементи на разстояние. Размяната пренася елемент над всичко между двете места, и равните между тях остават зад него.
Стабилността има значение само когато елементът носи нещо повече от ключа. Две равни числа 5 не се различават. Две поръчки на Мария се различават.
Виж го
Selection sort срещу insertion sort
Същият списък с insertion sort:
function insertionSort(orders) { for (let i = 1; i < orders.length; i++) { const current = orders[i]; let j = i - 1; while (j >= 0 && current.customer < orders[j].customer) { orders[j + 1] = orders[j]; j--; } orders[j + 1] = current; }}const orders = [ { date: "2026-10-03", customer: "Иван", amount: 42.50 }, { date: "2026-10-05", customer: "Мария", amount: 18.00 }, { date: "2026-10-07", customer: "Иван", amount: 120.00 }, { date: "2026-10-09", customer: "Петър", amount: 64.90 }, { date: "2026-10-12", customer: "Мария", amount: 35.20 }, { date: "2026-10-15", customer: "Иван", amount: 9.99 },];insertionSort(orders);for (const o of orders) { console.log(o.customer, o.date);}Иван 2026-10-03Иван 2026-10-07Иван 2026-10-15Мария 2026-10-05Мария 2026-10-12Петър 2026-10-09function selectionSort(orders) { for (let i = 0; i < orders.length - 1; i++) { let smallest = i; for (let j = i + 1; j < orders.length; j++) { if (orders[j].customer < orders[smallest].customer) { smallest = j; } } const tmp = orders[i]; orders[i] = orders[smallest]; orders[smallest] = tmp; }}function insertionSort(orders) { for (let i = 1; i < orders.length; i++) { const current = orders[i]; let j = i - 1; while (j >= 0 && current.customer < orders[j].customer) { orders[j + 1] = orders[j]; j--; } orders[j + 1] = current; }}Стъпка по стъпка, с думи
- Шест поръчки, подредени по дата. Сега по клиент, със selection sort.
- Място 0: най-малкият клиент оттук нататък е Иван 03.10, вече е на мястото си.
- Място 1: Иван 07.10 се разменя с Мария 05.10.
- Място 2: Иван 15.10 се разменя с Мария 05.10. Далечната размяна хвърля Мария 05.10 в края, зад Мария 12.10.
- Още две размени: Мария идва преди Петър.
- Подредено по клиент, но Мария 12.10 е преди Мария 05.10. Selection sort не е стабилен: равните ключове губят реда си.
- Същият списък с insertion sort. Поръчката се мести само покрай строго по-големи, затова Мария 05.10 остава преди Мария 12.10. Стабилно.
Поръчката, която insertion sort мести наляво, спира пред първата, която не е по-голяма. Равна поръчка не се прескача, и датите на Мария остават 05, после 12.
Пробвай
Намери грешката
Някой е сменил едно сравнение в insertion sort. Подреждането пак е по клиент, но датите вътре в клиента излизат обърнати:
function insertionSort(orders) { for (let i = 1; i < orders.length; i++) { const current = orders[i]; let j = i - 1; while (j >= 0 && current.customer <= orders[j].customer) { orders[j + 1] = orders[j]; j--; } orders[j + 1] = current; }}const orders = [ { date: "2026-10-03", customer: "Иван", amount: 42.50 }, { date: "2026-10-05", customer: "Мария", amount: 18.00 }, { date: "2026-10-07", customer: "Иван", amount: 120.00 }, { date: "2026-10-09", customer: "Петър", amount: 64.90 }, { date: "2026-10-12", customer: "Мария", amount: 35.20 }, { date: "2026-10-15", customer: "Иван", amount: 9.99 },];insertionSort(orders);for (const o of orders) { console.log(o.customer, o.date);}Иван 2026-10-15Иван 2026-10-07Иван 2026-10-03Мария 2026-10-12Мария 2026-10-05Петър 2026-10-09На кой ред е грешката?
Избери реда с грешката.
<= пуска текущата и пред равните. Поръчките на един клиент излизат в обратен ред. Върни строгото <.Виж го
Вграденият sort
На практика не пишеш свое подреждане, а ползваш вграденото. Двайсет поръчки, по дата, подредени по клиент. Печатат се датите на всеки клиент:
В JavaScriptArray.prototype.sort е стабилен от ES2019 нататък: стандартът го изисква. Сравнението връща отрицателно, нула или положително число.orders.sort((a, b) => a.customer.localeCompare(b.customer));show(orders);Иван: 01 04 06 09 12 15 18Мария: 02 05 08 11 13 16 19Петър: 03 07 10 14 17 20Идеята
Какво ползва вграденият sort на всеки език
- Java.
Arrays.sortза масив от примитиви (int[],double[]) е dual-pivot quick sort: не е стабилен, но при числата това няма значение. За обекти (Arrays.sort(Object[]),List.sort,Collections.sort) е TimSort: стабилен. - C#.
Array.SortиList<T>.Sortса introsort: quick sort, който минава на heap sort при твърде дълбоко разделяне и на insertion sort при малки части. Не е стабилен. LINQOrderByиThenByса стабилни. - Python.
list.sortиsortedса TimSort: стабилни. - JavaScript.
Array.prototype.sortтрябва да е стабилен от ES2019. V8 (Chrome, Node.js) ползва TimSort от версия 7.0.
TimSort е merge sort, който търси вече подредени парчета във входа и ги слива. Реалните данни често са почти подредени, и тогава той е близо до O(n).
По-точно, за любопитните
- Python: от версия 3.11 правилото, по което TimSort избира кои парчета да слее, е powersort (на Munro и Wild). Подреждането пак е стабилно, сливанията са по-добре подредени.
- Java: стабилността за примитиви наистина няма значение, защото две равни
intне се различават. Затова там е избран по-бързият quick sort. - C#: introsort пази O(n log n) в най-лошия случай благодарение на heap sort (heap sort). Цената е стабилността.
- JavaScript: преди ES2019 V8 ползваше quick sort за дълги масиви и подреждането не беше стабилно. Код, писан тогава, понякога има „поправки“ за това, които днес не трябват.
- И в четирите: сравнението трябва да е последователно. Ако казва, че А е преди Б, а Б е преди А, резултатът е непредвидим, а в Java
List.sortможе да хвърлиIllegalArgumentException: Comparison method violates its general contract!.
Виж го
Подреждане по няколко ключа
Често ключът е повече от едно поле: по клиент, а вътре в клиента най-голямата сума първа.
Сравнението връща число.|| минава към втория ключ само когато първият даде 0, тоест когато клиентите са равни. b.amount - a.amount е най-голямата сума първа.orders.sort((a, b) => a.customer.localeCompare(b.customer) || b.amount - a.amount);Иван 2026-10-07 120.00 €Иван 2026-10-03 42.50 €Иван 2026-10-15 9.99 €Мария 2026-10-12 35.20 €Мария 2026-10-05 18.00 €Петър 2026-10-09 64.90 €Има и втори начин, който работи само със стабилно подреждане: подреди първо по втория ключ, после по първия. Второто подреждане пази реда на равните, а той вече е по втория ключ. Последното подреждане е главното.
По-точно, за любопитните
Кога е полезен вторият начин: когато ключовете не се събират в едно сравнение. Например в Python -o.amount не става за текст: за „по клиент, а вътре по име на продукта в обратен ред“ се подрежда два пъти, първо key=lambda o: o.product, reverse=True, после key=lambda o: o.customer.
Пробвай
Предвиди изхода
Две стабилни подреждания едно след друго: първо по сума, най-голямата първа, после по клиент. Какво печата програмата?
const orders = [ { date: "2026-10-05", customer: "Мария", amount: 35.20 }, { date: "2026-10-06", customer: "Иван", amount: 9.99 }, { date: "2026-10-07", customer: "Мария", amount: 18.00 }, { date: "2026-10-08", customer: "Иван", amount: 42.50 },];orders.sort((a, b) => b.amount - a.amount);orders.sort((a, b) => a.customer.localeCompare(b.customer));for (const o of orders) { console.log(o.customer, o.amount.toFixed(2));}Упражнение
Доставките по град
Реши задачата в редактора си.
По град, после по сума. Шест доставки имат град и сума. Подреди ги по град, а в един град най-голямата сума първа, с вградения sort на езика си.
Варна 40.00 €Варна 9.90 €Пловдив 24.00 €Пловдив 15.30 €София 80.00 €София 12.50 €Още задачи
- Два пъти. Направи същото с две стабилни подреждания, без да слагаш два ключа в едно сравнение. Кое подреждане е първо?
- Нестабилно на ръка. Подреди доставките по град с heap sort от урока за heap. Остават ли сумите в един град в реда, в който бяха?
- Стабилен selection sort. Може ли selection sort да стане стабилен, ако вместо да разменя, премести най-малкия на мястото му и избута останалите надясно? Колко струва това?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Merge sort и „разделяй и владей“ · Quick sort и лошият пивот · Heap и heap sort · Counting sort и radix sort
- Продължава в: Приоритетна опашка · Trie · Минимално покриващо дърво · Как да разпознаеш DP задача
- Задачи по темата: Сортиране