计数排序C代码如下,显然这货是不能用于字符串排序的(非比较排序只有基数排序可以用来排序字符串):
#include <stdio.h> #include <malloc.h> #include <assert.h> #include <stdlib.h> #define MAX 100 //取值范围k=99,为了保持main形式上的一致性,这里用了宏 void counting_sort(int* A,int p,int r) { int* C=(int *)calloc(MAX,sizeof(int)); int* B=(int *)malloc(r*sizeof(int)); assert( C && B); for(int i=p;i<r;++i) C[A[i]]+=1; for(int j=1;j<MAX;++j) C[j]+=C[j-1]; for(int i=r-1;i>=p;--i) { //Attention here! B[C[A[i]]-1]=A[i]; --C[A[i]]; } for(int j=p;j<r;++j) A[j]=B[j]; //memcpy(A,B,sizeof(int)*r); free(C); free(B); } int main() { int A[30]; int i=0; printf("Original:\n"); for (;i<30;i++) { A[i]=rand()%(MAX); printf("%d\t",A[i]); } counting_sort(A,0,30); printf("\nRearrange:\n"); for(i=0;i<30;++i) { printf("%d\t",A[i]); } printf("\n"); }
基数排序的抽象程度比较高(这也是为啥伪代码那么短的原因),它的思想就是对待排序元素进行拆分,然后从低位到高位进行列排序。不同类型的抽取方式差别很大,前面写的大部分C程序,如果改用C++的模板来实现,代码并不用做太多修改,泛型函数和普通函数的差别并不大(只要重载了<操作符)。这里比较妥当的方法是写一个模板类,实现“抽取各位”这个行为。另外,这里有个比较蛋疼的事情,就是数据的类型与数据各位的类型并不一致,不过简单来说可以都认为是char。
桶排序假设输入平均分布,将输入范围均分为n个“桶”,然后遍历所有元素,将之放入对应的桶中。然后对所有的桶调用插入排序。
exercises:
8.1-1 显然是n
8.1-2 利用
\[ \int_{0+}^n lgk \le \sum_{k=1}^n lgk \le \int_1^{n+1} lg k \]
积分左端可得。
8.1-3 反证法,假设有一半以上的输入可以通过线性比较运算得出,那么有
\[ 2^n \ge \frac{n!}{2} \Rightarrow n \ge lg(n!)-1 \]
而右式显然不成立。
其余类似。
8.1-4 hint表示不能直接将n/k个klgk相加。实际上算法是先对子序列排序,然后再将所有子序列的首元素排序,复杂度为(n/k)*klgk+(n/k)*lg(n/k),忽略常数项后仍然是$\Omega(nlgk)$.
8.2-2 Couting-Sort的稳定性主要是因为line9,假设A中有元素A[i]=A[j](i<j),在line9会先将A[j]放入B[C[A[j]]],然后将C[A[j]]的值减1,这样放入A[i]的时候,A[i]必定在A[j]左侧,因此稳定性成立(似乎归纳法更严格一些)。
8.2-3 修改后算法不再稳定,除非把line11中的递减改为递增
8.2-4 这个…算法其实有很多吧,不一定非要用计数排序的思想,直接遍历求和不也行,只有$\Theta(n)$。
如果用计数排序的思想,就取其伪代码line1 to line 7,然后返回C[b]-C[a-1]即可。
8.3-2 insertion-sort和merge-sort都是稳定的,而quick-sort和heap-sort都是不稳定的。
想要保持稳定,必须有一个额外的数组记录其左右顺序,空间消耗为$\Theta(n)$,不过参考答案给的是nlgn,lgn代表的是bits…我搞不清楚为啥必须是lgn bits,这不和具体环境相关么?有人知道的话请告诉我:)
8.3-4 将这n个整数看成是n进制的两位数,那么使用基数排序+计数排序就可以搞定了(每位的范围都是0~n-1)
8.3-5 这个,我算的结果是n(1-1/(2d)),倒着算的,最低位最多有n/2堆,每堆元素有两个(如果元素只有一个就不用再建新堆),次低位就是n/4,这样加下去就是
n/2+n/4+...+n/2d,最后的结果也就是上面那个了…
8.4-2 最坏情况显然是输入非常不均匀——所有元素都在一个桶里;如果想要改善最坏状况,可以在桶内使用合并排序或者堆排序这些最坏nlgn的算法。
8.4-3 概率题。E[x2]=1.5,E2[x]=1.
8.4-4 将单位圆按面积均分成n份,依半径的区间分布是$[0,\sqrt{1/n}],[\sqrt{1/n},\sqrt{2/n}]\cdots [\sqrt{n-1/n},1] $
problems:
8-1 本题证明了比较排序的平均情况下界为nlgn
a>这是决策树的叶节点计算方法…因为n个不同元素的可能排列方式有n!个,所以最终的比较结果也只能到达这n!个结点,且概率是等可能的。
b>D(T)通过D(RT)和D(LT)到达叶节点的方式共有k条【可以从a中推出这个结论】,所以D(RT)+D(LT)+k=D(T).
c>根据提示,假设有一颗这样的决策树:其LT可达的叶节点数是$i_0$,其RT可达的叶节点数就是$k-i_0$,那么根据b,D(T)的最小值是
\[ D(T)_{min}=\min\{D(RT)+D(LT)+k\} \]
假设d(k)是D(T)的最小值,d(k)的定义是到达k个结点的所有路径总和的最小值,那么d(k)是关于k的函数,根据树的递归结构,D(RT)的最小值应该是关于$k-i_0$的函数,D(LT)就是关于$i_0$的函数,即
\[D(T)_{min}=\min\{d(i_0)+d(k-i_0)\} \]
而$i_0$的取值范围就是[1,k-1],所以题设得证。
d>简单但是通用的方法就是用导数证明单调性。由于i<=k-1,所以也可以直接用$a+b \ge 2\sqrt{ab}$当a=b时取得最小值。
e>将k=n!代入d中结论可得。
f>在随机化算法中抽取最短路径分支,然后剪掉同一随机化结点的其他分支,得到一个拥有最短结点的确定算法,该算法的复杂度显然不多于对应的随机化算法。
8-2 线性时间原地排序的可能性
a>计数排序(桶排序恐怕不行,因为并非均匀分布)
b>由于只有两个key,所以一轮快排是可以达到目的的。
c>插入排序
d>将这n个记录看做2进制数,进行Radix-sort,那么每一列就是只有0和1的情况了。
e>遍历A[1-n],在C[0-k]中存放A[i]出现的次数;
遍历C,对于每一个C[i]依次在A中push_back i个C[i];
以上算法运行时间为O(n+k),该算法显然是不稳定的。
8-3 长度不同的数据如何使用radix-sort
a>①将数据分成负数、0、整数三类——O(n);
②准备$\lceil n/2 \rceil$个桶,以位数对以上三类分别再分类【因为对正数而言,长度长的总比长度短的大;负数则相反】O(n);
③最后对桶中的数据进行线性排序——O(n)。
b>①以首字母对数据进行分类(计数排序);
②以次字母对所有分类进行再分类;
③递归以上过程直至完成排序。
由于总的字母是n个,因此需要的总时间也就是O(n+m),m是字母的种类(26个+空)
8-4 配对水壶
a> simple,对于每一个red,遍历blue得到pair
b>使用决策树,每次比较的结果是3ge(< > =),所以是三叉树;总的结果是n!个(第一个可能和n个中任一个,第二个则是剩下n-1任一个,依次类推);假设决策树的高度是h,那么有$3^h \ge n!$,解得T(n)=Ω(nlgn)
c>代码来自参考答案:
Match-Jugs(R,B)
if |R|=0
then return
if |R|=1
then let R={r} and B={b}
output "(r,b)"
return
else r←R[Random(1,n)]
compare r to every jug of B
$B_<$ ← the set of jugs in B that are smaller than r
$B_>$ ← the set of jugs in B that are greater than r
b ← the pair
compare b to ever jug of A
$A_<$ ← the set of jugs in A that are smaller than b
$B_<$ ← the set of jugs in A that are larger than b
Match-Jugs($A_<,B_<$)
Match-Jugs($A_>,B_>$)
显然,算法与插入排序的思想是一致的,其期望运行时间也是Θ(nlgn)
8-5 如果数组中的以任意元素i开始的k个元素的和小于等于任意从i+x开始的k个元素的和(x>0)那么称数组是k排序的
a>1排序就是指数组严格递增(严格来说是非递减)
b>1 2 3 4 5 6 7 8 10 9【随意调换两个相邻的元素貌似都可以】
c>展开题设中的不等式,消去等式两边的共有项就能得到A[i]<=A[i+k]
d>根据c中的结论,只需对A[1,k+1,2k+1...],A[2,k+2,2k+2...]...A[k,2k,3k...]分别进行严格排序就可以了
共k组,每组共n/k个元素,k*(n/k)lgn/k=nlg(n/k)
e>显然只需对n/k个长度为k个小数组进行排序即:(n/k)*klgk=nlgk
f>如果k是常数,根据d的结论…
8-6 合并两个已排序的序列
a>对于2n个数,划分成两个长度为n列表的方法显然就是$\binom{2n}{n}$,至于排序,和划分并无关系(一旦确定划分,排序是唯一的)
b>决策树应该是三叉树,叶子数计算相当于在n个数中插入另外n个数的问题,共有n+1个位置,而待插入的数又有不同的分类法,总数应该是
\[ \binom{n-1}{0}\binom{n+1}{1}+\binom{n-1}{1}\binom{n+1}{2}+\cdots+\binom{n-1}{n-1}\binom{n+1}{n} \]
也就是$\sum_{i=0}^{n-1}\binom{n-1}{i}\binom{n+1}{i+1}$,这个计算的结果是…抱歉我不会算= =,总之得出叶子的数目m然后用
$3^h \ge m$是可以算出这个下界的。不严格的推证:决策树最短路径应该是
\[ <a1:b1> \xrightarrow{\rm =}<a2:b1>\xrightarrow{\rm =}<a2:b2>\rightarrow\cdots<an:bn>\]
该路径的长度显然是2n-1,符合2n-o(n)的结论
c>如果两个数无须比较就能确定大小关系,必须利用关系操作的传递性,但是这与两者相邻是矛盾的(除非相等)
d>长度为2n,所有相邻的元素都必须比较过,即最少需要2n-1次比较才能确定。