在计算机科学领域一个重要的算法就是排序算法,事实上排序算法也是非常有用的,在现实生活或者工业生产中,产生的数据往往是无序的,为了能够直观地找出数据之间的联系,或者找到一组序列的最大最小值,都需要用到排序算法。排序算法始于计算机大师Knuth的研究,在1968年发表的《The Art of Computer programming》中,Knuth详细地介绍了各种排序算法,分析了各种算法的优劣。Knuth是算法领域负有盛名的大师,个人也非常崇拜他。

排序算法有很多种,比如插入排序(Insertion-Sort),选择排序(Selection-Sort),冒泡排序(Bubble-Sort)…它们都是O(n^2)的排序算法(此处的O(n^2)是指该算法的时间复杂度)。在某种程度上来说,这种O(n^2)是非常慢的排序算法。而本文讲到的两个排序算法,时间复杂度都是O(nlogn),当数据量越来越大的时候,O(nlogn)的算法优势就完全体现出来。

 

Divide-and-Conquer的基本思想

某些问题我们是无法直接求解的,但是我们可以求解该问题的子问题,在求解子问题的过程中,又递归地求解子问题的子问题。这样,我们就把原问题转化为多次递归调用自身来解决子问题,从而完成解决方案。这就是分治策略:

 将原问题划分为n个规模较小而结构与原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。
具体来说分治模式有三个步骤

分解(Divide):将原问题分解成一系列子问题。
解决(Conquer):递归地解决各子问题。若子问题足够小,则直接求解。
合并(Combine):将子问题的结果合并成原问题的解。

分治策略的思想很明确,就是分而治之。如何找到一个问题的划分很重要,大多数时候都要人为地找出一个合理的划分,使得求解划分出来的子问题时,能有与解决原问题的模式相似,这样就可以使用递归来解决。

快速排序与分治法

快速排序就是基于分治模式的一种排序算法。对于一个子序列A[p....r]排序的分治过程有以下三个步骤:

分解:数组A[p...r]被划分成两个子数组A[p...q-1]和A[q+1...r],使得A[p...q-1]每个元素都小于A[q+1...r]中的元素。下标q也在划分过程中进行计算。
解决:通过递归调用快速排序,对于子数组A[p...q-1]和A[q+1...r]排序
合并:因为两个子数组是就地排序的,将他们合并不需要额外的操作;整个数组A[p...r]已排好序。  

知道了算法的思想,就可以写出伪代码,进而用程序设计语言写出来。快速排序的重点是划分,也就是对于每一趟划分来说,在子序列中选定一个主元,然后把比主元小的移到前面,把比主元大的放在后面。那么这个主元就对该子序列进行了一趟划分。对整个序列排序就是递归地调用划分,也就是递归地处理每个子序列。最后得到的就是一个排好序的数字。

以下是Python代码:

 
def partition(arr, start, end):
    i = start - 1
    pivot = arr[end]
    for j in range(start, end):
        if a[j] < pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i+1], a[end] = a[end], arr[i+1]
    return i + 1
def quick_sort(arr, start, end):
    if start < end:
       p = partition(arr, start, end)
       quick_sort(arr, start, p-1)
       quick_sort(arr, p+1, end)

以下是C++代码:

 
int partition(int arr[], int start, int end)
{
    srand(unsigned(time(0)));               //Create a random seed
    int index = rand() % (end - start) + start;
    swap(arr[index], arr[end]);
    int pivot = arr[end];                   //Randomly choose pivot in array
    int i = start - 1 , j;
    
    // Make a partition
    for (j = start;j < end; j++)
    {
        if (arr[j] < pivot)
        {
            i++;
            swap (arr[i], arr[j]);
        }
    }
    swap (arr[i+1], arr[end]);
    return i + 1;
}

void quick_Sort(int arr[], int start, int end)
{
    // Sort subsequence by making partitions recursely
    if (start < end)
    {
        int p = partition(arr, start, end);
        quick_Sort(arr, start, p-1);
        quick_Sort(arr, p+1, end);
    }
}

快速排序的性能分析:

最坏情况划分:快速排序最坏情况发生在划分过程中的两个区域分别为n-1个元素和1个元素的时候。假设每次递归都产生了这种不对称的划分,划分的时间代价为O(n),对于大小为0的素质花费时间T(0) = 1。故运算时间可以递归地表示为:
T(n) = T(n-1) + T(0) + O(n) = T(n-1) + O(n) 
这是一个递推式,通过递推:
T(n) = T(n-1) + O(n) = T(n-2) + O(n-1) + O(n) = ... = T(1) + O(2 + ... + n) = O(n(n-1)/2) = O(n^2)
也就是在最坏的情况下,时间复杂对位O(n^2)

最佳的情况划分:最佳划分是,对于一个序列来说,划分的主元刚好位于序列的中间,这种对称的划分产生的两个子序列大小为n/2。这时,运行时间的递归式为:
T(n) <= 2T(n-1) + O(n) 
同样,通过这个递推式,可以得到:
T(n) <= 2T(n-1) + O(n) <= 2(2T(n-2) + O(n-1)) + O(n) <=... = O(nlogn) 
那么最好情况小的时间复杂度为O(nlogn)

为了使得快速排序每一次划分的效率更高,一个方法就是对于每次划分,随机选择序列中的一个元素作为主元。上面C++代码就是采用了随机化的快速排序。对于快速排序的一般情况来说,时间复杂度就是O(nlogn)。

快速排序还有多种不同的划分方法。有兴趣童鞋的可以查找相关论文和资料~

归并排序与分治法

上文给出了分治策略,那么我们只需要将分治策略应用于归并排序中即可,一般的三个步骤是:

分解:将n个元素分成各含n/2个元素的子序列
解决:用合并排序法对两个子序列递归地进行排序
合并:合并两个已排序的子序列以得到排序结果。

归并排序的关键在于理解Merge的过程,即:将两个有序序列合并为一个有序序列的过程。对两个递增有序序列合并的过程就是:从第一个元素开始,选择两个序列中第一个元素较小的,把较小的元素加入到新数组中,并且下标往后移,直到其中一个序列的所有元素都被遍历。最后将另一个序列的剩余元素复制到合并后的新数组中。

归并排序的C++代码:

 
void merge(int arr[], int start, int mid, int end)
{
    int n1 = mid - start + 1, n2 = end - mid;
    int *left = new int[n1];
    int *right = new int[n2];
    int i = 0, j = 0, k = start;
    
    //Copy arr[start]...arr[mid] to left, arr[mid+1]...arr[end-1] to right
    while (i < n1) left[i++] = arr[k++];
    while (j < n2) right[j++] = arr[k++];
    i = j = 0, k = start;
    //The most important step. Merage two sorted seqence
    while (i < n1 && j < n2)
    {
        if (left[i] < right[j])
            arr[k++] = left[i++];
        else
            arr[k++] = right[j++];
    }
    
    //Copy the rest to array
    while (i < n1) arr[k++] = left[i++];
    while (j < n2) arr[k++] = right[j++];
    delete []left;
    delete []right;
}

void merge_sort(int arr[], int start, int end)
{
    if (start < end)
    {
        int mid = (start + end) / 2;
        merge_sort(arr, start, mid);
        merge_sort(arr, mid + 1, end);
        merge(arr, start, mid, end);
    }
}

归并排序的时间复制度为O(nlogn),证明方法与快速排序类似。

Python与C++

快速排序我给出了Python代码,归并排序没有。因为我用python写的归并排序比较复杂,而Python素以简洁优美著称,所以就没有给出。python我接触时间也不久,用的也不多,但是我发现它确实是一个非常好用的编程语言。语法简洁易懂,尤其对字符串和元祖字典等操作非常简便,而且很适合做科学计算,它可以表示非常大的数,在计算方面效率很高。虽然还没有用到它丰富的库,不过从名校教授或者资深geek那里都可以感受到Python的强大之处。C++是大学里学习静态编程语言,相对于Python这种脚本语言来说语法较复杂,特性多,坑多。好处在于效率高,可以直接进行内存管理。事实上我用C++,大部分都是面向过程,以前用Qt框架的时候面向对象特性用得比较多。STL也用但是不多,模板几乎没有,至于lamada函数就更没有了。不过我仍觉得学好C++非常重要,因为真正懂它的人太少了,相比于其他语言,它的门槛也要高很多。

 

参考资料:

1.《算法导论》,Thomas H.Cormen , Charles E.Leiserson 等著,机械工业出版社,2008年7月

2.《Python基础教程 第2版》,Magnus Lie Hetland著,人民邮电出版社,2014年6月

3. 麻省理工大学(MIT)算法导论,http://v.163.com/special/opencourse/algorithms.html

4. 麻省理工大学(MIT)  计算机科学及编程导论,http://v.163.com/special/opencourse/bianchengdaolun.html

 posted on 2014-09-10 14:09  Clivia_zhou  阅读(143)  评论(0编辑  收藏  举报