算法导论随笔(十二):摊还分析(Amortized Analysis)之聚合分析、核算法和势能法

本文主要是介绍算法导论随笔(十二):摊还分析(Amortized Analysis)之聚合分析、核算法和势能法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在算法导论随笔(一): 操作计数与复杂度Big(O)中,我简单介绍了计算一个算法的时间复杂度的方法。该方法的计算结果虽然都是正确的,但有时不一定特别准确地代表复杂度函数的渐进上界。因此,人们创建了一个分析方式,可以更精确地表示一个复杂度函数的渐进上界。这个分析方式就是我今天要介绍的摊还分析

1.为什么要引入摊还分析

前文说到,使用分析操作计数的方法来计算时间复杂度时,有的时候求出的渐进上限并不准确,也就是说,由于我们一直是在考虑程序消耗时间的最坏情况,而程序并不是一直处于最坏情况。因此我们求出的复杂度经常高于真实的复杂度。这里我们举一个简单的例子。假如我们有一个算法,它的代码如下:

int test(int n){int sum = 0;for(int i=0; i<n; i++){if(i % 500 == 0){for(int j=0; j<n; j++){sum += i;}}else{sum++;}}
}

该代码中,存在两个嵌套的循环。其中第5行的for循环只有在i能被500整除的时候才会执行。但是由于我们要考虑最坏情况,因此计算出来的复杂度其实就是O(n2)。但我们心里清楚,其实真实情况远不会消耗O(n2)的时间。而摊还分析,其实就是在分析时考虑了这些情况,因此根据摊还分析计算出的复杂度,就会更低一些,也更精确一些。

2.摊还分析及其分析方式

那么摊还分析是怎样计算复杂度的呢?在《算法导论》第十七章里,对摊还分析是这样描述的:

摊还分析中,我们求数据结构的一个操作序列中所执行的所有操作的平均时间,来评价操作的代价。这样,我们就可以说明一个操作的平均代价是很低的,即使序列中某个单一操作的代价很高。

也就是说,对于一个数据结构,或者一组数据,例如上面代码中从1到n的遍历,可能其中某一个操作的代价特别高(比如当i能被500整除时,要多运行一个嵌套循环),但其他的操作代价很低。此时,使用摊还分析,可以求出对于该数据结构单个数据操作的平均代价。也就是说,通过对上面的代码进行摊还分析,我们可以求出对于任意一个i∈n,对i的操作代价都是一个相同的平均值,不论i能否被500整除。

在《算法导论》中,介绍了摊还分析的三种方式聚合分析(Aggregate Analysis)核算法(Accounting Method),以及势能法(Potential Method)。接下来我会分别对它们进行讨论。

3. 聚合分析Aggregate Analysis

聚合分析原本自成一派,不过后来被归档入摊还分析之下。《算法导论》里对聚合分析是这样写的:

利用聚合分析,我们证明对所有n,一个n个操作的序列最坏情况下花费的总时间为T(n)。因此,在最坏情况下,每个操作的平均代价,或摊还代价为T(n) / n。

上面的这段话表达的意思就是,对于一个数据结构,或者一组数据,如果我们计算出对它的操作的总时间为T(n),那么每一个对该数据结构的操作的复杂度都是T(n) / n,不管这个操作的类型是什么。举个例子,假如我们计算出对一个数组的操作总时间为T(n) ,那么对该数组单个数据的操作的复杂度都是T(n) / n,不论这个操作是添加、删除还是修改一个数据

下面以书上的例子来讲解聚合分析的方法。
假设我们有一个,对这个栈有入栈和出栈的操作,也就是PUSH跟POP
PUSH(S, x):将对象x压入栈中。
POP(S): 将栈S的栈顶对象弹出,并返回该对象。对空栈调用POP会产生一个错误。

这两个操作的复杂度都为O(1)。因此我们假设它的代价为1。所以对于n个由PUSH和POP组成的操作,其复杂度为O(n)。

现在我们增加一个操作MULTIPOP(S, k),它弹出栈顶的k个对象。 如果删除前栈S中对象数量少于k个,则弹出全部对象。这里我们规定k应为正整数,若参数k小于等于0,则该函数不做任何事情。来看该操作的伪代码。
在这里插入图片描述
在最坏情况下,k=n,因此该操作的复杂度为O(n)。那么问题来了,如果我多次调用MULTIPOP(S, k)函数,所需要的总代价(也就是复杂度)是多少呢?
按以往的计算方式来说,一次MULTIPOP(S, k)的复杂度为O(n)。那么n次MULTIPOP(S, k)的复杂度就是
T ( n ) = n × O ( n ) = O ( n 2 ) T(n) = n \times O(n) = O(n^2) T(n)=n×O(n)=O(n2)
如果使用聚合分析呢?
我们知道,PUSH和POP的复杂度均为O(1)。而MULTIPOP(S, k)这个操作,其有效操作(即操作之前栈不为空)的次数取决于栈内剩余对象的数量。由于初始状态下栈为空,因此,MULTIPOP所能进行的有效POP次数最多只能等于PUSH的次数,即:之前PUSH了多少次,则最多只能POP多少次。因此,若栈内有n个对象,则n个MULTIPOP的操作,所包含的有效POP次数最多也就是n。

书中是这样说的:

对于任意的n值,任意一个由n个PUSH、POP和MULTIPOP组成的操作序列,最多花费O(n)的时间。

也就是说,计算这n个操作全都是PUSH,栈中也才只有n个对象。因此,不论对栈进行多少次POP,最多也就只有O(n)个有效操作(无效操作不做任何事情,因此也不产生代价)。因此,n个操作的复杂度为O(n),则每个操作的平均复杂度为
T ( n ) = O ( n ) / n = O ( 1 ) T(n) = O(n) / n = O(1) T(n)=O(n)/n=O(1)
那么n个MULTIPOP的复杂度也就是O(n),比按之前方法计算出的O(n2)更好,也就是说,我们计算出的上界更紧凑(即更接近其实际运行的代价上界)。

4. 核算法Accounting Method

对于核算法,书中是这样介绍的。

核算法进行摊还分析时,我们对不同操作赋予不同费用,赋予某些操作的费用可能多于或少于其实际代价。我们将赋予一个操作的费用称为它的摊还代价。当一个操作的摊还代价超出其实际代价时,我们将差额存入数据结构中的特定对象,存入的差额称为信用(credit)。对于后续操作中摊还代价小于实际代价的情况,信用可以用来支付差额。

这段话是什么意思呢?用通俗的语言讲,这个方法类似于买往返票的操作。也就是把一个操作A的代价和该操作未来可能需要的代价都算在这个操作的头上,这样的话,未来对与该操作相关的操作B就不用再计算复杂度了,因为B操作的代价已经由A来支付了。

我们还是用上文所述的栈的例子来说明。对于每一个PUSH、POP和MULTIPOP操作,它们的代价分别如下(min(k, s)表示k值和当前栈内对象数量s之间的最小值):
在这里插入图片描述
而核算法,就是给PUSH操作买了一张往返票。其原因就是,每一个PUSH操作都会把一个对象压入栈中,因此该对象未来就可能会被一个POP操作所弹出。这一压一弹,代价为2。核算法把这2个代价都算在了PUSH头上,而把POP的代价设置为0。而又因为MULTIPOP其实就是多个POP操作,由于POP的代价为0,因此MULTIPOP的代价也是0。因此我们得到核算法赋予每个操作的代价如下图:
在这里插入图片描述
因此,意一个由n个PUSH、POP和MULTIPOP组成的操作序列,其代价最高的时候就是所有操作都是PUSH的时候(因为POP和MULTIPOP代价为0),此时代价为n×2=2n,因此n个操作的复杂度为
T ( n ) = O ( 2 n ) = O ( n ) T(n) = O(2n) = O(n) T(n)=O(2n)=O(n)
也就是说,就算所有操作都是MULTIPOP,其代价仍为O(n)。

5. 势能法Potential Method

势能法的思想与核算法类似,依旧是类似于买往返票这种提前支付的模式。不同的是,在核算法中,我们是用微观的方式对每一个操作赋予代价;而在势能法中,我们是用宏观的方式来对整个数据结构来进行代价的评估

这里的势能与物理学有点沾边,比如我们知道的物体的“重力势能”,是随着物体与地面距离增大而增大的。因此,以前文提到的栈这个数据结构为例,我们也可以规定它的势能。该势能初始为0,随着栈内对象数量的增大而增长,随对象数量的减少而减小。

书中把对于一个数据结构的第i个操作的代价定义为如下公式。
c ^ i = c i + ϕ ( D i ) − ϕ ( D i − 1 ) \hat c_i = c_i + \phi(D_i) - \phi(D_{i-1}) c^i=c

这篇关于算法导论随笔(十二):摊还分析(Amortized Analysis)之聚合分析、核算法和势能法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/292653

相关文章

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

找不到Anaconda prompt终端的原因分析及解决方案

《找不到Anacondaprompt终端的原因分析及解决方案》因为anaconda还没有初始化,在安装anaconda的过程中,有一行是否要添加anaconda到菜单目录中,由于没有勾选,导致没有菜... 目录问题原因问http://www.chinasem.cn题解决安装了 Anaconda 却找不到 An

Spring定时任务只执行一次的原因分析与解决方案

《Spring定时任务只执行一次的原因分析与解决方案》在使用Spring的@Scheduled定时任务时,你是否遇到过任务只执行一次,后续不再触发的情况?这种情况可能由多种原因导致,如未启用调度、线程... 目录1. 问题背景2. Spring定时任务的基本用法3. 为什么定时任务只执行一次?3.1 未启用

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

C++ 各种map特点对比分析

《C++各种map特点对比分析》文章比较了C++中不同类型的map(如std::map,std::unordered_map,std::multimap,std::unordered_multima... 目录特点比较C++ 示例代码 ​​​​​​代码解释特点比较1. std::map底层实现:基于红黑

Spring、Spring Boot、Spring Cloud 的区别与联系分析

《Spring、SpringBoot、SpringCloud的区别与联系分析》Spring、SpringBoot和SpringCloud是Java开发中常用的框架,分别针对企业级应用开发、快速开... 目录1. Spring 框架2. Spring Boot3. Spring Cloud总结1. Sprin

Spring 中 BeanFactoryPostProcessor 的作用和示例源码分析

《Spring中BeanFactoryPostProcessor的作用和示例源码分析》Spring的BeanFactoryPostProcessor是容器初始化的扩展接口,允许在Bean实例化前... 目录一、概览1. 核心定位2. 核心功能详解3. 关键特性二、Spring 内置的 BeanFactory

MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析

《MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析》本文将详细讲解MyBatis-Plus中的lambdaUpdate用法,并提供丰富的案例来帮助读者更好地理解和应... 目录深入探索MyBATis-Plus中Service接口的lambdaUpdate用法及示例案例背景

MyBatis-Plus中静态工具Db的多种用法及实例分析

《MyBatis-Plus中静态工具Db的多种用法及实例分析》本文将详细讲解MyBatis-Plus中静态工具Db的各种用法,并结合具体案例进行演示和说明,具有很好的参考价值,希望对大家有所帮助,如有... 目录MyBATis-Plus中静态工具Db的多种用法及实例案例背景使用静态工具Db进行数据库操作插入