數據結構與算法緒論

基本概念和術語

  • 數據

數據是信息的載體,是描述客觀事物屬性的數,字符以及全部能輸入到計算機中並被計算機程序識別和處理的符號的集合。python

  • 數據元素

數據元素是數據的基本單位,一般做爲一個總體進行考慮和處理。一個數據元素可由若干個數據項組成,數據項是構成數據元素的不可分割的最小單位。例如,學生記錄就是一個數據元素,它由學號、姓名、性別等數據項組成。算法

  • 數據對象

數據對象是具備相同性質的數據元素的集合,是數據的一個子集。例如,整數數據對象是集合 \(N = \lbrace 0, \pm 1, \pm 2, \cdots \rbrace\)數組

  • 數據類型

數據類型是一個值的集合和定義在此集合上一組操做的總稱。數據結構

(1)原子類型:其值不可再分的數據類型。app

(2)結構類型:其值能夠再分解爲若干成分的數據類型。函數

(3)抽象數據類型:抽象數據組織和與之相關的操做。性能

  • 抽象數據類型

抽象數據類型是指一個數學模型以及定義在該模型上的一組操做。抽象數據類型的定義僅取決於它的一組邏輯特性,而與其在計算機內部如何表示和實現無關,即不論其內部結構如何變化,只要它的數學特性不變,都有不影響其外部的使用。一般用(數據對象、數據關係、基本操做集)這樣的三元組來表示抽象數據類型。測試

  • 數據結構

在任何問題中,數據元素都不是孤立存在的,而是在它們之間存在着某種關係,這種數據元素相互之間的關係稱爲結構。數據結構是相互之間存在的一種或多種特定關係的數據元素集合。數據結構包括三方面的內容:邏輯結構、存儲結構和數據的運算。數據的邏輯結構和存儲結構時密不可分的兩個方面,一個算法的設計取決於所選定的邏輯結構,而算法的實現依賴於所採用的存儲結構。spa

數據的邏輯結構

邏輯結構是指數據元素之間的邏輯關係,即從邏輯關係上描述數據。它與數據的存儲無關,是獨立於計算機的。設計

劃分方法一

(1)線性結構

有且僅有一個開始和一個終端結點,而且全部結點都最多隻有一個直接前趨和一個後繼。

例如:線性表、棧、隊列、串

(2)非線性結構

一個結點可能有多個直接前趨和直接後繼。

例如:樹、圖

劃分方法二

集合
  • 數據元素間除「同屬於一個集合」外,無其它關係

線性結構
  • 一個對一個,如線性表、棧、隊列

樹形結構
  • 一個對多個,如樹

圖形結構
  • 多個對多個,如圖

數據的存儲結構

存儲結構是指數據結構在計算機中的表示(又稱映像),也稱物理結構。它包括數據元素的表示和關係的表示。數據的存儲結構是邏輯結構用計算機語言的實現,它依賴於計算機語言。數據的存儲結構主要有:順序存儲、鏈式存儲、索引存儲和散列存儲。

  • (1)順序存儲:把邏輯上相鄰的元素存儲在物理位置上也相鄰的存儲單元裏,元素之間的關係有存儲單元的鄰接關係來體現。其優勢是能夠實現隨機存取,每一個元素佔用最少的存儲空間;缺點是隻能使用相鄰的一整塊存儲單元,所以可能產生較多的外部碎片。
  • (2)連接存儲:不要求邏輯上相鄰的元素在物理位置上也相鄰,藉助指示元素存儲地址的指針表示元素之間的邏輯關係。其優勢是不會出現碎片現象,充分利用全部存儲單元;缺點是每一個元素因存儲指針而佔用額外的存儲空間,而且只能實現順序存儲。
  • (3)索引存儲:在存儲元素信息的同時,還創建附加的索引表。索引表中的每一項稱爲索引項,索引項的通常形式是:(關鍵字,地址)。其優勢是檢索速度快;缺點是增長了附加的索引表,會佔用較多的存儲空間。另外,在增長和刪除數據時要修改索引表,於是會花費較多額時間。
  • (4)散列結構:根據元素的關鍵字直接計算出該元素的存儲地址,又稱爲 Hash 存儲。其優勢是檢索、增長和刪除結點的操做都很快;缺點是若是散列函數很差可能出現元素存儲單元的衝突,而解決衝突會增長時間和空間開銷。

數據的邏輯結構是以面向實際問題的角度出發的,只採用抽象表達方式,獨立於存儲結構,數據的存儲方式有多種不一樣的選擇;而數據的存儲結構是邏輯結構在計算機上的映射,它不能獨立於邏輯結構而存在。數據結構包括三要素,缺一不可。

算法的特性

算法是對特定問題求解步驟的一種描述,它是指令的有限序列,其中每一條指令表示一個或多個操做。此外,一個算法還具備下列 5 個重要特性。

(1)有窮性: 一個算法必須老是(對任何合法的輸入值)在執行有窮步以後結束,且每一步均可在有窮時間內完成。

(2)肯定性: 算法中每一條指令必須有確切的含義,讀者理解時不會產生二義性。即對於相對的輸入只能得出相同的輸出。

(3)可行性: 一個算法是可行的,即算法中描述的操做都是能夠經過已經實現的基本運算執行有限次來實現的。

(4)輸入 : 一個算法有零個或者多個的輸入,這些輸入取自於每一個特定的對象的集合。

(5)輸出 : 一個算法有一個或者多個輸出,這些輸出是同輸入有着某種特定關係的量。

一般設計一個「好」的算法應考慮達到如下目標。

(1)正確性:算法應當可以正確地解決求解問題。

(2)可讀性:算法應當具備良好的可讀性,以助於人民理解。

(3)健壯性:當輸入非法數據時,算法也能適當地作出反應或進行處理,而不會產生莫名其妙的輸出結果。

(4)效率與低存儲量需求:效率是指算法執行的時間,存儲量需求是指算法執行過程當中所須要的最大存儲空間,
這二者都與問題的規模有關。

算法效率的度量

算法效率的度量是經過時間複雜度和空間複雜度來描述的。

一、時間複雜度

一個語句的頻度是指該語句在算法中被重複執行的次數。算法中全部語句的頻度之和記做 \(T(n)\),它是該算法問題規模 n 的函數,時間複雜度主要分析 \(T(n)\)數量級。算法中的基本運算的頻度與\(T(n)\)同數量級,因此一般採用算法中基本運算的頻度 \(f(n)\) 來分析算法的時間複雜度。所以,算法的時間複雜度記爲:\(T(n)=O(f(n))\)

上式中 O 的含義是 \(T(n)\) 的數量級,其嚴格的數學定義是:若 \(T(n)\)\(f(n)\) 是定義在正整數集合上的兩個函數,則存在正常數 \(C\)\(n_0\) ,使得\(n>=n_0\)時,都知足 \(\color{red}{0<=T(n)<=C*f(n)}\) 。其中 \(f(n)\)\(T(n)\) 的一個漸近函數。

算法的時間複雜度不只依賴於問題的規模 n,也取決於待輸入數據的性質。

例如 在數組 \(A[0, \cdots, n-1]\) 中,查找定值 k 的算法大體以下:

i = n - 1;
while(i>=0 && (A[i] != k))
    i--;
return i;

此算法中的語句(3)(基本運算)的頻度不只與問題規模有關,還與輸入實例中 A 的各元素取值及 k 的取值有關:

(1)若 A 中沒有與 k 相等的元素,則語句(3)的頻度\(f(n) =n\)

(2)若 A 中的最有一個元素等於 k,則語句(3)的頻度\(f(n)\) 是常數 0。

最壞時間複雜度是指在最壞狀況下,算法的時間複雜度。

平均時間複雜度是指全部可能輸入實例在等機率出現的狀況下,算法的指望運行時間。

最好時間複雜度是指在最好狀況下,算法的時間複雜度。

通常老是考慮在最壞狀況下的時間複雜度,以保證算法的運行時間不會比它更長。

在分析一個程序的時間複雜性時,有如下兩條規則:

  • 加法規則

\(T(n)=T1(n)+T2(n)=O(f(n))+O(g(x))=O(max(f(n),g(n)))\)

  • 乘法規則

\(T(n)=T1(n) \times T2(n)=O(f(n)) \times O(g(x))=O(f(n) \times g(n))\)

常見的漸近時間複雜度

  • 常量複雜度 \(O(1)\)
  • 對數複雜度 \(O(logn)\)
  • 線性複雜度 \(O(n)\)
  • 平方複雜度 \(O(n^2)\)
  • 指數複雜度 \(O(2^n)\)

基本的複雜度如上,基於以上的表達式能夠有不少的組合,其中 \(logn\) 默認狀況下等同於 \(log_{2}n\)

大小關係:

\(O(1) < O(log_2n) < O(n) < O(nlog_2n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)\)

二、空間複雜度

算法的空間複雜度\(S(n)\)定義爲該算法所耗費的存儲空間,它是問題的規模\(n\)的函數。漸進空間複雜度簡稱爲空間複雜度,記做\(S(n)=O(g(n))\)

一個上機程序除了須要存儲空間來存放自己所用指令、常數、變量和輸入數據外,也須要一些對數據進行操做的工做單位和存儲一些爲實現計算所需信息的輔助空間,若輸入數據所佔空間只取決於問題自己,和算法無關,組只需分析輸入和程序以外的額外空間了。

算法原地工做是指算法所需輔助空間是常量,即\(O(1)\)

算法複雜的意義

算法複雜度的分級至關於(高等數學)的無窮大的階,反映了在規模\(n\)趨於無窮大的過程當中,算法代價增加的速度。算法的複雜度越高,其實施的代價隨着規模增大而增加的速度越快。

\(example\)

\(斐波那契數列的第n項\)

  • 遞歸算法
def fib(n):
    if n < 2:
        return 1
    else:
        return fib(n-1) + fib(n-2)

將參數\(n\)看問題實例的規模,不難看出,計算\(F_n\)的時間代價(考慮求加法操做的次數)大體等於計算\(F_{n-1}\)\(F_{n-2}\) 的時間代價之和。這一狀況說明,計算\(F_n\)的時間代價大體等比於斐波那契數\(F_n\)的值。根據已有的結論:

\[\mathop {\lim }\limits_{n \to \infty } {F_n} = {(\frac{{\sqrt 5 + 1}}{2})^n}=1.618^n \]

能夠看到計算\(F_n\)的時間代價按\(n\)值的指數增加。

  • 遞推算法
def fib(n):
    f1 = f2 = 1
    for k in range(1,n):
        f1, f2 = f2, f2 + f1
    return f2

用這個算法計算\(F_n\)的值,循環的工做只作一次,循環須要作\(n-1\)次。每次循環中只執行了幾個簡單動做,總的工做量(基本操做執行次數)與\(n\)值呈現某種線性關係。

這個例子說明,解決同一問題的不一樣算法,其計算複雜度的差別很大,甚至具備大相徑庭的性質。經過分析算法複雜度,能夠幫助使用者選擇適用的算法;也可能發現已知算法的缺陷,促令人們設法開發更好的算法 。

Python 內置類型性能分析

timeit 模塊

timeit 模塊能夠用來測試一小段 Python 代碼的執行速度。

class timeit.Timer(stmt='pass', setup='pass', timer=<timer function>)

Timer 是測量小段代碼執行速度的類。

stmt 參數是要測試的代碼語句(statment);

setup 參數是運行代碼時須要的設置;

timer 參數是一個定時器函數,與平臺有關。

timeit.Timer.timeit(number=1000000)

Timer 類中測試語句執行速度的對象方法。number 參數是測試代碼時的測試次數,默認爲 1000000 次。方法返回執行代碼的平均耗時,一個 float 類型的秒數。

list 的操做測試

def test1():
    l = []
    for i in range(1000):
        l = l + [i]
def test2():
    l = []
    for i in range(1000):
        l.append(i)
def test3():
    l = [i for i in range(1000)]
def test4():
    l = list(range(1000))

from timeit import Timer

t1 = Timer("test1()", "from __main__ import test1")
print("concat ",t1.timeit(number=1000), "seconds")
t2 = Timer("test2()", "from __main__ import test2")
print("append ",t2.timeit(number=1000), "seconds")
t3 = Timer("test3()", "from __main__ import test3")
print("comprehension ",t3.timeit(number=1000), "seconds")
t4 = Timer("test4()", "from __main__ import test4")
print("list range ",t4.timeit(number=1000), "seconds")

# ('concat ', 1.7890608310699463, 'seconds')
# ('append ', 0.13796091079711914, 'seconds')
# ('comprehension ', 0.05671119689941406, 'seconds')
# ('list range ', 0.014147043228149414, 'seconds')

pop 操做測試

x = range(2000000)
pop_zero = Timer("x.pop(0)","from __main__ import x")pop_zero = Timer("x.pop(0)","from __main__ import x")pop_zero = Timer("x.pop(0)","from __main__ import x")pop_zero = Timer("x.pop(0)","from __main__ import x")pop_zero = Timer("x.pop(0)","from __main__ import x")
print("pop_zero ",pop_zero.timeit(number=1000), "seconds")
x = range(2000000)
pop_end = Timer("x.pop()","from __main__ import x")
print("pop_end ",pop_end.timeit(number=1000), "seconds")

# ('pop_zero ', 1.9101738929748535, 'seconds')
# ('pop_end ', 0.00023603439331054688, 'seconds')

測試 pop 操做:從結果能夠看出,pop 最後一個元素的效率遠遠高於 pop 第一個元素

list 內置操做的時間複雜度

\[\begin{array}{lc} Op & O \ Efficiency \\ indexx[\ ] & O(1) \\ index \ assignment & O(1) \\ append & O(1) \\ pop() & O(1) \\ pop(i) & O(n) \\ insert(i, item) & O(n) \\ del \ operator & O(n) \\ iteration & O(n) \\ contain(in) & O(n) \\ get \ slice[x:y] & O(k) \\ del \ slice & O(n) \\ set \ slice & O(n+k) \\ reverse & O(n) \\ concatenate & O(k) \\ sort & O(nlogn) \\ multiply & O(nk) \\ \end{array} \]

從上能夠看出 list 大概是順序表實現。

dict 內置操做的時間複雜度

\[\begin{array}{lc} Op & O \ Efficiency \\ copy & O(1) \\ get \ item & O(1) \\ set \ item & O(1) \\ del \ item & O(1) \\ contains(in) & O(1) \\ iteration & O(n) \\ \end{array} \]
相關文章
相關標籤/搜索