算法手记(6)归并排序

Changwei | 9/22/2014 8:11:00 PM


今天我主要学习基于分治思想的归并排序算法,这是分治法的典型应用。废话不多说,下面直切正题。

概述:

将两个有序数组归并成一个更大的有序数组,我们称之为归并,人们根据这一操作发明了一种简单的递归排序算法:归并排序。

归并排序最吸引人的是它能够保证任意长度为N的数组排序所需的时间和NlogN成正比;它的主要缺点是需要额外占用的内存空间与N成正比。

分析:
实现归并的一种最简单的方法是将两个不同的有序数组归并到第三个数组中,实现的方法很简单,创建一个适当的数组然后将两个数组中的元素一个个从小到大放入这个数组。但这样带来的问题是排序大数组时,需要多次归并,每次归并都创建新的数组,这无疑会带来很大的问题。因此,我们需要一种能够实现原地归并的方法,这样就可以先将左边排序,再将右边排序,然后在数组中移动元素而不需要额外的内存空间,我们将在实现部分描述这种方法。

 

实现:
原地归并方法:

private static void merge(IComparable[] a, int lo, int mid, int hi)
        {
            int i = lo, j = mid + 1;
            for (int k = lo; k <= hi; k++)
                aux[k] = a[k];
            for (int k = lo; k <= hi; k++)
            {
                if (i > mid)                       a[k] = aux[j++];
                else if (j > hi)                   a[k] = aux[i++];
                else if (less(aux[j], aux[i]))     a[k] = aux[j++];
                else                               a[k] = aux[i++];
            }
        }

下面分别是两种不同的最终实现算法,它们都是应用高效算法设计中分治思想的典型例子。

  自顶向下的归并实现:

public class Merge
    {
        private static IComparable[] aux;
        public static void sort(IComparable[] a)
        {
            aux = new IComparable[a.Length];
            sort(a, 0, a.Length - 1);
        }
        private static void sort(IComparable[] a,int lo,int hi)
        {
            if (hi <= lo) return;
            int mid = lo + (hi - lo) / 2;
            sort(a, lo, mid);
            sort(a, mid + 1, hi);
            merge(a, lo, mid, hi);
        }
        public static bool less(IComparable v, IComparable w)
        {
            return v.CompareTo(w) < 0;
        }
        public static void exch(IComparable[] a, int i, int j)
        {
            IComparable temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        public static void show(IComparable[] a)
        { 
            int N=a.Length;
            for (int i = 0; i < N; i++)
                Console.WriteLine(a[i]);
        }
        private static void merge(IComparable[] a, int lo, int mid, int hi)
        {
            int i = lo, j = mid + 1;
            for (int k = lo; k <= hi; k++)
                aux[k] = a[k];
            for (int k = lo; k <= hi; k++)
            {
                if (i > mid)                       a[k] = aux[j++];
                else if (j > hi)                   a[k] = aux[i++];
                else if (less(aux[j], aux[i]))     a[k] = aux[j++];
                else                               a[k] = aux[i++];
            }
        }
        public static void test(int size)
        {
            DateTime startDate = DateTime.Now;
            Data[] data = new Data[size];
            Random rd = new Random();
            for (int i = 0; i < size; i++)
            {
                data[i] = new Data { value = rd.Next(10000000) };
            }
            Console.WriteLine("After sort:");
            Merge.sort(data);
            DateTime endDate = DateTime.Now;
            double time = (endDate.Hour - startDate.Hour) * 3600 * 1000 + (endDate.Minute - startDate.Minute) * 60 * 1000 + (endDate.Second - startDate.Second) * 1000 + (endDate.Millisecond - startDate.Millisecond);
            if (time > 1000) time /= 1000;
            Console.WriteLine("time:" + time);
        }
        public static void Main(string[] args)
        {
            test(1000);
        }
    }

 

自底向上的归并排序:

 public static void sort(IComparable[] a)
        {
            int N = a.Length;
            aux = new IComparable[N];
            for(int sz=1;sz<N;sz++)
                for(int lo=0;lo<N-sz;lo+=sz+sz)
                    merge(a,lo,lo+sz-1,System.Math.Min(lo+sz+sz-1,N-1));
            //sort(a, 0, a.Length - 1);
        }

用自顶向下或者自底向上实现分治类算法都很自然。归并排序算法的实现说明,当能够用一种方式解决一个问题时,也应该试试另一种方法。其中自底向上的方式适合于组织链表结构,能够将链表原地排序。

 

实际上,我目前对归并算法理解的并不深,但是其代码精炼程度和执行速度很让我惊叹。我将它和希尔排序及.NET封装的Array.Sort()进行对比试验后,发现归并排序速度和原生API速度最接近,排序10000000个随机数只需要15.672s,希尔排序需要53.937s,原生方法需要15.062s.如果大家能有讲得好的相关文章,欢迎推荐给我,不胜感激ing。