1. 程式人生 > >算法復雜度分析

算法復雜度分析

排序 隨機 環境 次方 列表 一段 基本概念 二叉 dex

數據結構和算法

  • 基本概念

    數據結構指存儲數據的結構,算法指的是操作數據的方法.數據結構是算法是相輔相成的,算法需要作用到特定的數據結構.

  • 常用數據結構

    數組、鏈表、棧、隊列、散列表、二叉樹、堆、跳表、圖、Trie 樹

  • 常用算法:

    遞歸、排序、二分查找、搜索、哈希算法、貪心算法、分治算法、回溯算法、動態規 劃、字符串匹配算法

算法復雜度分析

由於相同算法在不同測試環境,硬件設備上處理數據的效率並不相同,且不同算法的執行效率受數據規模的影響很大(如下圖).所以在實際編碼和進行算法優時就需要有一個理論分析方向作為指導.算法復雜度分析使用大O復雜度分析法.

技術分享圖片

時間復雜度

也叫時間漸進復雜度,並不表示準確的代碼運行時間,而是表示代碼執行時間隨著數據規模增長的變化趨勢,以下為維基百科關於時間復雜度的解釋:

計算機科學中,算法時間復雜度(Time complexity)是一個函數,它定性描述該算法的運行時間。這是一個代表算法輸入值的字符串的長度的函數。時間復雜度常用大O符號表述,不包括這個函數的低階項和首項系數。使用這種方式時,時間復雜度可被稱為是漸近的,亦即考察輸入值大小趨近無窮時的情況。例如,如果一個算法對於任何大小為 n (必須比 n0 大)的輸入,它至多需要 5n3 + 3n 的時間運行完畢,那麽它的漸近時間復雜度是 O(n3).

func TestNN(n int) int {
	var sum int
    for i:=0;i<n;i++{
        for j:=0;j<n;j++{
            sum+=i*j
        }
    }
    return sum
}

func TestN(n int) int {
	var sum int
    for i:=0;i<n;i++{
        sum+=i
    }
    return sum
}

func Test1(n int) int {
    return n
}

假設每一行代碼 執行消耗cpu時間均為cpu_time, n表示數據規模,則以上三段代碼逐行執行總時間為:

TNN(n)=(1+n+2n2+1)*cpu_time

TN(n)=(1+2n+1)*cpu_time

T1(n)=(1)*cpu_time

由上面表達式可以知道代碼執行總時間 T(n) 與每行代碼的執行次數 n 成正比,引入大O時間復雜度後,可以表示為:T(n)=O(f(n)),其中T(n)表示代碼執行的時間;n 表示數據規模的大小;f(n) 表示每行代碼執行的次數總和.

則代碼執行時間TNN(n)=O(2n2+n+2);TN(n)=O(2n+2);T1(n)=O(1)

當 n 很大時,而公式中的低階項、常數項(無論是100,1000,10000,100000……)、系數三部分並不能對增長趨勢造成很大的影響,所以都可以忽略。 則上述3段代碼的時間復雜度,就可以記為:TNN(n) = O(n2);TN(n) = O(n);T1=O(1)

時間復雜度分析方法

  • 假設數據規模非常大

  • 關註循環最多的那部分代碼

  • 總復雜度等於量級最大的那段代碼的復雜度

  • 嵌套代碼的復雜度等於嵌套內外代碼復雜度的乘積

常見時間復雜度量級以及示例

  • O(1)
func T1() int {
    i,j:=1,2
    return i+j
}
  • O(logn)
func TLogN(n int) int {
    i:=1
    for i<n{
        i=3*n
    }
    return i
}
// 計算循環多少次, 3x次方=n 則x=log3 n,使用換底公式為 log3 2 * log2 n
// 由於log3 2是一個常量,則當n越來愈大時不影響代碼執行時間趨勢
  • O(n)
func TN(n int) int {
    var sum int
    for i:=0;i<n;i++ {
        sum+=i
    }
    return sum
}

// 循環了n次
  • O(nlog n)
func TNLonN(n int) int {
    var sum int
    for i:=0;i<n;i++ {
        sum+=i
TLogN(n) } return sum } // O(n)*O(log n)=O(nlog n)
  • O(n2)
func TN3(n int) int{
    var sum
    for i:=0;i<n;i++{
        for j:=0;j<n;j++{
        	sum+=i*j
    	}
    }
    return 
}
// O(n)*O(n)*O(n)=O(n3)

空間復雜度

也叫漸進空間復雜度,概念和時間復雜度類似,表示數據規模和存儲空間的增長關系

舉個例子:

func SpaceN(n int) {
    sli:=make([]string, n)
    for i:=0; i<n; i++ {
        sli[i] = "sli_" + string(i)
    }
}

上述代碼第2行代碼申請了容量為n的一個[]string 類型切片的存儲空間,其他行代碼申請的空間都是常量,所以空間復雜度為O(n)

四個算法復雜度的概念

因為同一段代碼,在不同輸入的情況下,復雜度量級有可能是不一樣,所以可以引入一下幾種時間復雜度相關概念.

  • 最好情況時間復雜度(best case timecomplexity)

    極端好的情況下算法時間復雜度

  • 最壞情況時間復雜度((worst case timecomplexity)

    極端壞的情況下算法時間復雜度

  • 平均情況時間復雜度(average case timecomplexity)

    隨機情況下算法時間復雜度

  • 均攤時間復雜度(amortized time complexity)

    存在時序規律的平均情況時間復雜度

// 從給出的切片中找出與t相等的元素的位置
// 切片長度為n
func find([]int sli, int t) int {
    for index,s := range sli {
        if (sli[index] == t){
            return index
        }
	}
	return -1;
}

從時間復雜度角度分析上述代碼,時間復雜度為O(n),n=len(sli).

有個問題是切片中元素位置隨機,當與t相等的元素的位置為0時,那麽查找1次的時候就被找到,那麽程序結束時實際時間復雜度為O(1);當與t相等的元素的位置為n-1時,那麽需要查找n次才被找到,那麽程序結束時實際時間復雜度為O(n)。這種情況下使用時間復雜度O(n)顯然無法相對準確表示該代碼的執行用時,所以引入最好情況時間復雜度,最壞情況時間復雜度,平均情況時間復雜度3個概念。

以上述代碼為例,最好情況時間復雜度為O(1), 最壞情況時間復雜度為O(n)。

那麽平均情況復雜度呢, 假設t在數組裏和不在數組裏概率均為1/2,則t取值sli[0]至sli[n-1]的概率均為1/2n,所以代碼執行次數為1x1/2n+2x1/2n+3x1/2n+4x1/2n+...+nx1/2n=(3n+1)/4,所以最終平均情況復雜度也為O(n)

const N = 1000
var arr = [N]int{}
var count = 0
func Insert(t int) {
	if count > n-1 {
        // 對arr進行軟清空
		count = 0
		sum := 0
		for _, a := range arr {
			sum += a
		}
        // 把sum放到arr第一個位置
		arr[count] = sum
		count++
	}
	arr[count] = t
	count++
}

上述代碼定義一個數組[N]int類型的arr,然後Insert方法實現插入操作,每插入N個數據,就需要進行一次累加,並將累加結果放到arr[0]一次類推。

Insert函數當arr有剩余空間時的復雜度在最好情況下是O(1),當arr沒有空余空間時最壞情況是O(N)

非最好情況下和最壞情況下(實際情況)時,代碼執行次數(加權平均) N+N/(N+1)=2N/(N+1)所以實際復雜度為O(1)

Insert跟find函數相比比較明顯的區別是find除了最好和最壞情況完全是隨機的,而insert在最好和最壞之外海有一定規律,在每一次復雜度為O(n)的操作後,都跟著N-1次復雜度為O(1),然後依次反復。

這種情況下引入均攤時間復雜度來表示時間復雜度,即把O(n)的復雜度均攤到後面的n-1次O(1)上,可以理解為只是常量系數的變化,而量級並沒有發生變化,所以復雜度還是O(1).

算法復雜度分析