1、八大排序
八大排序參考:http://www.lxweimin.com/p/7d037c332a9d
1. 直接插入排序
核心思想:將數組中的所有元素(從第二個元素開始,第一個默認已排好序)依次跟前面已經排好的所有元素相比較,如果選擇的元素比已排序的元素小,則交換位置,直到全部元素都比較過。
因此,直接插入排序可以用兩個循環完成:
- 第一層循環:遍歷待比較的所有數組元素(從第二個元素開始)
- 第二層循環:將上層循環選擇的元素(selected)與已經排好序的所有元素(ordered)相比較,從選擇元素的前面一個開始直到數組的起始位置。如果selected < ordered,那么將二者交換位置,繼續遍歷;反之,留在原地,選擇下一個元素。
代碼實現:
# 直接插入排序
def insert_sort(nums):
# 遍歷數組中的所有元素,其中0號索引元素默認已排序,因此從1開始
for i in range(1, len(nums)):
# 將該元素與已排序好的前序數組依次比較,如果該元素小,則交換
# range(x,-1,0):從x倒序循環到0,依次比較,
# 每次比較如果小于會交換位置,正好按遞減的順序
for j in range(i, 0, -1):
# 判斷:如果符合條件則交換
if nums[j] < nums[j-1]:
temp = nums[j]
nums[j] = nums[j-1]
nums[j-1] = temp
else:
break
return nums
2. 希爾(Shell)排序
算法思想:將待排序數組按照步長gap進行分組,然后將每組的元素利用直接插入排序的方法進行排序;每次將gap折半減小,循環上述操作;當gap=1時,利用直接插入,完成排序。
從上面的描述中我們可以發現希爾排序的總體實現應該由三個循環完成:
- 第一層循環:將gap依次折半,對序列進行分組,直到gap=1
- 第二、三層循環:也即直接插入排序所需要的兩次循環。對第一個gap進行排序時,實際只循環了序列長度的一半,最內層不會循環。gap值減小后,最內層也需要循環,利用直接插入的思想,只是間隔變為gap。
代碼實現:
# 希爾排序
def insert_shell(nums):
# 初始化gap值,此處利用序列長度的一半為其賦值
gap = len(nums) // 2
# 第一層循環:依次改變gap值對列表進行分組
while gap >= 1:
# 下面:利用直接插入排序的思想對分組數據進行排序
# range(gap, len(L)):從gap開始
for i in range(gap, len(nums)):
# range(i, 0, -gap):從i開始與選定元素開始倒序比較
# 每個比較元素之間間隔gap
for j in range(i, 0, -gap):
# 如果該組當中兩個元素滿足交換條件,則進行交換
if nums[j] < nums[j-gap]:
temp = nums[j-gap]
nums[j-gap] = nums[j]
nums[j] = temp
else:
break
gap = gap // 2
return nums
3. 簡單選擇排序
基本思想:比較+交換。從待排序序列中,找到最小的元素;如果最小元素不是待排序序列的第一個元素,將其和第一個元素互換;從余下的 N - 1 個元素中,找出最小的元素,重復,直到排序結束。
簡單選擇排序也是通過兩層循環實現:
- 第一層循環:依次遍歷序列當中的每一個元素
- 第二層循環:將遍歷得到的當前元素依次與余下的元素進行比較,符合最小元素的條件,則交換。
代碼如下:
# 簡單選擇排序
def select_sort(nums):
# 依次遍歷序列中的每一個元素
for i in range(len(nums)):
# 將當前位置的元素定義此輪循環當中的最小值
min_idx = i
# 將該元素與剩下的元素依次比較尋找最小元素
for j in range(i+1, len(nums)):
if nums[j] < nums[min_idx]:
min_idx = j
# 將比較后得到的真正的最小值賦值給當前位置
if i != min_idx:
temp = nums[i]
nums[i] = nums[min_idx]
nums[min_idx] = temp
return nums
4. 堆排序
堆:本質是一種數組對象。特別重要的一點性質:任意的葉子節點小于(或大于)它所有的父節點。對此,又分為大頂堆和小頂堆,大頂堆要求節點的元素都要大于其孩子,小頂堆要求節點元素都小于其左右孩子,兩者對左右孩子的大小關系不做任何要求。利用堆排序,就是基于大頂堆或者小頂堆的一種排序方法。下面,我們通過大頂堆來實現。
基本思想
堆排序可以按照以下步驟來完成:
- 首先將序列構建成為大頂堆;
(這樣滿足了大頂堆那條性質:位于根節點的元素一定是當前序列的最大值)
取出當前大頂堆的根節點,將其與序列末尾元素進行交換;
(此時:序列末尾的元素為已排序的最大值;由于交換了元素,當前位于根節點的堆并不一定滿足大頂堆的性質)對交換后的n-1個序列元素進行調整,使其滿足大頂堆的性質;
- 重復2.3步驟,直至堆中只有1個元素為止
代碼如下:
# -------------------------堆排序--------------------------------
# **********獲取左右葉子節點**********
def LEFT(i):
return 2*i + 1
def RIGHT(i):
return 2*i + 2
# ********** 調整大頂堆 **********
# nums:待調整序列 length: 序列長度 i:需要調整的結點
def adjust_max_heap(nums, length, i):
# 定義一個int值保存當前序列最大值的下標
largest = i
# 執行循環操作:兩個任務:1 尋找最大值的下標;2.最大值與父節點交換
while True:
# 獲得序列左右葉子節點的下標
left, right = LEFT(i), RIGHT(i)
# 當左葉子節點的下標小于序列長度 并且左葉子節點的值大于父節點時,
# 將左葉子節點的下標賦值給largest
if (left < length) and (nums[left] > nums[i]):
largest = left
# 當右葉子節點的下標小于序列長度 并且右葉子節點的值大于父節點時,
# 將右葉子節點的下標值賦值給largest
if (right < length) and (nums[right] > nums[largest]):
largest = right
# 如果largest不等于i 說明當前的父節點不是最大值,需要交換值
if largest != i:
temp = nums[i]
nums[i] = nums[largest]
nums[largest] = temp
i = largest
else:
break
#********** 建立大頂堆 **********
def build_max_heap(nums):
length = len(nums)
for x in range(length//2, -1, -1):
adjust_max_heap(nums, length, x)
#********** 堆排序 **********
def heap_sort(nums):
# 先建立大頂堆,保證最大值位于根節點;并且父節點的值大于葉子結點
build_max_heap(nums)
# i:當前堆中序列的長度.初始化為序列的長度
length = len(nums)
# 執行循環:1. 每次取出堆頂元素置于序列的最后(len-1,len-2,len-3...)
# 2. 調整堆,使其繼續滿足大頂堆的性質,注意實時修改堆中序列的長度
while length > 0:
temp = nums[length-1]
nums[length-1] = nums[0]
nums[0] = temp
# 堆中序列長度減1
length -= 1
# 調整大頂堆
adjust_max_heap(nums, length, 0)
return nums
5. 冒泡排序
冒泡排序思路比較簡單:
- 將序列當中的左右元素,依次比較,保證右邊的元素始終大于左邊的元素;
( 第一輪結束后,序列最后一個元素一定是當前序列的最大值;) - 對序列當中剩下的n-1個元素再次執行步驟1。
- 對于長度為n的序列,一共需要執行n-1輪比較
(利用while循環可以減少執行次數)
代碼實現:
# 冒泡排序
def bubble_sort(nums):
length = len(nums)
# 序列長度為length,需要執行length-1輪交換
for i in range(1, length):
# 對于每一輪交換,都將序列當中的左右元素進行比較
# 每輪交換當中,由于序列最后的元素一定是最大的,
# 因此每輪循環到序列未排序的位置即可
for j in range(length-i):
if nums[j] > nums[j+1]:
temp = nums[j]
nums[j] = nums[j+1]
nums[j+1] = temp
return nums
6. 快速排序
基本思想:挖坑填數+分治法
- 從序列當中選擇一個基準數(pivot)。在這里我們選擇序列當中第一個數最為基準數;
- 將序列當中的所有數依次遍歷,比基準數大的位于其右側,比基準數小的位于其左側;
- 重復步驟1.2,直到所有子集當中只有一個元素為止。
用偽代碼描述如下:
1.i = 序列最左側; j = 序列最右側; 將基準數挖出形成第一個坑a[i]。
2.j--由后向前找比它小的數,找到后挖出此數填前一個坑a[i]中。
3.i++由前向后找比它大的數,找到后也挖出此數填到前一個坑a[j]中。
4.再重復執行2,3二步,直到i==j,將基準數填入a[i]中
代碼實現:
# 快速排序
# nums:待排序的序列;start排序的開始index,end序列末尾的index
# 對于長度為length的序列:start = 0;end = length-1
def quick_sort(nums, start=0, end=None):
if end is None:
end = len(nums) - 1
if start < end:
i, j, pivot = start, end, nums[start]
while i < j:
# 從右開始向左尋找第一個小于pivot的值
while (i < j) and (nums[j] >= pivot):
j -= 1
# 將小于pivot的值移到左邊
if i < j:
nums[i] = nums[j]
i += 1
# 從左開始向右尋找第一個大于pivot的值
while (i < j) and (nums[i] <= pivot):
i += 1
# 將大于pivot的值移到右邊
if i < j:
nums[j] = nums[i]
j -= 1
# 循環結束后,說明 i=j,此時左邊的值全都小于pivot,右邊的值全都大于pivot
# pivot的位置移動正確,那么此時只需對左右兩側的序列調用此函數進一步排序即可
# 遞歸調用函數:依次對左側序列:從0 ~ i-1//右側序列:從i+1 ~ end
nums[i] = pivot
# 左側序列繼續排序
quick_sort(nums, start, i-1)
# 右側序列繼續排序
quick_sort(nums, i+1, end)
return nums
7. 歸并排序
歸并排序是建立在歸并操作上的一種有效的排序算法,該算法是采用分治法的一個典型的應用。它的基本操作是:將已有的子序列合并,達到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。
歸并排序其實要做兩件事:
- 分解----將序列每次折半拆分
- 合并----將劃分后的序列段兩兩排序合并
因此,歸并排序實際上就是兩個操作,拆分+合并
如何合并?
nums[:mid]為第一段,nums[mid:]為第二段,并且兩端已經有序,現在我們要將兩端合成達到nums[]并且也有序。
- 首先依次從第一段與第二段中取出元素比較,將較小的元素賦值給result[]
- 重復執行上一步,當某一段賦值結束,則將另一段剩下的元素賦值給result[]
- 返回result[]即可。
如何分解?
在這里,我們采用遞歸的方法,首先將待排序列分成A,B兩組;然后重復對A、B序列
分組;直到分組后組內只有一個元素,此時我們認為組內所有元素有序,則分組結束。
代碼實現:
# 歸并排序
# 這是合并的函數
def merge(left, right):
result = []
# 從兩個有順序的列表里邊依次取數據比較后放入result
# 每次我們分別拿出兩個列表中最小的數比較,把較小的放入result
while (len(left) > 0) and (len(right) > 0):
# 為了保持穩定性,當遇到相等的時候優先把
# 左側的數放進結果列表,因為left本來也是大數列中比較靠左的
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
result += left
result += right
# while循環出來之后 說明其中一個數組沒有數據了,
# 我們把另一個數組添加到結果數組后面
return result
# 歸并排序的函數
def merge_sort(nums):
# 不斷遞歸調用自己一直到拆分成成單個元素的
# 時候就返回這個元素,不再拆分了
if len(nums) == 1:
return nums
# 取拆分的中間位置
mid = len(nums) // 2
# 拆分過后左右兩側子串
left = nums[:mid]
rigt = nums[mid:]
# 對拆分過后的左右再拆分 一直到只有一個元素為止
# 最后一次遞歸時候ll和lr都會接到一個元素的列表
ll = merge_sort(left)
# ll一定會得到已經排好序的左序列
lr = merge_sort(rigt)
# lr得到排好序的右序列
# 我們對返回的兩個拆分結果進行排序后合并再返回正確順序的子列表
# 這里我們調用另一個函數幫助我們按順序合并ll和lr
return merge(ll, lr)
8. 基數排序
核心思想:通過序列中各個元素的值,對排序的N個元素進行若干趟的“分配”與“收集”來實現排序。
- 分配:我們將L[i]中的元素取出,首先確定其個位上的數字,根據該數字分配到與之序號相同的桶中
- 收集:當序列中所有的元素都分配到對應的桶中,再按照順序依次將桶中的元素收集形成新的一個待排序列L[ ]
對新形成的序列L[]重復執行分配和收集元素中的十位、百位...直到分配完該序列中的最高位,則排序結束
“基數排序”的展示,我們可以清楚的看到整個實現的過程:
代碼實現:
#************************基數排序****************************
# 基數排序
def radix_sort(nums):
max_num = max(nums)
pos = len(str(max_num)) # 獲取最大數的位數
buckets = [[] for i in range(10)] # 構建10個桶
# 從低位到高位依次執行循環
for i in range(pos):
# 對序列的每一個數字進行操作
for num in nums:
# 獲取每個數字的基數
radix = (num // (10 ** i)) % 10
# 將數字放到基數對應的桶中
buckets[radix].append(num)
# 將桶中的元素按順序放回原數列
idx = 0
for bt in range(10):
while len(buckets[bt]) > 0:
nums[idx] = buckets[bt].pop(0)
idx += 1
return nums
性能測試
運行時間對比(以sort()
函數為參照):
- 1W個數據
sort:0.0019979476928710938
直接插入排序:9.216700553894043
希爾排序:0.13592171669006348
簡單選擇排序:4.676285028457642
堆排序:0.0939633846282959
冒泡排序:12.259912252426147
快速排序:0.03697681427001953
歸并排序:0.0799551010131836
基數排序:0.0559844970703125
- 10W個數據
sort:0.041976213455200195
直接插入排序:1056.8331136703491
希爾排序:2.1497597694396973
簡單選擇排序:614.0669178962708
堆排序:1.3172402381896973
冒泡排序:1398.4518978595734
快速排序:0.448758602142334
歸并排序:2.179725408554077
基數排序:0.9644415378570557
從運行結果上來看:
- sort 是真的快:先快速排序,超過遞歸層數閾值后堆排序,剩余的插入。
- 快速排序、堆排序、歸并排序、基數排序也非常快。
2、三大查找
三大查找參考:https://www.cnblogs.com/lsqin/p/9342929.html
1. 二分查找
二分查找又稱折半查找,優點是比較次數少,查找速度快,平均性能好;其缺點是要求待查表為有序表,且插入刪除困難。因此,折半查找方法適用于不經常變動而查找頻繁的有序列表。
其基本思想是:首先,假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查找關鍵字比較,如果兩者相等,則查找成功;否則利用中間位置記錄將表分成前、后兩個子表,如果中間位置記錄的關鍵字大于查找關鍵字,則要找的元素一定在左子表中,則繼續對左子表進行折半查找;若中間位置記錄的關鍵字小于查找關鍵字,則要找的元素一定在右子表中,則繼續對右子表進行折半查找。重復以上過程,直到找到滿足條件的記錄,使查找成功,或直到子表不存在為止,此時查找不成功。
最優時間復雜度:O(1),最壞時間復雜度:O(logn)
代碼實現:
# 遞歸解決二分查找
def binary_search_rec(nums, data):
length = len(nums)
if length < 1:
return False
mid = length // 2
if nums[mid] > data:
return binary_search_rec(nums[0:mid], data)
elif nums[mid] < data:
return binary_search_rec(nums[mid+1:], data)
else:
return mid
# 非遞歸解決二分查找
def binary_search(nums, data):
length = len(nums)
first = 0
last = length - 1
while first <= last:
mid = (last + first) // 2
if nums[mid] > data:
last = mid - 1
elif nums[mid] < data:
first = mid + 1
else:
return mid
return False
2. 分塊查找/索引查找
分塊查找又稱索引順序查找,它是順序查找的一種改進方法。要求是順序表,將待查的元素均勻地分成塊,塊間按大小排序,塊內不排序,必須滿足分在第一塊中的任意數都小于第二塊中的所有數。先順序查找已在已建好的索引表中查出key所在的塊中,再在塊中順序查找key。所以要建立一個塊的最大關鍵字表,成為索引表。
算法思想:
- 將n個數據元素"按塊有序"劃分為m塊(m ≤ n);
- 每一塊中的結點不必有序,但塊與塊之間必須"按塊有序";
- 即第1塊中任一元素的關鍵字都必須小于第2塊中任一元素的關鍵字;
- 而第2塊中任一元素又都必須小于第3塊中的任一元素,……
算法流程:
- 先選取各塊中的最大關鍵字構成一個索引表;
- 先對索引表進行二分查找或順序查找,以確定待查記錄在哪一塊中;
- 然后在已確定的塊中用順序法進行查找
時間復雜度:O(log(m)+N/m)
3. 哈希查找
哈希表就是一種以鍵-值(key-indexed) 存儲數據的結構,只要輸入待查找的值即key,即可查找到其對應的值。
算法思想
哈希的思路很簡單,如果所有的鍵都是整數,那么就可以使用一個簡單的無序數組來實現:把鍵處理后作為索引,值即為原鍵值,這樣就可以快速訪問任意鍵的值。這是對于簡單的鍵的情況,我們將其擴展到可以處理更加復雜的類型的鍵。簡單的講,就是對原數組的所有數據進行一個編碼,存放到原數組中的編碼位置,查找時只需將查找值編碼后找到對應的值即可。
算法流程
1)用給定的哈希函數構造哈希表;
2)根據選擇的沖突處理方法解決地址沖突;常見的解決沖突的方法:拉鏈法和線性探測法。
3)在哈希表的基礎上執行哈希查找。
復雜度分析
單純論查找復雜度:對于無沖突的Hash表而言,查找復雜度為O(1)(注意,在查找之前我們需要構建相應的Hash表)。
算法實現:
class HashTable:
def __init__(self):
self.elem = [] # 使用list數據結構作為哈希表元素保存方法
self.count = 0 # 最大表長
def insert_hash(self, key):
"""插入關鍵字到哈希表內"""
address = key % self.count # 求散列地址,散列函數采用除留余數法,
# 數組有多長就對應最大有多少種索引值,剛好可以存放所有數據
while self.elem[address]: # 當前位置已經有數據了,發生沖突。
address = (address + 1) % self.count # 線性探測下一地址是否可用
self.elem[address] = key # 沒有沖突則直接保存。
def search_hash(self, nums, key):
# 創建hash表
self.count = len(nums)
self.elem = [None for i in range(len(nums))]
for num in nums:
self.insert_hash(num)
"""查找關鍵字,返回布爾值"""
star = address = key % self.count
while self.elem[address] != key:
address = (address + 1) % self.count
if not self.elem[address] or address == star: # 說明沒找到或者循環到了開始的位置
return False
return True