Разберите на примере: небольшой алгоритм сортировки на JavaScript (например, реализация quicksort) — опишите худший, средний и лучший случаи, стековый расход и как выбор опорного элемента влияет на производительность;
Код (простая in‑place реализация с Lomuto‑partition и случайным опорным элементом): function quicksort(arr, left = 0, right = arr.length - 1) { if (left >= right) return; // случайный опорный элемент — уменьшает шанс худшего случая const p = left + Math.floor(Math.random() * (right - left + 1)); [arr[p], arr[right]] = [arr[right], arr[p]]; const pivot = arr[right]; let i = left; for (let j = left; j < right; j++) { if (arr[j] < pivot) { [arr[i], arr[j]] = [arr[j], arr[i]]; i++; } } [arr[i], arr[right]] = [arr[right], arr[i]]; quicksort(arr, left, i - 1); quicksort(arr, i + 1, right); } Анализ производительности и памяти: - Время (количественные оценки): - лучший случай: O(nlogn) \;O(n \log n)\;O(nlogn) — когда опорный элемент постоянно делит массив примерно пополам. - средний (ожидаемый) случай: O(nlogn) \;O(n \log n)\;O(nlogn) — при случайном или «хорошем» выборе опорных элементов. - худший случай: O(n2) \;O(n^2)\;O(n2) — когда каждый раз получается сильно неравное разбиение (например, опора — минимум или максимум в уже отсортированном массиве). - Пространство: - дополнительная память (вне входного массива и вызовов): O(1) \;O(1)\;O(1) для in‑place версии (Lomuto/Hoare). - стек рекурсии: в среднем O(logn) \;O(\log n)\;O(logn), в худшем случае O(n) \;O(n)\;O(n) (при глубокой неравномерной рекурсии). Как выбор опорного элемента влияет на производительность: - фиксированный выбор (например, всегда первый/последний элемент) делает алгоритм уязвимым к уже отсортированным или почти отсортированным входам → частые плохие разбиения → худший случай O(n2) \;O(n^2)\;O(n2). - случайный выбор опоры (как в коде) даёт вероятностную гарантию: ожидаемое время O(nlogn) \;O(n \log n)\;O(nlogn), очень малый шанс встретить худший случай. - медиана из трёх (median‑of‑three) уменьшает вероятность плохих разбиений на «упорядоченных» входах и обычно ускоряет на практике. - три‑путёвое (3‑way) разбиение (Dutch national flag) полезно при множественных равных элементах — при большом числе одинаковых ключей это даёт выигрыш (в крайних случаях приводит к близкому к O(n) \;O(n)\;O(n) времени на равных элементах). - чтобы получить гарантию худшего случая O(nlogn) \;O(n \log n)\;O(nlogn), используют гибриды (introsort): при слишком большой глубине рекурсии алгоритм переключается на heapsort. Короткое резюме: - QuickSort — быстрый в среднем: O(nlogn) \;O(n \log n)\;O(nlogn); - но опасен на худших входах: O(n2) \;O(n^2)\;O(n2); - правильный выбор опоры (рандомизация, median‑of‑three, 3‑way) и/или гибрид с heap/intro делает его надёжным и эффективным на практике; - память: in‑place — O(1) \;O(1)\;O(1) + стек O(logn)\;O(\log n)O(logn) в среднем, O(n)\;O(n)O(n) в худшем.
function quicksort(arr, left = 0, right = arr.length - 1) {
if (left >= right) return;
// случайный опорный элемент — уменьшает шанс худшего случая
const p = left + Math.floor(Math.random() * (right - left + 1));
[arr[p], arr[right]] = [arr[right], arr[p]];
const pivot = arr[right];
let i = left;
for (let j = left; j < right; j++) {
if (arr[j] < pivot) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;
}
}
[arr[i], arr[right]] = [arr[right], arr[i]];
quicksort(arr, left, i - 1);
quicksort(arr, i + 1, right);
}
Анализ производительности и памяти:
- Время (количественные оценки):
- лучший случай: O(nlogn) \;O(n \log n)\;O(nlogn) — когда опорный элемент постоянно делит массив примерно пополам.
- средний (ожидаемый) случай: O(nlogn) \;O(n \log n)\;O(nlogn) — при случайном или «хорошем» выборе опорных элементов.
- худший случай: O(n2) \;O(n^2)\;O(n2) — когда каждый раз получается сильно неравное разбиение (например, опора — минимум или максимум в уже отсортированном массиве).
- Пространство:
- дополнительная память (вне входного массива и вызовов): O(1) \;O(1)\;O(1) для in‑place версии (Lomuto/Hoare).
- стек рекурсии: в среднем O(logn) \;O(\log n)\;O(logn), в худшем случае O(n) \;O(n)\;O(n) (при глубокой неравномерной рекурсии).
Как выбор опорного элемента влияет на производительность:
- фиксированный выбор (например, всегда первый/последний элемент) делает алгоритм уязвимым к уже отсортированным или почти отсортированным входам → частые плохие разбиения → худший случай O(n2) \;O(n^2)\;O(n2).
- случайный выбор опоры (как в коде) даёт вероятностную гарантию: ожидаемое время O(nlogn) \;O(n \log n)\;O(nlogn), очень малый шанс встретить худший случай.
- медиана из трёх (median‑of‑three) уменьшает вероятность плохих разбиений на «упорядоченных» входах и обычно ускоряет на практике.
- три‑путёвое (3‑way) разбиение (Dutch national flag) полезно при множественных равных элементах — при большом числе одинаковых ключей это даёт выигрыш (в крайних случаях приводит к близкому к O(n) \;O(n)\;O(n) времени на равных элементах).
- чтобы получить гарантию худшего случая O(nlogn) \;O(n \log n)\;O(nlogn), используют гибриды (introsort): при слишком большой глубине рекурсии алгоритм переключается на heapsort.
Короткое резюме:
- QuickSort — быстрый в среднем: O(nlogn) \;O(n \log n)\;O(nlogn);
- но опасен на худших входах: O(n2) \;O(n^2)\;O(n2);
- правильный выбор опоры (рандомизация, median‑of‑three, 3‑way) и/или гибрид с heap/intro делает его надёжным и эффективным на практике;
- память: in‑place — O(1) \;O(1)\;O(1) + стек O(logn)\;O(\log n)O(logn) в среднем, O(n)\;O(n)O(n) в худшем.