算法就是為了解決某一個問題而采取的具體有效的操作步驟
算法的復雜度,表示代碼的運行效率,用一個大寫的O加括號來表示,比如O(1),O(n)
認為算法的復雜度是漸進的,即對於一個大小為n的輸入,如果他的運算時間為n3+5n+9,那么他的漸進時間復雜度是n3
遞歸
遞歸就是在函數中調用本身,大多數情況下,這會給計算機增加壓力,但是有時又很有用,比如下面的例子:
漢諾塔游戲
把A柱的盤子,移動到C柱上,最少需要移動幾次,大盤子只能在小盤子下面
遞歸實現:
def hanoi(x, a, b, c): # 所有的盤子從 a 移到 c if x > 0: hanoi(x-1, a, c, b) # step1:除了下面最大的,剩余的盤子 從 a 移到 b print('%s->%s' % (a, c)) # step2:最大的盤子從 a 移到 c hanoi(x-1, b, a, c) # step3: 把剩余的盤子 從 b 移到 c hanoi(10, 'A', 'B', 'C') #計算次數 def h(x): num = 1 for i in range(x-1): num = 2*num +1 print(num) h(10)
用遞歸打印斐波那契數列
def fei(n): if n == 0: return 0 elif n == 1: return 1 else: return fei(n-1)+fei(n-2)
你會發現,即使n只有幾十的時候,你的計算機內存使用量已經飆升了
其實,如果結合生成器,你會發現不管n有多大,都不會出現卡頓,但這是生成器的特性,本篇博客不重點介紹
# 結合生成器 def fei(n): pre,cur = 0,1 while n >=0: yield pre n -= 1 pre,cur = cur,pre+cur for i in fei(400000): print(i)
關於遞歸次數,Python中有個限制,可以通過sys模塊來修改
import sys sys.setrecursionlimit(1000000)
查找
1.順序查找
這個沒的說,就是for循環唄,時間復雜度O(n)
def linear_search(data_set, value): for i in range(len(data_set)): if data_set[i] == value: return i return
2.二分查找
時間復雜度O(logn)
就是一半一半的查找,看目標值在左邊一半還是右邊一半,然后替換左端點或者右端點,繼續判斷
非遞歸版本:
def binary_serach(li,val): low = 0 high = len(li)-1 while low <= high: mid = (low+high)//2 if li[mid] == val: return mid elif li[mid] > val: high = mid-1 else: low = mid+1 else: return None
遞歸版本的二分查找
def bin_search_rec(data_set, value, low, high): if low < high: mid = (low + high) // 2 if data_set[mid] == value: return mid elif data_set[mid] > value: return bin_search_rec(data_set, value, low, mid - 1) else: return bin_search_rec(data_set, value, mid + 1, high) else: return None
排序
速度慢的三個:
1.冒泡排序
原理就是,列表相鄰的兩個數,如果前邊的比后邊的小,那么交換順序,經過一次排序后,最大的數就到了列表最前面
代碼:
def bubble_sort(li): for j in range(len(li)-1): for i in range(1, len(li)): if li[i] > li[i-1]: li[i], li[i-1] = li[i-1], li[i] return li
冒泡排序的最差情況,即每次都交互順序的情況,時間復雜度是O(n2)
存在一個最好情況就是列表本來就是排好序的,所以可以加一個優化,加一個標志位,如果沒有出現交換順序的情況,那就直接return
# 優化版本的冒泡 def bubble_sort_opt(li): for j in range(len(li)-1): flag = False for i in range(1, len(li)): if li[i] > li[i-1]: li[i], li[i-1] = li[i-1], li[i] flag = True if not flag: return li return li
2.插入排序
原理:把列表分為有序區和無序區兩個部分。最初有序區只有一個元素。然后每次從無序區選擇一個元素,插入到有序區的位置,直到無序區變空。
def insert_sort(li): for i in range(1,len(li)): tmp = li[i] j = i - 1 while j >= 0 and tmp < li[j]: # 找到一個合適的位置插進去 li[j+1] = li[j] j -= 1 li[j+1] = tmp return li
簡單形象的一張圖:
時間復雜度是O(n2)
示例如下:
def insert_sort(li): ''' 初始時手里(有序區)只有一張牌 每次(從無序區)摸一張牌,插入到手里已有牌的正確位置 :param li: :return: ''' for i in range(1, len(li)): tmp = li[i] j = i - 1 while j >= 0 and tmp < li[j]: li[j+1] = li[j] j = j - 1 li[j+1] = tmp print(li) return li li = [6,5,4,3,2,1] print('li: ', li) res = insert_sort(li) print('res: ', res) ''' li: [6, 5, 4, 3, 2, 1] [5, 6, 4, 3, 2, 1] [4, 5, 6, 3, 2, 1] [3, 4, 5, 6, 2, 1] [2, 3, 4, 5, 6, 1] [1, 2, 3, 4, 5, 6] res: [1, 2, 3, 4, 5, 6] '''
3.選擇排序
原理:遍歷列表一遍,拿到最小的值放到列表第一個位置,再找到剩余列表中最小的值,放到第二個位置。。。。
def select_sort(li): for i in range(len(li)-1): min_loc = i # 假設當前最小的值的索引就是i for j in range(i+1,len(li)): if li[j] < li[min_loc]: min_loc = j if min_loc != i: # min_loc 值如果發生過交換,表示最小的值的下標不是i,而是min_loc li[i],li[min_loc] = li[min_loc],li[i] return li
時間復雜度是O(n2)
速度快的幾種排序:
4.快速排序(快排)
原理:讓指定的元素歸位,所謂歸位,就是放到他應該放的位置(左變的元素比他小,右邊的元素比他大),然后對每個元素歸位,就完成了排序
可以參考這個動圖來理解下面的代碼
代碼:
# 歸位函數 def partition(data, left, right): # 左右分別指向兩端的元素 tmp = data[left] # 把左邊第一個元素賦值給tmp,此時left指向空 while left < right: # 左右兩個指針不重合,就繼續 while left < right and data[right] >= tmp: # right指向的元素大於tmp,則不交換 right -= 1 # right 向左移動一位 data[left] = data[right] # 如果right指向的元素小於tmp,就放到左邊現在為空的位置 while left < right and data[left] <= tmp: # 如果left指向的元素小於tmp,則不交換 left += 1 # left向右移動一位 data[right] = data[left] # 如果left指向的元素大於tmp,就交換到右邊 data[left] = tmp # 最后把最開始拿出來的那個值,放到左右重合的那個位置 return left # 最后返回這個位置 # 寫好歸位函數后,就可以遞歸調用這個函數,實現排序 def quick_sort(data, left, right): if left < right: mid = partition(data, left, right) # 找到指定元素的位置 quick_sort(data, left, mid - 1) # 對左邊元素排序 quick_sort(data, mid + 1, right) # 對右邊元素排序 return data
正常的情況,快排的復雜度是O(nlogn)
快排存在一個最壞情況,就是每次歸位,都不能把列表分成兩部分,此時復雜度就是O(n2)了,如果要避免設計成這種最壞情況,可以在取第一個數的時候不要取第一個了,而是取一個列表中的隨機數
5.歸並排序
原理:列表分成兩段有序,然后分解成每個元素后,再合並成一個有序列表,這種操作就叫做一次歸並
應用到排序就是,把列表分成一個元素一個元素的,一個元素當然是有序的,將有序列表一個一個合並,最終合並成一個有序的列表
圖示:
代碼:
def merge(li, left, mid, right): # 一次歸並過程,把從mid分開的兩個有序列表合並成一個有序列表 i = left j = mid + 1 ltmp = [] # 兩個列表的元素依次比較,按從大到小的順序放到一個臨時的空列表中 while i <= mid and j <= right: if li[i] < li[j]: ltmp.append(li[i]) i += 1 else: ltmp.append(li[j]) j += 1 # 如果兩個列表並不是平均分的,就會存在有元素沒有加入到臨時列表的情況,所以再判斷一下 while i<= mid: ltmp.append(li[i]) i += 1 while j <= right: ltmp.append(li[j]) j += 1 li[left:right+1] = ltmp return li def _merge_sort(li, left, right): # 細分到一個列表中只有一個元素的情況,對每一次都調用merge函數變成有序的列表 if left < right: mid = (left+right)//2 _merge_sort(li, left, mid) _merge_sort(li, mid+1, right) merge(li, left, mid, right) return li def merge_sort(li): return(_merge_sort(li, 0, len(li)-1))
照例,時間復雜度是O(nlogn)
特殊的,歸並排序還有一個O(n)的空間復雜度
6.堆排序
把這個放到最后,是因為這個是最麻煩的,把最麻煩的放到最后,是一種對工作負責的表現
如果要說堆排序,首先得先把‘樹’搞明白
樹
樹是一種數據結構;
樹是由n個節點組成的集合; -->如果n為0,那這是一顆空樹,如果n>0,那么那存在1個節點作為樹的根節點,其他節點可以分為m個集合,每個集合本身又是一棵樹。
一些可能會用到的概念:
根節點:樹的第一個節點,沒有父節點的節點
葉子節點:不帶分叉的節點
樹的深度(高度):就是分了多少層
孩子節點、父節點:節點與節點之間的關系
圖示:
二叉樹
然后在樹的基礎上,有一個二叉樹,二叉樹就是每個節點最多有兩個子節點的樹結構,比如這個:
滿二叉樹:除了葉子節點,所有節點都有兩個孩子,並且所有葉子節點深度都一樣
完全二叉樹:是有滿二叉樹引申而來,假設二叉樹深度為k,那么除了第k層,之前的每一層的節點數都達到最大,即沒有空的位置,而且第k層的子節點也都集中在左子樹上(順序)
二叉樹的存儲方式
有鏈式存儲和順序存儲的方式(列表),本篇只討論順序存儲的方式
思考:
父節點和左孩子節點的編號下標有什么關系? 0-1 1-3 2-5 3-7 4-9 i ----> 2i+1
父節點和右孩子節點的編號下標有什么關系? 0-2 1-4 2-6 3-8 4-10 i -----> 2i+2
再來了解下堆,堆說起來又麻煩了,我將在另一篇博客中單獨寫堆,棧等這些數據結構,本篇先討論與排序有關的東西
堆
堆是一類特殊的樹,要求父節點大於或小於所有的子節點
大根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點大 ,升序用大根堆
小根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點小
堆的調整:當根節點的左右子樹都是堆時,可以通過一次向下的調整來將其變換成一個堆。
所謂一次向下調整,就是說把堆頂的值,向下找一個合適的位置,是一次一次的找,跟他交換位置的值,也要找到一個合適的位置
“瀏覽器寫的沒保存,丟失了,所以這塊不想再寫了。。。”
堆排序的過程
1.構造堆
2.得到堆頂元素,就是最大的元素
3.去掉堆頂,將堆的最后一個元素放到堆頂,此時可以通過一次調整重新使堆有序
4.堆頂元素為第二大元素
5.重復步驟3,直到堆為空
其中構造堆的過程:
挨個出數的過程:
代碼:
def sift(li, left, right): # left和right 表示了元素的范圍,是根節點到右節點的范圍,然后比較根和兩個孩子的大小,把大的放到堆頂 # 和兩個孩子的大小沒關系,因為我們只需要拿堆頂的元素就行了 # 構造堆 i = left # 當作根節點 j = 2 * i + 1 # 上面提到過的父節點與左子樹根節點的編號下標的關系 tmp = li[left] while j <= right: if j+1 <= right and li[j] < li[j+1]: # 找到兩個孩子中比較大的那個 j = j + 1 if tmp < li[j]: # 如果孩子中比較大的那個比根節點大,就交換 li[i] = li[j] i = j # 把交換了的那個節點當作根節點,循環上面的操作 j = 2 * i + 1 else: break li[i] = tmp # 如果上面發生交換,現在的i就是最后一層符合條件(不用換)的根節點, def heap_sort(li): n = len(li) for i in range(n//2-1, -1, -1): # 建立堆 n//2-1 是為了拿到最后一個子樹的根節點的編號,然后往前走,最后走到根節點0//2 -1 = -1 sift(li, i, n-1) # 固定的把最后一個值的位置當作right,因為right只是為了判斷遞歸不要超出當前樹,所以最后一個值可以滿足 # 如果每遍歷一個樹,就找到它的右孩子,太麻煩了 for i in range(n-1, -1, -1): # 挨個出數 li[0], li[i] = li[i],li[0] # 把堆頂與最后一個數交換,為了節省空間,否則還可以新建一個列表,把堆頂(最大數)放到新列表中 sift(li, 0, i-1) # 此時的列表,應該排除最后一個已經排好序的,放置最大值的位置,所以i-1
時間復雜度也是O(nlogn)
來擴展一下,如果要取一個列表的top10,就是取列表的前十大的數,怎么做?
可以用堆來實現,取堆的前十個數,構造成一個小根堆,然后依次遍歷列表后面的數,如果比堆頂小,則忽略,如果比堆頂大,則將堆頂替換成改元素,然后進行一次向下調整,最終這個小根堆就是top10
其實Python自帶一個heapq模塊,就是幫我們對堆進行操作的
heapq模塊
隊列中的每個元素都有優先級,優先級最高的元素優先得到服務(操作),這就是優先隊列,而優先隊列通常用堆來實現
如果用heapq模塊來實現堆排序,就簡單多了:
import heapq def heapq_sort(li): h = [] for value in li: heapq.heappush(h,value) return [heapq.heappop(h) for i in range(len(h))]
而想取top10 ,直接一個方法就行了
heapq.nlargest(10,li)
這三種速度快的排序方式就說完了,其中,快排是速度最快的,即使這樣,也不如Python自帶的sort快
再來介紹兩種排序,希爾排序和計數排序
7.希爾排序
希爾排序是一種分組插入排序的算法
思路:
首先取一個整數d1=n/2,將元素分為d1個組,每組相鄰量元素之間距離為d1,在各組內進行直接插入排序;
取第二個整數d2=d1/2,重復上述分組排序過程,直到di=1,即所有元素在同一組內進行直接插入排序。
希爾排序每趟並不使某些元素有序,而是使整體數據越來越接近有序;最后一趟排序使得所有數據有序。
圖示:
代碼:
def shell_sort(li): gap = int(len(li)//2) # 初始把列表分成 gap個組,但是每組最多就兩個元素,第一組可能有三個元素 while gap >0: for i in range(gap,len(li)): tmp = li[i] j = i - gap while j>0 and tmp<li[j]: # 對每一組的每一個數,都和他前面的那個數比較,小的在前面 li[j+gap] = li[j] j -= gap li[j+gap] = tmp gap = int(gap//2) # Python3中地板除也是float類型 return li
通過diamante也能看出來,其實希爾排序和插入排序是非常相像的,插入排序就可以看做是固定間隔為1的希爾排序,希爾排序就是把插入排序分了個組,同一個組內,相鄰兩個數之間不是相差1,而是相差gap
時間復雜度:O((1+t)n),其中t是個大於0小於1的數,取決於gap的取法,當gap=len(li)//2的時候,t大約等於0.3
8.計數排序
需求:有一個列表,列表中的數都在0到100之間(整數),列表長度大約是100萬,設計算法在O(n)時間復雜度內將列表進行排序
分析:列表長度很大,但是數據量很少,會有大量的重復數據。可以考慮對這100個數進行排序
代碼:
def count_sort(li): count = [0 for i in range(101)] # 根據原題,0-100的整數 for i in li: count[i] += 1 i = 0 for num,m in enumerate(count): # enumerate函數將一個可遍歷的數據對象(如列表、元組或字符串)組合為一個索引序列,同時列出數據和數據下標,一般用在 for 循環當中。 for j in range(m): li[i] = num i += 1
此文摘抄來自 http://www.cnblogs.com/zhang-can/p/8011002.html 抄過來的目的是為了學習,並不是炫耀什么的,不喜勿噴,謝謝