什麼是數據結構?什麼是算法?python
數據結構和算法是相輔相成的,數據結構是爲了算法服務的,算法要做用在特定的數據結構之上。所以,咱們沒法孤立數據結構來說算法,也沒法孤立算法來說數據結構。算法
複雜度分析數據庫
經常使用的數據結構和算法編程
事半功倍的學習技巧數組
爲何須要複雜度分析?緩存
經過實際的代碼運行來統計運行效率的方法叫作是過後統計法,這種方法存在以下以下問題:安全
因此,咱們須要一個不用具體的測試數據來測試,能夠粗略地估計算法的執行效率的方法,這就是 時間、空間複雜度分析方法。bash
公式:T(n) = O(f(n))數據結構
這種複雜度表示方法只是表示一種變化趨勢,當 n 很大時,公式中的低階、常量、係數三部分並不左右增加趨勢,因此能夠忽略。多線程
示例代碼 01
int cal(int n){ int sum = 0 int i = 1; for(;i<=n;i++){ sum = sum + i; } }
假設每行代碼執行的時間都同樣,爲 unit_time,那麼上述代碼總的執行時間爲:(2n+2)*unit_time,大 O 表示法爲:T(n) = O(2n+2),當 n 很大時,可記爲 T(n) = O(n)
示例代碼 02
int cal(int n){ int sum = 0; int i = 1; int j = 1; for(;i<=n;++i){ j = 1; for(;<=n;++j){ sum = sum + i*j } } }
假設每行代碼執行的時間都同樣,爲 unit_time,那麼上述代碼總的執行時間爲:(2n2+2n+3)*unit_time, 大 O 表示法爲:T(n) = O(2n2+2n+3), 當 n 很大時,可記爲 T(n) = O(n2)
漸進時間複雜度
對於上述羅列的複雜度量級,能夠粗略地分爲兩類:多項式量級和非多項式量級。其中,非多項式量級只有兩個:O(2n) 和 O(n!)。當數據規模 n 愈來愈大時,非多項式量級算法的執行時間會急劇增長,求解問題的執行時間會無線增加。蘇歐陽,非多項式時間複雜度的算法實際上是效率很是低的算法。
漸進空間複雜度
表示算法的存儲空間與數據規模之間的增加關係,常見的空間複雜度以下:
是一種線性表數據結構,用一組連續的內存空間來存儲一組具備相同類型的數據。
使用建議:
爲何在大多數的編程語言中,數組要從 0 開發編號,而不是 1 ?
從數組存儲的內存模型上來看,下標 最確切的定義應該是 偏移(offset),這樣就能確保正確計算出每次隨機訪問的元素對於的內存地址,這樣就好理解了。
是一種線性數據結構,用一組非連續的內存空間來存儲一組具備相同類型的數據。
數組 VS 鏈表 時間複雜度比較:
數組 | 鏈表 | |
---|---|---|
插入、刪除 | O(n) | O(1) |
隨機訪問 | O(1) | O(n) |
常見的鏈表類型:
緩存策略常有以下三種方式:
如何基於鏈表實現 LRU 緩存淘汰算法?
思路:維護一個有序單鏈表,越靠近鏈表尾部的結點是越早以前訪問,當有一個新的數據被訪問時,從鏈表頭開始順序遍歷單鏈表。
若是此數據沒有在緩存鏈表中,又能夠分爲兩種狀況:
時間複雜度爲:O(n)
5 種常見的鏈表操做
當某個數據集合只涉及在一端插入和刪除數據,而且知足後進先出、先進後出的特性,咱們就應該首選 棧 這種數據結構
無論是順序棧仍是鏈式棧,入棧、出棧只涉及棧頂個別數據的操做,全部時間複雜度都是 O(1)。棧是一種操做受限的數據結構,只支持入棧和出棧操做。後進先出是它最大的特色。棧既能夠經過數組實現,也能夠經過鏈表實現。
內存中的堆棧和數據結構中的堆棧不是一個概念,內存中的堆棧是真實存在的物理區,數據結構中的堆棧是抽象出來的數據存儲結構:
內存空間在邏輯上分爲三部分:
先進者先出
無論是順序隊列仍是鏈式隊列,主要的兩個操做是入隊和出隊,最大特色是先進先出。
幾種高級的隊列結構:
## 10 遞歸
遞歸須要知足的三個條件:
如何編寫遞歸代碼?
缺點:
常見排序算法:
排序算法 | 時間複雜度 | 是否基於比較 |
---|---|---|
冒泡、插入、選擇 | O(n2) | 是 |
快排、歸併 | O(nlogn) | 是 |
桶、計數、基數 | O(n) | 否 |
如何分析一個 「排序算法」?
冒泡排序只會操做相鄰的兩個數據。每次冒泡操做都會對相鄰的兩個元素進行比較,看是否知足大小關係要求。若是不知足就讓它倆互換。一次冒泡會讓至少一 個元素移動到它應該在的位置,重複n次,就完成了n個數據的排序工做。
示例代碼:
class Solution(): def bubbleSort(self, lis: list, n: int): if n <= 1: return for i in range(len(lis)): flag = False for j in range(len(lis)-i-1): if lis[j] > lis[j+1]: lis[j], lis[j+1] = lis[j+1], lis[j] flag = True if not flag: break arr = [4, 5, 6, 3, 2, 1] print(arr) Solution().bubbleSort(arr, len(arr)) print(arr)
插入算法的核心思想是取未排序區間中的元素,在已排序區間中找到合適的插入位置將其插入,並保證已排序區間數據一直有序。重複這個過程,直到未排序區間中元素爲空,算法結束。
示例代碼:
class Solution(): def insertionSort(self, lis: list, n: int): if n <= 1: return for i in range(1, len(lis)): val = lis[i] j = i-1 while j >= 0: if lis[j] > val: lis[j+1] = lis[j] j -= 1 lis[j+1] = val attr = [4, 5, 6, 3, 2, 1] print(attr) Solution().insertionSort(attr, len(attr)) print(attr)
選擇排序算法的實現思路有點相似插入排序,也分已排序區間和未排序區間。可是選擇排序每次會從未排序區間中找到最小的元素,將其放到已排序區間的末 尾。
示例代碼:
class Solution(): def selectSort(self, lis: list, n: int): if n <= 1: return for i in range(0, len(lis) - 1): index = i for j in range(i+1, len(lis)): if lis[index] > lis[j]: index = j lis[i], lis[index] = lis[index], lis[i] attr = [4, 5, 6, 3, 2, 1] print(attr) Solution().selectSort(attr, len(attr)) print(attr)
是否原地排序 | 是否穩定 | 最好 | 最壞 | 平均 | |
---|---|---|---|---|---|
冒泡 | 是 | 是 | O(n) | O(n2) | O(n2) |
插入 | 是 | 是 | O(n) | O(n2) | O(n2) |
選擇 | 是 | 否 | O(n2) | O(n2) | O(n2) |
核心思想:利用分而治之的思想,遞歸解決問題。若是要排序一個數組,咱們先把數組從中間分紅先後兩部分,而後對先後兩部分分別排序,再將排好序的兩部分合並在一 起,這樣整個數組就都有序了。
示例代碼:
class Solution(): def mergeSort(self, arr): print("Splitting ", arr) if len(arr) > 1: mid = len(arr)//2 lefthalf = arr[:mid] righthalf = arr[mid:] self.mergeSort(lefthalf) self.mergeSort(righthalf) i = 0 j = 0 k = 0 while i < len(lefthalf) and j < len(righthalf): if lefthalf[i] < righthalf[j]: arr[k] = lefthalf[i] i = i+1 else: arr[k] = righthalf[j] j = j+1 k = k+1 while i < len(lefthalf): arr[k] = lefthalf[i] i = i+1 k = k+1 while j < len(righthalf): arr[k] = righthalf[j] j = j+1 k = k+1 print("Merging ", arr) arr = [4, 5, 6, 3, 2, 1] print(arr) Solution().mergeSort(arr) print(arr)
性能分析:
快排核心思想就是分治和分區。若是要排序數組中下標從p到r之間的一組數據,咱們選擇p到r之間的任意一個數據做爲pivot(分區點)。 咱們遍歷p到r之間的數據,將小於pivot的放到左邊,將大於pivot的放到右邊,將pivot放到中間。通過這一步驟以後,數組p到r之間的數據就被分紅了三個部分,前 面p到q-1之間都是小於pivot的,中間是pivot,後面的q+1到r之間是大於pivot的。
示例代碼:
class Solution(): def quickSort(self, arr: list): self.quickHelper(arr, 0, len(arr)-1) def quickHelper(self, arr: list, first: int, last: int): if first < last: splitpoint = self.partition(arr, first, last) self.quickHelper(arr, first, splitpoint-1) self.quickHelper(arr, splitpoint+1, last) def partition(self, arr: list, first: int, last: int): pivot = arr[first] left = first + 1 right = last done = False while not done: while left <= right and arr[left] <= pivot: left = left + 1 while arr[right] >= pivot and right >= left: right = right - 1 if right < left: done = True else: temp = arr[left] arr[left] = arr[right] arr[right] = temp temp = arr[first] arr[first] = arr[right] arr[right] = temp return right arr = [4, 5, 6, 3, 2, 1] print(arr) Solution().quickSort(arr) print(arr)
性能分析:
可是,公式成立的前提是每次分區操做,咱們選擇的pivot都很合適,正好能將大區間對等地一分爲二。但實際上這種狀況是很難實現的
核心思想是將要排序的數據分到幾個有序的桶裏,每一個桶裏的數據再單獨進行排序。桶內排完序之 後,再把每一個桶裏的數據按照順序依次取出,組成的序列就是有序的了。
桶排序比較適合用在外部排序中。所謂的外部排序就是數據存儲在外部磁盤中,數據量比較大,內存有限,沒法將數據所有加載到內存中。
計數排序實際上是桶排序的一種特殊狀況。當要排序的n個數據,所處的範圍並不大的時候,好比最大值是k,咱們就能夠把數據劃分紅k個桶。每一個桶 內的數據值都是相同的,省掉了桶內排序的時間。
示例代碼:
class Solution: def countingSort(self, arr: list, n: int): if n <= 1: return mv = arr[0] for v in arr: if mv < v: mv = v c = [0 for x in range(mv+1)] for i in range(n): c[arr[i]] += 1 for i in range(1, mv+1): c[i] = c[i-1] + c[i] r = [0 for x in range(n)] i = n-1 while i >= 0: index = c[arr[i]] - 1 r[index] = arr[i] c[arr[i]] -= 1 i -= 1 for i in range(n): arr[i] = r[i] arr = [4, 5, 6, 3, 2, 1] print(arr) Solution().countingSort(arr, len(arr)) print(arr)
計數排序只能用在數據範圍不大的場景中,若是數據範圍 k 比要排序的數據 n 大不少,就不適合用計數排序了。並且,計數排序只能給非負整數排序,若是要排序的數據是其餘類型的,要將其在不改變相對大小的狀況下,轉化爲非負整數。
基數排序對要排序的數據是有要求的,須要能夠分割出獨立的「位」來比較,並且位之間有遞進的關係,若是a數據的高位比b數據大,那剩下的低 位就不用比較了。除此以外,每一位的數據範圍不能太大,要能夠用線性排序算法來排序,不然,基數排序的時間複雜度就沒法作到O(n)了。
時間複雜度 | 是否穩定排序 | 是否原地排序 | |
---|---|---|---|
冒泡排序 | O(n2) | 是 | 是 |
插入排序 | O(n2) | 是 | 是 |
選擇排序 | O(n2) | 否 | 是 |
快速排序 | O(nlog2) | 否 | 是 |
歸併排序 | O(nlog2) | 是 | 否 |
計數排序 | O(n+k) k是數據範圍 | 是 | 否 |
桶排序 | O(n) | 是 | 否 |
基數排序 | O(dn) d 是維度 | 是 | 否 |
如何優化快速排序?
二分查找(Binary Search)算法,也叫折半查找算法。時間複雜度爲 O(longn)
示例代碼:
class Solution: def bsearch(self, arr: list, n: int, val: int): return self.bsearchInternally(arr, 0, n-1, val) def bsearchInternally(self, arr: list, low: int, high: int, val: int): if low > high: return -1 mid = low + ((high-low) >> 1) if arr[mid] == val: return mid elif arr[mid] < val: return self.bsearchInternally(arr, mid+1, high, val) else: return self.bsearchInternally(arr, low, mid-1, val) arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 4) print(v)
class Solution: def bsearch(self, arr: list, n: int, val: int): low = 0 high = n - 1 while low <= high: mid = (low+high) // 2 if arr[mid] == val: return mid elif arr[mid] < val: low = mid + 1 else: high = mid - 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 4) print(v)
應用場景的侷限性:
二分查找的變形問題:
示例代碼:
class Solution: def bsearch(self, arr: list, n: int, val: int): low = 0 high = n-1 while low <= high: mid = low + ((high-low) >> 1) if arr[mid] > val: high = mid - 1 elif arr[mid] < val: low = mid + 1 else: if mid == 0 or arr[mid-1] != val: return mid else: high = mid - 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 4) print(v)
示例代碼:
# 待修改 class Solution: def bsearch(self, arr: list, n: int, val: int): low, high = 0, n-1 while low <= high: mid = low + ((high-low) >> 1) if arr[mid] > val: high = mid - 1 elif arr[mid] < val: low = mid + 1 else: if mid == n-1 or arr[mid+1] != val: return mid else: low = mid + 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 3) print(v)
示例代碼:
# 待修改 class Solution: def bsearch(self, arr: list, n: int, val: int): low, high = 0, n-1 while low <= high: mid = low + ((high-low) >> 1) if arr[mid] >= val: if mid == 0 or arr[mid - 1] < val: return mid else: high = mid-1 else: low = mid + 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 3) print(v)
示例代碼:
# 待修改 class Solution: def bsearch(self, arr: list, n: int, val: int): low, high = 0, n-1 while low <= high: mid = low + ((high-low) >> 1) if arr[mid] > val: high = mid - 1 else: if mid == n - 1 or arr[mid + 1] > val: return mid else: low = mid + 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 3) print(v)
Redis 的有序集合就是使用跳錶來實現的。
跳錶使用空間換時間的設計思路,經過後見多級索引來提升查詢訂單效率,實現了基於鏈表的 「二分查找」。調錶是一種動態結構,支持快速的插入、刪除、查找操做,時間複雜度都是 O(longn)
跳錶的空間複雜度是 O(n),不過,跳錶的實現很是靈活,能夠經過改變索引構建策略,有效平衡執行效率和內存消耗。雖然跳錶的代碼實現起來並不簡單,可是做爲一種動態結構,比起紅黑樹來講,實現要簡單不少。因此不少時候,咱們爲了代碼的簡單、易讀,比起紅黑樹,咱們更傾向用跳錶。
Word 文檔中的單詞拼寫檢查功能
散列表是由數組演化而來的,藉助散列函數堆數組進行擴展,利用的是數組支持按照下標隨機訪問元素的特性。
散列衝突的解決方法:
散列表的查詢效率不能籠統地說成是 O(1),它跟散列函數、裝載因子、散列衝突等都有關係。若是散列函數涉及得很差,或者裝載因子太高,均可能致使散列衝突發生的機率升高,查詢效率降低。
如何設計散列函數?
直接尋址法、平方取中法、摺疊法、隨機數法等
裝載因子過大怎麼辦?
裝載因子閾值的設置要權衡時間、空間複雜度。若是內存空間沒關係,對執行效率要求很高,能夠下降負載因子的閥值;相反,若是內存空間緊張,對執行效率要求又不高,能夠增長負載因子的值,甚至能夠大於 1。
如何避免低效地擴容?
經過均攤的方法,將一次性擴容的代價,均攤到屢次插入操做中,就避免了一次性擴容耗時過多的狀況。這種實現方式,任何狀況下,插入一個數據的時間 複雜度都是O(1)。
工業級散列表分析要素:
工業級散列表特徵:
工業級散列表設計思路:
將任意長度的二進制值串映射爲固定長度的二進制值串,這個映射的規則就是哈希算法,而 經過原始數據映射以後獲得的二進制值串就是哈希值。
知足以下幾點要求:
應用場景:
想要存儲一棵二叉樹,咱們有兩種方法,一種是基於指針或者引用的二叉鏈式存儲法,一種是基於數組的順序存儲法。
二叉樹的遍歷:
實際上,二叉樹的前、中、後序遍歷就是一個遞歸的過程。
二叉查找樹
二叉查找樹是二叉樹中最經常使用的一種類型,也叫二叉搜索樹。顧名思義,二叉查找樹是爲了實現快速查找而生的。不過,它不只僅支持快速查找一個數據,還支 持快速插入、刪除一個數據。
二叉查找樹要求,在樹中的任意一個節點,其左子樹中的每一個節點的值,都要小於這個節點的值,而右子樹節點的值都大 於這個節點的值。
知足要求:
紅黑樹是一種平衡二叉查找樹,它是爲了解決普通二叉查找樹在數據更新的過程當中,複雜度退化的問題而產生的,紅黑樹的高度近似 log2n,因此它是近似平衡,插入、刪除、查找操做的時間複雜度都是 O(logn)。
由於紅黑樹是一種性能很是穩定的二叉查找樹,因此,在工程中,但凡是用到動態插入、刪除、查找數據的場景,均可以用到它。不過,它實現起來比較複雜,若是本身寫代碼實現,難度會有些高,這個時候,咱們其實更傾向用跳錶來代替它。
堆的特色:
對於每一個節點值都大於等於子樹中每一個節點值的堆,咱們叫作 「大頂堆」;對於每一個節點的值都小於等於子樹中每一個節點值的堆,咱們叫作 「小頂堆」。
爲何快速排序要比堆排序性能好?
堆的應用:
非線性數據結構
相關概念:
存儲方法:
鄰接矩陣存儲方法的缺點是比較浪費空間,可是優勢是查詢效率高,並且方便矩陣運算。鄰接表存儲方法中每一個頂點都對應一個鏈表,存儲與其相鏈接的其餘頂 點。儘管鄰接表的存儲方式比較節省存儲空間,但鏈表不方便查找,因此查詢效率沒有鄰接矩陣存儲方式高。針對這個問題,鄰接表還有改進升級版,即將鏈表換成更加高效的動態數據結構,好比平衡二叉查找樹、跳錶、散列表等。
搜索方法:
廣度優先搜索和深度優先搜索是圖上的兩種最經常使用、最基本的搜索算法,比起其餘高級的搜索算法,好比A、IDA等,要簡單粗暴,沒有什麼優化,因此,也被 叫做暴力搜索算法。因此,這兩種搜索算法僅適用於狀態空間不大,也就是說圖不大的搜索。 廣度優先搜索,通俗的理解就是,地毯式層層推動,從起始頂點開始,依次往外遍歷。廣度優先搜索須要藉助隊列來實現,遍歷獲得的路徑就是,起始頂點到終 止頂點的最短路徑。深度優先搜索用的是回溯思想,很是適合用遞歸實現。換種說法,深度優先搜索是藉助棧來實現的。在執行效率方面,深度優先和廣度優先搜索的時間複雜度都是O(E),空間複雜度是O(V)。
匹配算法
BF 算法
全稱叫 Brute Force 算法,中文叫做暴力匹配算法,也叫樸素匹配算法。
RK 算法
全稱叫 Rabin-Karp 算法,是 BF 算法的改進版。
BM 算法
全稱叫 Boyer-Moore 算法。是一種很是搞笑的字符串匹配算法。
BM 算法核心思想是,利用模式串自己的特色,在模式串中某個字符與主串不能匹配的時候,將模式串日後多滑動幾位,以此來減小沒必要要的字符比較,提升匹配的效率。BM算法構建的規則有兩類,壞字符規則和好後綴規則。好後綴規則能夠獨立於壞字符規則使用。由於壞字符規則的實現比較耗內存,爲了節省內存,咱們能夠只用好後綴規則來實現 BM 算法。
MKP 算法
KMP算法的核心思想是:咱們假設主串是a,模式串是b。在模式串與主串匹配的過程當中,當遇到不可匹配的字符的時候,咱們但願找到一些規律,能夠將模式串日後多滑動幾位,跳過那些確定不會匹配的狀況。
BM算法有兩個規則,壞字符和好後綴。KMP算法借鑑BM算法的思想,能夠總結成好前綴規則。這裏面最難懂的就是next數組的計算。若是用最笨的方法來計 算,確實不難,可是效率會比較低。因此,我講了一種相似動態規劃的方法,按照下標i從小到大,依次計算next[i],而且next[i]的計算經過前面已經計算出來 的next[0],next[1],……,next[i-1]來推導。 KMP算法的時間複雜度是O(n+m)。
Trie樹,也叫「字典樹」。顧名思義,它是一個樹形結構。它是一種專門處理字符串匹配的數據結構,用來解決在一組字符串集合中快速查找某個字符串的問題。
若是用來構建Trie樹的這一組字符串中,前綴重複的狀況不是不少,那Trie樹這種數 據結構整體上來說是比較費內存的,是一種空間換時間的解決問題思路。
儘管比較耗費內存,可是對內存不敏感或者內存消耗在接受範圍內的狀況下,在Trie樹中作字符串匹配仍是很是高效的,時間複雜度是O(k),k表示要匹配的字符串的長度。 可是,Trie樹的優點並不在於,用它來作動態集合數據的查找,由於,這個工做徹底能夠用更加合適的散列表或者紅黑樹來替代。Trie樹最有優點的是查找前綴匹配的字符 串,好比搜索引擎中的關鍵詞提示功能這個場景,就比較適合用它來解決,也是Trie樹比較經典的應用場景。
AC自動機是基於Trie樹的一種改進算法,它跟Trie樹的關係,就像單模式串中,KMP算法與BF算法的關係同樣。KMP算法中有一個很是關鍵的next數組,類比 到AC自動機中就是失敗指針。並且,AC自動機失敗指針的構建過程,跟KMP算法中計算next數組極其類似。因此,要理解AC自動機,最好先掌握KMP算法,由於AC自動機其實就是KMP算法在多模式串上的改造。
整個AC自動機算法包含兩個部分,第一部分是將多個模式串構建成AC自動機,第二部分是在AC自動機中匹配主串。第一部分又分爲兩個小的步驟,一個是將模 式串構建成Trie樹,另外一個是在Trie樹上構建失敗指針。
貪心算法有不少經典的應用,好比霍夫曼編碼(Huffman Coding)、Prim和Kruskal最小生成樹算法、還 有Dijkstra單源最短路徑算法。
實際上,貪心算法適用的場景比較有限。這種算法思想更多的是指導設計基礎算法。好比最小生成樹算法、單源最短路徑算法,這些算法都用到了貪心算法。
分治算法(divide and conquer)的核心思想其實就是四個字,分而治之 ,也就是將原問題劃分紅n個規模較小,而且結構與原問題類似的子問題,遞歸地解決這些 子問題,而後再合併其結果,就獲得原問題的解。
分治算法是一種處理問題的思想,遞歸是一種編程技巧。實際上,分治算法通常都比較適合用遞歸來實現。分治算法的遞歸實現中,每一層遞歸都會涉及這樣三個操做:
分治算法能解決的問題,通常須要知足下面這幾個條件:
回溯算法的思想很是簡單,大部分狀況下,都是用來解決廣義的搜索問題,也就是,從一組可能的解中,選擇出一個知足要求的解。回溯算法很是適合用遞歸來 實現,在實現的過程當中,剪枝操做是提升回溯效率的一種技巧。利用剪枝,咱們並不須要窮舉搜索全部的狀況,從而提升搜索效率。