本文只是自己的筆記,並不具備任何指導意義。
代碼的初衷是便於理解,網上大神優化過的代碼很多,也不建議在項目中copy本文代碼。
目錄
-
時間複雜度
- 常數時間的操作
- 時間複雜度的計算
- 常數操作表達式類型的時間複雜度
- 時間複雜度相同的比對
-
冒泡排序
- 改進版的冒泡排序
-
選擇排序
- 二元選擇排序
- 插入排序
- 時間複雜度的最差情況,最好情況,平均情況
- 對數器
時間複雜度
衡量代碼的好壞,包括兩個非常重要的指標:運行時間與佔用空間。
而時間複雜度正代表前者,後者由空間複雜度(即算法在運行過程中臨時佔用存儲空間大小的量度)表示。
-
常數時間的操作
一個操作如果和數據量沒有關係,每次都是固定時間內完成的操作,叫做常數操作。
比如數組下標的尋址,一對下標交換。
-
常數操作數量
單次常數時間的操作,寫作做O(1)。讀作big O 1 。
-
時間複雜度的計算
法中基本操作重複執行的次數是問題規模n的某個函數,用T(n)表示。
若有一個函數,f(N)。當N趨近於無窮大時,使得T(n)/f(n)趨近於一個不爲0的常數。
則稱f(n)是T(n)的同數量級函數。記作T(n)=O(f(n)),稱O(f(n)) 爲算法的漸進時間複雜度,簡稱時間複雜度。
-
線性的時間複雜度
比如一個算法共需要執行N次循環,每次循環內部都是常數操作O(1)
for i in 1..<N+1 {
//常數操作
let firstItem = arr[i-1]
let secondItem = arr[i]
if firstItem > secondItem {
arr.swapAt(i-1, i)
}
}
的T(N)=F(N)=N,時間複雜度爲O(F(N))=O(N)。
-
常數操作表達式類型的時間複雜度
對於T(N)爲達式類型的時間複雜度,F(N)簡而言之就是要簡化成當N趨近無窮大時,表達式中對其影響最大的一項的表達式。
具體來說。在常數操作數量的表達式中, 只要高階項,不要低階項,也不要高階項的係數,剩下的部分 如果記爲f(N),那麼時間複雜度爲O(f(N))。
借用百度百科上的例子:
for(i=1; i<=n; ++i)
{
c[i];//該步驟屬於基本操作執行次數:n
for(j=1; j<=n; ++j)
{
c[i][j] = 0;//該步驟屬於基本操作執行次數:n的平方次
for(k=1; k<=n; ++k)
c[i][j] += a[i][k] * b[k][j];//該步驟屬於基本操作執行次數:n的三次方次
}
}
T(N) = A×N³+B×N²+C×N。當N趨近於無窮大時,三次方的影響遠大於二次方以及一次方。當然也大於常數項A的影響。
所以表達式f(N)=N³。
時間複雜度爲O(N)=O(f(N))=O(N³)
領附一張圖方便理解高階項在基數變大時的變化:
-
時間複雜度相同的比對
評價一個算法流程的好壞,先看時間複雜度的指標,然後再分
析不同數據樣本下的實際運行時間,也就是常數項時間。
冒泡排序
在冒泡排序過程中會將數組分成兩部分,前半部分是無序的數列,後半部分爲有序的數列。無序數列中不斷的將其中最大的值往有序序列中冒泡,泡冒完後,我們的序列就創建好了。
具體操作上,如果相鄰的兩個數字前者較大,則將二者交換,到達無序數組邊界則停止。
func bubbleSort(arr: inout [Int]) {
if arr.count < 2 {
return
}
for i in 0..<arr.count {
for j in 0..<arr.count - (i+1) {
if arr[j] > arr[j+1] {
let temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
}
時間複雜度O(N²),額外空間複雜度O(1)。
時間複雜度的來源f(N) = N +( N -1) + (N-2) + ...+ 2 + 1 爲一個等差數列。前N項和的通用公式爲:N*(N-1)/2化簡後f(N)=N²。
-
改進版的冒泡排序
經典的冒泡排序,無論數組是否已經有序。都會一次次的遍歷,從這一點上我們可以進行改進
func bubbleSort2(arr: inout [Int]) {
if arr.count < 2 {
return
}
var swapped = false //記錄是否有交換動作的變量
for i in 0..<arr.count {
swapped = false
for j in 0..<arr.count - (i+1) {
if arr[j] > arr[j+1] {
let temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
swapped = true //有交換動作則記錄
}
}
if swapped == false {
break //沒有交換動作,說明已經有序
}
}
}
極端情況下(對於一個已經有序的數組),算法完成第一次外層循環後就會返回。
實際上只發生了 N - 1次比較,所以最好的情況下,該算法複雜度是O(N)。
選擇排序
基本思想與冒泡排序相同。前半部分爲序的數列,只不過後半部分是無序的數列。無序數列中不斷的將其中最大的值往有序序列中冒泡,泡冒完後,我們的序列就創建好了。
具體操作上,每次遍歷記錄無序序列中最小值的位置,並在結束時與無序序列的首位置交換,使其變成有序序列的最後一位。
func selectionSort(arr : inout [Int]) {
if arr.count<2 {
return
}
var minIndex :Int
for i in 0..<arr.count {
minIndex = i
for j in i+1..<arr.count { //循環從i+1開始,也就是無序數組的第二位開始
minIndex = arr[j]<arr[minIndex] ? j:minIndex //比對當前位置與記錄位置的值,記錄其中最小的。
}
arr.swapAt(i, minIndex) //將無序數組的第一位與最小一位交換
}
}
-
二元選擇排序
選擇排序本身沒有什麼可改進的,但是我們可以左右開弓。
將序列分成三個部分,前段有序部分,中段無序部分,後端有序部分。
每次循環,將最大值與最小值分別置入前後兩個有序序列。
func selectionSort2(arr : inout [Int]) {
if arr.count<2 {
return
}
var minIndex :Int
var maxIndex :Int
for i in 0..<arr.count {
minIndex = i
maxIndex = i
if i+1 >= arr.count - i {
return // 由於這一步的存在,實際上會在i=arr.count/2處結束循環
}
for j in i+1..<arr.count - i { //循環從i+1開始,也就是無序數組的第二位開始。並且在後端有序序列的前一位停止
minIndex = arr[j]<arr[minIndex] ? j:minIndex //比對當前位置與記錄位置的值,記錄其中最小的。
maxIndex = arr[j]>arr[maxIndex] ? j:maxIndex //比對當前位置與記錄位置的值,記錄其中最大的。
}
if maxIndex == i && minIndex == arr.count - (i+1) {
//如果最大值與最小值恰好處於邊界,直接交換會導致亂序。需要手動賦值
let maxValue = arr[maxIndex];
let minValue = arr[minIndex];
let maxToValue = arr[arr.count - (i+1)]
let minToValue = arr[i]
arr[maxIndex] = maxToValue
arr[arr.count - (i+1)] = maxValue
arr[minIndex] = minToValue
arr[i] = minValue
}else if maxIndex == i{
//如果最大值位置處於最小值將要交換的位置,先交換最大值
arr.swapAt(arr.count - (i+1) , maxIndex) //將無序數組的最後一位與最大一位交換
arr.swapAt(i, minIndex) //將無序數組的第一位與最小一位交換
}else {
arr.swapAt(i, minIndex) //將無序數組的第一位與最小一位交換
arr.swapAt(arr.count - (i+1) , maxIndex) //將無序數組的最後一位與最大一位交換
}
}
}
這樣雖然複雜度還是O(N²),但實際上的表達式係數比經典選擇排序不止縮小了1/2。
插入排序
基本思想也是前半部分爲序的數列,後半部分是無序的數列。無序數列不斷將其首位元素推給有序數列,有序數列將其插入適當的位置。
具體操作上,會從有序數列的尾部依次向前比較,若前位大於後位則進行交換。
func insertionSort(arr : inout [Int]) {
if arr.count<2 {
return
}
for i in 1..<arr.count { //無序數組從i=1到末尾
for j in (0...i-1).reversed() { //從 i-1 位置到 0位置的有序數組內進行循環
if arr[j+1] > arr[j] { //j+1當第一次執行的時候,正位於無序數組的首位置
break //如果後位置大於前位置,說明已經有序。退出當前循環
}
arr.swapAt(j, j+1)//否則交換
}
}
}
改進的話,或許可以試試用二分法確定具體位置然後進行整體後移並插入。
時間複雜度的最差情況,最好情況,平均情況
對於插入排序這種有明確終止條件的排序,實際的時間複雜度與數據的實際狀況有關。
最好情況是最開始便有序,我們只需要執行一次大循環,複雜度爲O(N)。
最差情況是將整個數組倒序排列一次,複雜度爲O(N²)。
平均情況是指在大數狀況下的平均期望複雜度。
在數據的實際狀況對算法流程存在影響時,使用最差情況作爲時間複雜度。
不過,我們可以利用主動打亂數據狀況影響的方式。將複雜度易數學期望的方式表達(參考隨機快排)。
對數器
對數器是用來測試代碼正確性的,我們在找不到合適的oj系統測試自己的代碼時,可以自己寫一個對數器對代碼進行測試。
-
設計對數器的一般步驟爲:
1.有一個你要測的方法a; 自己寫的方法
2.實現一個絕對正確即使複雜度不好的方法b; 系統自帶方法即可
3.實現一個隨機樣本產生器;
4.實現比對的方法; 比對兩個結果最後是否一致
5.把方法a和方法b比對很多次來驗證方法a是否正確
6.如果有一個樣本使得比對出錯,打印樣本分析是哪個方法出錯
7.當樣本數量很多時比對測試依然正確,可以確定方法a已經正確
-
實現對數器
其中1,2,4已經說了。6,7也沒啥好說的。
3.實現一個隨機樣本產生器
/// 隨機數組生成器
///
/// - Parameters:
/// - size: 最大長度
/// - value: 最大值
/// - Returns: 隨機數組
func generateRandomArray(size : Int ,value : Int) -> [Int] {
var arr :[Int]
arr = Array.init()
for i in 0..<Int(arc4random() % 10) * size / 10 {
let item = Int(arc4random() % 10)*value/10
arr.append(item)
}
print(arr)
return arr
}
-
把方法a和方法b比對很多次來驗證方法a是否正確
var checkOK = true
for i in 0..<10000 {
var arr1 = generateRandomArray(size: 5, value: 20)
var arr2 = arr1 //數組在swift裏屬於值類型,賦值動作會自動copy
let originalArr = arr1
arr1.sort()
heapSort(arr: &arr2)
if arr1 != arr2 {
checkOK = false
print(originalArr)
print(arr2)
break
}
}
print(checkOK ? "比對成功":"比對失敗")
//打印
[0, 6, 2, 12, 18]
[0, 6, 12, 2, 18]
比對失敗
錯誤的原始數據已經打印出來了,你就可以隨意重現這個數據進行調試了。
var arr = [0, 6, 12, 2, 18];
print(arr)
heapSort(arr: &arr)
print(arr)