《數(shù)據(jù)結(jié)構(gòu)與算法之美》01~05筆記

關(guān)于我的倉庫

  • 這篇文章是我為面試準備的學習總結(jié)中的一篇
  • 我將準備面試中找到的所有學習資料,寫的Demo,寫的博客都放在了這個倉庫里iOS-Engineer-Interview
  • 歡迎star????
  • 其中的博客在簡書,CSDN都有發(fā)布
  • 博客中提到的相關(guān)的代碼Demo可以在倉庫里相應(yīng)的文件夾里找到

前言

  • 該系列為學習《數(shù)據(jù)結(jié)構(gòu)與算法之美》的系列學習筆記
  • 總結(jié)規(guī)律為一周一更,內(nèi)容包括其中的重要知識帶你,以及課后題的解答
  • 算法的學習學與刷題并進,希望能真正養(yǎng)成解算法題的思維
  • LeetCode刷題倉庫:LeetCode-All-In
  • 多說無益,你應(yīng)該開始打代碼了

01講為什么要學習數(shù)據(jù)結(jié)構(gòu)和算法

  • 想要通關(guān)大廠面試,千萬別讓數(shù)據(jù)結(jié)構(gòu)和算法拖了后腿
  • 我們學任何知識都是為了“用”的,是為了解決實際工作問題的
  • 業(yè)務(wù)開發(fā)工程師,你真的愿意做一輩子CRUD boy嗎?【crud是指在做計算處理時的增加(Create)、讀取查詢(Retrieve)、更新(Update)和刪除(Delete)幾個單詞的首字母簡寫。crud主要被用在描述軟件系統(tǒng)中數(shù)據(jù)庫或者持久層的基本操作功能。】
  • 不需要自己實現(xiàn),并不代表什么都不需要了解。
  • 在基礎(chǔ)框架中,一般都揉和了很多基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)和算法的設(shè)計思想。
  • 掌握數(shù)據(jù)結(jié)構(gòu)和算法,不管對于閱讀框架源碼,還是理解其背后的設(shè)計思想,都是非常有用的。
  • 基礎(chǔ)架構(gòu)研發(fā)工程師,寫出達到開源水平的框架才是你的目標!
  • 對編程還有追求?不想被行業(yè)淘汰?那就不要只會寫湊合能用的代碼!
  • 掌握了數(shù)據(jù)結(jié)構(gòu)與算法,你看待問題的深度,解決問題的角度就會完全不一樣。

課后題:你為什么要學習數(shù)據(jù)結(jié)構(gòu)和算法呢?在過去的軟件開發(fā)中,數(shù)據(jù)結(jié)構(gòu)和算法在哪些地方幫到了你?

  • 在最開始對算法有概念是在剛大一的時候,當時學校沒開C語言,都是自己在那里做算法題,對于算法也是在那個時候開始用些許認識的,當時就會覺得很奇妙
  • 后來參加了藍橋杯,ACM校賽,開始意識到自己和高手之間有多深的差距,進入實驗室學習iOS開發(fā)后,算法也是基本放下,也是明顯感到和后臺舍友之間的差距越來越大
  • 目前數(shù)據(jù)結(jié)構(gòu)和算法,算法課都已經(jīng)上完,對于各種算法好像懂點,看到題也能像孔乙己一樣支支吾吾說個DP出來,可一到上手敲的時候,還是gg
  • 即將參加面試,算法作為重要的一環(huán),我不希望會拖我后腿,甚至希望能是個加分項,為此,我會?到底
  • 過去的軟件開發(fā),在iOS開發(fā)階段,調(diào)的都是Apple的API,沒怎么用到算法與數(shù)據(jù)結(jié)構(gòu)知識,但最近閱讀RunTime源碼的時候,由于Apple用了很多Hash的知識,沒有這方面的知識很容易對源碼產(chǎn)生困惑

02講如何抓住重點,系統(tǒng)高效地學習數(shù)據(jù)結(jié)構(gòu)與算法

  • 數(shù)據(jù)結(jié)構(gòu)是為算法服務(wù)的,算法要作用在特定的數(shù)據(jù)結(jié)構(gòu)之上。
  • 復雜度分析對于學習算法有很大的用處,?學好
img
  • 十個數(shù)據(jù)結(jié)構(gòu):數(shù)組、鏈表、棧、隊列、散列表、二叉樹、堆、跳表、圖、Trie樹

  • 十個算法:遞歸、排序、二分查找、搜索、哈希算法、貪心算法、分治算法、回溯算法、動態(tài)規(guī)劃、字符串匹配算法

  • 不要為了學習而學習,而是要學習它的“來歷”“自身的特點”“適合解決的問題”以及“實際的應(yīng)用場景”。

課后題:請思考一下你自己學習這個專欄的方法,你在之前學習數(shù)據(jù)結(jié)構(gòu)和算法的過程中,遇到過什么樣的困難或者疑惑嗎?

  • 這篇文章就是方法的一部分,在每一講學完以后都會寫成這樣一塊的總結(jié),另外每天在LeetCode兩道題
  • 困難與疑惑真的是在于總感覺代碼寫不出來,想想挺對,一寫就錯

03講復雜度分析(上):如何分析、統(tǒng)計算法的執(zhí)行效率和資源消耗

  • 復雜度分析是整個算法學習的精髓,只要掌握了它,數(shù)據(jù)結(jié)構(gòu)和算法的內(nèi)容基本上就掌握了一半。
  • 監(jiān)控得到的運行時間受限于測試環(huán)境,測試數(shù)據(jù),無法真正體現(xiàn)復雜度,因此我們需要一個不用具體測試數(shù)據(jù),可以粗略估計算法的執(zhí)行效率的方法
img
  • 其中T(n)表示代碼執(zhí)行的時間;n表示數(shù)據(jù)規(guī)模的大??;f(n)表示每行代碼執(zhí)行的次數(shù)總和。因為這是一個公式,所以用f(n)來表示。公式中的O,表示代碼的執(zhí)行時間T(n)與f(n)表達式成正比。
  • 這也就到了大O時間復雜度的本質(zhì),它不是代碼真正的執(zhí)行時間,而是代碼執(zhí)行時間隨數(shù)據(jù)規(guī)模增長的變化趨勢,真正名字應(yīng)該是漸進時間復雜度(asymptotic time complexity),簡稱時間復雜度。
img
  • 對于復雜度量級,我們可以粗略地分為兩類,多項式量級非多項式量級。其中,非多項式量級只有兩個:O(2n)和O(n!)。
  • 當數(shù)據(jù)規(guī)模n越來越大時,非多項式量級算法的執(zhí)行時間會急劇增加,求解問題的執(zhí)行時間會無限增長。所以,非多項式時間復雜度的算法其實是非常低效的算法。

O(1)

  • 由于大O表示的是代碼執(zhí)行時間的變化趨勢,因此O(1)其實很好理解,就是時間是定死的
  • 一般情況下,只要算法中不存在循環(huán)語句、遞歸語句,即使有成千上萬行的代碼,其時間復雜度也是Ο(1)。

O(logn)、O(nlogn)

  • 不管是以2為底、以3為底,還是以10為底,我們可以把所有對數(shù)階的時間復雜度都記為O(logn),因為log3n就等于log32 * log2n,O(log3n) = O(C * log2n),在采用大O標記復雜度的時候,可以忽略系數(shù),即O(Cf(n)) = O(f(n))

空間復雜度

  • 時間復雜度的全稱是漸進時間復雜度,表示算法的執(zhí)行時間與數(shù)據(jù)規(guī)模之間的增長關(guān)系。類比一下,空間復雜度全稱就是漸進空間復雜度(asymptotic space complexity),表示算法的存儲空間與數(shù)據(jù)規(guī)模之間的增長關(guān)系。

復雜度增進曲線

img

課后題:有人說,我們項目之前都會進行性能測試,再做代碼的時間復雜度、空間復雜度分析,是不是多此一舉呢?而且,每段代碼都分析一下時間復雜度、空間復雜度,是不是很浪費時間呢?你怎么看待這個問題呢?

  • 這個問題我覺得主要還是推出了時間復雜度,空間復雜度的概念后,我們在交流算法的時候有了一個交流的平臺,我們能感性的了解到算法中的好壞,效率的高低
  • 對于真正投入使用的時候,肯定實際測試要測,復雜度分析也要做

04講復雜度分析(下):淺析最好、最壞、平均、均攤時間復雜度

加權(quán)平均時間復雜度 = 平均時間復雜度 = 期望時間復雜度

// n表示數(shù)組array的長度
int find(int[] array, int n, int x) {
  int i = 0;
  int pos = -1;
  for (; i < n; ++i) {
    if (array[i] == x) {
       pos = i;
       break;
    }
  }
  return pos;
}
  • 我們在長度為n的數(shù)組里搜索x的時候,由于一旦找到就會退出循環(huán),導致不是每次運行都會肯定走n次,因此需要引入平均時間復雜度概念
  • 要查找的變量x在數(shù)組中的位置,有n+1種情況:在數(shù)組的0~n-1位置中不在數(shù)組中。我們把每種情況下,查找需要遍歷的元素個數(shù)累加起來,然后再除以n+1,就可以得到需要遍歷的元素個數(shù)的平均值,即:
img
  • 我們現(xiàn)在假設(shè)在數(shù)組與不在數(shù)組里的概率都是1/2,x出現(xiàn)在數(shù)組中每個位置的可能性都是1/n,這樣,在搜索數(shù)組里的數(shù)據(jù)的時候,x出現(xiàn)在數(shù)組里的概率就是1/(2n),x不出現(xiàn)在數(shù)組里的概率就是1/2
  • 這里我的理解方式就是,x要能出現(xiàn)在數(shù)組里,前提條件就是先有了出現(xiàn)在數(shù)組里的那個可能性2/1,在此基礎(chǔ)上才有了接下來來的1/n
  • 現(xiàn)在的平均復雜度就是:
img

均攤時間復雜度

舉例:

 // array表示一個長度為n的數(shù)組
 // 代碼中的array.length就等于n
 int[] array = new int[n];
 int count = 0;
 
 void insert(int val) {
    if (count == array.length) {
       int sum = 0;
       for (int i = 0; i < array.length; ++i) {
          sum = sum + array[i];
       }
       array[0] = sum;
       count = 1;
    }

    array[count] = val;
    ++count;
 }
  • 這是一個插入函數(shù),兩種情況:
    • count == array.length,count記錄者當前插入到第幾位,當count == len的時候,也就意味著數(shù)組里元素被插滿了,此時便會遍歷整個數(shù)組,求和,輸入到首位。并令count = 1,等待下一次循環(huán)
    • 正常情況下賦值,count++

加權(quán)平均時間復雜度

  • 假設(shè)數(shù)組的長度是n,根據(jù)數(shù)據(jù)插入的位置的不同,我們可以分為n種情況,每種情況的時間復雜度是O(1)。除此之外,還有一種“額外”的情況,就是在數(shù)組沒有空閑空間時插入一個數(shù)據(jù),這個時候的時間復雜度是O(n)。而且,這n+1種情況發(fā)生的概率一樣,都是1/(n+1)。所以,根據(jù)加權(quán)平均的計算方法,我們求得的平均時間復雜度就是:
img

均攤時間復雜度

  • 我們可以注意到一個規(guī)律,每次進行完O(n)的操作后,由于count回到了1,下面的n - 1個操作都是O(1)復雜度的
  • 在此規(guī)律上,我們可以把耗時長的那個O(n)操作平均分配下去,這樣平均下去,等同于每次操作都是O(1)【2次】
  • 這樣來看,時間復雜度就是O(1)
  • 因此,均攤時間復雜度等同于就是特殊的時間復雜度算法,本身沒有什么特別之處

課后題:你可以用今天學習的知識,來分析一下下面這個add()函數(shù)的時間復雜度。

// 全局變量,大小為10的數(shù)組array,長度len,下標i。
int array[] = new int[10]; 
int len = 10;
int i = 0;

// 往數(shù)組中添加一個元素
void add(int element) {
   if (i >= len) { // 數(shù)組空間不夠了
     // 重新申請一個2倍大小的數(shù)組空間
     int new_array[] = new int[len*2];
     // 把原來array數(shù)組中的數(shù)據(jù)依次copy到new_array
     for (int j = 0; j < len; ++j) {
       new_array[j] = array[j];
     }
     // new_array復制給array,array現(xiàn)在大小就是2倍len了
     array = new_array;
     len = 2 * len;
   }
   // 將element放到下標為i的位置,下標i加一
   array[i] = element;
   ++i;
}
  • 該算法的最好情況時間復雜度(best case time complexity)為O(1);
  • 最壞情況時間復雜度(worst case time complexity)為O(n);
  • 平均情況時間復雜度(average case time complexity)為O(1);
  • 時間復雜度感覺還是沒必要這么復雜,像這道題,只有一種情況會是O(n),其他情況都肯定是O(1),沒有糾結(jié)的必要

05講數(shù)組:為什么很多編程語言中數(shù)組都從0開始編號

  • 數(shù)組(Array)是一種線性表數(shù)據(jù)結(jié)構(gòu)。它用一組連續(xù)的內(nèi)存空間,來存儲一組具有相同類型的數(shù)據(jù)。
  • 線性表(Linear List)。顧名思義,線性表就是數(shù)據(jù)排成像一條線一樣的結(jié)構(gòu)。每個線性表上的數(shù)據(jù)最多只有前和后兩個方向。其實除了數(shù)組,鏈表、隊列、棧等也是線性表結(jié)構(gòu)?!揪€性表只有前后有聯(lián)系】
img
  • 與之相反的,在非線性表中,數(shù)據(jù)不是簡單的前后關(guān)系
img
  • 連續(xù)的內(nèi)存空間和相同類型的數(shù)據(jù)

  • 不精確:鏈表適合插入、刪除,時間復雜度O(1);數(shù)組適合查找,查找時間復雜度為O(1)

  • 精確:數(shù)組是適合查找操作,但是查找的時間復雜度并不為O(1)。即便是排好序的數(shù)組,你用二分查找,時間復雜度也是O(logn)。所以,正確的表述應(yīng)該是,數(shù)組支持隨機訪問,根據(jù)下標隨機訪問的時間復雜度為O(1)。

  • 數(shù)組刪除的優(yōu)化:將多次刪除操作集中在一起執(zhí)行,每次刪除都是標記一待刪除的數(shù)據(jù),真正的刪除等到內(nèi)存不夠的時候一口氣進行,同時這也是java中的JVM刪除

數(shù)組越界:黃金體驗鎮(zhèn)魂曲--永遠無法到達的彼岸

int main(int argc, char* argv[]){
    int i = 0;
    int arr[3] = {0};
    for(; i<=3; i++){
        arr[i] = 0;
        printf("hello world\n");
    }
    return 0;
}
  • 數(shù)組其實就是一塊連續(xù)的內(nèi)存,當我們越界的時候就會訪問到其他內(nèi)存空間
  • 函數(shù)體內(nèi)的局部變量存在棧上,且是連續(xù)壓棧。在Linux進程的內(nèi)存布局中,棧區(qū)在高地址空間,從高向低增長。變量i和arr在相鄰地址,且i比arr的地址大,所以arr越界正好訪問到i。當然,前提是i和arr元素同類型,否則那段代碼仍是未決行為。
  • 組越界在C語言中是一種未決行為,并沒有規(guī)定數(shù)組訪問越界時編譯器應(yīng)該如何處理。因為,訪問數(shù)組的本質(zhì)就是訪問一段連續(xù)內(nèi)存,只要數(shù)組通過偏移計算得到的內(nèi)存地址是可用的,那么程序就可能不會報任何錯誤。
  • 當然這個具體還是要看編譯器與電腦具體情況,在我的CodeRunner上就是非常普通的崩潰了,在打印三次之后
  • 總之,如果是在符合上述入棧規(guī)則的編譯器中使用的話,就會出現(xiàn)無限打印的情況
  • 因為arr[3]訪問到的就是是i的地址,等于就是不停的在修改i的值,自然會導致無限循環(huán)
  • 數(shù)組尋址公示:a[i]_address = base_address + i * data_type_size

數(shù)組編號為什么從0開始,而不是從1開始

  • 我們已經(jīng)知道在內(nèi)存里面不存在真正的數(shù)組,只有一段內(nèi)存,我們有的只是首地址以及單位長度
  • 因此下標其實描述的是數(shù)組的編譯量,也就是從首地址開始的偏移程度
  • 當我們的下標是從0開始的時候,我們的尋址公式是:a[i]_address = base_address + i * data_type_size
  • 而當我們的下標是從1開始的時候,我們的尋址公式就會變成:a[k]_address = base_address + (k-1)*type_size
  • 這樣多了一個減法操作,對于CPU來說,就多了一個減法指令

課后題

前面我基于數(shù)組的原理引出JVM的標記清除垃圾回收算法的核心理念。我不知道你是否使用Java語言,理解JVM,如果你熟悉,可以在回顧下你理解的標記清除垃圾回收算法。

  • 大多數(shù)主流虛擬機采用可達性分析算法來判斷對象是否存活,在標記階段,會遍歷所有 GC ROOTS,將所有 GC ROOTS 可達的對象標記為存活。只有當標記工作完成后,清理工作才會開始。
  • 不足:1.效率問題。標記和清理效率都不高,但是當知道只有少量垃圾產(chǎn)生時會很高效。2.空間問題。會產(chǎn)生不連續(xù)的內(nèi)存空間碎片。

前面我們講到一維數(shù)組的內(nèi)存尋址公式,那你可以思考一下,類比一下,二維數(shù)組的內(nèi)存尋址公式是怎樣的呢?

  • 對于 m * n 的數(shù)組,a [ i ][ j ] (i < m,j < n)的地址為:address = base_address + ( i * n + j) * type_size
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一些概念 數(shù)據(jù)結(jié)構(gòu)就是研究數(shù)據(jù)的邏輯結(jié)構(gòu)和物理結(jié)構(gòu)以及它們之間相互關(guān)系,并對這種結(jié)構(gòu)定義相應(yīng)的運算,而且確保經(jīng)過這...
    Winterfell_Z閱讀 6,603評論 0 13
  • [數(shù)據(jù)結(jié)構(gòu)與算法之美:如何分析、統(tǒng)計算法的執(zhí)行效率和資源消耗?(03)] 一、如何分析、統(tǒng)計算法的執(zhí)行效率和資源消...
    luke_閱讀 949評論 0 0
  • 早上的一杯清咖,到現(xiàn)在小心臟還直突突,下午練了會兒古箏,看了會兒書,彬哥想倦著我再睡個午覺,可我困意全無,閑來無事...
    雙魚鑫炎閱讀 432評論 7 6
  • 2015時裝周珍珠成為了配飾的搶手貨, 每到時裝周的時候最讓人期待的還是細節(jié)圖片,細節(jié)處正是體現(xiàn)品牌品質(zhì)的地方。雖...
    D3舍閱讀 482評論 0 2
  • 誰說我們是流星轉(zhuǎn)瞬即逝,要遇到就要等下一個永恒; 誰說我們是溪流形單影只,要匯聚才能成為一股力量; 你們說的滿身規(guī)...
    字母Reddish閱讀 185評論 0 0

友情鏈接更多精彩內(nèi)容