《编程珠玑》笔记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); }