算法基本思想之时空权衡和利用已知输入信息:计数排序和桶排序

时空权衡思想

算法设计

算法设计中的时空权衡一般是用空间换时间,主要分为两种:输入增强和预构造。输入增强的一般表述是,我们对数据进行某种预处理,并对预处理的结果进行存储。存储的这个中间结果有利于提高后续算法的时间效率,但中间结果的存储需要占用额外空间。一个例子是,如果我们要计算某个函数的多个值。那我们可以先把这个函数定义域上所有的可能值都算出来放到一张表里,需要调用这个函数求值的时候直接查表就可以。很显然,新建表的过程回占用大量空间,但后续直接查表的过程相对于每次都要计算函数值来说降低了时间复杂度。有的同学可能会有疑问,那最开始算所有函数值的时候不是更耗时间吗?确实是这样。所以,在使用输入增强改进算法时间效率时,需要已知某些数据信息。如果不使用,很可能在时间效率上没有改变。例如,在本文要讨论的计数排序中,如果采用比较计数法,时间复杂度仍然是 O ( n 2 ) O(n^2) O(n2),所以我们一般采用的是分布计数法,时间复杂度就可以降到 O ( n ) O(n) O(n)

系统设计

在系统设计中也时常用到时空权衡的思想。缓存的设计就是一个典型的时间换空间的思想。首先回顾一下计算机的基本组件:中央处理器(CPU)、(内部)存储器、I/O设备、总线。因此,直观上我们可以设计一套系统,将运行时的数据(程序和程序处理的数据)都放在内存中,CPU只负责通过寄存器访问内存,读取数据,再通过算术逻辑单元计算,计算结果传回寄存器,最后传回内存。这样的设计会有什么问题呢?在实践中人们发现,CPU的读取速度很快,而内存的相应速度会慢很多,这就是所谓的“冯诺依曼瓶颈”,在CPU等待内存加载数据的过程中,是不会进行其他处理的,这段时间就浪费了。因此,为了更高效利用这部分时间,我们可以想到,直接在CPU中开辟出一个地方用于可以长期存储经常从内存中调用的数据。当然,在CPU容量有限的情况下,这样的操作会压缩其他部件的可用空间。这便是典型的空间换时间的思想。“高速缓存”就是这样构造出来的,CPU从这块缓存里加载数据的速度就比从主存中加载要快多了(现在已经有了三级缓存,速度越快的缓存容量越小,这就是时间和费用的权衡了)。当CPU发起向内存加载数据的请求时,会先从缓存中查找,缓存中没有才会从内存加载数据,并更新缓存。这里又利用了系统设计中另一个非常重要的思想:局部性原理(locality),即最近访问过的内存位置以及周边的内存位置很容易会被再次访问。

计数排序

原理

计数排序分为比较计数和分布计数。由于比较计数没有利用已知信息,现实意义不大,此处只讨论分布计数排序。这种算法要求待排序数组A中的数是0-k之间的整数,且这个k已知。这个对输入分布的要求保证了:新数组的索引可以直接和原数组中的数字对应。这也就解释了为什么输入必须是整数,因为如果不是整数,没法对应新数组的索引。
有的同学可能会问,即使有小数或负数,不是也可以对数组进行线性变换都化成正整数再用计数排序吗?以及,如果实际中并不知道k是多少,理论上不是也可以遍历一遍数组A找最大值吗?
对于第一个问题,一个回应是,我们需要知道进行什么线性变换,而找到正确变换的复杂程度又取决于我们对输入数组已知的信息量。如果我们对输入数组一无所知,那必须要遍历所有元素,找出负数的大小及小数的位数等信息,以决定进行什么样的变换。
对于第二个问题,一个回应是,确实我们可以通过遍历找到A的最大值,但这里的问题不在于如何知道A的最大值,而是这个最大值有多大。如果这个值远远大于数组中元素个数n,后文中会证明这会大大降低该算法的效率,从而使这个算法不再适用于这个问题。因此,当我们知道A的最大值k相对于n并非过大时,即可以用计数排序。

从这里可以看出,分布计数排序之所以能降低时间复杂度,一个关键的地方在于我们可以利用一些待排序数组的已知信息(正整数、最大值)。

过程

计数排序过程主要分为两个部分。
建立并更新数组C(中间输出)
该过程就是把原数组中元素所有存在的排序的过程。具体过程是先建一个新数组C,长度为0-k,对每个索引下元素赋值0。该步的复杂度为 O ( k ) O(k) O(k)。再扫描一遍A,遇到一个元素,就将其与C索引对应,C中该索引对应元素就加1,表示这个索引下的元素又多了一个。每个步骤操作时间为常数,因此建立并更新C数组的复杂度为 O ( n ) O(n) O(n)。当然,也可以扫描新数组C,但要更新C中每个元素需要对A中所有元素遍历一遍,复杂度显然高于前一种方法。

这里可以看出,计数排序的本质是对问题进行了一个预处理,并对预处理后的信息进行存储,以便降低之后问题的时间复杂度。而这个预处理足够简单的前提是我们已知数组的一些输入信息,同时数据分散程度较小。其原因在于,预处理本身需要一定时间复杂度,已知的信息越多,数据分散程度越小,预处理的复杂程度越低。并且,对预处理后的信息进行存储也需要一定空间复杂度。举例来说,当数据分散程度很大,即k很大时候,如 k = O ( n 2 ) k=O(n^2) k=O(n2),则该算法总的时间、空间复杂度均为 O ( n 2 ) O(n^2) O(n2)。因此,保证计数排序高效的前提是预处理导致的时间、空间复杂度的上升小于后续处理中时间复杂度的下降。同时,我们可以发现,当数据分散程度很大时,即k很大时候,时间和空间复杂度会很高。如,当 k = O ( n 2 ) k=O(n^2) k=O(n2),则该算法总的时间复杂度即为 O ( n 2 ) O(n^2) O(n2)。因此,计数排序对于分布范围较小,分布信息已知的输入较友好。

这里,我们可以发现,分布计数排序能降低时间复杂度的另一个关键在于我们对待排序数组进行了预处理,并将预处理结果进行存储以备之后调用。这是一个典型的以空间换时间的思想。

建立并更新数组B(最终输出)
C更新完之后,再建一个新数组B长度等于原序列n,在建的同时确定其每个索引下的元素。确定B中元素需要用到一个很巧妙的对应关系链:A中索引-A元素值-C中索引-C元素值-B中索引-B元素值。这样即可以让B元素和A元素一一对应,且对应完成即排好了序。直观上理解,是因为在数组C建立之初已经把A中元素值重排,相当于给了每个元素值一个标好号的小盒子,只要把A中元素塞到对应标号的盒子中,也就是A各元素之间的相对位置只要通过计数就可以确定。但这里,信息进行了压缩。C中并非直接显示A中元素,而仅表示排好

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值