算法 排序NB二人組 堆排序 歸並排序
堆排序
堆排序前傳 - 樹與二叉樹
樹是一種很常見的非線性的數據結構,稱為樹形結構,簡稱樹。所謂數據結構就是一組數據的集合連同它們的儲存關系和對它們的操作方法。樹形結構就像自然界的一顆樹的構造一樣,有一個根和若幹個樹枝和樹葉。根或主幹是第一層的,從主幹長出的分枝是第二層的,一層一層直到最後,末端的沒有分支的結點叫做葉子,所以樹形結構是一個層次結構。在《數據結構》中,則用人類的血統關系來命名,一個結點的分枝叫做該結點的“孩子”,而它的上層結點叫做該結點的父親。根結點沒有父親,葉子結點沒有孩子。在《數據結構》中,用遞歸的方法給出了樹的嚴格數學定義。
樹的標準定義:
樹(tree)是包含n(n>0)個節點的有窮集合,其中:
(1)每個元素稱為節點(node);
(2)有一個特定的節點被稱為根節點或樹根(root)。
(3)除根節點之外的其余數據元素被分為m(m≥0)個互不相交的結合T1,T2,……Tm-1,其中每一個集合Ti(1<=i<=m)本身也是一棵樹,被稱作原樹的子樹(subtree)。
樹具有以下特點:
(1)每個節點有零個或多個子節點。
(2)每個子節點只有一個父節點。
(3)沒有父節點的節點稱為根節點。
關於樹的一些術語
節點的度:一個節點含有的子樹的個數稱為該節點的度;
葉節點或終端節點:度為零的節點稱為葉節點;
非終端節點或分支節點:度不為零的節點;
雙親節點或父節點:若一個結點含有子節點,則這個節點稱為其子節點的父節點;
孩子節點或子節點:一個節點含有的子樹的根節點稱為該節點的子節點;
兄弟節點:具有相同父節點的節點互稱為兄弟節點;
樹的高度或深度:定義一棵樹的根結點層次為1,其他節點的層次是其父結點層次加1。一棵樹中所有結點的層次的最大值稱為這棵樹的深度。
節點的層次:從根開始定義起,根為第1層,根的子結點為第2層,以此類推;
樹的度:一棵樹中,最大的節點的度稱為樹的度;
節點的祖先:從根到該節點所經分支上的所有節點;
子孫:以某節點為根的子樹中任一節點都稱為該節點的子孫。
森林:由m(m>=0)棵互不相交的樹的集合稱為森林;
總結如下:
1、樹是一種數據結構;
2、樹是一種可以遞歸定義的數據結構;
3、樹是由n個節點組成的集合:
如果n=0空集,那這是一棵空樹;
如果n>0,那存在1個節點作為樹的根節點,其他節點可以分為m個集合,每個集合本身又是一棵樹。
4、樹是由葉子構成的單個結點的樹。除根外,每一個結點都有唯一的路徑連到根上(否則有環)。這條路徑由根開始,而末端就在該結點上。這條路徑的長度叫做該結點的深度。所有結點深度的最大者叫做樹的高度或樹的深度。路徑上一個結點的前驅就是它的父結點,而子結點則就是這個結點的後繼。因為前驅是唯一的,也就是非根結點的入度都是1。所以我們只需要關心出度。結點的出度就是節點的子節點數,稱為該結點的度。
特殊且常用的樹——二叉樹
二叉樹是由n(n≥0)個結點組成的有限集合、每個結點最多有兩個子樹的有序樹。它或者是空集,或者是由一個根和稱為左、右子樹的兩個不相交的二叉樹組成。
特點:
(1)二叉樹是有序樹,即使只有一個子樹,也必須區分左、右子樹;
(2)二叉樹的每個結點的度不能大於2,只能取0、1、2三者之一;
(3)二叉樹中所有結點的形態有5種:空結點、無左右子樹的結點、只有左子樹的結點、只有右子樹的結點和具有左右子樹的結點。
總結:二叉樹是每個結點最多有兩個孩子,且其子樹有左右之分的有序樹;二叉樹,度不超過2的樹(節點最多有兩個叉)
二叉樹的遞歸定義:
二叉樹是一棵有序樹,它的任一結點至多只有兩個孩子,分別叫做左孩子和右孩子。根的左孩子L和右孩子R也是二叉樹,稱為根的左子樹和右子樹。
兩種特殊二叉樹
⑴滿二叉樹:
如果一棵深度為K的二叉樹,共有2K-1個結點,即任意第I層有2I-1的結點,稱為滿二叉樹。
⑵完全二叉樹:
如果一棵二叉樹最多只有最下層但不是葉結點的度數可以小於2,並且最下層的結點如果只有一個孩子,它必須是左孩子,則稱此二叉樹為完全二叉樹)
換句話說,滿二叉樹就是兒女雙全的,每個生育了的結點都有兩個孩子。完全二叉樹就是雖然不滿,但生育了就先生男孩(左),沒有只養女兒的。
二叉樹的存儲方式
完全二叉樹可以用列表【順序存儲方式】來存儲,通過規律可以從父親找到孩子或從孩子找到父親。父親節點以 i 表示
父節點與左孩子節點 下標關系:i = 2i+1
父節點與右孩子節點 下標關系:i = 2i+2
堆排序 Heap sort
這裏的堆(二叉堆),指得不是堆棧的那個堆,而是一種數據結構。堆可以視為一棵完全的二叉樹,完全二叉樹的一個“優秀”的性質是,除了最底層之外,每一層都是滿的。這使得堆可以利用數組來表示,每一個結點對應數組中的一個元素。
二叉堆一般分為兩種:大根堆(大頂堆)和小根堆(小頂堆)。
大根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點大;
特點:最大元素出現在根節點上,處於最大堆的根節點的元素一定是這個堆中的最大值。
小根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點小;
特點:最小元素出現在根節點上
註意:當根節點的左右子樹都是堆時,但自身不是堆,可以通過一次向下的調整來將其變換成一個堆。
堆排序(Heap Sort)就是利用大根堆或小根堆的性質進行排序的方法。堆排序的總體時間復雜度為O(nlogn)。以大根堆為例:我們的堆排序算法就是抓住了堆的這一特點,每次都取堆頂的元素,將其放在序列最後面,然後將剩余的元素重新調整為最大堆,依次類推,最終得到排序的序列。
核心思想:
將待排序的序列構造成一個大頂堆;此時,整個序列的最大值就是堆的根節點。將它與堆數組的末尾元素交換,然後將剩余的n-1個序列重新構造成一個大頂堆。剩余部分調整為大頂堆後,再次將堆頂的最大數取出,再將剩余部分調整為大頂堆。反復執行前面的操作,這個過程持續到剩余數只有一個時結束,最後獲得一個有序序列。
堆排序過程:
1、建立堆,挨個出數
2、得到堆頂元素,為最大元素
3、去掉堆頂,將堆最後一個元素放到堆頂,此時可通過一次調整重新使堆有序。
4、堆頂元素為第二大元素。
5、重復步驟3,直到堆變空。
構造堆:
對一個無序的數列,每次都從最後一個子樹位置開始,往上依次比較,同層排列結束,再去判斷上一層!
堆挨個出數:
找最後的一個數作為棋子,然後取堆頂的值,放在最後;依次執行取出的數放在上一次取出的數前。
算法實現:
def cal_time(func): def wrapper(*args, **kwargs): t1 = time.time() x = func(*args, **kwargs) t2 = time.time() print("%s running time %s secs." % (func.__name__, t2 - t1)) return x return wrapper def sift(data, low, high): """ 調整函數 data: 列表 low:待調整的子樹的根位置 high:待調整的子樹的最後一個節點的位置 """ i = low #子樹的根 j = 2 * i + 1 #根的左孩子 tmp = data[i] #根的值 # i指向空位置 while j<=high: #j一定要在範圍之內,此時領導已經擼到底了 if j != high and data[j] < data[j+1]: j += 1 #j指向數值大的孩子 if tmp < data[j]: #如果小領導比擼下來的大領導能力值大 data[i] = data[j] #把大值放在高位 i = j # 把當前的j賦給i,指向新的空位 根 j = 2*i+1 #生成新的j,執向新的左孩子 #帶著新的i,j重新循環 else: break #擼下來的領導比候選的領導能力值大 #循環結束,tmp是最小的值 寫在大根堆最後 data[i] = tmp #查找已經到底了,把數寫到最後的位置上。 @cal_time def heap_sort(data): """ 堆排序 """ n = len(data) # 建堆 從最後一個非葉子節點的子樹【最後一個葉子節點與其父節點比較】 開始構建 # n//2-1 代表最後一個非葉子節點的索引位置,從這個節點開始,一直往上比較 for i in range(n//2-1, -1, -1): sift(data, i, n - 1) #為了方便,我們把最後子樹的high設成堆的high。及列表最後一個元素的索引 # 挨個出數 出數的過程中,high值變化而low值不變 for high in range(n - 1, -1, -1): data[0], data[high] = data[high], data[0] #把堆頂和堆尾位置交換(省空間,僅在當前列表操作) sift(data, 0, high - 1) #因為每次都會出一個數,所以真正的high位置是high-1 return data li = list(range(100000)) rd.shuffle(li) heap_sort(li)
堆排序的運行時間主要消耗在初始構建堆和重建堆的反復篩選上。
其初始構建堆時間復雜度為O(n)。
正式排序時,重建堆的時間復雜度為O(nlogn)。
所以堆排序的總體時間復雜度為O(nlogn)。
堆排序對原始記錄的排序狀態不敏感,因此它無論最好、最壞和平均時間復雜度都是O(nlogn)。在性能上要好於冒泡、簡單選擇和直接插入算法。空間復雜度上,只需要一個用於交換的暫存單元。但是由於記錄的比較和交換是跳躍式的,因此,堆排序也是一種不穩定的排序方法。此外,由於初始構建堆的比較次數較多,堆排序不適合序列個數較少的排序工作。
nlargest 現在有n個數(n>10000),設計算法,按大小順序得到前10小的數。 應用場景:榜單TOP 10 解決思路: 取列表前10個元素建立一個小根堆。堆頂就是目前第10大的數。 依次向後遍歷原列表,對於列表中的元素,如果小於堆頂,則忽略該元素;如果大於堆頂,則將堆頂更換為該元素,並且對堆進行一次調整; 遍歷列表所有元素後,倒序彈出堆頂。 時間復雜度:O(nlogm) def topn(li, n): heap = li[0:n] # 建堆 for i in range(n // 2 - 1, -1, -1): sift(heap, i, n - 1) # 遍歷 for i in range(n, len(li)): if li[i] > heap[0]: heap[0] = li[i] sift(heap, 0, n - 1) # 出數 for i in range(n - 1, -1, -1): heap[0], heap[i] = heap[i], heap[0] sift(heap, 0, i - 1)解決前幾大的數問題!
Python內置模塊——heapq 利用heapq模塊實現堆排序 #每次把一個數加入堆中,向上調整(建立的是小根堆) def heapsort(li): h = [] for value in li: heappush(h, value) return [heappop(h) for i in range(len(h))] 利用heapq模塊實現取top-k heapq.nlargest(10, li) #取前10大的數
歸並排序 Merge sort
原理:
把原始數組分成若幹子列表,對每一個子列表進行排序,繼續把子列表與子列表合並,合並後仍然有序,直到全部合並完,形成有序的列表。
分解:將列表越分越小,直至分成一個元素。
一個元素是有序的。
合並:將兩個有序列表歸並,列表越來越大。
算法實現:
#!/usr/bin/env python # _*_ coding:utf-8 _*_ # 一次歸並排序代碼 def merge(li, low, mid, high): """ 歸並排序,取之間值,對左右兩個臨時的列表進行排序,排序完成之後再把兩個有序列表歸並 """ i = low j = mid + 1 ltmp = [] # low ~ high 這一小塊的列表 while i <= mid and j <= high: # 中間數左右還有數 if li[i] <= li[j]: ltmp.append(li[i]) i += 1 else: # li[i]>li[j] ltmp.append(li[j]) j += 1 # 判斷左邊一直有 while i <= mid: ltmp.append(li[i]) i += 1 # 判斷右邊一直有 while j <= high: ltmp.append(li[j]) j += 1 li[low:high + 1] = ltmp#寫回原列表 def merge_sort(li, low, high): """ 利用遞歸分解【二分】 和歸並 """ if low < high: mid = (low + high)//2 # 正當中的值 merge_sort(li, low, mid) # 遞歸調用左半部分 merge_sort(li, mid+1, high) # 遞歸調用右半部分 merge(li, low, mid, high) # 歸並 return li li = [10, 4, 6, 3, 8, 2, 5, 7] high = len(li)-1 print(merge_sort(li,0,high))
歸並排序對原始序列元素分布情況不敏感,其時間復雜度為O(nlogn)。
歸並排序在計算過程中需要使用一定的輔助空間【空列表】,用於遞歸和存放結果,因此其空間復雜度為O(n)。
歸並排序中不存在跳躍,只有兩兩比較,因此是一種穩定排序。
總之,歸並排序是一種比較占用內存,但效率高,並且穩定的算法。
快速排序、堆排序、歸並排序-小結
三種排序算法的時間復雜度都是O(nlogn)
一般情況下,就運行時間而言:
快速排序 < 歸並排序 < 堆排序
三種排序算法的缺點:
快速排序:極端情況下排序效率低
歸並排序:需要額外的內存開銷
堆排序:在快的排序算法中相對較慢
算法 排序NB二人組 堆排序 歸並排序