09. К-порядковая статистика
Обратите внимание на другие наши ресурсы: Youtube | Telegram | VK | Rutube | Dzen | Boosty
Определение
K-порядковая статистика (или K-й порядковый элемент) - это алгоритм, который находит -й элемент в отсортированной версии массива, не сортируя весь массив.
Сферы применения
K-порядковая статистика находит свое применение в задачах, где требуется быстро определить -й элемент по величине в массиве, не прибегая к полной сортировке. Это делает алгоритм особенно полезным в ситуациях, где важен быстрый доступ к данным и их анализ.
- Медиана: часто используется для нахождения медианы в массиве, особенно в задачах, связанных с анализом данных.
- Квантили: применяется для поиска различных квантилей (например, -й или -й перцентиль) в статистике. Квантили — это статистические меры, которые делят данные на равные части. Например, медиана — это -й перцентиль или -квантиль, которая делит данные на две равные части. Квантили позволяют понять распределение данных и найти значения, которые отделяют определенные доли набора данных.
- Поиск элементов: подходит для поиска -й по величине или -й по порядку элемента в больших наборах данных.
Принцип работы алгоритма
Алгоритм -порядковой статистики использует принцип "разделяй и властвуй", аналогичный быстрой сортировке (QuickSort). Весь процесс делится на несколько шагов, которые позволяют эффективно найти -й элемент без полной сортировки массива.
Шаги алгоритма
-
Выбираем опорный элемент из списка.
-
Разделяем список на три части:
- Левая часть содержит все элементы, которые меньше опорного.
- Правая часть содержит все элементы, которые больше опорного.
- Средняя часть содержит все элементы, которые равны опорному.
-
Сравниваем значение с размерами левой и средней частей:
- Если меньше размера левой части, то искомая -порядковая статистика находится в левой части, и рекурсивно запускаем наш алгоритм для левой части.
- Если больше размера левой и средней частей, то искомая -порядковая статистика находится в правой части, и мы рекурсивно запускаем наш алгоритм для правой части. При этом, уменьшаем на сумму длин левой и средней частей, из-за смещения индексов.
- Если находится между размерами левой и средней частей, то опорный элемент является искомой k-порядковой статистикой, и возвращаем его значение.
-
Продолжаем рекурсивно делить исходный список и выбирать опорный элемент, пока k-порядковая статистика не будет найдена.
Особенности
- Быстрее полной сортировки: в большинстве случаев выполняется быстрее, чем сортировка всего массива.
- Простота реализации: относительно простой в реализации и понимании.
- Универсальность: может быть использован для поиска любого -го элемента в массиве.
- Неустойчивость к худшему случаю: в худшем случае может работать медленно, особенно если опорный элемент выбирается неудачно.
- Зависимость от выбора опорного элемента: эффективность сильно зависит от удачного выбора опорного элемента.
Вычислительная сложность
Средний случай
В среднем случае вычислительная сложность алгоритма составляет . Это достигается благодаря тому, что опорный элемент (pivot) в среднем делит массив на достаточно равные части, что позволяет алгоритму быстрее находить нужный элемент без полного обхода массива.
Доказательство
-
Равномерное разбиение: предположим, что в среднем случае опорный элемент делит массив примерно на две равные части. То есть, каждый раз массив делится на две части размером .
-
Рекурсивное уравнение: для оценки средней сложности можем записать следующее рекурсивное уравнение:
Здесь:
-
— сложность вызова алгоритма на половине массива.
-
— время, необходимое для разбиения массива и поиска порядка элемента.
-
Решение уравнения: это уравнение можно решить методом разложения (Master Theorem):
Сумма геометрической прогрессии:
- Сходимость прогрессии: Сумма этой геометрической прогрессии равна:
Таким образом, средняя вычислительная сложность поиска -й порядковой статистики составляет . Это достигается за счет эффективного разбиения массива опорным элементом, что позволяет сократить количество элементов для последующих рекурсивных вызовов.
Худший случай
В худшем случае вычислительная сложность алгоритма составляет . Это происходит, когда опорный элемент (pivot) каждый раз выбирается так, что он оказывается либо минимальным, либо максимальным элементом текущего подмассива. В результате массив разделяется на части размером и , что приводит к наибольшему количеству рекурсивных вызовов.
Доказательство
-
Выбор опорного элемента: в худшем случае на каждом этапе опорный элемент делит массив на части размером и . Это происходит, когда опорный элемент всегда оказывается самым большим или самым маленьким элементом подмассива.
-
Рекурсивный процесс: из-за неудачного выбора опорного элемента алгоритм вынужден обрабатывать все элементов в каждой рекурсии, затем элементов, и так далее. В результате количество операций, необходимых для обработки каждого уровня, составляет .
-
Общее количество операций: поскольку количество операций на каждом уровне составляет , а число уровней в худшем случае — , общее количество операций равно:
-
Сумма арифметической прогрессии: эта сумма представляет собой арифметическую прогрессию, которая может быть записана как:
Опуская константы и младшие члены, сложность составляет .
Таким образом, в худшем случае, когда опорный элемент всегда делит массив на максимально неравные части, алгоритм имеет вычислительную сложность .
Пространственная сложность
Средний и худший случай: . Алгоритм работает на месте, не требуя дополнительной памяти пропорциональной размеру входных данных, кроме стека вызовов в рекурсивной версии.
Доказательство
Поскольку алгоритм работает на месте (in-place) и использует только константное количество дополнительной памяти, его пространственная сложность составляет . Стек вызовов для рекурсивной версии требует в среднем и в худшем случае, но это всё равно меньше, чем для хранения дополнительных данных.
Схематическая работа алгоритма
Исходный массив :
Предположим, мы ищем -й порядковый элемент (то есть -й по величине элемент).
Шаг 1:
Выберем в исходном массиве опорным элементом центральное число -
Разделим исходный массив на подмассива: меньше, равные и больше
Шаг 2:
- В "меньших" три элемента.
- В "равных" два элемента. Итого, элементов, так что -й элемент находится в "больших".
Теперь ищем -й элемент минус элементов в первых двух группах, то есть -й элемент среди оставшихся.
Шаг 3:
Теперь мы ищем -й порядковый элемент среди массива. Выбираем опорным элементом и разделяем массив на три подмассива.
Теперь ищем -й элемент:
- Меньших элементов нет.
- Равные — это -й элемент, так что мы ищем -й элемент в массиве .
Шаг 4
Теперь мы ищем -й элемент среди:
Выбираем опорным элементом и разделяем массив на три подмассива: меньше, равные и больше.
Равные — это -й элемент, так что -й элемент — это первый элемент из "больших", то есть .
Результат
-я порядковая статистика для массива — это .
Псевдокод
функция k_порядковая_статистика(массив, k): если длина(массив) == 1: вернуть массив[0] опорный_элемент = массив[длина(массив) // 2] левый = [x для x в массив если x < опорный_элемент] правый = [x для x в массив если x > опорный_элемент] равные = [x для x в массив если x == опорный_элемент] если k < длина(левый): вернуть k_порядковая_статистика(левый, k) иначе если k < длина(левый) + длина(равные): вернуть равные[0] иначе: вернуть k_порядковая_статистика(правый, k - длина(левый) - длина(равные))
Реализация на Python
Аннотация: k_order_statistic - функция K-порядковой статистики arr - массив, в котором происходит поиск k - искомая статистика pivot - опорный элемент left - массив с элементами, которые меньше опорного right - массив с элементами, которые больше опорного equal - массив с элементами, которые равны опорному
def k_order_statistic(arr, k): if len(arr) == 1: return arr[0] pivot = arr[len(arr) // 2] left = [x for x in arr if x < pivot] right = [x for x in arr if x > pivot] equal = [x for x in arr if x == pivot] if k < len(left): return k_order_statistic(left, k) elif k < len(left) + len(equal): return equal[0] else: return k_order_statistic(right, k - len(left) - len(equal))
Олимпиадная реализация на C++
Аннотация: typedef - переопределение имени типа данных kOrder - функция K-порядковой статистики arr - массив, в котором происходит поиск k - искомая статистика p - опорный элемент l - массив с элементами, которые меньше опорного r - массив с элементами, которые больше опорного equ - массив с элементами, которые равны опорному
typedef vector<int> vi; int kOrder(vi& arr, int k) { if (arr.size() == 1) return arr[0]; int p = arr[arr.size() / 2]; std::vector<int> l, r, equ; for (int x : arr) { if (x < p) l.push_back(x); else if (x > p) r.push_back(x); else equ.push_back(x); } if (k < l.size()) return kOrder(l, k); else if (k < l.size() + equ.size()) return equ[0]; else return kOrder(r, k - l.size() - equ.size()); }
Продуктовая реализация на C++
Аннотация: kOrderStatistic - функция K-порядковой статистики array - массив, в котором происходит поиск k - искомая статистика pivot - опорный элемент left - массив с элементами, которые меньше опорного right - массив с элементами, которые больше опорного equal - массив с элементами, которые равны опорному
int kOrderStatistic(std::vector<int>& array, int k) { if (array.size() == 1) return array[0]; int pivot = array[array.size() / 2]; std::vector<int> left, right, equal; for (int element : array) { if (element < pivot) left.push_back(element); else if (element > pivot) right.push_back(element); else equal.push_back(element); } if (k < left.size()) return kOrderStatistic(left, k); else if (k < left.size() + equal.size()) return equal[0]; else return kOrderStatistic(right, k - left.size() - equal.size()); }