大牛領導單獨找我聊了兩句:搞框架的同時別忘了演算法

Code綜藝圈發表於2021-03-29

前言

程式=資料結構+演算法,好的演算法能讓程式更高效的執行;在當今資料資訊時代,資料分析和資料處理肯定是避免不了,而演算法便成為了很多公司門檻級的要求,特別是大廠;

趕緊搞起來,說不定離進大廠就只差一步呢(演算法)~~~

演算法簡介

演算法是一組完成任務的指令,任何程式碼片段都可視為演算法。如下:

image-20210324175721795

1. 演算法五大特性

  • 有窮性:一個演算法必須在執行有限步之後結束,且每一步都可在有限時間內完成。通俗一點理解就是不能出現類似死迴圈這樣,導致演算法無法結束。
  • 確定性:演算法中每條指令必須有確切的含義,對於相同的輸入只能得出相同的輸出
  • 可行性:演算法中描述的每一步操作都可以通過已經實現的基本運算執行有限次來實現。
  • 輸入:一個演算法有零個或多個輸入。就好比一個方法,可以不傳遞引數,也可以傳遞引數。零個輸入時其實代表演算法本身設有初始條件。
  • 輸出:一個演算法有一個或多個輸出,這些輸出是與輸入有著對應關係的量。沒有輸出的演算法是毫無意義的。

一個好的演算法還應該有如下特徵:

  • 正確性:能正確解決問題,結果正確;
  • 可讀性:演算法實現步驟容易讀懂;
  • 健壯性:演算法能處理異常情況;比如輸入不合法時,演算法能給出對應處理;
  • 高效率、低儲存:時間複雜度低,空間複雜度低;即執行快,佔用記憶體少。

2. 衡量演算法好壞的標準

度量一個演算法好壞,可以從兩個維度進行判斷:

  • 時間複雜度:事先預估執行完演算法的時間開銷數量級

    由於資料量多少、硬體配置、程式語言等因素會直接影響到演算法的執行時間,比如同樣的演算法,資料量少的肯定快,硬體配置高的肯定快,所以不能用演算法執行完成後的具體時間來衡量一個演算法的好壞。

    一個演算法,可以預估其時間開銷級別(不受外界其他條件影響),通常使用大O表示法來表示,來個例子:

    image-20210325084348799

    上圖方法,為了方便理解,假設每一步需要1ms,當傳入的n=1000時,每一步耗時如下:

    ①只執行一次,所以消耗1ms;

    ②由於每次迴圈需要判斷,需要則需要1001次;消耗1001ms;

    ③和④在迴圈體中,所以分別需要執行1000次,總共消耗2000ms;

    所以總耗時為:T(1000)=1+1001+2*1000; 具體時間和傳入的n有關係,則總耗時為:

    T(n)=1+(n+1)+2n;

    這裡T代表時間,通常說時間複雜度的時候都不帶單位。為了更加簡潔直觀,會使用大O表示法去掉常數部分和係數部分,如下:

    T(n)=1+(n+1)+2n=O(n);

    因為當n足夠大時,係數和常數對演算法度量的影響不大;這裡就不細說啦;

  • 空間複雜度:事先預估執行完演算法的記憶體開銷數量級

    空間複雜度和時間複雜度類似,同樣可以用大O表示,只是這個表示的是演算法所消耗的記憶體,比如int佔用4個位元組,上圖中用到中間變數nResult,在不考慮其他容量的情況下,消耗了4個位元組,用大O表示法,依然是去掉常數和係數,對於常量的的表示為O(1);

對於時間複雜度和空間複雜度,對應的數量級別越小,演算法越高效。常遇到到級別從好到差的順序如下:

O(1)<O(log2 n)<O(n)<O(nlog2 n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

3. 演算法的穩定性

若待排序資料中有兩個相等的元素A和B,在排序前A在B前面,如果使用某種排序演算法後,A仍在B前面,則稱這種排序演算法穩定,否則就不穩定。但穩定性不能用來衡量一個演算法的好壞,只能算是演算法的一個性質,對於一些場景,根本就不在乎兩個相等元素的順序。

從排序開始

排序在實際開發中用的比較多,就先從這入手吧;排序分為內部排序和外部排序兩種:

  • 內部排序:在排序期間元素全部存在記憶體中進行排序;常見的插入排序、交換排序、選擇排序都是內部排序。
  • 外部排序:在排序期間元素無法全部存放在記憶體中,必須在排序過程中不斷地在內、外存之間移動。

1. 先來說說直接插入排序

1.1 演算法思想

插入排序就是每次將一個待排序的資料插入到一個前面已排好序的子序列中,初始認為第一個元素就是排好序的序列,依次比較,然後插入到合適位置,直到完成排序為止。

插入排序的關鍵如下:

  • 將待排序資料分為三部分,已經排好序的資料、下一個需要插入的資料、待排序的資料
  • 每一次都從待排序資料中取出一個需要插入的資料,將其放在哨兵位置;
  • 將哨兵位置的資料(其實就是要插入的資料)與已排好序的資料進行比較,如果符合條件就插入到對應位置,其他資料統一向後移位即可;
1.2 演算法實現與解析

演算法程式碼如下(升序):

image-20210325135045509

執行結果如下:

image-20210325134622616

解析排序步驟過程,如下:

image-20210325214501972

步驟說明:

圖中綠線框部分代表是已經排好序的列表,箭頭指的元素是下一個要插入的元素,黃線框部分為剩下的無序元素。黃方塊為每次移動的資料,綠方塊表示最後有序列表騰出的位置。

  • 將原始資料array複製到新陣列中arrayb中,這步的主要目的是後續不需要宣告額外臨時變數,也為了後續核心程式碼實現邏輯簡單易懂,減少過多的判斷;

  • 第1步將第一個元素作為有序列表(第一元素為2),下一個要插入的元素為5,將5放入哨兵位置,即索引為0的位置;然後依次遍歷有序列表中的元素,與哨兵位的值5比較,這裡只有2和5比較,2小於5,所以不需要改變位置;

  • 第2步有序列表中的元素有2和5,下一個要插入的元素為6,將6放入哨兵位置,即索引為0的位置;然後依次遍歷有序列表中的元素(2和5),與哨兵位的值6比較,都小於6,所以不需要改變位置;

  • 第3步有序列表中的元素有2、5、6,下一個要插入的元素為1,將1放入哨兵位置,即索引為0的位置;然後依次遍歷有序列表中的元素(2、5、6),與哨兵位的值1比較;

    第3-1步,由於是倒序遍歷,先用有序列表中的6與1進行比較,6大於1,所以6往後移一位;

    第3-2步,繼續遍歷,用有序列表中的5與1進行比較,5大於1,所以5往後移一位;

    第3-3步,繼續遍歷,用有序列表中的2與1進行比較,2大於1,所以2往後移一位;

    第3-4步,遍歷完有序列表中的元素,要插入的元素和哨兵位的元素相等,終止遍歷;然後將哨兵位的元素(當前哨兵位為1)賦值給騰出的空間(騰出的索引位為1);

  • 第4步有序列表中的元素有1、2、5、6,下一個要插入的元素為9,將9放入哨兵位置,即索引為0的位置;然後依次遍歷有序列表中的元素(1、2、5、6),都小於哨兵位的值9,所以不用插入,位置不變;

  • 第5步有序列表中的元素有1、2、5、6、9,下一個要插入的元素為3,將3放入哨兵位置,即索引為0的位置;然後依次遍歷有序列表中的元素(1、2、5、6、9),與哨兵位的值3比較;

    第5-1步,由於是倒序遍歷,先用有序列表中的9與3進行比較,9大於3,所以9往後移一位;

    第5-2步,繼續遍歷,用有序列表中的6與3進行比較,6大於3,所以6往後移一位;

    第5-3步,繼續遍歷,用有序列表中的5與3進行比較,5大於3,所以5往後移一位;

    第5-4步,繼續遍歷,用有序列表中的2與3進行比較,2小於3,終止遍歷;然後將哨兵位的元素(當前哨兵位為3)賦值給騰出的空間(騰出的索引位為3);

第5步完成之後,已完成黃線框中無序元素的排序,排序也就完成啦;最終的結果就是1、2 、3 、5 、6 、9。

這樣對比著圖看詳細說明,是不是好理解了很多。

如果有小夥伴不太理解上面的程式碼,可以使用定義臨時變數作為哨兵的方式,步驟和上面基本一樣,只是哨兵不一樣,如下:

image-20210325235648402

1.3 演算法分析

主要從時間複雜度、空間複雜度、是否穩定來進行分析:

時間複雜度

分析時間複雜度時,會從最好、平均、最壞三種情況進行分析;

最好時間複雜度:傳入的資料是有序的(和最終的結果一致),所以每次遍歷,一次就能找到位置,所以插入排序的最好時間複雜度為O(n),和傳入的元素個數有關;

最壞時間複雜度:傳入的資料完全和要的結果相反,所以每次都需要進行兩次迴圈進行找到合適位置插入,所以最壞時間複雜度為O(n2);

平均時間複雜度也就是:O(n2);

空間複雜度

在演算法核心部分只採用了固定的幾個中間變數(i,j,arrayb[0]),所以演算法過程中消耗的記憶體是一個常量,則空間複雜度為O(1);

穩定性

由於在演算法過程中採用的是小於符號進行比較,遇見相等的資料時就終止判斷,所以不會影響原有的資料順序,則直接插入排序是穩定的。

綜上所述,插入排序的時間複雜度為O(n2),空間複雜度為O(1),是穩定演算法;

總結

第一篇複習了一下關於演算法相關知識,然後以簡單的直接插入排序收尾,後面會依次總結其他演算法,還是圖解加說明的方式,讓每一個演算法學起來更簡單。

感謝小夥伴的:點贊收藏評論,下期繼續~~~

一個被程式搞醜的帥小夥,關注"Code綜藝圈",跟我一起學~~~

img

相關文章