主定理与均摊复杂度
前置
如果你对时间复杂度和渐进符号有基本了解,可以直接跳过这一部分。时间复杂度是代表算法用时随数据规模而增长的趋势。而渐进符号有很多种,其中大写表示严格含等于,小写则不含等于。包含以下几种,可以简单理解一下,
主定理
内容
对于任意递推式
且对于第二种情况来说,需要满足
解释
如果你熟悉多项式,那么类比一下。对于情况一来说,
应用
快速排序
我们都知道快速排序的最优时间复杂度为
任意递推式
给出任意递推式
均摊复杂度
以下文字摘自 oi-wiki:
均摊分析(Amortized Analysis)是一种用于分析算法和动态数据结构性能的技术。它不仅仅关注单次操作的成本,还通过评估一系列操作的平均成本,为整体性能提供更加准确的评估。均摊分析不涉及概率,且只能确保最坏情况性能的每次操作耗费的平均时间,并不能确认系统的平均性能。在最坏情况下,均摊分析通过将高成本操作的开销分摊到低成本操作上,确保整体操作的平均成本保持在合理范围内。
均摊分析通常采用三种主要分析方法:聚合分析、记账分析和势能分析。这些方法各有侧重,分别适用于不同的场景,但它们的共同目标是通过均衡操作成本,优化数据结构在最坏情况下的整体性能表现。
聚合分析法
聚合分析法就是通过计算一系列操作的总成本,并将其平均到每次操作上,从而得出每次操作的均摊时间复杂度。那么我们看动态数组,它在空间不够的时候会自动扩容,它主要有以下两种操作,第一种是如果还有空间,那么直接加入,时间复杂度为
那么假设我们要插入
记账分析法
以下文字摘自 oi-wiki:
记账分析法(Accounting Method)通过为每次操作预先分配一个固定的均摊成本来确保所有操作的总成本不超过这些预分配的成本总和。记账法类似于一种费用前置支付的机制,其中较低成本的操作会存储部分费用,以支付未来高成本的操作。
还拿动态数组举例,假设我给它每一个位置开始每次预设的成本为
具体举例:假如我一直插入
- 第一步:数组
a = [1,2,3,4] ,需要扩容,最开始成本为8 ,因为最开始为4 \times 3 = 12 ,插入每个元素消耗1 ,总共消耗4 ,目前12 - 4 = 8 - 第二步:此时数组
a = [1,2,3,4,0,0,0,0] ,消耗4 进行扩容,目前8 - 4 = 4 - 第三步:插入元素,此时数组
a = [1,2,3,4,5,6,7,8] ,每个元素初始带来4 \times 3 = 12 ,插入每个元素消耗1 ,总共消耗4 ,小计12 - 4 = 8 ,目前12
手推一下或者可以严格证明发现动态数组的每次插入操作所存储的均摊成本足够支付未来的扩容操作,就相当于开店不管挣不挣钱,都不需要再次往里砸钱了。那么每次插入的均摊复杂度为
势能分析法
以下文字摘自 oi-wiki:
势能分析(Potential Method)通过定义一个势能函数(通常表示为
\Phi ),度量数据结构的 潜在能量,即系统状态中的预留资源,这些资源可以用来支付未来的高成本操作。势能的变化用于平衡操作序列的总成本,从而确保整个算法的均摊成本在合理范围内。
内容
首先我们定义分析的数据结构,设
那么我们定义均摊成本为:
那么总均摊复杂度可以表示为:
我们可以把求和展开,发现有许多可以互相消掉的项,得到的最终结果为:
那么如果我们在定义势函数时,满足
解释
分类讨论一下
那么如果保证
应用
一道经典例题。定义一个栈,设
那么三种操作的实际成本分别为
那么三种操作分别分类讨论当前操作的类别:
- 当前第
i 次操作为操作一,那么对于栈的大小来说,满足\mid S_i \mid = \mid S_{i-1} \mid + 1 ,那么当前操作的均摊成本\hat{c_i} = c_i + \Phi(P_i) - \Phi(P_{i-1}) = 1 + \mid S_i \mid - \mid S_{i-1} \mid = 1 + 1 = 2 - 当前第
i 次操作位操作二,那么对于栈的大小来说,满足\mid S_i \mid = \mid S_{i-1} \mid - 1 ,那么当前操作的均摊成本\hat{c_i} = c_i + \Phi(P_i) - \Phi(P_{i-1}) = 1 + \mid S_i \mid - \mid S_{i-1} \mid = 1 - 1 = 0 - 当前第
i 次操作位操作三,那么对于栈的大小来说,满足\mid S_i \mid = \mid S_{i-1} \mid - k ,那么当前操作的均摊成本\hat{c_i} = c_i + \Phi(P_i) - \Phi(P_{i-1}) = k + \mid S_i \mid - \mid S_{i-1} \mid = k - k = 0
综上,我们发现总均摊成本为
练习
动态数组在空间不够的时候会自动扩容,它主要有以下两种操作,第一种是如果还有空间,那么直接加入,时间复杂度为
那么我们想让本来成本消耗较大的操作势能降低,那么针对这道题,我们就需要把扩容复制的势能降低,那么也就是说我要设计一个势函数,满足剩余数量越多势能越小,那么势函数可以设为
那么依然分类讨论操作类型:
- 如果当前是不需要扩容的,那么直接插入即可,那么满足
num_i = num_{i-1} + 1 且size_i = size_{i-1} ,那么均摊代价\hat{c_i} = c_i + \Phi(P_i) - \Phi(P_{i-1}) = 1 + (2 \times num_i - size_i) - (2 \times num_{i-1} - size_{i-1}) = 1 + 2 = 3 - 如果当前是需要扩容的,那么需要复制后再插入,那么满足
num_i = num_{i-1} + 1 且size_i = 2 \times size_{i-1} 。而且额外的,因为此时满了,所以满足size_{i-1} = num_{i-1} 。那么均摊代价\hat{c_i} = c_i + \Phi(P_i) - \Phi(P_{i-1}) = num_i + 1 - (2 \times num_i - size_i) - (2 \times num_{i-1} - size_{i-1}) = 1 + 2 = 3
综上,我们发现总均摊成本为
拓展
对于上面的两个经典案例,你可以用其他的方法计算它的均摊复杂度吗?