MIT算法导论学习-Lecture5 排序

第五讲 排序

学习了多种排序算法之后,一个基本的问题就是:How fast can we Sort?

e.g. 

插入排序——Theta(n^2)

归并排序——Theta(nlgn)

快速排序——Theta(nlgn)——(这是随机化的快排运行时间的期望)

堆排序    ——Theta(nlgn)

以上算法最快的为Theta(nlgn),那么便出现了一个问题——可以比Theta(nlgn)更快吗??

该问题的答案是:yes or no,可以或者不可以在特定情况下都是正确答案。那么真正的答案是:这依赖于计算模型(计算模型就是你可以对元素做什么操作),在比较模型的框架下,下面我们将证明不会存在比Theta(nlgn)快的排序算法,而脱离了这个模型之后,我们可以得到线性时间复杂度的排序算法。

5.1 比较排序

比较排序即是,只使用比较来决定元素间的相对位置。

引入一个决策树(Decision Tree)方法来帮助解决这个问题,对于n个元素A=<a1,a2,……,an>,的排序,

决策树的定义为:

  1. 每一个内节点都有一个下表(i,j),i,j属于{1,2,…,n}
  2. 如果ai <= aj,则左子树给出了其后的排序;
  3. 如果ai > aj,则右子树给出了接下来的排序;
  4. 每一个叶节点给出了一种排序结果。

下图的示例给出了对三个元素排序的决策树(箭头和标号给出了决策树排序的执行过程):

决策树模型本质上可以理解为一种多路径的方式,而每一个路径的结束即代表了一种排序的结果,它可以模拟任何一种比较排序的执行:

         ——对于一个特定的n,n代表待排序元素个数,有一个决策树模型与之对应;

         ——每当遇到一个节点的比较,则按可能结果分成了两个子树;

         ——决策树概括了所有可能的执行方式;

         ——算法的运行时间,即为路径的长度;

         ——算法的最坏运行时间,即为树的高度。

视频中有定理可以证明,对于size 为n 的决策树,其高度至少为Omega(nlgn).也就是说任何一种比较排序,其算法的下界为Omega (nlgn)。

上式中用到了一个公式:n!>= (n/e)^n,由于lg是单调的,则由不等式的传导性可以有第一步得到第二步。

由上面的定理可以知道:在比较模型下,任何排序方法不可能比Theta(nlgn)更快了,且可以知道堆排序和归并排序是渐近最优的,随机化的快速排序的平均运行时间也是渐近最优的。

5.2 线性时间内排序

5.2.1 计数排序

输入:A[1,2,……,n],而且A[i]属于{1,2,3,……,k}

输出:B[1,2,……,n]

辅助存储:C[1,2,……,k]

算法伪代码为:

	for i = 1 to k
		do C[i] = 0		//初始化C
	for j = 1 to n
		do C[A[j]] = C[A[j]] + 1//统计a[j] = i频率,i属于1~k-1
	for i = 2 to k
		do C[i] = C[i] + C[i-1] //统计a[j] <= i累计结果,i属于1~k-1
	for j = n downto 1		//分配
		do B[C[A[j]]] = A[j]
		   C[A[j]] = C[A[j]] - 1

辅助解释:

由于每个元素大小都在范围1~k,所以,在n>k时,肯定有一些元素是重复出现的。

第一个for循环,将C初始化为0;

第二个for循环,统计了key = i的频率;

第三个for循环,统计了key<=i的累计数目;

第四个for循环,该步骤称为分配步骤(distributionstep),由于C中存储了key<=i的元素个数,那么C[i-1]存储了k<=i-1的元素个数,假设A中有5个元素且在1~4之间,C即为C[1]~C[4],A中元素如下:

         4                1                3                4                3

那么经过第三个for循环之后,C中元素为:

         1                1                3                5

即,key<=1的元素个数为1,A[2]

         key<=2的元素个数为1,A[2]

         key<=3的元素个数为3,A[2],A[3],A[5]

         key<=1的元素个数为5,全部

第四步的分配按如下的方式来执行:

         J= 5时,A[5] = 3,then,C[3] = 3,so,B[3] = A[5] = 3;//该步骤的意思是,对A从n到1遍历,,假设A[n] =m,则C[m](假设其值为r)表示的是A中所有小于等于m的元素个数,则其在B中的位置就是B[r]。其实该步骤就是在预留的空间上填数字

C[3]= C[3] – 1 = 2;//该步骤的意思是,由于上一步已经在B[r](也即B[C[m]])上填了一个数字,则应该把该数字保留,下一个填的数字即是向前挪动一位,并且由于j是从n到1遍历的,这样保证了算法是稳定的

J= 4时,以此类推……

其执行的最终结果为:

图中的黄色数字所标注为执行顺序,其实这个顺序是A的倒序遍历,由执行结果也可以看出该排序方法是稳定的,这点很重要,因为在下面基数排序中需要用到这点

时间复杂度分析:

即,该排序方法的时间复杂度是Theta(n+k),如果k和n在同一个数量级上,即k=O(n),则该排序算法的时间复杂度为Theta(n)。这一点与上面的比较排序的下界是Omega(nlgn)并不矛盾,因为在该算法中没有一次比较的发生,该算法是脱离了比较排序的模型,即也能不受其下界的限制。

一个简单的代码实现为:

//============5.1 Counting Sort
//a为输入,b为输出,共有n个数,且数的范围都在0~k-1
void CountingSort(int a[],int b[],int n,int k)
{
	//申请辅助存储空间C
	int *c ;
	c = (int *)malloc(k*sizeof(int)) ;
	for (int i = 0; i < k; i ++)
	{
		c[i] = 0 ;// initialize c
	}
	for (int j = 0; j < n; j ++)
	{
		c[a[j]] = c[a[j]] + 1 ; //统计a[j] = i频率,i属于1~k-1
	}
	for (int j = 1; j < k; j ++)
	{
		c[j] = c[j] + c[j-1] ; //统计a[j] <= i累计结果,i属于1~k-1
	}
	for (int j = n-1; j >= 0 ; j --)
	{
		b[c[a[j]]-1] = a[j] ;   //分配,在这点特别注意数组的上下界问题
		c[a[j]] -- ;
	}
       free(c) ;
}

5.2.2 基数排序(Radix Sort)

顾名思义,该方法就是按数的基数进行排序,比如说十进制数的基数是10等。

最开始的想法:从高位到低位进行排序,该方法是可行的,可以参考如下博客:

http://www.cnblogs.com/Braveliu/archive/2013/01/21/2870201.html

但是,发明人,也就是IBM的开山祖师爷了,用这个算法是为了给一个打卡器什么东西的来分类,这种方法需要先将卡片分成小类(按最高位来分),则每一个小类需要一个桶或者其他容器来装,这种多次装取不适合其装置。

第二种思想:按从低位到高位进行排序,这种方法用于上述的分类是可行的,因为所有的元素从始至终都在一个桶内。

算法的运行过程可以参考如下的例子,e.g.

5.2.2.1 算法分析

 1.正确性分析

采用数学归纳法很好证明。

假设要对第t位(从低到高)进行排序,其前提是我们已经对t-1位排好序了,那么按第t位排序可以分为两种情况:

 I 第t位不相等,这时,按第t位排序,显然是直接得到了正确的排序;

II 第t位相等,此时则保留其位置不变(稳定排序),由于t-1位已经是有序的了,则由于位置不变,则按第t位排序其也是有序的。

2.时间复杂度分析

我们假设每一位的排序都是用的计数排序(因为计数排序是线性时间排序),计数排序的时间复杂度为Theta(n+k)。

本能地来看,假设一共有n个数(假设为十进制),而其最大为d位,即数的范围为0~10^d-1,由此可以看到我们要进行d轮排序,每一轮的排序时间复杂度为Theta(n+10),这是由于k=10,那么显然基数排序法的时间复杂度为:

T(n)= Theta(d*(n+10)) = Theta(dn)

值得注意的是,基数排序法的这个基数并不一定非要是数制的基数,我们可以把两位算成一个基数,那么此时计数排序的时间复杂度为Theta(n+100),而基数排序的时间复杂度为:

 T(n)= Theta((d/2)*(n+100)) = Theta(dn/2)

也就是说,在n足够大的时候,我们可以适当地将基数选择的大一些来获得更好的效果。

一个简单的代码实现为:

//============5.2 Radix Sort
//x为数字,d为要取的位数
int GetDigit(int x, int d)
{
	int a[] = {1, 1, 10, 100,1000,10000,100000,1000000};   //最大6位数,所以这里需要1000000。
	return (x/a[d]) % 10; 

}

void RadixSort(int a[],int begin,int end,int d)
{
	const int radix = 10 ;
	int count[radix], i, j ;

	int *C = (int*)malloc((end-begin+1)*sizeof(int));  

	for (int k = 1; k <= d; k ++)
	{
		//初始化count
		for (i = 0; i < radix; i ++)
		{
			count[i] = 0 ;
		}
		//统计该位分别为0,1,…,9的频率
		for (i = begin; i <= end; i++ )
		{
			count[GetDigit(a[i],k)] ++ ;
		}
		//统计该位小于等于1,…,9的次数
		for(i = 1; i < radix; i++) 
		{
		   count[i] = count[i] + count[i-1];
		}
		//分配
		for(i = end;i >= begin; --i)        //这里要从右向左扫描,保证排序稳定性   
		{    
			j = GetDigit(a[i], k);        //求出关键码的第k位的数字, 例如:576的第3位是5   
			C[count[j]-1] = a[i];		
			--count[j];				
		} 
		//收集数据
		for(i = begin,j = 0; i <= end; ++i, ++j)  
		{
			a[i] = C[j];    
		}        
	}     
	free(C); 
}

注:这段代码参考了如下博客的内容,而且该博客对于基数排序讲的更加通俗易懂:

http://www.cnblogs.com/Braveliu/archive/2013/01/21/2870201.html






  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值