packages feed

swish-0.2.1: Swish/HaskellRDF/Sort/MergeSort.lhs

%-------------------------------=  --------------------------------------------
\chapter{Sorting by merging}
%-------------------------------=  --------------------------------------------

%align

> module Swish.HaskellRDF.Sort.MergeSort 
> where
> import Swish.HaskellRDF.Sort.LibBase
> import Swish.HaskellRDF.Sort.ListLib

> infixr {-"\,"-} `merge`, \+/

%align 33

> sort				:: (Ord a) => [a] -> [a]
> sort				=  bottomUpMergeSort

%-------------------------------=  --------------------------------------------
\section{Top-down merge sort}
%-------------------------------=  --------------------------------------------

The archetypical functional sorting algorithm is without any doubt
merge sort. It follows the divide and conquer scheme: the input list is
split into two halves, both are sorted recursively and the results are
finally merged together.

> merge				:: (Ord a) => [a] -> [a] -> [a]
> merge				=  mergeBy (<=)
>
> (\+/)				:: (Ord a) => [a] -> [a] -> [a]
> (\+/)				=  mergeBy (<=)
>
> mergeBy			:: Rel a -> [a] -> [a] -> [a]
> mergeBy (<=)			=  merge
>     where
>     merge [] bs		=  bs
>     merge as@(_ : _) []	=  as
>     merge as@(a : as') bs@(b : bs')
>         | a <= b		=  a : merge as' bs
>	  | otherwise		=  b : merge as  bs'
>
> mergeSort			:: (Ord a) => [a] -> [a]
> mergeSort			=  mergeSortBy (<=)
>
> mergeSortBy			:: Rel a -> [a] -> [a]
> mergeSortBy (<=) as
>     | simple as		=  as
>     | otherwise		=  mergeBy (<=) (mergeSortBy (<=) as1)
>				                (mergeSortBy (<=) as2)
>     where (as1, as2)		=  halve as

Since the divide phase takes $\Theta(n\log n)$ time, |mergeSort| is
not lazy: |head . mergeSort| has a running time of $\Theta(n\log n)$.

%-------------------------------=  --------------------------------------------
\section{Bottom-up merge sort}
%-------------------------------=  --------------------------------------------

The function |bottomUpMergeSort| improves the divide phase to
$\Theta(n)$ and is consequently asymptotically optimal, stable, and
lazy, but alas not in any way adaptive.

> bottomUpMergeSort		:: (Ord a) => [a] -> [a]
> bottomUpMergeSort		=  bottomUpMergeSortBy (<=)
>
> bottomUpMergeSortBy		:: Rel a -> [a] -> [a]
> bottomUpMergeSortBy (<=)	=  gfoldm [] (\a -> [a]) (mergeBy (<=))

%-------------------------------=  --------------------------------------------
\section{Straight merge sort}
%-------------------------------=  --------------------------------------------

Both |mergeSort| and |bottomUpMergeSort| take $\Theta(n\log n)$
irrespective of the presortedness of the input. If we replace the test
|simple| by |ordered| we obtain an adaptive variant which is optimal
with respect to the measure |Runs| and adaptive wrt |Inv| and |Rem|
\cite[p.~449]{ECW92Sur}.

> straightMergeSort		:: (Ord a) => [a] -> [a]
> straightMergeSort		=  straightMergeSortBy (<=)

> straightMergeSortBy		:: Rel a -> [a] -> [a]
> straightMergeSortBy (<=) as
>     | orderedBy (<=) as	=  as
>     | otherwise		=  mergeBy (<=) (straightMergeSortBy (<=) as1)
>				                (straightMergeSortBy (<=) as2)
>     where (as1, as2)		=  halve as

\Todo{To adapt to ascending as well as descending sequences we could
reverse the sublists in every step and apply Augustson's
stable/anti-stable trick.}

%-------------------------------=  --------------------------------------------
\section{Odd-even merge sort}
%-------------------------------=  --------------------------------------------

If we use a different partioning scheme, |uninterleave| instead of
|halve|, we obtain an adaptive variant which is optimal wrt |Dis = Max|
\cite[p.~450]{ECW92Sur}.

> oddEvenMergeSort		:: (Ord a) => [a] -> [a]
> oddEvenMergeSort		=  oddEvenMergeSortBy (<=)
>
> oddEvenMergeSortBy		:: Rel a -> [a] -> [a]
> oddEvenMergeSortBy (<=) as
>     | orderedBy (<=) as	=  as
>     | otherwise		=  mergeBy (<=) (oddEvenMergeSortBy (<=) as1)
>						(oddEvenMergeSortBy (<=) as2)
>     where (as1, as2)		=  uninterleave as

Unfortunately, |oddEvenMergeSort| is no longer stable. Consider, for
instance, |uninterleave [a1, a2, a3] = ([a1, a3], [a2])| and assume
that the three elements are equal. However, |oddEvenMergeSort| can be
improved so that the divide phase takes only linear time. This is left
as an instructive exercise to the reader.

%-------------------------------=  --------------------------------------------
\section{Split sort}
%-------------------------------=  --------------------------------------------

A partioning scheme which adapts to |Rem| was given by Levcopoulos
and Petersson \cite[p.~451]{ECW92Sur} and is based on a method by
Cook and Kim for removing $\Theta(|Rem(as)|)$ elements from a list
|as|, such that an ordered sequence is left over.

The function |lpDivision as| divides its input into three lists |g|,
|s|, and |l| such that |s| is sorted and |g| and |l| have the same
length which is at most |Rem(as)|. The tricky thing is to ensure that
the splitting is performed in a stable way, ie the order in which the
elements in |g| and |l| appear is the same in which they appear in
|as|. Consider the sequence |1 2 5 1 4 3 1 9 2 8 9 1|:
%
\[
\begin{array}{l||l||r}
\text{|g| and |s|} & |l| & |as| \\\hline
|1 2 5| & & |1 4 3 1 9 2 8 9 1| \\
|1 2 [5]| & |1| & |4 3 1 9 2 8 9 1| \\
|1 2 [5] 4| & |1| & |3 1 9 2 8 9 1| \\
|1 2 [5 4]| & |3 1| & |1 9 2 8 9 1| \\
|1 [2 5 4]| & |1 3 1| & |9 2 8 9 1| \\
|1 [2 5 4] 9| & |1 3 1| & |2 8 9 1| \\
|1 [2 5 4 9]| & |2 1 3 1| & |8 9 1| \\
|1 [2 5 4 9] 8| & |2 1 3 1| & |9 1| \\
|1 [2 5 4 9] 8 9| & |2 1 3 1| & |1| \\
|1 [2 5 4 9] 8 [9]| & |1 2 1 3 1| &
\end{array}
\]
The data type |Region| is designed to represent |g| and |s|. Elements
in |g| are grouped to allow for efficient access to the last element in
|s|. For instance, |1 [2 5 4 9] 8 [9]| is essentially represented by |G
(S (G (S Nil 1) [2, 5, 4, 9]) 8) [9]|.

> type Sequ a			=  [a] -> [a]
>
> data Region a			=  Nil
>				|  S (Region a) a
>				|  G (Region a) (Sequ a)
>
> single			:: a -> Sequ a
> single a			=  \x -> a : x
>
> g				:: Region a -> Sequ a -> Region a
> g Nil gs			=  G Nil gs
> g (S s a) gs			=  G (S s a) gs
> g (G s gs1) gs2		=  G s (gs1 . gs2)
>
> lpDivisionBy			:: Rel a -> [a] -> ([a], [a], [a])
> lpDivisionBy (<=) []          =  ([], [], [])
> lpDivisionBy (<=) (a : as)    =  lp (S Nil a) [] as
>     where
>     lp s l []              	=  (g [], reverse s', reverse l)
>         where (g, s')		=  lpPart s
>     lp Nil l (a : as)		=  lp (S Nil a) l as
>     lp s@(G Nil gs) l (a : as)=  lp (S s a) l as
>     lp s@(G (S s' m) gs) l (a : as)
>         | m <= a		=  lp (S s a) l as
>         | otherwise		=  lp (g s' (single m . gs)) (a : l) as
>     lp (G (G _ _) _) _ (_ : _)=  error "lp"
>     lp s@(S s' m) l (a : as)
>         | m <= a		=  lp (S s a) l as
>         | otherwise		=  lp (g s' (single m)) (a : l) as
>
> lpPart			:: Region a -> ([a] -> [a], [a])
> lpPart Nil			=  (id, [])
> lpPart (S s a)		=  (g, a : s')
>     where (g, s')		=  lpPart s
> lpPart (G s gs)		=  (gs . g, s')
>     where (g, s')		=  lpPart s

Unfortunately, the relative order between equal elements in |g ++ s ++ l|
is not the same as in |as|. Hence, |lpMergeSort| is not stable either.

> lpMergeSort			:: (Ord a) => [a] -> [a]
> lpMergeSort			=  lpMergeSortBy (<=)
>
> lpMergeSortBy			:: Rel a -> [a] -> [a]
> lpMergeSortBy (<=) as
>     | simple as		=  as
>     | otherwise		=  mergeBy (<=) (lpMergeSortBy (<=) as1)
>				       (mergeBy (<=) s (lpMergeSortBy (<=) as2))
>     where (as1, s, as2)	=  lpDivisionBy (<=) as

\Todo{To adapt to ascending as well as descending sequences we could
reverse the sublists in every step. \NB stability is already lost.}

%-------------------------------=  --------------------------------------------
\section{Adaptive merge sort}
%-------------------------------=  --------------------------------------------
%format sort1			=  sort "_1"
%format sort2			=  sort "_2"
%format sort3			=  sort "_3"

If we combine |halve|, |uninterleave|, and |lpDivision| we obtain
a sorting algorithm which is adaptive wrt |Exc|, |Dis|, |Inv|, |Rem|,
and |Runs| \cite[p.~451]{ECW92Sur}.

> adaptiveMergeSort		:: (Ord a) => [a] -> [a]
> adaptiveMergeSort		=  adaptiveMergeSortBy (<=)
>
> adaptiveMergeSortBy		:: Rel a -> [a] -> [a]
> adaptiveMergeSortBy (<=)	=  sort1
>     where
>     (\+/)			=  mergeBy (<=)
>
>     sort1 as
>         | simple as		=  as
>         | otherwise		=  sort2 as1 \+/ s \+/ sort2 as2
>         where (as1, s, as2)	=  lpDivisionBy (<=) as
>
>     sort2 as			=  sort3 as1 \+/ sort3 as2
>         where (as1, as2)	=  uninterleave as
>
>     sort3 as			=  sort1 as1 \+/ sort1 as2
>         where (as1, as2)	=  halve as

\Todo{How about reversing the lists in the first step in order to adapt
to descending sequences as well? cf.~\cite[p.~52]{ECW91Pra}}

%-------------------------------=  --------------------------------------------
\section{Natural merge sort}
%-------------------------------=  --------------------------------------------

The function |straightMergeSort| somehow guesses the number of runs. It is
more efficient to group the input into runs beforehand.

> naturalMergeSort		:: (Ord a) => [a] -> [a]
> naturalMergeSort		=  naturalMergeSortBy (<=)
>
> naturalMergeSortBy		:: Rel a -> [a] -> [a]
> naturalMergeSortBy (<=)	=  foldm (mergeBy (<=)) [] . runsBy (<=)
>
> runsBy			:: Rel a -> [a] -> [[a]]
> runsBy (<=) []		=  [[]]
> runsBy (<=) (a : as)		=  upRun a [] as
>     where
>     upRun m r []		=  [reverse (m : r)]
>     upRun m r (a : as)
>         | m <= a		=  upRun a (m : r) as
>         | otherwise		=  reverse (m : r) : upRun a [] as

Natural merge sort was first studied in the context of external
sorting.  The hbc library contains a similar function.

The function |runs| recognized only ascending runs. With little
additional effort we can also detect descending runs.

> symmetricNaturalMergeSort	:: (Ord a) => [a] -> [a]
> symmetricNaturalMergeSort	=  symmetricNaturalMergeSortBy (<=)
>
> symmetricNaturalMergeSortBy	:: Rel a -> [a] -> [a]
> symmetricNaturalMergeSortBy (<=)
>				=  foldm (mergeBy (<=)) [] . upDownRunsBy (<=)
>
> upDownRunsBy			:: Rel a -> [a] -> [[a]]
> upDownRunsBy (<=) []		=  []
> upDownRunsBy (<=) (a : as)	=  upDownRun a as
>     where
>     upDownRun a []		=  [[a]]
>     upDownRun a1 (a2 : as)
>         | a1 <= a2		=  upRun   a2 [a1] as
>         | otherwise		=  downRun a2 [a1] as
>     upRun m r []		=  [reverse (m : r)]
>     upRun m r (a : as)
>         | m <= a		=  upRun a (m : r) as
>         | otherwise		=  reverse (m : r) : upDownRun a as
>
>     downRun m r []		=  [m : r]
>     downRun m r (a : as)
>         | m <= a		=  (m : r) : upDownRun a as
>         | otherwise		=  downRun a (m : r) as

\NB To preserve stability |downRun| uses only \emph{strictly}
decreasing sequences: |[n, n, n-1, n-1, .. 1, 1]| is split into |n|
runs. Does anybody know of a better solution?