\documentclass[12pt,fleqn,a4paper]{book}
%
%
%
\usepackage[koi8-r]{inputenc}
\usepackage[russian,english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{amsfonts}
\usepackage{amsthm}
\usepackage{url}
\usepackage{EZlist}
%
%
%
\pagestyle{headings}
\newtheorem{theorem}{Теорема}[chapter]
\newtheorem{lemma}{Лемма}[chapter]
\newtheorem{proposition}{Утверждение}[chapter]
\newtheorem{fact}{Факт}[chapter]
\theoremstyle{definition}
\newtheorem{problem}{Задача}[chapter]
\newtheorem{exercise}{Упражнение}[chapter]
\newtheorem{example}{Пример}[chapter]
\newtheorem{definition}{Определение}[chapter]
\newtheorem{remark}{Замечание}[chapter]
\newtheorem{algorithm}{Алгоритм}[chapter]
\newcommand{\marginnote}[1]{}
\def\gap{\medskip\centerline{\fbox{\Huge\bfseries{ПРОБЕЛ В КОНСПЕКТЕ.}}}\medskip}
\advance\headheight by 7pt
\def\headsep{15mm}
\newcommand{\lecture}[3]{%
\def\rightmark{\fbox{\parbox{125mm}{\lecturername: \coursetitle%
\hfil\phantom{.}}}}
\def\leftmark{\fbox{\parbox{125mm}{Лекция {#1}. {#2}%
\hfil\phantom{.}}}}
\renewcommand\chaptername{Лекция}\renewcommand\thechapter{#1}\chapter{{#2}\\{\small }}
\thispagestyle{headings}}
%
%
%
\newcommand\lecturername{Э. А. Гирш}
\newcommand\coursetitle{``Информатика'' (отд.математики, 1 курс)}
%
%
%
%
%
%
\begin{document}
\selectlanguage{russian}
\sloppy
%
%
%
\lecture{3}{Представление данных (II).\\{\small Массив (сортировка, поиск $k$-го элемента).}}{1:30}
%
%
%
%
\section{Массив.}

Абстрактное понятие массива. Линейный (полный) порядок.
Сортировка массива.
\marginnote{5}

\gap

\subsection{HeapSort}\marginnote{40}
Алгоритм HeapSort получил свое название от английского 
слова \emph{heap} --- куча.
Неформально говоря, в этом алгоритме
данные из массива организуются в виде <<\emph{кучи}>>:
двоичного дерева, в каждой вершине которого хранится элемент,
не превосходящий элемента, хранящегося в родителе этой вершины.

Как нетрудно видеть, в таком представлении легко найти наибольший
элемент массива: он находится в корне дерева. Удалив его из дерева
и восстановив структуру кучи, мы сможем так же легко найти следующий
по убыванию элемент, и$\;$т.$\;$д.

Таким образом, для достижения цели нам достаточно научиться строить
кучу и восстанавливать правильность ее структуры после удаления ее корня.
И то, и другое мы будем делать при помощи рекурсивной операции 
<<\emph{утапливания}>> вершины: если в некоторой вершине хранится элемент,
строго меньший элемента, хранящегося в одном из сыновей этой вершины,
то этот элемент надо поменять местами с его сыном (с тем из двух сыновей, 
в котором ключ наибольший), а затем, если необходимо, продолжить его
<<утапливание>>.

Для того, чтобы описать этот алгоритм более строго, зафиксируем способ
представления нашей кучи. Будем ее поддерживать в том же самом массиве,
который нам дан. Укладка дерева в массив $a$ производится следующим образом.
Занумеруем дерево по уровням: корень --- это $a[1]$,
вершины следующего уровня --- это $a[2]$ и $a[3]$, и$\;$т.$\;$д.
При такой нумерации массив $a$ содержит все элементы этого дерева,
причем сыновья вершины $a[i]$ расположены в $a[2i]$ и $a[2i+1]$.
Нам достаточно такого представления дерева, поскольку
нам нужны лишь две операции: чтение конкретного элемента $a[i]$
и перестановка \emph{содержимого}
двух его вершин: swap$(a[i],a[j])$.

\medskip
\begin{description}
\item[] <<Утопим>> вершину:\\

\noindent
procedure pushnode ( $i$ : integer );\\
begin
\begin{itemize}
\item[] if $i$ --- лист then return;\\[-7pt]
\item[] выбрать из потомков $i$ наибольший (назовем $j$, это $2i$ или $2i+1$);
\item[] if $a[j]>a[i]$ \hfill (* \emph{что неправильно!} *)
\item[] then begin swap$(a[i],a[j])$; pushnode($j$) end
\end{itemize}
end;\\

\begin{remark}
Количество вершин в нашем дереве будет сокращаться.
Процедура {\rm pushnode} должна отслеживать это;
в частности, правильно определять, сколько в текущий момент
времени потомков у $i$ (ноль, один или два).
Заметим, что мы используем массив $a$,
количество элементов в нем и текущее количество элементов в дереве как глобальные переменные.
\qed
\end{remark}
\medskip

\item[] Построим правильную кучу (пусть всего в ней $n$ вершин):\\

\noindent
procedure pushall;\\
begin
\begin{itemize}
\item[] for $i$:=$n$ downto $1$ do pushnode($i$)
\end{itemize}
end;\\

\begin{lemma}\label{lem:pushall:corr}
В дереве, построенном процедурой {\rm pushall}, 
никакой потомок не превосходит родителя.
\end{lemma}
\begin{proof}
Индукция по \emph{убыванию} номера вершины (от $n$ до $1$).
Иначе говоря, по построению дерева (добавлению корня к двум поддеревьям).
На первом же шаге новая вершина становится больше всех своих потомков,
на втором --- единственная вершина, в которой что-то могло испортиться,
также становится больше всех своих потомков, и$\;$т.$\;$д.
\end{proof}

\begin{lemma}\label{lem:pushall:time}
Процедура {\rm pushall} (вместе с вызовами процедуры {\rm pushnode})
использует $O(n\log n)$ операций обмена ({\rm swap}).
\end{lemma}
\begin{proof}
Для каждой из $n$ вершин вызывается процедура {\rm pushnode}.
Она делает не более $\log n$ рекурсивных вызовов
(поскольку такова высота дерева),
в каждом из них происходит лишь константное число обращений к элементам
массива.
\end{proof}

\begin{exercise}
Показать, что на самом деле используется лишь $O(n)$ операций
(хотя для дальнейших рассуждений нам это не будет важно).
\qed
\end{exercise}
\medskip

\item[] Наконец, отсортируем массив:\\

\noindent
procedure heapsort;\\
begin
\begin{itemize}
\item[] pushall;\\[-7pt]
\item[] for $i$:=$n$ downto $1$ do
\item[] begin
\begin{itemize}
\item[] swap($a[1],a[i]$); \mbox{\small(*\emph{$a[1]$ --- наибольший из оставшихся ---
                                      в конец!}*)}
\item[] pushnode($1$); \hfill {\small(*\emph{ведь $a[1]$ <<испортился>>}*)}
\end{itemize}
\item[] end
\end{itemize}
end;\\
\end{description}

\begin{theorem}\label{th:heapsort}
Процедура {\rm heapsort} правильно сортирует массив
и затрачивает на это лишь $O(n\log n)$ операций
с элементами массива.
\end{theorem}
\begin{proof}
\begin{description}
\item[Время работы] складывается из времени работы {\rm pushall}
(см. лемму~\ref{lem:pushall:time}) и времени работы
процедуры pushnode (в доказательстве леммы~\ref{lem:pushall:time}
мы уже видели, что это $O(\log n)$ операций), вызванной $n$ раз.
\item[Корректность] \emph{построения} кучи
доказана в лемме~\ref{lem:pushall:corr}.
То, что на каждом шаге после отправки $a[1]$ в конец
куча восстанавливается правильно,
можно доказать аналогично индуктивному шагу 
в доказательстве леммы~\ref{lem:pushall:corr}.
Наконец, благодаря основному свойству кучи,
на каждом шаге мы действительно <<вытаскиваем>> из нее (отправляем в конец массива)
наибольший из оставшихся элементов.
\end{description}
\end{proof}

\begin{remark}
Теорема~\ref{th:heapsort} справедлива для \emph{любого} массива $a$
(с \emph{любыми} значениями). Таким образом, мы оценили время работы
алгоритма \emph{в наихудшем случае}.\qed
\end{remark}

\begin{exercise}
Точное время работы зависит от того, какие элементы мы сортируем.
Какое время займет сортировка массива целых чисел на RAM-машине
при помощи алгоритма {\rm heapsort}?
\qed
\end{exercise}


%
\subsection{QuickSort}\marginnote{25}

Алгоритм QuickSort:
возьмем какой-нибудь (скажем, первый в массиве) элемент,
поставим его на нужное место $i$
(так что все меньшие его элементы находятся слева, все б\'ольшие --- справа)
и рекурсивно отсортируем полученные массивы,
состоящие из $i-1$ и $n-i$ элементов соответственно.

\begin{theorem}\label{th:quicksort:wc:upper}
В алгоритме QuickSort
количество операций над элементами массива
в наихудшем случае составляет $O(n^2)$.
\end{theorem}
\begin{exercise}
Доказать теорему~\ref{th:quicksort:wc:upper}.
\qed
\end{exercise}

\begin{remark}
Говорят, что $f=\Omega(g)$, если $g=O(f)$.
Однако, есть и другое определение
(часто используемое в теории сложности): $f=\Omega(g)$,
если $f\neq o(g)$, т.е. $f(n)$ бесконечно часто бывает больше
$cg(n)$ для некоторой константы $c>0$.
\end{remark}

\begin{theorem}
В алгоритме QuickSort
количество операций над элементами массива
в наихудшем случае составляет $\Omega(n^2)$.
\end{theorem}
\begin{proof}
Рассмотрим поведение алгоритма на уже отсортированном массиве.
\end{proof}

\begin{theorem}\label{th:quicksort:avg}
В алгоритме QuickSort
количество операций над элементами массива
\emph{в среднем} составляет $O(n\log n)$.
\end{theorem}

\begin{proof}
Пусть $t(\alpha)$ обозначает количество операций,
затрачиваемое на массив, исходное упорядочение которого
задано перестановкой $\alpha$ (как легко заметить,
количество операций зависит только от этой перестановки,
а не от конкретных элементов массива: $(9,5,7)$ и $(3,1,2)$
сортируются за одно и то же время).
Количество операций, затрачиваемых в среднем на массивы размера $n$,
обозначим через $T(n)$. Размер массива (или соответствующей перестановки)
$\alpha$ обозначим через $|\alpha|$. Часть массива (или перестановки) $\alpha$
с индексами от $j$ до $k$ обозначим через $\alpha[j:k]$.
\begin{eqnarray}
T(n)&=&\frac{1}{n!} \sum_{|\alpha|=n} t(\alpha)=\nonumber\\
&=&\frac{1}{n!} \sum_{i=1}^n \sum_{|\alpha|=n,\,\alpha[1]=i} t(\alpha)\le\nonumber\\ 
&\le&\frac{1}{n!} \sum_{i=1}^n \sum_{|\alpha|=n,\,\alpha[1]=i}
( cn + t(\alpha[1:i-1]) + t(\alpha[i+1:n]) )=\nonumber\\
&=&
cn+\frac{1}{n!} \sum_{i=1}^n \left(
 i (i\!+\!1) \ldots (n\!-\!1) \!\!\sum_{|\beta|=i-1} t(\beta) +\right.\nonumber\\
&&\left.\qquad\qquad\qquad
+(n\!-\!i\!+\!1) (n\!-\!i\!+\!2) \ldots (n\!-\!1) \!\!\sum_{|\gamma|=n-i} t(\gamma)
\right) =\nonumber\\
&=&
cn+\frac{1}{n} \sum_{i=1}^n \left(
 \frac{1}{(i-1)!} \sum_{|\beta|=i-1} t(\beta) +
 \frac{1}{(n-i)!} \sum_{|\gamma|=n-i} t(\gamma)
\right) =\nonumber\\
&=&cn + \frac{1}{n} \sum_{i=1}^n (T(i-1)+T(n-i))= \nonumber\\
&=&cn + \frac{2}{n} \sum_{i=0}^{n-1} T(i).\label{eq:qs:av:rec}
\end{eqnarray}

Остается показать, что решение этого рекуррентного неравенства
удовлетворяет условию $T(n)=O(n\log n)$. 
Предварительно убедимся, что
$$
\sum_{i=2}^{n-1} i \ln i \le 
\int_2^n x \ln x\ \textrm{d}x \le
\frac{n^2\ln n}{2}-\frac{n^2}{4}
$$
(в этом можно убедиться при помощи интеграла --- по выпуклости 
функции $x\ln x$; или, вместо интеграла, по индукции).
%
%

Пусть $b=\max\{T(0),T(1)\}$, $k=2b+2c$.
Покажем по индукции, что для всех $n\ge2$ выполняется $T(n)\le kn\ln n$.
База ($n=2$) очевидна из (\ref{eq:qs:av:rec}).
Для $n\ge3$ имеем
\begin{eqnarray*}
&&T(n)\le cn + \frac{2}{n} \sum_{i=0}^{n-1} T(i)
\le
cn+\frac{4b}{n} + \frac{2}{n} \sum_{i=2}^{n-1} T(i)
\le
\\
&&\le
cn+\frac{4b}{n}+\frac{2}{n} \sum_{i=2}^{n-1} ki \ln i
\le
cn+\frac{4b}{n}+\frac{2k}{n}  
\left(\frac{n^2\ln n}{2}-\frac{n^2}{4}\right)\\
&&=kn\ln n + cn+\frac{4b}{n} - (b+c)n \le kn\ln n.
\end{eqnarray*}
\end{proof}

\subsection{Randomized QuickSort}\marginnote{5}

Алгоритм Randomized QuickSort отличается от QuickSort тем, что
на каждом шаге элемент выбирается случайным образом.
Оценим время его работы \emph{в наихудшем случае}.
Оно будет зависеть от того, какие нам достанутся случайные числа.

\begin{theorem}\label{th:quicksort:rand}
Для \emph{любого} входного массива
c вероятностью не менее $1/2$
в алгоритме Randomized QuickSort
количество операций над элементами массива
составляет $O(n\log n)$.
\end{theorem}

\begin{exercise}
Доказать теорему~\ref{th:quicksort:rand}.
%
%
%
%
%
%
%
\qed
\end{exercise}

\begin{remark}
В исходном алгоритме QuickSort для некоторых массивов 
соответствующая вероятность равна нулю.
\qed
\end{remark}

\subsection{Поиск $k$-го элемента}

\paragraph{Аналогично QuickSort}\marginnote{5} --- все так же, как и в QuickSort,
но рекурсивный вызов делаем только для той половины массива, в которой содержится
интересующий нас элемент.

\begin{exercise} 
Оценить время работы этого алгоритма в наихудшем случае и в среднем.
\end{exercise}

\begin{exercise}
Оценить время работы алгоритма, работающего наподобие \emph{Randomized} QuickSort.
\end{exercise}

\paragraph{За линейное время в наихудшем случае.}\marginnote{10}

Разобьем массив на пятерки; возьмем медианы (третьи элементы) полных пятерок
и вычислим их медиану. Полученным элементом и разобьем массив <<пополам>>
(как в предыдущем алгоритме). Время работы (количество операций над элементами
массива) в наихудшем случае
составляет время на поиск медианы + время на поиск искомого элемента в одной
из полученных <<половинок>>:
$
T(n) \le T(\frac{n}{5}) + T(n-\lfloor\frac{3n}{10}\rfloor) + cn.
$
При $n\ge50$ это выражение $\le T(\frac{n}{5}) + T(\frac{3n}{4}) + cn$.
Нетрудно по индукции доказать, что $T(n)\le cn$,
где $c$ выбрана так, чтобы $T(n)\le cn$ при $n\le49$.
Мы доказали следующую теорему.
\begin{theorem}
Приведенный алгоритм затрачивает в наихудшем случае
лишь $O(n)$ операций на поиск
$k$-го элемента в массиве из $n$ элементов.
\end{theorem}

\end{document}

