算法與數據結構入門一篇就搞定

算法與數據結構開篇

你真的會數據結構嗎?

公司開發一個客服電話系統,小菜須要完成客戶排隊模塊的開發,通過三次修改:算法

第一次:小菜使用了數據庫設計了一張客戶排隊表,而且設置了一個自動增加的整型id字段,來一個用戶,就在這張表的末尾插入一條數據,等客服系統一空閒,就將表中最前的的客戶提交,而後刪除這條記錄。數據庫

  • 實時排隊模塊,在內存中實現便可,無序用數據庫

第二次:小菜用數組變量從新實現了這個功能,懼怕數組不夠大,選擇遠大於實際狀況的100做爲數組長度編程

  • 數組雖然能夠知足必定需求,可是須要考慮溢出問題,以及新增和刪除後的數據移動,顯然不是很方便

第三次:小菜使用了數據結構中的 「隊列結構」 終於知足了需求數組

說明:此例子概括參考自《大話數據結構》微信

爲何你的程序比別人的慢?(算法問題)

來看一個問題:數據結構

公元前五世紀,我國古代數學家張丘建在《算經》一書中提出了「百雞問題」:雞翁一值錢五,雞母一值錢三,雞雛三值錢一。百錢買百雞,問雞翁、雞母、雞雛各幾何?請設計一個「高效」的算法求解。數據結構和算法

也就是說:數據庫設計

買一隻公雞要五文,買一隻母雞要三文,而一文能夠買三隻小雞,共有100文,問公雞母雞小雞各能買幾隻?函數

話很少說,咱們先給出一種最容易想到的方式,也就是列兩個三元方程組學習

也就是知足雞的總數爲100,同時花錢數也爲100,咱們來看一下代碼實現

方式一:

//i j k 分別表明公雞 母雞 雛雞的數量 
//20、3四、300 爲100元最多能買公雞、母雞、小雞的數量
for (int i = 0; i < 20; i++) {
    for (int j = 0; j < 34; j++) {
        for (int k = 0; k < 300; k = k + 3) { //k = k + 3 是爲了知足小雞實際存在的要求
            if (5*i + 3*j +  k/3 == 100 && i + j + k == 100) {
                cout << "公雞 母雞 雛雞 數量分別爲:" << i <<", "<< j <<", "<< k << endl;
            }
        }
    }
}

咱們用了三層循環,有沒有辦法能簡化如下代碼呢?能不能減小循環層數呢?這顯然是可行的,上面小雞的數量這一層明顯能夠省略的,由於總數是已知的

方式二

for (int i = 0; i < 20; i++) {
    for (int j = 0; j < 34; j++) {
            
        k_temp = 100 - i - j;
            
        if (k_temp % 3 == 0)
            k = k_temp;
        
        if (5*i + 3*j +  k/3 == 100 && i + j + k == 100) {
            cout << "公雞 母雞 雛雞 數量分別爲:" << i <<", "<< j <<", "<< k << endl;
        }
    }
}

確實程序更加優化了一些,可是這樣是否是就能夠了呢?其實還能夠優化爲一層循環!

方式三

int i, j, k, t;
for (int t = 0; t <= 25/7; t++) {
    i = 4 * t;
    j = 25 - 7 * t;
    k = 75 + 3 * t;
    cout << "公雞 母雞 雛雞 數量分別爲:" << i <<", "<< j <<", "<< k << endl; 
}

上例中對程序的優化涉及到了數學的運算

//根據花錢的總數爲100列式
5x + 3y + (100 - x - y)/3 = 100

//對上式進行化簡
y = 25 -(7/4)x
  = 25 - 2x + x/4
    
//設 x/4 = t,原式子可變爲
y = 25 - 7t
    
//能夠獲得三個不等式
x = 4t >= 0
y = 25 - 7t >= 0
z = 75 + 3t >= 0

//解得
t >= 0
t <= 25/7

爲了增長說服力,咱們測試每個程序的運算時間,不太熟悉的朋友我下面已經貼出了代碼

#include<ctime>
clock_t startTime,endTime;
startTime = clock();

......測試代碼

endTime = clock();
cout << "The run time is: " << (double) (endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;

咱們將數據放大一些,方便體現差別,咱們將總金額和總數量均改成1000 下面是三種方式的運算時間

The run time is: 0.114s
The run time is: 0.03s
The run time is: 0.026s

很顯然程序逐步的到了優化,減小了for循環 大大的減小了循環次數,因此節省了時間

爲何要一塊兒學習數據結構和算法?

想要回答這個問題,咱們先開看一下數據結構和算法的概念

數據結構概念

數據結構是指相互之間存在一種或多種特定關係數據元素的集合

也就是說,計算機在對數據進行存儲的時候並非雜亂無序的而是擁有必定規則的,一個或多個數據元素之間擁有必定的相互關係,因此也能夠說數據結構是計算機存儲和組織數據的方式

算法的概念

算法是一種能夠被計算機使用來解決問題的的方法,這種方法也正是解決問題的具體步驟

對於小型的程序而言,即便這個算法比較差勁,解決問題的步驟比較累贅繁瑣,也不會有很大的關係,只要能解決問題的就是好程序,可是若是對於數據量較大的中大型程序,咱們就須要對方法的時間和空間進行有效的利用,也就是設計一個高效的算法,在一樣硬件設備的狀況下,有時候甚至能夠將速度提升十倍甚至百倍

程序 = 數據結構 + 算法

數據結構是對計算機所存儲的數據之間的一種組織管理,算法是對數據進行操做從而將你解決問題的方法在計算機中具體實現,也就是說,在計算機中而言,其實這二者單獨拿出來討論是沒有什麼實際意義的,就像魚離不開水同樣,即便一個優秀的算法,若是數據之間沒有任何結構關係可言,算法也就無從實現,也就沒有意義了,而即便數據之間有了組織關係,可是不對其進行操做這顯然也沒有意義。

簡單總結:只有數據結構的程序是沒有靈魂的,只有算法的程序卻只有魂魄,沒有軀體

不能否認,數據結構是很是重要的,但我更加傾向於算法爲核心,正如 Algorithms(4th.Edition) 中的觀點,數據結構是算法的副產品或者結果,在我看來,如何創建一個有效且高效的算法以及模型是更重要一些的,開發的最終目的就是經過程序利用科技設備實現咱們實際生活中的一些需求,咱們遇到問題的第一步都是在現實中對問題進行分析,而後設計出合適的方法,而後就要想辦法將這種方法表達給計算機設備,可是這個時候就須要數據結構這樣一種知足須要的產物,來支持咱們對咱們的設備具體表達咱們的方法,尤爲隨着數據量的檢驗,不一樣的算法就會對解決實際問題的效率產生更大的影響,我用一個不是很恰當卻很形象的例子表達就是:你的身體(數據結構)死了,你可能還活着,但你的靈魂(算法)死了,你即便活着也已經死了

固然數據結構的重要性也是無可置疑的,一個好的數據結構,能夠幫助咱們的程序更加高效,若是何時,模型以及算法的選用已經能夠根據數據的特色以及需求選用,咱們就能夠將更多的精力投入到數據結構的設計中去,不過這也僅僅是一種美好的假想,因此咱們仍是要學好算法,可是學好算法,咱們也就要對支持咱們算法的數據結構進行必定的研究

說了一些我的的觀點,那讓咱們趕忙介紹一些入門的必備知識

數據結構的基本概念和術語

  • 數據:描述客觀事物的數字和符號,是可以被計算機識別且進行處理的的符號集合

    • 整型和實數型數據是能夠直接進行數值計算的

    • 文字、圖像、圖形、聲音、視頻等多媒體信息則能夠經過合適的編碼保存處理

      例如:文本經過字符編碼處理、聲音經過儲存波形 (WAV) 或在波形分解後存儲其振幅特性而後壓縮 (MP3)

  • 數據元素:組成數據且有意義的的基本單位 (個體)

    • 例如:貓和狗都是動物羣體中的一個數據元素
  • 數據項:組成數據元素具備特定意義的最小不可分割單位

  • 做爲人這個類羣中具體的個體,一我的而言,其所擁有的手、腳、眼、鼻,亦或者姓名、年齡、身高、等都屬於數據項

  • 數據對象:性質相同的數據元素的集合,是數據的子集

    • 例如:人類中一些人的生日、身高相同

邏輯結構和物理結構(存儲結構)

邏輯結構

邏輯結構:數據對象中數據元素之間的相互關係

集合結構:數據元素之間的惟一關係就是屬於同一個集合

線性結構:數據元素之間存在一對一的關係(除首尾元素均存在前驅和後繼)

樹形結構:數據元素之間存在一對多的關係

圖形結構:數據元素之間存在多對多的關係

  • (每個數據元素看作一個節點,元素之間的邏輯關係用連線表示,若是關係有方向則連線帶箭頭)

物理結構(存儲結構)

物理結構:數據的邏輯結構關係在計算機中的存儲形式

順序存儲結構:把元素分別放在地址連續的存儲單元中的存儲方式

  • 也就是說:元素一個一個有序的排好隊,各自佔據必定的空間,例如定義一個含有6個浮點型數據的數組:而後內存中的一塊大小爲6個浮點型數據大小空間就會被計算機所開闢,而後數據存入時,依次順序擺入

鏈式存儲結構:把元素存儲在任意的存儲單元中的存儲方式

  • 由於數據元素位置不肯定,因此須要經過指針指向到元素的存儲地址,從而肯定不一樣數據元素之間的位置

  • 舉例:200人同時在一個階梯教室上課,同窗們坐的位置是沒有關係的,老師點名簽到的時候,你只須要關注你學號前一位的同窗有沒有被點到,點到後,你就知道下一個該你了

散列 (哈希) 存儲方式:是一種力圖將數據元素的存儲位置與關鍵碼之間創建肯定對應關係的查找技術

  • 你別慌,我這就來解釋了,它的原理就是,將一個節點的關鍵字key做爲自變量,經過一個肯定的函數運算f(key),其函數值做爲節點的存儲地址,將節點存入到指定的位置上,查找的時候,被搜索的關鍵字會再次經過f(key)函數計算地址,而後讀取對應數據

  • 咱們後面會專篇講解這個內容,如今作一個簡單的瞭解便可

索引存儲方式:存儲時,除了存儲節點,還附加創建了索引表來表示節點的地址

算法的特徵

  • 輸入:算法具備零個或者多個輸入(零個的狀況例如打印輸出字符串,或者算法自身已經給定了初始條件)

  • 輸出:算法具備一個或者多個輸出,用來反映算法對輸入數據加工後的結果

  • 有窮性:算法必須在執行有限個步驟後終止
    • 「有限」 的定義不是絕對的,而是實際應用中合理的可接受的
  • 肯定性: 算法的每一步驟都具備肯定的含義,不會出現二義性
    • 也就是說,惟一的輸入只有惟一的輸出
  • 可行性:算法的每一步都是可行的,經過有限步驟能夠實現

算法的設計要求

  • 正確性:合理的數據輸入下,最終能夠輸出能解決問題需求的正確答案

    • 對正確的理解:
      • 無語法錯誤
      • 輸入合法和非法的數據都可以獲得正確答案
      • 輸入刁難的數據依舊能夠輸出知足須要的答案
  • 可讀性:算法便於閱讀和理解

    • 算法應該井井有條,易讀易懂,方便二次調試和修改

    • 複雜一些的算法,變量的命名儘可能恰當一些,用阿里的開發手冊中的一句話就是說:「正確的英文拼寫和語法可讓閱讀者易與理解避免歧義」「爲了達到代碼自解釋的目標,任何自定義編程元素在命名時,使用盡可能完整的單詞組合來表達其意」

  • 健壯性:當數據不合理的時候,算法也能對各類狀況做出處理,而不是報出異常,或者輸出錯誤的答案

  • 高效性:儘可能知足時間效率高,存儲率低的需求

函數的漸進增加

次數 算法A :2n + 4 算法B :3n + 3 算法C :2n 算法D :3n
n = 1 6 6 2 3
n = 2 8 9 4 6
n = 3 10 12 6 9
n = 10 24 33 20 30
n = 100 204 303 200 300

n = 1的時候 算法 A 和 B 的效率一致,可是從n = 2的時候開始算法 A 的效率已經大於算法 B 了,n值變大後,算法 A 相比 B 變的更加快速

  • -因此在n值沒有限定的狀況下,只要在超過某數值N後,函數值就一直大於另外一個函數,那麼咱們稱函數是漸進增加的

    函數的漸近增加:給定兩個函數f(n)和g(n),若是存在一個整數N,使得對於全部的n > N,f(n)老是比g(n)大,那麼,咱們說f(n)的漸近增加快於g(n)

  • 接着咱們分別用 AC 、BD進行對照,你會發現其實後面的加數對算法的影響幾乎是忽略不計的,因此咱們通常選擇忽略這些加法常數

次數 算法A :2n + 6 算法B :3n² + 3 算法C :n 算法D :n²
n = 1 8 6 1 1
n = 2 10 15 2 4
n = 3 12 30 3 9
n = 10 26 303 10 100
n = 100 206 30003 100 10000

算法 A 和 B 比較的時候,n = 1的時候 B效率更高,從n = 2開始 算法 A 就比較高效,隨着數據的輸入,體現的越明顯,可是咱們同時將這兩個算法去掉最高次項的係數,接着能夠看到對數據的走向仍然影響不大

  • 最高次項的係數對數據走向的影響不大

  • 最高項的指數大的函數,隨着n的增加,結果也會快速增加

  • 判斷算法效率的時候,應主要關注最高次項,其餘次要項或者常數項經常能夠忽略

算法的時間複雜度

算法的時間複雜度是一個函數 T(n),它定性描述該算法的運行時間,一般用大O表示法,記做:T(n) = O(f(n)) 例如3n² + 2n + 1 的時間複雜度爲 O(n²)

注意:

  • T(n) = O(f(n)) 解釋:隨着n增大算法執行時間的增加率和f(n)的增加率相同,同時也考察輸入值大小趨於無窮的狀況,也稱做算法的漸進時間複雜度

  • 在這個過程當中,不考慮函數的低階項、常數項和最高項係數,

常見的時間複雜度

O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2^n) < O(n!) < O(n^n)

實際開發中,咱們常見到的仍是前幾個加粗的

咱們還須要提到兩個概念:

最壞狀況平均狀況

最壞狀況就是,運行時間的最大值,狀況不能再壞了

平均時間就是

通常狀況下,咱們都是指最壞時間複雜度,但平均時間是最有意義的時間,由於它與咱們的指望值更接近

做者的話

這些文章,固然談不上算什麼有深度的技術,可是我在儘量的將一些概念經過舉例、圖表等方式給對這方面知識有須要的朋友,快速的入門,經過一些通俗的說法,先對一些知識有必定的瞭解,在此基礎上,去看一些深刻的權威書籍,或者大牛的博文,就會不至於勸退,自知技術有限,但仍然想給一些剛剛接觸這部分知識的朋友們一些幫助,總得一我的,經歷一些難捱的日子,你纔會變得更增強大!

結尾:

若是文章中有什麼不足,或者錯誤的地方,歡迎你們留言分享想法,感謝朋友們的支持!

若是能幫到你的話,那就來關注我吧!若是您更喜歡微信文章的閱讀方式,能夠關注個人公衆號

在這裏的咱們素不相識,卻都在爲了本身的夢而努力 ❤

一個堅持推送原創Java技術的公衆號:理想二旬不止

相關文章
相關標籤/搜索