平摊分析
我们经常在处理数据结构的时间复杂度的时候,大多数操作代价很低,可是由于某些个别操作的代价较高,导致最后求得时间复杂度的上界不是那么的紧凑。在平摊分析中,执行一系列数据结构操作所需要的时间是通过对执行的所有操作求平均而得出的。平摊分析可用来证明在一系列操作中,即使单一的操作具有较大的代价,通过对所有操作求平均后,平均代价还是很小的。平摊分析与平均情况分析的不同之处在于它不牵涉到概率。这种分析保证了在最坏情况下每个操作具有平均性能。
平摊分析一般有三种方法:聚集分析,记账方法,势能方法。
我们将由一个动态表的例子引入这三种方法,利用平摊分析来分析动态表操作的时间代价。动态表的实现有很多种,堆、栈、哈希等。这里我们采用哈希表hashtable,因为这里有一个哈希因子α,我们利用这个哈希因子α才能更好的分析这三种平摊分析方法。哈希表在之前文章已经详细的讲过,现在就是利用哈希表分析一下这个动态表的插入和删除带来的扩张和收缩的代价。
聚集分析
先来说聚集分析,聚集分析致力于确定一个有n个操作的数据结构的总代价的上界T(n),每个操作的平摊代价为T(n)/n。这个平摊代价对所有的操作都是成立的。
对于一个动态表,我们可以随时插入一个元素和删除一个元素。用哈希表来实现动态表,我们当然希望查找越快越好,内存空间越小越好。可是鱼和熊掌不可兼得,所以我们取个折中的办法,一个动态表T的空间大小size[T]是表内元素num[T]的2倍,即size[T]=2*num[T]。此时哈希因子α=1/2<1,可以在O(1)的时间内找到需要的元素,而且浪费的空间不会多于表空间大小的一半。
可是随着插入的进行,动态表总有一天会满,那时候再插入元素的时候就会出现错误,无法插入任何元素,你不能依靠删除元素来等待再次插入。此时我们要做的就是先扩张动态表。重新申请内存空间,然后插入。我们要申请一个更大的内存空间,由于要满足α<1,所以新的内存空间是原来的两倍。然后将旧表中的元素复制到新表之中,同时释放旧表空间,插入新的元素。如图所示,表的空间是越来越大。
随着删除的进行,表中的元素会越来越少,如果删除之后,哈希因子α>=1/2,直接删除就好。可是删除之后,哈希因子α<1/2(下文会讲这是不确切的),我们为了节省内存就要进行动态表的收缩,同样的需要申请内存空间,新申请的空间是原来空间的1/2,然后复制旧表的元素到新表里面。此时我们是先收缩表,然后再删除。如图,表的空间是越来越小。
我们来具体分析一下,对于一个动态表T来说,插入第i个元素所需要的代价ci是多少呢?如果插入之前当前动态表未满,还有空间,那么插入一个元素的代价ci=1。如果插入之前表已满,扩张表,然后插入的代价ci=(i-1)+1。删除一个元素的代价ci呢?如果删除之后表中元素至少是半满状态,删除的代价ci=1。如果删除之后表中元素不足空间一半,先进行表的收缩,再删除元素,删除代价ci=num[T]-(i-1)+1(见下面)。总的来说动态表的一次操作的代价要么是1,要么是O(n)。
也就是说最坏的情况下动态表一次操作的代价是O(n),那么n次操作的总代价在最坏情况下就是O(n^2)喽!真的是这样吗?这也就是我们开头说的不紧凑的上界,O(n^2)确实是一个上界,可是真的不够紧凑,这里我们要用平摊分析的方法来证明这个确切的上界是O(n)。不相信?不骗人的,继续看吧。
插入
对于动态表T的插入操作来说,只有当i-1次操作之后表满,第i次插入的代价才会是i。既然第i次操作要扩张表,说明i-1是2的幂。So插入操作代价一览表,size[i]是第i插入之后的表空间大小,num[i]是第i次插入之后表内元素数量,ci是插入代价。
插入的代价
i |
1 |
2 |
3 |
4 |
5 |
6
|
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
|