數據結構與算法的重溫之旅(二)——複雜度進階分析

學習本篇文章前若是你是個算法的初學者,建議查看本篇文章的上一章數據結構與算法的重溫之旅(一)——複雜度分析​​​​​​​來初步的瞭解時間複雜度。本系列全部文章的代碼都是用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)。

上一篇文章:數據結構與算法的重溫之旅(一)——複雜度分析​​​​​​​

下一篇文章:數據結構與算法的重溫之旅(三)——數組

相關文章
相關標籤/搜索