緒論
緒論一道冒泡排序拍懵我了,我以爲 複雜度的經典冒泡排序沒有優化空間了,結果一個bool標識打臉,可以提前終止冒泡,如果已經是按順序了的數組的話:
void bubblesort1A(int A[], int n) { //起泡排序算法(版本1A):0 <= n
bool sorted = false; //整體排序標誌,首先假定尚未排序
while (!sorted) { //在尚未確認已排序之前,逐行掃描交換
sorted = true; //假定已經排序
for (int i = 1; i < n; i++) { //自左向右逐對檢查弼前範圍A[0, n)內癿各相鄰元素
if (A[i - 1] > A[i]) { //一旦A[i - 1]不A[i]逆序,則
swap(A[i - 1], A[i]); //交換
sorted = false; //因整體排序不能保證,需要清除排序標誌
}
}
n--; //至此末元素必然就位,故可以縮短待排序序列癿有效長度
}
} //布爾型標誌位sorted,可及時提前退出,而不致總是蠻力地做n - 1趟掃描交換
看來數據結構,路漫漫其修遠兮,吾將上下而求索
算法具備的要素:
- 輸入與輸出
- 基本操作、確定性與可行性
- 有窮性與正確性
- 退化與魯邦性
- 重用性(便捷的用於其他場合,如冒泡可推廣至float和char)
1.4 遞歸
以下將從遞歸的基本模式入手,循序漸進地介紹如何選擇和應用(線性遞歸、二分遞歸和多分支遞歸等)不同的遞歸形式,以實現(遍歷、分治等)算法策略,以及如何利用遞歸跟蹤和遞
推方程等方法分析遞歸算法的複雜度。
1.4.1 線性遞歸
算法sum()可能朝着更深一層進行自我調用,且每一遞歸實例對自身的調用至多一次。於是,
每一層次上至多隻有一個實例,且它們構成一個線性的次序關係。此類遞歸模式因而稱作“線性遞歸”(linear recursion),它也是遞歸的最基本形式。
int sum(int A[], int n) { //數組求和算法(線性遞歸版)
if (1 > n) //平凡情況,遞歸基
return 0; //直接(非逑弻式)計算
else //一般情冴
return sum(A, n - 1) + A[n - 1]; //遞歸:前n - 1頃和,再累計第n - 1頃
} //O(1)遞歸深度 = O(1)*(n + 1) = O(n)
1.4.2 遞歸分析
整個sum()算法的運行時間爲:
(n + 1) X O(3) = O(n)
sum()算法的空間複雜度又是多少呢?在創建了最後一個遞歸實例(即到達遞歸基)時,佔用的空間量達到最大。準確地說,等於所有遞歸實例各自所佔空間量的總和。
這裏每一遞歸實例所需存放的數據,無非是調用參數(數組A的起始地址和長度n)以及用於累加總和的臨時變量。這些數據各自只需常數規模的空間,其總量也應爲常數。故此可知,sum()算法的空間複雜度線性正比於其遞歸的深度,亦即O(n)。
求解
power2(n) = 1 n==0
power2(b) = 2*power2(n-1) else
每層只有一個實例,這是一個線性遞歸, O(n)的空間複雜度,時間複雜度
優化:
power2(n) =
1 n==0
power2(n/2)^2 * 2 n奇數
power2(n/2) *2 n偶數
(n/2後按二進制展開)
還是線性遞歸,因爲二者只選一條路。但是O(logn)的複雜度
1.4.4遞歸消除
空間成本 由於遞歸深度,空間佔用往往較大;系統創建、銷燬實例需要時間;因而往往寫成等價的非遞歸版本(一般是利用棧結構)
尾遞歸及消除 線性遞歸算法中恰好以最後一步操作的形式出現(即所有實例都會終止在這一個遞歸調用)。稱作尾遞歸tail recursion,這類均可轉換爲迭代
1.4.5 二分遞歸
分而治之。
int mi = (lo + hi) >> 1; //以居中單元爲界,將原區間一分爲二
return sum(A, lo, mi) + sum(A, mi + 1, hi); //遞歸對各子數組求和,然後合計
算法啓動後經連續m = log 2 n次遞歸調用,每一刻活躍的實例不會超過m+1個,m爲深度3,到達2^k的任一個遞歸實例之前,已執行的遞歸調用總比遞歸返回多m-k次
因此任意時刻實例總數不會超過m+1,是常數,空間複雜度O(m)即O(logn),比線性少。
每一次遞歸中非遞歸時間是常數,遞歸實例共2n-1,時間複雜度O(2n-1)即O(n)
必須保證子問題相互獨立,可獨立求解。否則就會變成遞歸的斐波那契數列。
的時間複雜度
- 藉助輔助空間,子問題求解夠記錄下來(製表策略)
- 從遞歸基觸發向上推(動態規劃)
__int64 fibI(int n){
__int64 f=0 , g=1;
while(0 < n--){g +=f ; f = g-f;}
return f;
}
1.5 抽象數據類型abstract data type , ADT
數據集合和對應操作超脫於具體程序設計語言,催生了面向對象程序設計語言
數據結構大致分爲線性結構,半線性結構、非線性結構
線性結構中,按邏輯詞語與物理存儲地址的對應,區分
向量: 物理存放位置與邏輯次序吻合,邏輯次序也叫秩rank
列表: 採用間接定址的方式通過封裝後的位置相互引用
數組,嚴格對應A0 A1 A2
向量
vector 容量:私有變量_capacity確定,當前實際規模由_size指示
向量中秩爲r的元素,對應於內部數組中的_elem[r],其物理地址爲_elem + r
若以複製形式copyFrom()初始化,則採用雙倍現有容量來申請空間,O(n)時間(因爲要一個一個的複製過去)(共有好幾種構造函數方式可選)
需強調的是,由於向量內部含有動態分配的空間,默認的運算符"="不足以支持向量之間的
直接賦值。
(只允許有一種析構函數)O(1)時間
向量中元素可能不是程序語言直接支持的基本類型。所以,向量析構之前應當釋放各元素所指的對象,這個由上層調用者負責
動態空間管理
2.4.2 可擴充向量extendable vector
申請更大的B,把A複製過來,再刪除A
分攤複雜度,分攤運行時間 與平均運行時間不同,後者稱爲期望運行時間。前者,比較複雜,具體問題再看。
最壞情況下,考慮不斷連續insert,插入到很大很大的n時,共做過log2n次擴容,累計時間2N+4N+…+capacity(n) < 2*n = O(n)
平攤到n次操作,單次操作分攤運行時間O(1),令人滿意
2.4.5縮容
_size/_capacity < 0.25時縮小一半,同上,單次運行確實是O(n),但是平攤之後變成O(1)。當然,閾值,策略有需求時可以改。
向量重載了A[i]的取值方式, return _elem[i] (其中0<=i < _size) ,取代了get()這種不方便的方式