數據結構 C++語言版 清華大學第三版 學習筆記

緒論

緒論一道冒泡排序拍懵我了,我以爲O(n2)O(n^2) 複雜度的經典冒泡排序沒有優化空間了,結果一個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趟掃描交換 

看來數據結構,路漫漫其修遠兮,吾將上下而求索

算法具備的要素:

  1. 輸入與輸出
  2. 基本操作、確定性與可行性
  3. 有窮性與正確性
  4. 退化與魯邦性
  5. 重用性(便捷的用於其他場合,如冒泡可推廣至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)

求解power(2,n)=2npower(2,n) = 2^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)

必須保證子問題相互獨立,可獨立求解。否則就會變成遞歸的斐波那契數列。
O(2n)O(2^n ) 的時間複雜度

  1. 藉助輔助空間,子問題求解夠記錄下來(製表策略)
  2. 從遞歸基觸發向上推(動態規劃)
__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

圖2.1
申請更大的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()這種不方便的方式

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