【程序人生】數據結構雜記(一)

說在前面

個人讀書筆記

起泡排序

在由一組整數組成的序列A[0,n1]A[0, n - 1]中,滿足A[i1]<=A[i]A[i - 1] <= A[i]的相鄰元素稱作順序的,否則是逆序的。不難看出,有序序列中每一對相鄰元素都是順序的,亦即,對任意1<=i<n1 <=i < n都有A[i1]<=A[i]A[i - 1] <= A[i];反之,所有相鄰元素均順序的序列,也必然整體有序。

由有序序列的上述特徵,我們可以通過不斷改善局部的有序性實現整體的有序:從前向後依次檢查每一對相鄰元素,一旦發現逆序即交換二者的位置。對於長度爲nn的序列,共需做n1n - 1次比較和不超過n1n - 1次交換,這一過程稱作一趟掃描交換。

在這裏插入圖片描述
可見,經過這樣的一趟掃描,序列未必達到整體有序。果真如此,則可對該序列再做一趟掃描交換。事實上,很有可能需
要反覆進行多次掃描交換,直到在序列中不再含有任何逆序的相鄰元素。多數的這類交換操作,都會使得越小(大)的元素朝上(下)方移動,直至它們抵達各自應處的位置。

排序過程中,所有元素朝各自最終位置亦步亦趨的移動過程,猶如氣泡在水中的上下沉浮,起泡排序(bubblesort)算法也因此得名。

在這裏插入圖片描述
經過kk趟掃描交換之後,最大的前kk個元素必然就位;經過k趟掃描交換之後,待求解問題的有效規模將縮減至nkn - k

時間複雜度

隨着輸入規模的擴大,算法的執行時間將如何增長?執行時間的這一變化趨勢可表示爲輸入規模的一個函數,稱作該算法的時間複雜度(time complexity)。具體地,特定算法處理規模爲n的問題所需的時間可記作T(n)T(n)

OO記號

同樣地出於保守的估計,我們首先關注T(n)T(n)的漸進上界。爲此可引入所謂“大OO記號”(big-O notation)。具體地,若存在正的常數cc和函數f(n)f(n),使得對任何n>>2n >> 2,都有T(n)<=cf(n)T(n) <= c * f(n)。則可認爲在nn足夠大之後,f(n)f(n)給出了T(n)T(n)增長速度的一個漸進上界。此時,記之爲:
T(n)=O(f(n))T(n)=O(f(n))
由這一定義,可導出大OO記號的以下性質:

  • 對於任一常數c>0c > 0,有O(f(n))=O(cf(n))O(f(n)) = O(c * f(n))
  • 對於任意常數a>b>0a > b > 0,有O(na+nb)=O(na)O(n^a + n^b )=O(n^a )

前一性質意味着,在大OO記號的意義下,函數各項正的常係數可以忽略並等同於1。後一性質則意味着,多項式中的低次項均可忽略,只需保留最高次項。可以看出,大O記號的這些性質的確體現了對函數總體漸進增長趨勢的關注和刻畫。

以大OO記號形式表示的時間複雜度,實質上是對算法執行時間的一種保守估計,稱作最壞實例或最壞情況。

起泡排序的時間複雜度

bubblesort1A()算法由內、外兩層循環組成。內循環從前向後,依次比較各對相鄰元素,如有必要則將其交換。故在每一輪內循環中,需要掃描和比較n - 1對元素,至多需要交換n - 1對元素。元素的比較和交換,都屬於基本操作,故每一輪內循環至多需要執行2(n - 1)次基本操作。另外,外循環至多執行n - 1輪。因此,總共需要執行的基本操作不會超過2(n1)22(n - 1)^2次。若以此來度量該算法的時間複雜度,則有T(n)=O(2(n1)2)T(n)=O(2(n-1)^2)

根據大OO記號的性質,可進一步簡化和整理爲:
T(n)=O(n2)T(n) = O(n^2)

複雜度分析

常數時間複雜度O(1)O(1)

一般地,僅含一次或常數次基本操作的算法均屬此類。此類算法通常不含循環、分支、子程序調用等。

對數時間複雜度O(logn)O(logn)

考查如下問題:對於任意非負整數,統計其二進制展開中,數位1的總數。
在這裏插入圖片描述
在這裏插入圖片描述

根據右移運算的性質,每右移一位,n都至少縮減一半(n是輸入的十進制整數)。也就是說,至多經過1+log2n1 + log_2n次循環,n必然縮減至0,從而算法終止。實際上從另一角度來看,1+log2n1 + log_2n恰爲n二進制展開的總位數,每次循環都將其右移一位,總的循環次數自然也應是1+log2n1 + log_2n

無論是該循環體之前、之內還是之後,均只涉及常數次(邏輯判斷、位與運算、加法、右移等)基本操作。因此,countOnes()算法的執行時間主要由循環的次數決定,亦即:
O(1+log2n)=O(log2n)O(1 + log_2n)=O(log_2n)
由大OO記號定義,在用函數logrnlog_rn界定漸進複雜度時,常底數rr的具體取值無所謂,故通常不予專門標出而籠統地記作O(logn)O(logn)

線性時間複雜度O(n)O(n)

對於輸入的每一單元,此類算法平均消耗常數時間。就大多數問題而言,在對輸入的每一單元均至少訪問一次之前,不可能得出解答。以數組求和爲例,在尚未得知每一元素的具體數值之前,絕不可能確定其總和。

遞歸

以數組求和問題爲例。易見,若n = 0則總和必爲0,這也是最終的平凡情況;否則一般地,總和可理解爲前n - 1個整數(A[0,n1))(A[0, n - 1))之和,再加上末元素(A[n1])(A[n - 1])
按這一思路,可基於線性遞歸模式,設計出另一sum()算法如下圖所示。
在這裏插入圖片描述
由此實例,可以看出保證遞歸算法有窮性的基本技巧:
首先判斷並處理n = 0之類的平凡情況,以免因無限遞歸而導致系統溢出。這類平凡情況統稱“遞歸基”(base case of recursion)。平凡情況可能有多種,但至少要有一種(比如此處),且遲早必然會出現。

線性遞歸的模式,往往對應於所謂減而治之(decrease-and-conquer)的算法策略:
遞歸每深入一層,待求解問題的規模都縮減一個常數,直至最終蛻化爲平凡的小(簡單)問題。
按照減而治之策略,此處隨着遞歸的深入,調用參數將單調地線性遞減。因此無論最初輸入的n有多大,遞歸調用的總次數都是有限的,故算法的執行遲早會終止,即滿足有窮性。當抵達遞歸基時,算法將執行非遞歸的計算(這裏是返回0)。

爲保證有窮性,遞歸算法都必須設置遞歸基,且確保總能執行到。爲此,針對每一類可能出現的平凡情況,都需設置對應的遞歸基,故同一算法的遞歸基可能(顯式或隱式地)不止一個。

遞歸算法所消耗的空間量主要取決於遞歸深度
遞歸要保證子問題與原問題在接口形式上的一致

結語

如果您有修改意見或問題,歡迎留言或者通過郵箱和我聯繫。
手打很辛苦,如果我的文章對您有幫助,轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章