算法导论 第8章 线性时间排序

一、概念

1.比较排序

比较排序是指通过输入元素间的比较来确定各元素次序的排序算法。

任何比较排序在最坏情况下都要用O(nlgn)次比较来进行排序

合并排序和堆排序是渐近最优的


2.非比较排序

非比较排序指使用一些非比较的操作来确定排序顺序的排序算法

对于非比较排序,下界O(nlgn)不适用

计数排序是稳定排序,若n个数据的取值范围是[0..k],则运行时间为O(n+k),运行空间是O(n+k)

基数排序也是稳定排序,需要另一个稳定排序作为基础,若n个d位数,每一位有k种取值可能,所用的稳定排序运行时间为O(n+k),则基数排序的时间是O(d(n+k))

桶排序也是稳定排序,当输入数据符合均匀分布时,桶排序可以以线性时间运行。所设所有元素均匀分布在区间[0,1)上,把区间[0,1)划分成n个相同大小的子区间(桶),对各个桶中的数进行排序,把依次把各桶中的元素列出来。

 

二、代码

#include <iostream>
#include <cmath>
using namespace std;

int length_A, digit;

void Print(int *A, int start, int end)
{
	int i;
	for(i = start; i <= end; i++)
	{
		if(i == start)cout<<'{';
		else cout<<' ';
		cout<<A[i];
	}
	cout<<'}'<<endl;
}

//计数排序
void Counting_Sort(int *A, int *B, int k)
{
	int i, j;
	//将C数组初始化为0,用于计数
	int *C = new int[k+1];
	for(i = 0; i <= k; i++)
		C[i] = 0;
	//C[j]表示数字j在数组A中出现的次数
	for(j = 1; j <= length_A; j++)
		C[A[j]]++;
	//C[i]表示所以<=i的数字出现过的次数
	for(i = 1; i <= k; i++)
		C[i] = C[i] + C[i-1];
	//初始化B为0,B用于输出排序结果
	for(i = 1; i <= length_A; i++)
		B[i] = 0;
	for(j = length_A; j >= 1; j--)
	{
		//如果<=A[j]的数字的个数是x,那么排序后A[j]应该出现在第x个位置,即B[x]=A[j]
		B[C[A[j]]] = A[j];
		C[A[j]]--;
	}
	delete C;
}
//基数排序调用的稳定排序
void Stable_Sort(int *A, int *B, int k, int d)
{
	int i, j;
	//将C数组初始化为0,用于计数
	int *C = new int[k+1];
	for(i = 0; i <= k; i++)
		C[i] = 0;
	int *D = new int[length_A+1];
	for(j = 1; j <= length_A; j++)
	{
		//D[j]表示第[j]个元素的第i位数字
		D[j] = A[j] % (int)pow(10.0, d) / (int)pow(10.0, d-1);
		//C[j]表示数字D[j]在数组A中出现的次数
		C[D[j]]++;
	}
	//C[i]表示所以<=i的数字出现过的次数
	for(i = 1; i <= k; i++)
		C[i] = C[i] + C[i-1];
	//初始化B为0,B用于输出排序结果
	for(i = 1; i <= length_A; i++)
		B[i] = 0;
	for(j = length_A; j >= 1; j--)
	{
		//如果<=D[j]的数字的个数是x,那么排序后A[j]应该出现在第x个位置,即B[x]=A[j]
		B[C[D[j]]] = A[j];
		C[D[j]]--;
	}
	delete []C;
	delete []D;
}
//基数排序
void Radix_Sort(int *A, int *B)
{
	int i, j;
	//依次对每一位进行排序,从低位到高位
	for(i = 1; i <= digit; i++)
	{
		Stable_Sort(A, B, 9, i);
		//输入的是A,输出的是B,再次排序时要把输出数据放入输出数据中
		for(j = 1; j <= length_A; j++)
		A[j] = B[j];
	}
}

int main()
{
	cin>>length_A>>digit;
	int *A = new int[length_A+1];
	int *B = new int[length_A+1];
	int i;
	//随机产生length_A个digit位的数据
	for(i = 1; i <= length_A; i++)
	{
		A[i] = 0;
		while(A[i] < (int)pow(10.0, digit-1))
			A[i] = rand() % (int)pow(10.0, digit);
	}
	Print(A, 1, length_A);
//	Counting_Sort(A, B, 9);
	Radix_Sort(A, B);
	Print(A, 1, length_A);
	delete []A;
	delete []B;
	return 0;
}

 

 

三、练习

8.1 排序算法时间的下界

8.1-1
<span style="line-height: 20px; ">n-1,因为一个有序数组的排序中最少会进行n-1次比较</span>
8.1-2
数学题目不要看我
8.1-3
幸好有答案,不然题目的意思都看不懂。先解释一下题目的意思,高手跳过。
对于一个组包含n个元素的数据,可以有n!种不同的排列,也就是有n!种不同的输入。
对于一个排序算法,可以用一个决策树来表示。
对于任意一种输入排列,从树顶出发,根据该内结点的提示作对应的比较和调整,并决定进入其左子树还是右子树。当这个排列成为一个有序序列时到达叶子结点。这个叶结点就是这个输入排列对应的叶结点。从根结点到叶结点的长度就是这个排列的比较次数。
具有线性时间的输入是指n!种输入排列中满足以下条件的排列:排列的运行时间小于或等于O(n)
假设对于一组包含n个元素的数据,有n!种不同的输入排列,其中具有线性时间的输入排列有m种,求k = m/(n!)
若k<=1/2,那么所证明的命题成立。
后面一问是指k和1/n、1/(2^n)的比较
证明:
这m种输入排列分别对应决策树中的m个叶结点。一棵高度为h的树最多有2^h个叶结点。
2^h >= m =====> h >= lg(m)
若要一棵决策树包含m个叶结点,这棵决策树的高度最少为lg(m)
根据《算法导论》P98中定理8.1的公式,h>=O(nlgn)
只需要证明lg(m) <= O(nlgn),那么k就是可以取到的值。
分别令k等于1/2、1/n、1/(2^n),代入m = k * (n!)得:
计算结果略,这几个值都不满足
8.1-4
不能简单地将子序列的下界合起来,这样做不准确。因为有可能存在一种比“独立解决各个子序列的排序”更快的算法。
这种计算比较次数的一般思路是:
(1)统计有多少种不同的输入排列
(2)每一种输入排列对应决策树的一个叶子结点。
(3)根据叶结点的数量与树的高度的关系,计算决策树的高度
(4)从树根到叶结点的长度就是这个叶结点对应的输入排列的比较次数
对于这道题,可以这样计算:
(1)每个子序列有k个元素,因此有k!种不同的输入排列。
共有n/k个子序列,因此共有(k!)^(n/k)种输入排列
(2)决策树至少有(k!)^(n/k)个叶子
(3)一棵高度为h的决策树,最多有2^h个叶子结点
2^h >= (k!)^(n/k)   =====>   h >= (n/2)lg(k/2)
(4)对于任意一树决策树,至少有一条叶子路径长度超过(n/2)lg(k/2),因此比较次数下界是O(nlgk)


 

8.2 计数排序

8.2-1因为假设待排序数据的范围中[0,k],所以B被初始化为-1
    A = {6 0 2 0 1 3 4 6 1 3 2}
==> C = {2 2 2 2 1 0 2}
==> C = {2 4 6 8 9 9 11}
==> B = {-1 -1 -1 -1 -1 2 -1 -1 -1 -1 -1}
    C = {2 4 5 8 9 9 11}
==> B = {-1 -1 -1 -1 -1 2 -1 3 -1 0 -1 -1}
    C = {2 4 5 7 9 9 11}
==> B = {-1 -1 -1 1 -1 2 -1 3 -1 -1 -1}
    C = {2 3 5 7 9 9 11}
==> B = {-1 -1 -1 1 -1 2 -1 3 -1 -1 6}
    C = {2 3 5 7 9 9 10}
==> B = {-1 -1 -1 1 -1 2 -1 3 4 -1 6}
    C = {2 3 5 7 8 9 10}
==> B = {-1 -1 -1 1 -1 2 3 3 4 -1 6}
    C = {2 3 5 6 8 9 10}
==> B = {-1 -1 1 1 -1 2 3 3 4 -1 6}
    C = {2 2 5 6 8 9 10}
==> B = {-1 0 1 1 -1 2 3 3 4 -1 6}
         C = {1 2 5 6 8 9 10}
==> B = {-1 0 1 1 2 2 3 3 4 -1 6}
         C = {1 2 4 6 8 9 10}
==> B = {0 0 1 1 2 2 3 3 4 -1 6}
         C = {0 2 4 6 8 9 10}
==> B = {0 0 1 1 2 2 3 3 4 6 6}
         C = {0 2 4 6 8 9 9}
8.2-2
辅助数组C[j]记录的是小于或等于i的元素的个数。若几个元素的大小相等,则把这些元素依次从后往前填入排序数组中。因为处理元素的顺序是从后往前的(L9),所以晚出现的元素会填到后面。因此是稳定排序
8.2-3
不稳定
8.2-4
COUNTING-SORT(A, B, k)中步骤L1-L4求出C,ans(a, b) = C[b] - C[a-1], C[-1]=0


 

8.3 基数排序


8.3-1
    A = {COW, DOG, SEA, RUG, ROW, MOB, BOX, TAB, BAR, EAR, TAR, DIG, BIG, TEA, NOW, FOX}
==> A = {SEA, TEA, MOB, TAB, DOG, RUG, DIG, BIG, BAR, EAR, TAR, COW, ROW, NOW, BOX, FOX}
==> A = {TAB, BAR, EAR, TAR, SEA, TEA, DIG, BIG, MOB, DOG, COW, ROW, NOW, BOX, FOX, RUB}
==> A = {BAR, BIG, BOX, COW, DIG, DOG, EAR, FOX, MOB, NOW, ROW, TAB, TAR, TEA, SEA, RUB}
8.3-2
稳定排序有:插入排序,合并排序
方法:为每一个元素加入一个最初位置的属性,但两个元素的value一样大时,比较position,这样不会有相同的两个元素
额外空间:O(4n)
8.3-3
(1)当d=1时,元素只有一位,对这一位做排序就相当于对整个数组做排序了。
(2)证明当1到d-1位排序是正确的时,对d位的排序也是正确的。
(3)对于数组中的任意两个数a和b,假设它们的第d位分别是ad和bd
若ad<bd,则a会排到b的前面
若ad>bd,则a会排到b的后面
若ad=bd,则a和b的相对位置不变,因为是稳定排序,这一点可以保证。
8.3-4
把整数转换为n进制再排序,见 算法导论8.3-4
8.3-5
最坏情况下需要d遍

8.4 桶排序

8.4-1
    A = {0.79, 0.13, 0.16, 0.64, 0.39, 0.20, 0.89, 0.53, 0.71, 0.42}
==> A = {0.13, 0.16, 0.20, 0.39, 0.42, 0.53, 0.64, 0.79, 0.71, 0.89}
==> A = {0.13, 0.16, 0.20, 0.39, 0.42, 0.53, 0.64, 0.71, 0.79, 0.89}
8.4-2
最坏情况是O(n^2)
修改:使用最坏情况下时间为O(nlgn)的算法来处理桶的插入操作
8.4-3
E(X^2)=1/4 * 0 + 1/2 * 1 + 1/4 * 4 = 3/2
E^2(X)=[1/4 * 0 + 1/2 * 1 + 1/4 * 2]^2 = 1^2 = 1
8.4-4
假设分为n个桶
BUCKET-SORT(A)
1    n <- length[A]
2    for i <- 1 to n
3    do insert A[i] to list B[n*(A[i].x^2 + A[i].y^2)]
4    for i <- 0 to n-1
5        do sort list B[i] with insertion sort
6    concatenate the lists B[0], B[1], ……,B[n-1] together in order
8.4-5

不会,网上找了份答案,不懂
X合适分布P,不必然是均匀分布,且不知局限。但P(x)值属于[0,1],且对于X严格单调递增,排序P(x)即排序X。将P(x)均匀分为n个项目组,因为X随机拔取,所以落入每个局限的概率雷同。
若是(i-1)/n <= p(xi) < i/n,则将它放入第i个桶中。
http://www.mysjtu.com/page/M0/S696/696126.html

四、思考题

8-1 比较排序的平均情况下界

a)n个不同的元素对应n!个不同的输入,因此只有这n!个输入所对应的叶结点是有概率的,其余概率均为0。
由于这n!种输入的出现是等概率的,每一种的都是1/n1,因此其对应的叶结点的概率也是1/n!
b)设T(n)是一个叶子结点n的在决策树T上的深度,RT(n)、LT(n)分别表示n在T的右、左子树上的深度。
那么T(n)=RT(n)+1或T(n)=LT(n)+1
因此D(T)=D(RT)+D(LT)+k
c)后面这几题,以我有限的智商和数学能力理解不了,也看不懂网上的答案,不写了


 

8-2 以线性时间原地转换排序

a)计数排序
b)快速排序
c)稳定排序
d)基数排序要求所使用的排序方法是满足的(条件2),如果要在O(bn)时间内完成,所使用算法的时间应该是O(n)(条件1),所以只有a可以
e)不稳定
COUNTING-SORT(A, k)
 1    for i <- 0 to k
 2        do C[i] <- 0
 3    for j <-1 to length[A]
 4        C[A[j]] <- C[A[j]] + 1
 5    cnt <- 1
 6    for i <- 1 to k
 7        while C[i] > 0
 8            A[cnt] <- i
 9            C[i] <- C[j] - 1
10            cnt <- cnt + 1


8-3 排序不同长的数据项

算法导论-8-3-排序不同长度的数据项
a)先用计数排序算法法按数字位数排序O(n),再用基数排序的方法分别对每个桶中的元素排序O(n)
b)递归使用计数排序,先依据第一个字母进行排序,首字相同的放在同一组,再对每一组分别使用计数排序的方法比较第二个字母
见到有人用字典树,也是可以的


8-4 水壶

a)最原始的比较方法,所有的红水壶与所有的蓝水壶依次比较
c)算法类似于快排,由于不是同种颜色的水壶之间比较,所以采用交叉比较
step1:从红色水壶中随机选择一个
step2:以红色水壶为主元对蓝色水壶进行划分
step3:划分的结果是两个数组,并得到与红色水壶相同大小的蓝色水壶
step4:以这个蓝色水壶为主元,对红色水壶进行划分
step5:用step1到step4的过程不断迭代


8-5 平均排序

算法导论6.5-8堆排序-K路合并
a)1排序是完全排序
b)1,3,2,4,5,7,6,8,9,10
c)分工展开化简即可
d)
step1:分别把1, 1+k, 1+2k, 1+3k,……;2, 2+k, 2+2k, 2+3k,……;……当作是单独的集合
step2:对每个集合进行排序时间为O(nlg(n/k))
e)
step1:同d)step1
step2:用堆排序进行k路合并


8-6 合并已排序列表的下界

a)2n个数,随机选n个数,可选的方法有C(2n,n)种
b)2^h >= C(2n,n)
=====>   h >= lg((2n)!) - 2lg(n!)
=====>   h >= 2nlg2n - 2nlgn
=====>   h >= 2nlg2
只能推到这了,最后怎么算,还希望高手指点
c)d)看上去很显然的事情,不知道怎么证


 

你可能感兴趣的:(算法,算法导论,基数排序,桶排序,计数排序)