算法-排序
算法
算法(Algorithm):一個計算過程,解決問題的方法。
時間復雜度
時間復雜度是用來估計算法運行時間的單位。一般來說時間復雜度低的算法更快。常見的時間復雜度如下(按效率從高到低):
- O(1)
- O(logn)
- O(n)
- O(nlogn)
- O(n*n),應該是n的平方,打不出來
不常見的時間復雜度(特別復雜的):
- O(n!),N的階乘
- O(2^n),2的N次方
- O(n^n),N的N次方
快速判斷時間復雜度:
- 有循環減半的情況:O(logn)
- 有m次循環:O(n^m),n的m次方
空間復雜度
空間復雜度是用來評估算法內存占用大小的一個式子。
不展開了,有個概念叫“空間換時間”。
排序
排序講了下面九種方法:
- 下面3個算法算比較Low的,但是好理解
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 下面2個是比較難的
- 堆排序
- 歸并排序
- 最后的三個地位比較尷尬,沒什么人用
- 基數排序
- 希爾排序
- 桶排序
排序算法關鍵點:有序區和無序區。
一開始都是無序區,然后慢慢的產生有序區,有序區逐步變大,無序區逐步變小。最后全部都是有序區,排序完成。
冒泡排序
冒泡算是最基礎和常用的了,下面的兩個算法了解一下就好了,一般用還是用冒泡的。
代碼如下:
從頭開始,每次比較相鄰的兩個數,如果前面的大,就交換位置。然后往后移一位再進行比較。一直比較到最后,這是一趟。一趟完成后,最大的數就移到最后了,有序區就增加了一位。這一趟進行了n-1次比較。
下一趟的話,由于無序區少了一位,所以比上一趟少比較一次。如果已經比較了i趟,這趟只需要比較n-i-1次。所以每一趟比較的次數是n-i-1次,就是n-i-1次循環,這個是內層的循環。
總共需要比較n-1躺,這個是外層的循環,循環n-1次。
時間復雜度:O(n^2)
優化:如果某一趟里沒有進入if進行交換,那么實際排序就完成了,可以直接結束了。下面是優化了的算法:
選擇排序
一趟遍歷最小的數,放到第一個位置;再一趟,遍歷余下的數,找到最小的放到下一個位置……
代碼如下:
時間復雜度:O(n^2)
插入排序
插入排序,先用撲克摸牌來描述一下。首先摸到一張牌,直接加入手牌,你手上就是有序區。然后摸到下一張,遍歷你的手牌,把新摸到的牌插到合適的位置。如此循環,直到把牌摸完,你手上的牌就是排好序的了。不過這和下面代碼的邏輯有很大差別。
最初,認為第一個位置是有序區,后面的是無序區。然后有序區向無序區擴充一位,把新加入到有序區的元素插到對應的位置,直到無序區變空。
插入的過程是,其實也是一次冒泡,依次一個一個比較,如果大小反了,就交換位置。
這里不知道寫成這樣還是不是插入排序了。不過故意寫的和冒泡很像。
冒泡算法是,每一趟在無序區做一次冒泡,每一趟冒泡的區域減少1個元素。而這里,每一趟,在有序區的最后加入一個元素,做一次冒泡,每一趟冒泡的區域增加一個元素。
冒泡算法,優化后,外層的循環有可能提前結束。這里是內層的循環有可能提前結束。
所以時間復雜度,包括各種理想情況下的復雜度,這兩種算法感覺是一樣的。
時間復雜度:O(n^2)
如果不是一次就能獲得整個列表,而是像開頭描述的那樣,一張牌一張牌的摸上來,這種場景下,應該是插入算法更合適。
快速排序
這個是好寫的排序算法里最快的,快的排序算法里最好寫的。
思路:去一個元素P(就取第一個元素),使P歸位(某個方法);歸位后的效果是列表被P粉塵了2個部分,左邊都比P小,右邊都比P大;然后遞歸完成排序:
上面的partition就是歸位的方法,接下來是歸位的思路:mid、left、right都是下標。先把P元素取出來,現在left的位置空出來了,right的位置向左移動,同時和P比較,找到的第一個比P小的元素,移動到left的位置。然后right左邊停在新的位置,left的位置向右移動,同時和P比較,找到的第一個比P大的元素,移動到right的位置。上面的步驟交替進行直到left和tight重疊,把P元素放到這個位置完成歸位。
這里想到了一句歌詞:跟著我,左手右手一個慢動作,右手左手慢動作重播
下面是加上歸位方法的完整代碼:
遞歸函數不要加裝飾器:
這里再封裝了一層,不只是因為要傳3個參數。入口函數值需要傳列表就可以了,另外2個參數是列表下標的最小值和最大值。還有一個好處是,可以加裝飾器,但是裝飾器不能直接給遞歸函數加,否則每次遞歸都會帶上裝飾器。如果要計算運行時間的話,裝飾器不能加載遞歸函數上。解決辦法就是這樣,外面再封裝一層不會調用自己的然后可以加裝飾器。
關于這里效率的提高,可以這么理解。在一個時間復雜度里,做了更多的事情。之前的3種方法,一趟只把一個數放到了該去的位置,其他數不管。而這里,在一個時間復雜度里,不光把一個數放到了它該在的位置,還把兩邊的數字大小分開了。
內建的排序
python原本就提供列表的排序 li.sort() ,大多數語言包括python,他們的排序算法也都是快速排序。不過python原生的方法比我們自己寫的方法更快,這個主要是因為內建方法的底層是C語言實現的。結論就是,學這個也沒用,主要就是掌握算法。要排序還是調用.sort()就好了。
時間復雜度:O(nlogn)
不嚴謹的計算,每次都切一半,然后循環N次,就是上面的復雜度。但是其實并不一定每次正好切一半的。極端情況下,甚至每次都是拿到了最大或最小的數,并沒有把其他元素分在兩邊,而是都在一邊,另外一邊是沒有的。這樣時間復雜度還是O(N^2)。
遞歸深度
由于這是用遞歸實現的,python有最大遞歸深度。如果深度不夠,那么要手動設置一下 sys.setrecursionlimit(999) 。
堆排序
理解堆排序之前,首先得理解二叉樹
二叉樹
先要理解一下二叉樹,二叉樹是度不超過2的樹。
滿二叉樹,像下面這樣全填滿就是了。
完全二叉樹,看圖更好理解。
完全二叉樹從根結點到倒數第二層滿足完美二叉樹,最后一層可以不完全填充,其葉子結點都靠左對齊。
依次從上往下,從左往右排,排出來的就是完全二叉樹。
看下面的這個完全二叉樹,每個元素里表了數字。父節點的數字是i,那么它左孩子節點就是2i+1,它的右孩子節點就是2i+2。
可以把上面的每個圈圈看做是一個元素,里面的數字就是這個元素在數組里的下標,現在把內容填進去就是這樣的:
小結
現在可以把數組存放到一個完全二叉樹里。通過規律可以從父親的下標得到孩子的下標,或從孩子的下標找到父親的下標。
堆排序
堆是一顆特殊的完全二叉樹,有下面2種:
- 大根堆:任意節點都比其孩子節點大
- 小根堆:任意節點都比其孩子節點小
堆排序過程:
歸并排序
假設列表分兩段有序,如何將其合并為一個有序列表?
上面的操作稱作一次歸并,一次歸并的代碼:
def merge(li, low, mid, high):"""一次歸并low是這次歸并第一個元素的下標mid是第一個列表的最后一個元素,判斷還有數high是第二個列表的最后一個元素,判斷還有數"""i = low # 第一個列表的第一個元素的下標j = mid+1 # 第二個列表的第一個元素的下標li_tmp = []while i <= mid and j <= high: # 兩個列表必須都有數字if li[i] < li[j]:li_tmp.append(li[i])i += 1else:li_tmp.append(li[j])j += 1while i <= mid:li_tmp.append(li[i])i += 1while j <= high:li_tmp.append(li[j])j += 1li[low:high+1] = li_tmp # 最后把臨時的列表寫回去運用歸并排序
下面是完整的歸并代碼。分解用了遞歸,代碼簡單了,但是理解起來難一點:
import randomdef merge_sort(li):def merge(li, low, mid, high):"""一次歸并low是這次歸并第一個元素的下標mid是第一個列表的最后一個元素,判斷還有數high是第二個列表的最后一個元素,判斷還有數"""i = low # 第一個列表的第一個元素的下標j = mid+1 # 第二個列表的第一個元素的下標li_tmp = []while i <= mid and j <= high: # 兩個列表必須都有數字if li[i] < li[j]:li_tmp.append(li[i])i += 1else:li_tmp.append(li[j])j += 1while i <= mid:li_tmp.append(li[i])i += 1while j <= high:li_tmp.append(li[j])j += 1li[low:high+1] = li_tmp # 最后把臨時的列表寫回去def _merge_sort(li, low, high):if low < high:mid = (low + high) // 2# 下面是遞歸,每次分一半,直到分完_merge_sort(li, low, mid) # 列表前一半_merge_sort(li, mid+1, high) # 列表后一半# 上面的遞歸,如果列表只有一個元素了,low 和 high就相等,就不滿足if條件,遞歸結束merge(li, low, mid, high)low, high = 0, len(li)-1_merge_sort(li, low, high)return liif __name__ == '__main__':li = random.sample(range(10), 10)print(merge_sort(li))自己寫了一個
上面的例子中的分解是通過遞歸從大到小每次切一半,完成的分解。
我的邏輯第一次0和1做歸并,2和3做歸并。然后一趟跑完后,2個2個做歸并。依次4+4、8+8,直到全部做完:
和遞歸比較一下,明顯遞歸簡單,但是有點難理解。遞歸的邏輯是從大到小切,一層一層套,直到切到最小,然后再逐層合并。我是從小到大合上去。
小結-快速排序、堆排序、歸并排序
時間復雜度:O(nlogn)
3種排序的時間復雜度都一樣,一般情況下的運行時間是:
快速排序 < 歸并排序 < 堆排序
3種排序算法的缺點:
- 快速排序:極端情況下排序效率低
- 歸并排序:需要額外的內存開銷
- 堆排序:速度是3種算法里相對較慢的
希爾排序
希爾排序是一種分組插入排序算法
希爾排序每趟并不是元素有序,而是整體數據越來越接近有序。最后一趟才使得所有的數據有序。代碼如下:
def _insert_sort(li):for i in range(1, len(li)):tmp = li[i]j = i - 1while j >= 0 and li[j] > tmp:li[j+1] = li[j]j -= 1li[j+1] = tmpdef shell_sort(li):gap = len(li) // 2while gap >= 1:for i in range(gap, len(li)):tmp = li[i]j = i - gapwhile j >= 0 and li[j] > tmp:li[j+gap] = li[j]j -= gapli[j+gap] = tmpgap //= 2return li上面貼了一下插入排序的算法做比較。希爾排序只是在插入排序的外面再加個循環,進行分組。做插入排序的時候,間隔不是1而是分組的大小gap。
時間復雜度:O((1+τ)n)
τ就是圓周率的大小,大概也就O(1.3n),比O(nlogn)大。所以并不快,就沒什么用了。
排序的穩定性
穩定性是一個概念,如果大小相同的兩個元素在排序后其相對位置不發生變化,那么這個方法是穩定的。如果可能發生變化,那么這個方法稱作是不穩定的。
如果要按多個維度進行排序,比如排序第一關鍵字是姓名,第二關鍵字是年齡。那么先做一個年齡的排序(穩定不穩定無所謂),然后再在原來的基礎上做一個姓名的排序(必須穩定),就能得到正確的結果。
快速排序,是不穩定的排序。這個最常用,但是不穩定。
冒泡排序、歸并排序,是穩定的排序
其他
其他和排序有有關的內容
計數排序
比如有100萬個數要進行排序,但是每個數都是0到100之間的整數。這種情況數據很多,但是每條數據的值的范圍是有限的,用下面的算法可以很快的做出來:
import randomdef count_sort(li, min_num, max_num):# 先統計每個數出現的次數,計數count = [0 for i in range(min_num, max_num+1)]for num in li:count[num] += 1# 遍歷上面列表,從小到大,每個數出現過幾次就輸出幾次i = 0for num, m in enumerate(count):for j in range(m):li[i] = numi += 1return lidef create_li(min_num, max_num):"""生成計數排序用的列表"""li = []for i in range(100000):li.append(random.randint(min_num, max_num))return liif __name__ == '__main__':li = create_li(0, 100)print(li)print(count_sort(li, 0, 100))做2次O(n)的循環就出來了,時間復雜度是O(n)。但是有限制,就是必須是小范圍的集合,并且依靠一個輔助數組,空間復雜度比較大。
內置模塊-heapq
heapq模塊,提供了基于堆的優先排序算法。
heappush(heap, item) :往heap堆的最后加一個元素,然后是列表重新變成一個堆。(據我測試,應該是從最后一個父元素開始往上做調整,重新變成一個堆)
heappop(heap) :從堆里彈出一個元素(就是把堆頂的元素提出來,然后再做一次調成,變成堆)
利用heapq模塊實現堆排序:
下面的兩個方法,直接從ireable里取出n個最大或最小的元素。算法是用堆實現的:
nlargest(n, iterable)
nlargest(n, iterable)
轉載于:https://blog.51cto.com/steed/2152725
總結
- 上一篇: 魔兽世界怀旧服荒芜之地怎么去
- 下一篇: 个人手机银行app(个人自我总结篇1)