数据结构与算法的重温之旅(二)——复杂度进阶分析

学习本篇文章前若是你是个算法的初学者,建议查看本篇文章的上一章数据结构与算法的重温之旅(一)——复杂度分析​​​​​​​来初步的了解时间复杂度。本系列全部文章的代码都是用JavaScript实现,之因此用JavaScript实现是由于它能够直接在浏览器宿主中运行代码,即在浏览器中按f12打开控制台,选择console按钮,在下面空白的文本框把本例的代码黏贴上去回车便可运行。方便各位同窗学习和调试。算法

本博客的做者与csdn里的Tank_in_the_street是同一做者,转载文章时需声明,谢谢合做。数组

1、前言

上一篇文章主要是初步的入门了复杂度分析,包括讲了大O表示法、时间复杂度、空间复杂度和他们的复杂程度O(1)O(logn)O(n)O(nlogn)O(n^{2})O(2^{n})O(n!)​​​​​​​等,那么本章则进一步深刻的时间复杂度里的最好状况时间复杂度(best case time complexity)、最坏状况时间复杂度(worst case time complexity)、平均状况时间复杂度(average case time complexity)和均摊时间复杂度(amortized time complexity)。浏览器

首先咱们为何要引用这四个概念呢。即便同一段在不一样状况下时间复杂度会出现量级的差别,还记得上篇文章所说的冒泡排序和快速排序的对比吗,在通常状况下,快速排序的时间复杂度是 O(nlogn),而冒泡排序的时间复杂度是 O(n^{2}),可是若是数据是有序的状况下,快速排序的时间复杂度会去到 O(n^{2}),而冒泡排序的则会到 O(n),因此这个时候就要引入上面所说的概念。

2、最好、最坏时间复杂度

在讲以前,咱们先举个例子:bash

/**
* @param {string[]} arr
* @param {number} length
* @param {string} x
* @description length变量表示的是arr数组的长度,x则是一个字符串变量
**/
function test(arr, length, x) {
	var i = 0
	var pos = -1
	for (; i < length; ++i) {
		if (array[i] == x) {
			pos = i
			break
		}
	}
	return pos
}
复制代码

这个函数主要的功能就是遍历数组,直到找到数组内某个元素与变量x相等的时候则跳出循环。若是按照上一篇文章所说的,那它的时间复杂度则是O(n),可是若是数组里第一个元素正好是和变量相等,则不须要遍历后面n-1个数据了,这时的时间复杂度就是O(1),可是若是这个元素刚好在最后或者是根本都不在数组的时候,则要遍历完整个数组,这个时候的时间复杂度则是。因此本例进一步的阐述了上面所说的结论:不一样状况下,相同代码的时间复杂度是不同的数据结构

有时候为了表示代码在不一样状况下的不一样时间复杂度,咱们这里须要引入两个概念:最好状况时间复杂度和最坏状况时间复杂度。什么是最好状况时间复杂度,就是在最理想的状况下,执行代码的时间复杂度。上面所说的代码例子里最好状况的时间复杂度为O(1)。同理,最坏时间复杂度则是在最坏状况下,执行代码的时间复杂度。上面所说的代码例子里最坏状况的时间复杂度为O(n)函数

3、平均时间复杂度

上面所说的最好时间复杂度和最坏时间复杂度都是极端状况下才会出现的状况,自己极端状况出现的几率并非很大,因此为了更好地表示平均状况下的时间复杂度,这里引入一个概念叫平均状况时间复杂度,简称平均时间复杂度
平均时间复杂度的分析是这样的,咱们要查找与变量x相同的元素,总共有n+1种状况:在数组的0到n-1位置种和不在数组中,咱们把每种状况给累加起来,最后除以总数n+1,便可获得须要遍历元素个数的平均值,公式以下:post

\frac{1+2+3+...+n+n}{n+1}=\frac{n(n+3)}{2(n+1)}​​​​​​​

首先说明一下这个公式,这个公式左边分子部分表示的是最好+最坏+剩余的普通状况,之因此后面才会加多一个n,是由于最坏状况有两种状况,一种是目标值恰好在数组最后,另外一种状况则是不在数组的状况。学习

经过上一篇文章所说在复杂度分析中能够忽略掉常量、低阶和系数,得出来的时间复杂度则是O(n)。ui

虽然这条式子看起来没错,可是这里每种状况出现的几率是不同的,首先这个与x相等的元素值可能不在这个数组,也有可能可能在数组种,因此在数组种和不在数组中的几率是1/2。另外假设该元素在数组0到n-1中的几率是相同的,则几率为1/n,因此咱们可得当前目标值在数组任意位置的几率是1/2n,所以通过充分考虑后,咱们的公式是这样子的:spa

1\times\frac{1}{2n}+2\times\frac{1}{2n}+3\times\frac{1}{2n}+...+n\times\frac{1}{2n}+n\times\frac{1}{2}=\frac{3n+1}{4}​​​​​​​

这个值就是几率论中的加权平均值,也叫作指望值,因此平均时间复杂度的全称也叫加权平均时间复杂度或者指望时间复杂度。利用刚刚讲的去掉常量和系数以后,获得的时间复杂度仍然为O(n)。

4、均摊时间复杂度

讲到如今,上面讲到的三种方法是比较经常使用的时间复杂度分析方法,下面则要讲的是更加进阶的概念:均摊时间复杂度。在讲本例以前先举个代码例子:

var array = new Array(10)
var count = 0
function insert(val) {
    if (count == array.length) {
       var sum = 0;
       for (var i = 0; i < array.length; ++i) {
          sum = sum + array[i];
       }
       array[0] = sum;
       count = 1;
    }

    array[count] = val;
    ++count;
}
复制代码

我先来解释一下这段代码。这段代码实现了一个往数组中插入数据的功能。当数组满了以后,也就是代码中的 count == array.length 时,咱们用 for 循环遍历数组求和,将求和以后的 sum 值放到数组的第一个位置,而后再将新的数据插入。但若是数组一开始就有空闲空间,则直接将数据插入数组。

那本程序的时间复杂度是多少呢?理想状况下就是count不等于array.length的状况,这个时候的时间复杂度则是O(1),最坏的状况则是count等于array.length的时候,时间复杂度为O(n)。那平均时间复杂度是多少呢,答案是O(1)。下面论证来讲明:
假设数组的长度为n,根据数组插入的位置的不一样,咱们能够分为n种状况,每种状况的时间复杂度为O(1),除此以外,还有一个额外状况就是count等于array.length的状况时,时间复杂度为O(n)。而且这n+1种状况发生的几率都同样,都是1/(n+1),因此经过加权平均,获得的公式以下:


这里经过以前所说忽略掉常数和系数,则平均时间复杂度为O(1)。其实在这里能够经过另外一种逻辑方法来分析平均时间复杂度,咱们先对比本篇文章的两个函数,一个test函数一个insert函数。

首先test函数在极端状况下时间复杂度才为O(1),而insert函数在大多数状况的时间复杂度都为O(1),只有极端状况下当count等于array.length时才会执行一次循环累加操做。其次就是不一样的地方。对于 insert(函数来讲,O(1) 时间复杂度的插入和 O(n) 时间复杂度的插入,出现的频率是很是有规律的,并且有必定的先后时序关系,通常都是一个 O(n) 插入以后,紧跟着 n-1 个 O(1) 的插入操做,循环往复。

针对这种特殊场景,咱们引入了一种更加简单的分析方法:均摊分析法。经过均摊分析法获得的时间复杂度叫作均摊时间复杂度。那究竟如何使用摊还分析法来分析算法的均摊时间复杂度呢?

咱们仍是继续看在数组中插入数据的这个例子。每一次 O(n) 的插入操做,都会跟着 n-1 次 O(1) 的插入操做,因此把耗时多的那次操做均摊到接下来的 n-1 次耗时少的操做上,均摊下来,这一组连续的操做的均摊时间复杂度就是 O(1)。这就是均摊分析的大体思路。

对一个数据结构进行一组连续操做中,大部分状况下时间复杂度都很低,只有个别状况下时间复杂度比较高,并且这些操做之间存在先后连贯的时序关系,这个时候,咱们就能够将这一组操做放在一起分析,看是否能将较高时间复杂度那次操做的耗时,平摊到其余那些时间复杂度比较低的操做上。并且,在可以应用均摊时间复杂度分析的场合,通常均摊时间复杂度就等于最好状况时间复杂度。我我的认为:均摊时间复杂度是一种特殊的时间复杂度。

5、总结

经过上面所讲的最好时间复杂度、最坏时间复杂度、平均时间复杂度和均摊时间复杂度,之因此会用到这四个几率,是由于同一段代码,在不一样输入的状况下,复杂度量级有多是不同的。最后经过今天的学习,咱们来分析一下下面代码的各类时间复杂度:

// 全局变量,大小为 10 的数组 array,长度 len,下标 i。
var array = new Array(10) 
var len = 10;
var i = 0;
// 往数组中添加一个元素
function add(element) {
   if (i >= len) { // 数组空间不够了
     // 从新申请一个 2 倍大小的数组空间
     var new_array = new Array(len*2);
     // 把原来 array 数组中的数据依次 copy 到 new_array
     for (var j = 0; j < len; ++j) {
       new_array[j] = array[j];
     }
     // new_array 复制给 array,array 如今大小就是 2 倍 len 了
     array = new_array;
     len = 2 * len;
   }
   // 将 element 放到下标为 i 的位置,下标 i 加一
   array[i] = element;
   ++i;
}
复制代码

这里咱们能够看出,最好时间复杂度确定是O(1),即i小于len的时候,最坏时间复杂度是O(n),即i大于等于len的时候。平均时间复杂度是,这里跟上面的insert函数例子相似。均摊时间复杂度也是O(1),由于n-1次状况下都是时间复杂度为O(1),只有n时才是时间复杂度为O(n)。

上一篇文章:数据结构与算法的重温之旅(一)——复杂度分析​​​​​​​

下一篇文章:数据结构与算法的重温之旅(三)——数组

相关文章
相关标签/搜索