算法圖解--學習筆記

一、算法簡介

 

線性時間:如果一個算法的時間複雜度爲O(n),則稱這個算法具有線性時間,或O(n)時間。

對數時間:若算法的T(n) = O(log n),則稱其具有對數時間

 

 

  • 算法的速度指的並非時間,而是操作數的增速;
  • 談論算法的速度時,我們說的是隨着輸入的增加,其運行時間將以什麼樣的速度增加;
  • 算法的運行時間用大O表示法表示;
  • 大O表示法計算的是最糟糕情況的運行時間。
  • 算法運行時間是從其增速的表示去度量的。
  • 大O表示法在計算時n實際上爲c*n,c指的是算法所需的固定時間量,這被稱爲常量,在兩種算法的時間複雜度不同時,這種常量無關緊要,而當時間複雜度相同時,就需要考慮這個常量,這也是快速排序和合並排序雖然運行時間都爲O(n*log n),但快速排序的速度比合並排序會快(因爲快速排序遇到最糟情況的可能性比遇到平均情況的可能性小得多)。

 

旅行商(最短路徑問題)問題的算法的運行時間爲O(n!),當前計算機科學領域沒有更有效率的算法。

 

二、選擇排序

 

 

 

數組:

 

使用數組的優點:

每個元素在數組中的地址是被開發者所知的,所以讀取時可直接按照索引進行讀取(元素的位置稱爲索引)。

 

使用數組的缺點:

  • 數據在內存中的儲存地址是相連的,所以在聲明的時候需要額外定義空地址以便保留新數據,若這些地址用不上,將會浪費內存。
  • 儲存數據超過數組容量時,需要重新定義。

 

鏈表:

鏈表的元素存儲了下一個元素的地址,從而使一些不相連的內存地址串在了一起。也就是說只要有足夠的內存空間就能爲鏈表分配內存(優點)。

 

鏈表的缺點:

鏈表在讀取元素時,由於不知道每個元素的地址,於是只能進行順序訪問,其時間複雜度爲O(n);

 

名詞解釋:

索引:我們把元素的位置稱爲索引。

順序訪問:從第一個元素開始逐個地讀取元素的訪問方式。

隨機訪問:可直接通過索引進行讀取的訪問方式。

 

常見數組和鏈表操作的運行時間:

 

 

數組

鏈表

讀取

O(1)

O(n)

插入

O(n)

O(1)

刪除

O(n)

O(1)

 

注意:僅當能夠立即訪問要刪除的元素時,刪除操作的運行時間才爲O(1);

 

 

三、遞歸

 

遞歸:

 

遞歸只是讓解決方案更清晰,並沒有性能上的優勢。

實際上,在有些情況下,使用循環的性能更好。

Leigh Caldwell在Stack Overflow上說的一句話:“如果使用循環,程序的性能可能更高;如果使用遞歸,程序可能更容易理解。如何選擇要看什麼對你來說更重要。”

 

每個遞歸函數都有兩部分:基線條件(base case)和遞歸條件(recursive case)。

  • 遞歸條件:指函數調用自己
  • 基線條件:指函數不再調用自己,從而避免形成無限循環。
def countdown(i):
    print(i)
  if i <= 0:  # 基線條件
    return
  else:  # 遞歸條件
    countdown(i-1)

# 調用:
countdown(4)

4
3
2
1
0

 

 

 

棧:

 

調用棧(call stack): 用於存儲多個函數的變量。

  • 調用另一個函數時,當前函數暫停並處於未完成狀態。該函數的所有變量的值都還在內存中。執行完調用函數後,回到當前函數,並從離開的地方開始接着往下執行。

# 階乘的遞歸 def fact(x): if x == 1: # 每個fact調用都有自己的x變量。在一個函數調用中不能訪問另一個的x變量。 return 1 else: return x * fact(x-1)

 

 

用棧(遞歸調用棧)雖然很方便,但是也要付出代價:存儲詳盡的信息可能佔用大量的內存。每個函數調用都要佔用一定的內存,如果棧很高,就意味着計算機存儲了大量函數調用的信息。在這種情況下,有兩種選擇:

  • 重新編寫代碼,轉而使用循環。
  • 使用尾遞歸。但並非所有的語言都支持尾遞歸。

 

 

小結:

  • 遞歸指的是調用自己的函數。
  • 每個遞歸函數都有兩個條件:基線條件和遞歸條件。
  • 棧有兩種操作:壓入和彈出。
  • 所有函數調用都進入調用棧。
  • 調用棧可能很長,這將佔用大量的內存。

 

 

四、快速排序

 

分治法(divide and conquer,D&C):

工作原理:

  • 找出簡單的基線條件;
  • 確定如何縮小問題的規模,使其符合基線條件。

 

 

分治法的精髓:

分--將問題分解爲規模更小的子問題;

治--將這些規模更小的子問題逐個擊破;

合--將已解決的子問題合併,最終得出“母”問題的解;

 

編寫涉及數組的遞歸函數時,基線條件通常是數組爲空或只包含一個元素。

 

練習:

4.1 編寫sum函數的代碼:

def sum(arr):
    if arr == []:
        return 0
    else:
        return arr.pop()+sum(arr)

arr = [1,2,3,4]
summ = sum(arr)
print summ

 

標準答案:

def sum(list):
    if list ==[]:
        return 0
    return list[0]+sum(list[1:])

4.2 編寫一個遞歸函數來計算列表包含的元素數。

# -*- coding: utf8 -*-
def count(l) :   #統計列表包含的元素數
    if l==[]:     #基線條件:列表包含元素爲1
        return 0
    else:
        del l[-1]
        return 1 + count(l)
l =[1,2,3,4,5]
print count(l)

 

標準答案:

def count(list):
    if list==[]:
        return 0
    return 1 + count(list[1:])

4.3 找出列表中最大的數字。

def findMaximum(arr):      #使用的是循環方式
    Maximum = arr[0]
    for i in range(1,len(arr)):
        if arr[i]>Maximum:
            Maximum = arr[i]

    return Maximum

arr = [1,5,8,2]
print findMaximum(arr)

標準答案:

def max(list):
    if len(list) == 2:     #基準條件
        return list[0] if list[0] >list[1] else list[1]
    sub_max = max(list[1:])
    return list[0] if list[0] > sub_max else sub_max

print max([1,5,2,7])

4.4 二分查找法也是一種分治法,那麼你能簡述二分查找法的基線條件與遞歸條件嗎?

 

基線條件:數組只包含一個元素。如果要查找的值與這個元素相同,就找到了,否則,就說明它不在數組中。

 

遞歸條件:當數組不止包含一個元素時,將數組分成兩半,將其中一半丟棄,並對另一半執行二分查找。

 

快速排序:

 

步驟:

  • 選擇基準值。
  • 將數組分成兩個子數組:小於基準值的元素和大於基準值的元素。
  • 對這兩個子數組進行快速排序。

 

代碼:

# -*- coding: UTF-8 -*-
def quicksort(array):
    if len(array) < 2:
        return array      #基線條件:爲空或只包含一個元素的數組是“有序”的
    else:
        pivot = array[0]  #遞歸條件
        less = [i for i in array[1:] if i<= pivot] #由所有小於基準值的元素組成的子數組

        greater =[i for i in array[1:] if i> pivot] #由所有大於基準值的元素組成的子數組

        return quicksort(less) + [pivot] + quicksort(greater)

print quicksort([10,5,2,3])

 

小結:

 

  • 實現快速排序時,請隨機地選擇用作基準值的元素。快速排序的平均運行時間爲O(n*log n)。
  • 大O表示法中的常量有時候事關重大,這就是快速排序比合並排序快的原因所在。
  • 比較簡單查找和二分查找時,常量幾乎無關緊要,因爲列表很長時O(log n) 的速度比O(n)快得多。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章