選擇排序 每次在n個記錄中選擇一個最小的需要比較n-1次,但是這樣的操作并沒有把每一趟的比較結果保存下來,在后一趟的比較中,有許多的比較在前一趟就已經做過了,但是由于前一趟排序時并未保存這些比較結果,所以后一趟排序時又重復執行了這些比較操作,因而記錄的比較次數比較多
如果可以做到每次在選擇最小記錄時,并根據比較結果對其他記錄做出相應的調整,那樣排序的總體效率就會非常高了,而堆排序就是對簡單選擇排序進行的一種改進,這種改進的效率是非常明顯的
一.堆結構
1.堆是具有下列性質的完全二叉樹:
(1)每個結點的值都大于或等于其左右孩子結點的值,稱為大頂堆;
(2)或者每個結點的值都小于或等于其左右孩子結點的值,稱為小頂堆;
從堆的定義可知:根節點一定是堆中所有結點中共的最大值或者最小值
2.按照層序遍歷的方式給結點進行編號,那么非葉結點的編號滿足如下關系:
1<=i<=[n/2] [n/2]表示不超過n/2的最大整數
因為完全二叉樹的性質:(這里的i指的是編號)
(1)如果2i>n,那么這個i對應的節點是葉節點,且沒有左孩子,反之,我們知道不是葉節點的節點就滿足2i<=n,即得到了上面的表達式
(2)編號為i的節點的左右子節點編號分別是2i和2i+1
那么按照層序遍歷的方式,將最大堆和最小堆存入數組,那么一定滿足上面的關系
二.堆排序算法
1.基本思想
將待排序的序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點,將它移走,然后將剩余的n-1個序列重新構造一個堆,這樣就會得到n個元素中的次大值,如此反復執行,便能得到一個有序序列了
那么實現這個思想要解決兩個問題:
(1)如何由一個無序序列構建成一個堆
(2)在輸出堆頂元素后,如何調整剩余元素稱為一個新的堆
2.代碼實現思路:
(1)無序序列調整為大頂堆
調整為大頂堆主要是遍歷非葉子節點,對每個非葉子節點都找到其和左右子樹中的最大值,然后調換順序,調整最大值到雙親節點,按照這個過程,可以將一個無序序列調整為完全二叉樹。
復雜度分析:
非葉子節點的個數大約為所有節點個數的一半,而每個非葉子節點的處理時間都是常量時間,因此時間復雜度為O(n)
(2)排序:
外循環:i=1,2,,,k:
第i次循環,將堆頂元素和倒數第i個元素交換,然后對前面的n-i個元素調整為最大堆,這里的調整略和上面的不同(原理,可參照算法導論),每次循環是從根節點到最大編號的葉節點進行遍歷,調整每個節點和其子節點的最大值到雙親節點,每一次循環過后,序列的最后i個元素都是有序的
復雜度分析:
每次外循環從根節點到最后一層的葉節點只需要經過log(n-i)次內循環,如果我們需要得到序列的全排序,復雜度計算就是logn+log(n-1)+log(n-2)+...+log1=log(n!),根據時間復雜度計算原則:只保留最高次方的,去掉常數系數,調整n個無序元素為有序的時間復雜度為O(log(n^n))=O(nlogn);
如果是topk,那么我們只需要外循環k次,那么復雜度就是logn+log(n-1)+log(n-2)+...log(n-k)=O(log(n^k))=klogn
"""
heap-sort
"""
def heap_sort(lst,k):
"""
contrust a max-heap based on given array ranging from the last non-leaf node to root node
in python:non-leaf node number reduce from half of the last index of the array minus 1 to 0
"""
for i in range((len(lst)-1-1)/2,-1,-1):
heap_adjust(lst,i,len(lst)-1)
print "\n"
"""
put the the top of the heap in the end of array in turn,then re-construct a max-heap
finally we can get a array whose element is sorted from small to large,if we only get
the top k ,then we iterate k times ,in other words,we need put the top of the heap in
the end k times
"""
for j in range(len(lst)-1, len(lst)-1-k, -1):
"""
swap the top of the heap and the last of unordered part ,if we iterate
k times ,then we get a array whose the last k elements is the top k
"""
# print lst[0]
lst[j], lst[0]=lst[0], lst[j]
heap_adjust(lst, 0, j-1)
def heap_adjust(lst,s,m):
"""
re-contruct a max-heap from s to m based on array
"""
i = 2*s+1 #the left node of the s
temp=lst[s]
while i <= m:
if (i < m) and (lst[i] < lst[i+1]):
i = i+1
if temp >= lst[i]:
break
else:
lst[s] = lst[i]
s = i
i=i*2+1
# print lst
lst[s]=temp
if __name__=="__main__":
sequence1=[50,10,90,30,70,40,80,60,20]
k=5
heap_sort(sequence1,k)
topk=sequence1[-k:len(sequence1)]
topk.reverse()
print "topk:"+str(topk)
2. 堆排序的應用
海量數據的topk,即得到海量數據的topk元素
(1)如果沒有內存限制,可以采用內排序,將數據全部加載進來進行排序,可以采用類冒泡排序的思想,循環k次,每次將第k大的元素調整到上面,內存循環用來比較大小和交換元素順序,復雜度是n-1+n-2+...+n-k)=O(kn)
#coding=UTF-8
"""
inner sort
"""
sequence = [-23,18,2,3,9,-4,5,7]
k = 5
for i in range(k):
for j in range(i + 1, len(sequence)):
if sequence[i] < sequence[j]:
sequence[i], sequence[j] = sequence[j], sequence[i]
print sequence
print "topk:"+str(sequence[:k - 1])
復雜度還是比較大,我們可以采用堆排序的方法:
(2)如果在有內存限制的情況下,即我們無法將數據全部加載進來,只能采用外排序方法,這里我們仍然采用堆排序,但是和上面的堆排序思路和過程都不一樣,區別在于上面我們是將數據全部加載到內存,實現的全排序(k=len(sequence1),但是這里因為在不消耗內存的情況下:
先初始化一個k維的數組存放海量數據的前k個元素,然后將這個k個元素構建成一個最小堆;
循環以下過程:再從海量數據的第k+1個元素進行遍歷,每次比較前面的最小堆的根節點與后面的每個元素的大小,如果根節點元素小于后面的元素,那么將前面的根節點元素替換為這個元素;再重新調整這個數組為一個最小堆,這樣每次都會扔掉更小的元素,加進來更大的元素,直至遍歷完所有元素,得到的數組就是我們的topk
時間復雜度分析:一開始構建最小堆的復雜度是O(K),然后后面遍歷了n-K個元素,每次的復雜度是O(K),因此總復雜度是O(k+(n-k)logk)=O(nlogK)
空間復雜度分析:這里比上面的堆排序增加了一個K維的數組作為緩存topk元素
總的算法效率分析:減少了內存的消耗,空間復雜度和時間復雜度都比上面增加了
具體實現如下:
#coding=UTF-8
"""
heap-sort
"""
def heap_adjust(lst,s,m):
i = 2*s+1 #the left node of the s
temp=lst[s]
while i <= m:
if (i < m) and (lst[i] > lst[i+1]):
i = i+1
if temp <= lst[i]:
break
else:
lst[s] = lst[i]
s = i
i=i*2+1
# print lst
lst[s]=temp
def heap_sort(lst,k):
topk = []
m=0
while len(topk) < k:
topk.append(lst[m])
m+=1
print "初始tok:"+str(topk)
for i in range((k-1-1)/2,-1,-1):
heap_adjust(topk,i,k-1)
min_k=topk[0]
print "初始最小值:"+str(min_k)
print "初始topk構成的最小堆:"+str(topk)
print "\n"
for j in range(k,len(lst)):
# print lst[0]
if min_k<lst[j]:
topk[0]=lst[j]
for i in range((k-1-1)/2, -1, -1):
heap_adjust(topk,i,k-1)
min_k=topk[0]
# print topk
return topk
if __name__=="__main__":
sequence1=[50,10,90,30,70,40,80,60,20]
k=5
print "最后得到的topk:"+str(heap_sort(sequence1,k))
可以看出來:這里得到的topk不是內部排序的,因為我們上面每次只是構建了最小堆,如果我們想要得到有序的topk,進一步實現如下:
if __name__=="__main__":
sequence1=[50,10,90,30,70,40,80,60,20]
k=5
topk=heap_sort(sequence1,k)
print "小頂堆tok:"+str(topk)
for j in range(len(topk)-1,-1,-1):
topk[0],topk[j]=topk[j],topk[0]
heap_adjust(topk,0,j-1)
print "有序的topk:"+str(topk)
即在內存限制的情況下運用堆排序實現了海量數據的topk