《编程珠玑》笔记11 排序

这部分主要讲述排序算法,先给出两个库函数排序:

C库函数<stdlib.h>:  void qsort ( void * base, size_t num, size_t size, int ( * comparator ) ( const void *, const void * ) );如:

1 int values[] = { 40, 10, 100, 90, 20, 25 };
2 
3 int compare (const void * a, const void * b)
4 {
5   return ( *(int*)a - *(int*)b );
6 }
7 //////////////////////////////
8 qsort (values, 6, sizeof(int), compare);

C++库函数<algotithm>: void sort( iterator begin, iterator end, Compare comp);  有三个重载函数,前两个参数是容器的迭代器,最后一个用于指定比较器,可有可无,比较器可以是函数,也可以是类:如

 1 bool myfunction (int i,int j) { return (i<j); }
 2 
 3 struct myclass {
 4   bool operator() (int i,int j) { return (i<j);}
 5 } myobject;
 6 /////////////////////////////
 7  // using default comparison (operator <):
 8   sort (myvector.begin(), myvector.begin()+4);           //(12 32 45 71)26 80 53 33
 9 
10   // using function as comp
11   sort (myvector.begin()+4, myvector.end(), myfunction); // 12 32 45 71(26 33 53 80)
12 
13   // using object as comp
14   sort (myvector.begin(), myvector.end(), myobject);     //(12 26 32 33 45 53 71 80)

C++的STL算法还有其他多种排序方法实现稳定排序等。

下面回到本章正题

  (后面的伪代码中,假设对数组x[n]进行排序)

1.插入排序

  插入排序是最简单的排序方法,在数组规模较小时常使用,代码也比较简单:

for (int i = 1; i < n; i++)
  for(int j = i; j > 0 && x[j-1]>x[j]; j--)
    swap(x[j-1], x[j]);

  对该代码进行改进,一方面可以将在循环内的swap函数展开,相当于使用内联方法调用swap,避免函数调用是的栈开销。

  另一方面,由于i前面部分已知排好了序,那么在判断x[i]的插入位置时,可以 1)保存x[i], 2)位置后移一位;3)插入x[i].

for(int i = 1; i < n; i++)
  int t = x[i];
  for(int j = i; j > 0 && x[j-1] > x[j]; j--)
    x[j] = x[j-1];
x[j] = t;

2.快速排序

  快速排序使用分治法来达到排序目的,每次将一个元素放到最终位置,然后将数组分成两部分,递归排序。

  快速排序的关键是选择当前数t和数组划分方法,框架如下:

void qsort(l, u)
{
   if l >= u then
    return;
   //TODO:选择基准元素,划分数组,设最终位置为p

  qsort(l, p-1);
  qsort(p+1, u);
}

  最简单的快速排序方法,选择第一个元素作为基准,直接从左到右遍历。

  m指向最后的小于t的元素,i指向当前欲判断的元素,所以l到m之间为小于部分,m到i之间为大于等于部分。i到u之间为待判断部分。

  对当前x[i]的判断如下:若>=t,则直接子增,若<t,需要将x[i]与x[m+1]调换,然后m和i都自增。最后将x[l]和x[m](x[m]此时是小于x[l]的)调换。

 1 void qsort1(l, u)
 2 {
 3     int (l>=u)
 4         return;
 5 
 6     m = l;
 7     for(int i = l+1; i <= u; i++)
 8         if(x[i] < x[l])
 9                 swap(x[++m], x[i]);
10     swap(x[l], x[m]);
11 
12     qsort(l, m-1);
13     qsort(m+1, u);
14 }

  快速排序在最坏情况下时间占用达到平方级,占用线性空间,可以采用一些方法来避免这种情况出现:

  (1)第一种情况是 假如所有数组元素都相等。快速排序每次的划分变得很不均衡,所以出现最坏情况。这种情况可以采用双向划分的策略来避免

  如上面的图中所示,主循环内有两个循环,第一个用i来移过小元素,遇到大于t就停止,第二个用j移过大元素,遇到小于t就停止。主循环测试是否交叉(若交叉,则退出),交换x[i]和x[j],(当两个都停止时)

  当遇到相同的元素时停止扫描,并交换i和j的值。这样虽然增加了交换次数,但所有元素都相同的情况也只需要O(nlogn)次比较就可以了。

  (因为我们让i和j达到了数组的中间,避免了划分不均的情况)

 1 void qsort2(l, u)
 2 {
 3     if (l >= u)
 4         return;
 5     t = x[l]; i = l; j = u+1;
 6     while(1)
 7     {
 8         do{
 9             i++;
10         }while(i <=u && x[i] < t);
11         do{
12             j--;
13         }while(x[j] > t);
14 
15         if(i > j)
16             break;
17         swap(x[i], x[j]);
18     }
19 
20     swap(x[l], x[j]);
21 
22     qsort2(l, j-1);
23     qsort2(j+1, u);
24 }

  (2)第二种达到最坏情况的原因是数组已经有序,在使用快速排序,同样会使得划分极不均横。

  可以在开始加上swap(l, randint(l, u)); 来避免这一点。

  (其他排序方法在习题4.6中有部分说明)

  (3)我们知道对于小数组而言,插入排序速度往往更快,所以可以设置一个cutoff,在小数组(l和u非常接近)上调用qsort时,直接返回。

    if(u - l < cutoff)

      return;

    排序的写法变为: qsort4(0, n-1);  //该步使序列基本有序

             isort3();    //插入排序。

3.原理

  C库函数中的qsort相对较慢,C++的sort实现非常高效。

  注意分治思想的运用。

  代码调优。

4.习题

  4.1 前题是:数组是无序的。计算最大值,最小值用选择法最快;O(n)时间内。均值应该直接遍历相加吧;中值利用习题9的方法,计算第n/2小的元素;众数不懂。关于这种问题,最典型的应该是TopK问题。

  4.2 由于始终选择第一个元素x[l]作为划分基准,所以可以从后往前遍历划分。 

   

  如上面的示意图,在TODO部分,使用如下代码:

  m = u+1;

  for(int k = u; k >= l; i--)

    if(x[k] >= t)  //t即为x[l]的值

      swap(x[--m], x[k])

  通过对哨兵的使用,可以在循环中减少判断条件:

  m = u+1;

  k = u;

  while(k != l){

    while(x[k] <t)

      k--;

    swap(x[--m], x[k]);

  }

  4.6 选择排序:

  for i = [0, n)

    for j = [i+1, n)    //每次确定x[i]位置上的数据

      if(x[i] > x[j])

        swap(x[i], x[j]);

  4.9 选择第k个最小的元素

  这类题与搜索某一个元素类似,使用二分法最为有效。不过此处我们只需要确定第k个元素,所以在递归过程中只需要 根据条件选择前面的或后面的区间 即可。

 1 int selectk(int l, int u, int k)
 2 {
 3     if(l > u)
 4         return -1;
 5     int p = u+1;
 6     for(int k = u; k >= l; k--)        //与快排的划分过程完全相同    
 7     {
 8         while(x[k] < x[l])
 9             k--;
10         swap(x[--p], x[k]);
11     }
12     
13     if(p < k)
14         selectk(p+1, u, k);
15     else if(p > k)
16         selectk(l, p-1, k);
17     else
18         return x[p];  
20 }

  4.11 编写“宽支点”划分函数

 x[l]               k(当前判断点)          m(=t位置)      n(>t位置)             u

                         

void qqsort(int l, int u)
{
    if(l <= u)    //快速排序是的结束条件是l=u,而二分搜索或selectk要对l=u的情况继续判定!!
        return; 
    int m, n;
    m = n = u+1;
    for(int i = u; i >= l; i--)
    {
        while(x[i] < x[l])
            i--;
        if(x[i] == x[l])
            swap(x[--m], x[i]);
        else    
        {
            swap(x[--n], x[i]);
            swap(x[--m], x[i]);        //该步使得新的快排算法仍不是一个稳定的排序算法
        }
    }

    qqsort(l, m-1);
    qqsort(n, u);
} 

  4.14 使用void qsort(int x[], int n)接口实现快排:

void qsort5(int x[], int n)
{
    if(n <= 0)
        return;
    int mid = n/2;
    int p = n;
    for(int i = n-1; i >= 0; i--)
    {
        while(x[i] < x[0])
            i--;
        swap(x[--p], x[i]);
    }
    qsort5(x, p);
    qsort5(x+p, n-p-1);
}

    

posted @ 2012-09-09 15:33  dandingyy  阅读(1068)  评论(0编辑  收藏  举报