1. 程式人生 > >程式設計菜鳥到大佬之路:資料結構(二)

程式設計菜鳥到大佬之路:資料結構(二)

資料結構C++版——鄧俊輝課堂筆記

第一章 緒論


複雜度度量

  • 時間複雜度

    • 問題例項的規模往往是決定計算成本的主要因素。一般地,問題規模越接近,相應的計算成本也越接近;而隨著問題規模的擴大,計算成本通常也呈上升趨勢。
    • 執行時間的這一變化趨勢可表示為輸入規模的一個函式,稱作該演算法的時間複雜度(time complexity)。具體地,特定演算法處理規模為n的問題所需的時間可記作T(n)。
    • 根據規模並不能唯一確定具體的輸入,規模相同的輸入通常都有多個,而演算法對其進行處理所需時間也不盡相同。
    • 嚴格說來,以上定義的T(n)並不明確。為此需要再做一次簡化,即從保守估計的角度出發,在規模為n的所有輸入中選擇執行時間最長者作為T(n),並以T(n)度量該演算法的時間複雜度。
  • 漸進複雜度

    • 對於同一問題的兩個演算法A和B,通過比較其時間複雜度 T A ( n )
      T_A(n)
      T B ( n )
      T_B(n)
      ,即可評價二者對於同一輸入規模n的計算效率高低。然而,藉此還不足以就其效能優劣做出總體性的評判,比如對於某些問題, 一些演算法更適用於小規模輸入,而另一些則相反。
    • 在評價演算法執行效率時,我們往往可以忽略其處理小規模問題時的能力差異,轉而關注其在處理更大規模問題時的表現。這種著眼長遠、更為注重時間複雜度的總體變化趨勢和增長速度的策略與方法,即所謂的漸進分析( asymptotic analysis)。
  • 大O記號

    • 首先關注T(n)的漸進上界。為此可引入所謂“大O記號”(big-O notation)。具體地,若存在正的常數c和函式f(n),使得對任何n >> 2都有 T ( n ) c f ( n ) T(n)\leq c∙f(n) 則可認為在n足夠大之後, f(n)給出了T(n)增長速度的一個漸進上界。此時,記之為:T(n) = O(f(n))。
      • 對於任一常數c > 0,有O(f(n)) = O(c∙f(n));
      • 對於任意常數a > b > 0,有O( n a + n b n^a+n^b ) = O( n a n^a ) 。
      • 在大O記號的意義下,函式各項正的常係數可以忽略並等同於1。
      • 多項式中的低次項均可忽略,只需保留最高次項。
  • 環境差異

    • 在實際環境中直接測得的執行時間T(n),雖不失為衡量演算法效能的一種指標,但作為評判不同演算法效能優劣的標準,其可信度值得推敲。
    • 事實上,即便是同一演算法、同一輸入,在不同的硬體平臺上、不同的作業系統中甚至不同的時間,所需要的計算時間都不盡相同。
  • 基本操作

    • 一種自然且可行的解決辦法是,將時間複雜度理解為演算法中各條指令的執行時間之和。
    • 不妨將T(n)定義為演算法所執行基本操作的總次數。也就是說,T(n)決定於組成演算法的所有語句各自的執行次數,以及其中所含基本操作的數目。
  • 起泡排序

    • bubblesort1A()演算法由內、外兩層迴圈組成。
    • 內迴圈從前向後,依次比較各對相鄰元素,如有必要則將其交換。故在每一輪內迴圈中,需要掃描和比較n-1對元素,至多需要交換n-1對元素。
    • 元素的比較和交換,都屬於基本操作,故每一輪內迴圈至多需要執行2(n-1)次基本操作。
    • 外迴圈至多執行n-1輪。因此,總共需要執行的基本操作不會超過 2 ( n 1 ) 2 2(n-1)^2 次。
    • 若以此來度量該演算法的時間複雜度,則有 T ( n ) = O ( 2 ( n 1 ) 2 ) T(n)=O(2(n-1)^2) ,根據大O記號的性質,可進一步簡化和整理為: T ( n ) = O ( 2 n 2 4 n + 2 ) = O ( 2 n 2 ) = O ( n 2 ) T(n)=O(2n^2-4n+2)=O(2n^2)=O(n^2)
  • 最壞、最好與平均情況

    • 以大O記號形式表示的時間複雜度,實質上是對演算法執行時間的一種保守估計,對於規模為n的任意輸入,演算法的執行時間都不會超過O(f(n))。
    • 比如:起泡排序演算法複雜度 T ( n ) = O ( n 2 ) T(n) = O(n^2) 意味著,該演算法處理任何序列所需的時間絕不會超過 O ( n 2 ) O(n^2)
    • 的確需要這麼長計算時間的輸入例項,稱作最壞例項或最壞情況(worst case)。
    • 需強調的是,這種保守估計並不排斥更好情況甚至最好情況(best case)的存在和出現。比如,對於某些輸入序列,起泡排序演算法的內迴圈的執行輪數可能少於n-1,甚至只需執行一輪。
    • 當然,有時也需要考查所謂的平均情況(average case),也就是按照某種約定的概率分佈,將規模為n的所有輸入對應的計算時間加權平均。
  • Ω \Omega 記號

    • 為了對演算法的複雜度最好情況做出估計,需要藉助另一個記號。
    • 如果存在正的常數c和函式g(n),使得對於任何n >> 2都有 T ( n ) c g ( n ) T(n)\geq c∙g(n) ,就可以認為,在n足夠大之後,g(n)給出了T(n)的一個漸進下界。此時,我們記之為:T(n) = Ω \Omega (g(n))這裡的 Ω \Omega 稱作“大 Ω \Omega 記號” (big-omega notation)。
    • 與大O記號恰好相反,大 Ω \Omega 記號是對演算法執行效率的樂觀估計,對於規模為n的任意輸入,演算法的執行時間都不低於 Ω \Omega (g(n))。
  • Θ \Theta 記號

    • 藉助大O記號、大 Ω \Omega 記號,可以對演算法的時間複雜度作出定量的界定,亦即,從漸進的趨勢看,T(n)介於 Ω \Omega (g(n))與O(f(n))之間。
    • 若恰巧出現g(n) = f(n)的情況,則可以使用另一記號來表示。
    • 如果存在正的常數c1 < c2和函式h(n),使得對於任何n >> 2都有 c 1 h ( n ) T ( n ) c 2 h ( n ) c1∙h(n) \leq T(n) \leq c2∙h(n) ,就可以認為在n足夠大之後, h(n)給出了T(n)的一個確界。此時,我們記之為:T(n) = Θ \Theta (h(n))。
    • 這裡的 Θ \Theta 稱作“大 Θ \Theta 記號” (big-theta notation),它是對演算法複雜度的準確估計,對於規模為n的任何輸入,演算法的執行時間T(n)都與 Θ \Theta (h(n))同階。
  • 大O記號,大 Ω \Omega 記號,大 Θ \Theta 記號:
    在這裡插入圖片描述

  • 空間複雜度

    • 除了執行時間的長短,演算法所需儲存空間的多少也是衡量其效能的一個重要方面,此即所謂的空間複雜度(space complexity)。
    • 實際上, 以上針對時間複雜度所引入的幾種漸進記號,也適用於對空間複雜度的度量, 其原理及方法基本相同。
    • 空間複雜度通常並不計入原始輸入本身所佔用的空間,對於同一問題,這一指標對任何演算法都是相同的。反之, 其它(如轉儲、中轉、索引、對映、緩衝等)各個方面所消耗的空間,則都應計入。
    • 就漸進複雜度的意義而言,在任一演算法的任何一次執行過程中所消耗的儲存空間,都不會多於其間所執行基本操作的累計次數。
    • 實際上根據定義,每次基本操作所涉及的儲存空間,都不會超過常數規模;縱然每次基本操作所佔用或訪問的儲存空間都是新開闢的,整個演算法所需的空間總量,也不過與基本操作的次數同階。從這個意義上說,時間複雜度本身就是空間複雜度的一個天然的上界。

複雜度分析

  • 常數O(1)

    • 執行時間可表示和度量為T(n) = O(1)的這一類演算法,統稱作“常數時間複雜度演算法”(constant-time algorithm)。
    • 一般地, 僅含一次或常數次基本操作的演算法均屬此類。
    • 此類演算法通常不含迴圈、分支、子程式呼叫等,但也不能僅憑語法結構的表面形式一概而論。
    • 演算法僅需常數規模的輔助空間即僅需O(1)輔助空間的演算法,亦稱作就地演算法( in-place algorithm)。
  • 對數O(logn)

    • 問題與演算法
      • 考查如下問題:對於任意非負整數,統計其二進位制展開中數位1的總數。
      • 整數二進位制展開中數位1總數的統計:
        int countOnes(unsigned int n) 
        { 	//統計整數n的二進位制展開中數位1的總數:O(logn)
        	int ones = 0; //計數器復位
        	while (n > 0) 
        	{	//在n縮減至0之前迴圈 
         		ones += (1 & n); //檢查最低位,若為1則計數
         		n >>= 1; //右移一位
        	}
        	return ones;    //返回計數
        } 	//等效於glibc的內建函式int __builtin_popcount (unsigned int n)
        
      • 該演算法使用一個計數器ones記錄數位1的數目,其初始值為0。隨後進入一個迴圈:通過二進位制位的與(and)運算,檢查n的二進位制展開的最低位,若該位為1則累計至ones。由於每次迴圈都將n的二進位制展開右移一位,故整體效果等同於逐個檢驗所有數位是否為1。
    • 複雜度
      • 根據右移運算的性質,每右移一位,n都至少縮減一半。也就是說,至多經過 1 + l o g 2 n 1+\lfloor log_2n \rfloor 次迴圈,n必然縮減至0,從而演算法終止。實際上從另一角度來看, 1 + l o g 2 n 1+\lfloor log_2n \rfloor 恰為n二進位制展開的總位數,每次迴圈都將其右移一位,總的迴圈次數自然也應是 1 + l o g 2 n 1+\lfloor log_2n \rfloor
      • 由大O記號定義,在用函式 l o g r n log_rn 界定漸進複雜度時,常底數 r r 的具體取值無所謂,故通常不予專門標出而籠統地記作logn。此類演算法稱作具有“對數時間複雜度”(logarithmic-time algorithm)。
      • 更一般地,凡執行時間可以表示和度量為T(n) = O( l o g c n log^cn )形式的這一類演算法(其中常數c >0),均統稱作“對數多項式時間複雜度的演算法(polylogarithmic-time algorithm)。
      • 此類演算法的效率雖不如常數複雜度演算法理想,但從多項式的角度看仍能無限接近於後者,故也是極為高效的一類演算法。
  • 線性O(n)

    • 問題與演算法

      • 考查如下問題:計算給定n個整數的總和。
      • 陣列元素求和演算法sumI() :
        int sumI(int A[], int n)	//陣列求和演算法(迭代版)
        {
        	int sum = 0;	//初始化累計器,O(1)
        	for (int i = 0; i < n; i++)		//對全部共O(n)個元素求和 
        		sum += A[i];	//累計,O(1)
        	return sum;		//返回累計值,O(1)
        }	// O(1) + O(n)*O(1) + O(1) = O(n+2) = O(n)
        
    • 複雜度

      • 首先,對s的初始化需要O(1)時間。
      • 演算法的主體部分是一個迴圈,每一輪迴圈中只需進行一次累加運算,這屬於基本操作,可在O(1)時間內完成。
      • 每經過一輪迴圈,都將一個元素累加至s,故總共需要做n輪迴圈,於是該演算法的執行時間應為:O(1) + O(1)*n = O(n + 1) = O(n)
      • 凡執行時間可以表示和度量為T(n) = O(n)形式的這一類演算法,均統稱作“線性時間複雜度演算法” (linear-time algorithm)。
      • 也就是說,對於輸入的每一單元,此類演算法平均消耗常數時間。就大多數問題而言,在對輸入的每一單元均至少訪問一次之前,不可能得出解答。以陣列求和為例,在尚未得知每一元素的具體數值之前,絕不可能確定其總和。故就此意義而言,此類演算法的效率亦足以令人滿意。
      • 若執行時間可以表示和度量為T(n) = O(f(n))的形式,而且f(x)為多項式,則對應的演算法稱作“多項式時間複雜度演算法” (polynomial-time algorithm)。
      • 多項式級的執行時間成本,在實際應用中一般被認為是可接受的或可忍受的。 某問題若存在一個複雜度在此範圍以內的演算法,則稱該問題是可有效求解的或易解的(tractable) 。
  • 指數O( 2 n 2^n )

    • 問題與演算法
      • 考查如下問題:在禁止超過1位的移位運算的前提下,對任意非負整數n,計算冪 2 n 2^n
      • 冪函式演算法( 蠻力迭代版)
        __int64 power2BF_I(int n) 
        {	//冪函式2^n演算法(蠻力迭代版),n >= 0
        	__int64 pow = 1;	//O(1):累積器初始化為2^0
        	while ((n --) > 0)	//O(n):迭代n輪
        	    pow <<= 1;	    //O(1):將累積器翻倍
        	return pow;	//O(1):返回累積器
        }	//O(n) = O(2^r),r為輸入指數n的位元位數
        
    • 複雜度
      • 演算法power2BF_I()由n輪迭代組成,各需做一次累乘和一次遞減,均屬於基本操作,故整個演算法共需O(n)時間。
      • 若以輸入指數n的二進位制位數 r = 1 + l o g 2 n r = 1+\lfloor log_2n \rfloor 作為輸入規模,則執行時間為O( 2 r 2^r )。
      • 一般地,凡執行時間可以表示和度量為T(n) = O( a n a^n )形式的演算法( a > 1),均屬於“指數時間複雜度演算法”(exponential-time algorithm)。
      • 當問題規模較大後,指數複雜度演算法的實際效率將急劇下降,計算時間之長很快就會達到令人難以忍受的地步。因此通常認為,指數複雜度演算法無法真正應用於實際問題中,它們不是有效演算法,甚至不能稱作演算法。相應地,不存在多項式複雜度演算法的問題,也稱作難解的(intractable)問題。
  • 複雜度層次

    • 利用大O記號,不僅可以定量地把握演算法複雜度的主要部分,而且可以定性地由低至高將複雜度劃分為若干層次。
    • 典型的複雜度層次包括 O ( 1 ) O ( l o g n ) O ( l o g l o g n ) O ( l o g n ) O ( s q r t ( n ) ) O ( n ) O ( n l o g n ) O ( n l o g l o