少年,我看你骨骼精奇,見與你有緣,這套演算法贈你

chenhongdong發表於2018-04-20

講個故事

從身邊的好朋友那裡聽說一個做程式的大哥,主要就是寫演算法的,然後從上家公司離職的時候,公司要求籤訂保密協議,並保證如果沒有洩密的情況下,每月依然會發工資,持續兩年

What?對,你沒聽錯,就是這樣,喵。為什麼呢?因為他會演算法啊,演算法是根基,此處省略N多字,我也不會告訴你他年薪接近百萬了。看到了吧,這就是我們要不斷學習演算法的小目標,先掙它一個億!

演算法的世界

Hi,歡迎來到演算法的世界,演算法無處不在,其實就在我們身邊

  • 《社交網路》中祖克伯在創立Facebook之初靠著數學公式,寫出了對比兩個女孩照片誰更漂亮的演算法
  • 阿爾法狗打敗了圍棋世界冠軍
  • 進入X寶,會推薦你喜歡的商品
  • 點個外賣,下次會推薦你常點的店家
  • 打個農藥,會盡量匹配和你選擇英雄相同的玩家
  • N多栗子,全舉出來可能掘金都容不下這篇文章了

以上這些身邊的例子其實都是依靠著演算法來實現的

演算法之於程式,是質變的過程(其實就是1分鐘變1秒鐘的差距)

程式 = 演算法 + 資料結構

現在火熱的AI技術,也並不是突然出現的,也是靠著之前積累的各種演算法組合試驗去執行的。

演算法如同機器人的大腦,告訴機器人如何行動,思考等一系列行為。不然你以為,它們天生麗質難自棄嘛,那是不可能的

說了那麼多,那我們為什麼要學演算法呢?為了掙它一個億?可以是,也可以不是

沒有一身好內功,招式再多都是空

對於我們大自然的搬運工(程式設計師)來說,演算法可以讓你的程式更加高效

好了,難道這還不夠你臭屁的嘛!

下面開始今天的主題,先從簡單的排序和搜尋演算法說起,因為很多中、高階面試裡都會問到。連這些都答不上來還怎麼當資深,做專家,掙它一個億!

排序和搜尋演算法

排序演算法

常用的排序演算法有很多,如氣泡排序、選擇排序、插入排序、歸併排序、快速排序以及堆排序,下面我們就針對這幾個排序演算法來看看,瞭解一下思想

氣泡排序

// 建立一個陣列列表來處理排序和搜尋的資料結構
function ArrayList() {
    let arr = [];   
    
    this.insert = function(item) {  // 將資料插入到陣列中
        arr.push(item);
    };
    
    this.show = function() {    // 展示陣列的結構
        return arr.join(' < ');
    };

    // 氣泡排序     眼熟?沒錯,面試裡常考,而且它是排序演算法中最簡單的
    this.bubbleSort = function () {
        let len = arr.length;
        for (let i = 0; i < len; i++) { 
            // 這裡之所以再-i,是因為外層迴圈已經跑完一輪
            // 內迴圈就沒有必要再比較一回了
            for (let j = 0; j < len - 1 - i; j++) {  
                if (arr[j] > arr[j + 1]) {  // 當前項和下一項做比較,如果大於的話就按照下面交換位置
                    // ES6利用解構賦值的方式輕鬆實現j和j+1值的交換
                    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                }
            }
        }
    };
}

// 測試用例,此測試用例在之後的演算法中皆可使用
let arr = [5, 4, 3, 2, 1];
let createList = function(arr) {
    let list = new ArrayList();
    for (let i = 0; i < arr.length; i++) {
        list.insert(arr[i]);
    }
    return list;
};
let item = createList(arr);
console.log(item.string()); // 排序前 5 < 4 < 3 < 2 < 1
item.bubbleSort();
console.log(item.string()); // 排序後 1 < 2 < 3 < 4 < 5
複製程式碼

從實現方式來看氣泡排序是最簡單的一種,而且從執行時間的角度來看,氣泡排序是最差的一個,因為做了兩層迴圈導致,複雜度為O(n^2)了

下面再給大家看一下氣泡排序的工作流程:

少年,我看你骨骼精奇,見與你有緣,這套演算法贈你
既然是最差的排序,那麼我們就繼續往下看,看看挖掘技術哪家強?一個一個的來比較一番

選擇排序

所謂選擇排序演算法,其實就是拿陣列中的一個值做標杆,和其他的值做比較,如果比標杆還小的就替換標杆的值,以此類推就可以了。不多囉嗦直接上程式碼

function ArrayList() {
    // 省略...
    // 選擇排序
    this.selectSort = function () {
        let len = arr.length,
            min;
        for (let i = 0; i < len - 1; i++) {
            min = i;    // 我們取第一個值當標杆
            for (let j = i; j < len; j++) { // 內部迴圈從i開始到陣列結束
                if (arr[min] > arr[j]) {    // 比標杆的值還小,就替換新值
                    min = j;
                }
            }
            if (i !== min) {    // 上面經過一頓比較替換,如果標杆的值和之前取的第一個值不同了,就交換位置
                [arr[i], arr[min]] = [arr[min], arr[i]];
            }
        }
    };
}
複製程式碼

選擇排序我們看到其實也是巢狀了兩層迴圈的操作,這樣的話,其實複雜度和氣泡排序是一樣的了,我們先來看下選擇排序的工作流程吧

少年,我看你骨骼精奇,見與你有緣,這套演算法贈你
相比兩層迴圈的冒泡和選擇排序,接下來介紹的插入排序效能上會好了很多

插入排序

插入排序演算法是假定陣列的第一項已經是排好序的了,直接用它和第二項去比較,如果比第二項大就將索引和值都進行交換,以此來推來實現排序

function ArrayList() {
    // 省略...
    // 插入排序
    this.insertSort = function () {
        let len = arr.length,
            index, tmp;
        // 這裡預設第一項已經排序了,直接從第二項開始
        for (let i = 1; i < len; i++) {
            index = i;        // 用來記錄一個索引 
            tmp = arr[i];   // 儲存一個臨時變數,方便之後插入位置
            // 索引必須是大於0,並且陣列前一項的值如果大於臨時變數的值
            // 就將前一項的值賦給當期項,並且index--
            while (index > 0 && arr[index - 1] > tmp) { 
                arr[index] = arr[index - 1];   
                index--;  
            }
            arr[index] = tmp; // 最後在一頓替換後插入到了正確的位置上
        }
    };
}

// 測試用例寫在氣泡排序那裡
let arr = [3, 5, 1, 4, 2];
// 按照這個陣列,先來分析前兩項,剩下的大家自己來推到即可了
/*
    Tips: -> 符號為最後對應的值
    進入for迴圈i從1開始那此時
        index = i -> 1;  next -> 2
        tmp = arr[1] -> 5;  next -> arr[2] -> 1
        while迴圈條件   1 > 0 && (arr[1-1]-> arr[0] -> 2) > 5
                                    不成立繼續迴圈
                                    next  2>0 && 5 > 1
                                    next  1>0 && 3 > 1
            arr[2] = arr[1] -> 5
            arr[1] = arr[0] -> 3
            index--;  index -> 1  index -> 0
        最後
        arr[index] = tmp -> arr[1] = 5;   next  arr[1] = 1  arr[0] = 1
        
        就實現了值的交換
        次時arr為[1, 3, 5, 4, 2]
        剩下的以此類推即可了
*/
複製程式碼

還是老樣子,依然展示一下插入排序的工作流程:

少年,我看你骨骼精奇,見與你有緣,這套演算法贈你
插入排序顯然已經比冒泡和選擇排序演算法在效能上好很多了,不過有一點小遺憾啊,就是排序的陣列不要太大,太大了它就受不鳥啦!

有時候想想同樣是排序演算法,怎麼這三種演算法是這也不行,那也不行。試問有沒有靠譜的呢,大家時間寶貴不能耽誤的。答案當然是肯定的,有,來看

歸併排序

這裡首先緬懷一位偉大的人物,馮·諾伊曼,‘計算機之父’。之所以要緬懷是因為這個靠譜的排序演算法就是由這個開了掛的人提出的。恩,此時此景,我想吟詩一首,不簡單啊,不簡單!

function ArrayList() {
    // 省略...
    
    // 歸併排序
    this.mergeSort = function () {
        arr = mergeRecurve(arr);    // 由於需要不停的拆分直到陣列只有一項,所以使用遞迴來做
    };
    // 遞迴
    function mergeRecurve(arr) {
        let len = arr.length;
        // 遞迴的停止條件,如果陣列只有一項,就直接返回了
        // 這也是我們遞迴的目的,直到陣列只有一項
        if (len === 1) return arr;      
        let mid = Math.floor(len / 2);
        let left = arr.slice(0, mid);
        let right = arr.slice(mid, len);    // 到這裡把陣列一分為二

        // 為了不斷對原陣列拆分,對left和right陣列繼續遞迴,並作為引數傳給merge函式
        // merge函式負責合併和排序小陣列為大陣列
        return merge(mergeRecurve(left), mergeRecurve(right));  
    }
    function merge(left, right) {   // 接收兩個陣列,最後合併到一起返回一個大陣列
        let res = [],
            lLen = left.length,
            rLen = right.length,
            l = 0,
            r = 0;

        while (l < lLen && r < rLen) {
            // 如果left陣列的項比right陣列的項小的話,就將left這裡小的項新增到大陣列裡
            if (left[l] < right[r]) {   
                res.push(left[l++]);    // 並繼續下一項比較
            } else {
                res.push(right[r++]);   // 否則將right裡小的項先新增到大陣列中
            }
        }
        // 將left和right陣列中剩餘的項也都新增到大陣列中
        while (l < lLen) {
            res.push(left[l++]);
        }
        while (r < rLen) {
            res.push(right[r++]);
        }

        return res;  // 返回排好序的大陣列
    }
}    
複製程式碼

歸併排序採用的是一種分治演算法,不明白分治演算法?很簡單,你就當做是二分法。之後會講,就是將一個大問題拆成小問題來解決,等每個小問題都解決了,大問題也就搞定了

實現思想:將原陣列切分成小陣列,直到每個陣列只有一項,然後再將小陣列合併成大陣列,最後得到一個排好序的大陣列。可以看下圖的執行過程加深理解

少年,我看你骨骼精奇,見與你有緣,這套演算法贈你
如果覺得歸併排序演算法不好理解,我們準備了簡單的測試,挑個重點來分析一下

let arr = [7, 9, 8];    // 就選3個值的陣列來簡單分析吧
/*
    arr = mergeRecurve([7, 9, 8])
        二分拆分後,left = [7], right = [9, 8]

        merge(mergeRecurve([7]), mergeRecurve([9, 8]))
          [7]這個已經拆分完畢,先放著不管
          由於陣列並不是只有一項,所以[9, 8]繼續遞迴
              拆分後得到,left = [9], right = [8]
              merge([9], [8])
                 res = [], l = 0, r = 0
                 while (0<1 && 0<1) {
                     if (9 < 8) {
                        不走這裡 
                     } else {
                         res.push(8)
                     }
                 }
                 while (0<1) {
                     res.push(9);
                 }
                 while(1<1) {
                    不走這裡
                 }
                 return [8, 9]
        再回到上一層處理merge([7], [8, 9]),推到過程同上
*/
複製程式碼

少年,看到這裡,我不得不說你果然骨骼驚奇,接下來放個大招,來介紹一下最常用的排序演算法-快速排序,它的效能要比其他複雜度為O(nlog^n)的排序演算法都好

快速排序

快速排序演算法,需要在陣列中找到一箇中間項作為標記,然後建立雙指標,左邊第一項,右邊最後一項。移動左指標直到比標記的值大,再移動右指標直到比標記小,然後交換位置,重複此過程,實現一個劃分的操作

function ArrayList() {
    // 省略...
    // 快速排序
    this.quickSort = function () {
        quick(arr, 0, arr.length - 1);  // 遞迴
    }
    function quick(arr, left, right) {
        let index;
        if (arr.length > 1) {
            index = partition(arr, left, right);  // 劃分

            if (left < index - 1) {
                quick(arr, left, index - 1)
            }
            if (index < right) {
                quick(arr, index, right);
            }
        }
    }
    // 劃分函式
    function partition(arr, left, right) {
        let point = arr[Math.floor((left+right)/2)],
            i = left,
            j = right;  // 雙指標
        
            while (i <= j) {
                while (arr[i] < point) {
                    i++;
                }
                while (arr[j] > point) {
                    j--;
                }
                if (i<=j) {
                    [arr[i], arr[j]] = [arr[j], arr[i]];  // 交換位置
                    i++;
                    j--;
                }
            }
            return i;
    }
}
複製程式碼

直接看程式碼可能有點懵,but不用怕,看見蟑螂也不怕不怕了。我們來具體問題具體分析一下就好了,直接看個簡單的栗子

/*
let arr = [7, 9, 8];
quickSort
    // 遞迴
    quick([7, 9, 8], 0, 2)  // 傳遞開頭和末尾,兩個位置
    index   // 用來分離小值陣列和大值陣列,一左一右
    length > 1
        對子陣列進行劃分,第一次傳入的是原陣列[7, 9, 8]
        
        partition(arr, left, right) 用來劃分位置
        point = [7, 9, 8][Math.floor(0+2)/2] -> 9
        i -> 0, j -> 2    
        // i 和 j 其實就是雙指標,一個從左開始,一個從右開始

        while (i <= j) { 0 <= 2
            // 第一次 7 < 9
            // 下一次 arr[1] -> 9  9 < 9不成立
            while (arr[i] < 9) { 
                i++;    i -> 1
            }
            // arr[2] -> 8 > 9不成立
            while (arr[j] > 9) {
                j--;
            }
            // 此時i->1, j->2
            if (i <= j) {   1 <= 2
                // 交換位置
                [arr[i], arr[j]] = [arr[2], arr[1]] -> [8, 9]
                i++;    i -> 2
                j--;
            }
            // 劃分重排後的陣列為[7, 8, 9]
            return i; 返回劃分的座標i -> 2
        }

        此時計算出了index為2
        if (left < index - 1) { 0 < 2-1
            // 繼續遞迴
            quick([7, 8, 9], 0, 1);
            // 這步遞迴完後,index->1,right->-1
        }
        if (index < right) {
            quick([7,8,9], 2, 3);
        }
    按照以上步驟繼續推到即可理解了,加油加油
*/
複製程式碼

此時此刻,是不是該來一杯香濃的咖啡來提提神了呢?沒有嗎?沒關係,那就抬起自己的右手慢慢向上抬起,高過頭頂,然後......使勁拍一下腦袋,這下大家滿足了吧,神清氣爽,暢快淋漓了!哈哈,不扯淡了,不扯淡了

上面我們就聊了一下排序演算法的幾種不同方式,可以說是實現了初步勝利吧,演算法的路還很長,路漫漫其修遠兮,吾將上下而求索。大家一起共勉,下面再來聊聊搜尋演算法

請看大螢幕

搜尋演算法

搜尋演算法這裡著重講的就是面試中常被問到的二分查詢法

我們大家都有用過array.indexOf方法,它的用法和字串裡的indexOf是一樣的,都是返回所需元素的位置,沒找到就是-1

說白了,這其實也算做一種搜尋,只不過它的實現其實很low,不夠高效,來看程式碼

let arr = [1, 9, 8, 2, 3];  
arr.indexOf(8);     // 2
arr.indexOf(0);     // -1

function ArrayList() {
    // 省略...
    // indexOf
    this.indexOf = function(item) {
        for (let i = 0; i < arr.length; i++) {
            if (item === arr[i]) {
                return i;
            }
        }
        return -1;
    };
}
複製程式碼

這樣的程式碼實際上是按照順序一個一個去做對比的,效率上有些低下。還是來看看我們的重點保護物件,二分查詢法吧

二分查詢法

看過早期李詠的《幸運52》的觀眾應該記得一個環節,節目會給嘉賓上一款商品,讓他在一定時間內來猜價格,根據報價,李詠會說高了或者低了,直至回答正確拿到商品為止

沒看過的朋友們,沒關係,就是個猜價格的遊戲,會說高了還是低了or對了

懷念當時還是個小盆友,無憂無慮的小盆友,哈哈

看程式碼

function ArrayList() {
    // 省略...
    // 二分查詢法
    this.binarySearch = function (item) {
        this.quickSort();   // 先來排個序,方便查詢
        let low = 0,
            high = arr.length - 1,
            mid, ele;

        while (low <= high) {
            mid = Math.floor((low + high) / 2); // 一刀切,取中間
            ele = arr[mid];     // 用中間值去和搜尋值item比較
            if (ele < item) {   // 如果中間值是4,要查的是5
                // 那就將左側low的指標升級變成從中間加1項那裡查詢
                // 就是在高價位那裡查
                low = mid + 1; 
            } else if (ele > item) { // 中間值還是4,要查的變成3了
                // 比中間值還小,那就在靠左側低價位裡面了
                // 將右側指標變成中間值-1的位置,控制了最大範圍
                high = mid - 1;  
            } else {    // 中間值是4,你查的正好也是4,那麼,bingo,返回
                return mid;
            }
        }
        return -1;  // 查不到的當然就-1了,這還用說嘛
    };
}
複製程式碼

二分查詢法也聊完了,節目不早,時間剛好了。讓我們一起來梳理一下上面的內容吧

梳理一下

排序演算法

  • 氣泡排序 -> 巢狀兩層迴圈,和下面的選擇排序一樣效能不好
  • 選擇排序 -> 樓上的沒腦子
  • 插入排序 -> 相比樓上兩位,它的效能會好很多,不過只適用於資料量少的陣列
  • 歸併排序 -> 很有來頭,被認可的排序演算法
  • 快速排序 -> 很常用的排序演算法,效能也很好

搜尋演算法

  • 二分查詢法 -> 猜猜猜,押大押小,小了就在小的一側找,大了就去大的一側找

好了,以上內容在面試中、高階前端的時候也會被問到,在這裡也希望看過此文章的同學,面試順利,找到滿意的工作

演算法很有用,提高工作效率,重構的時候也很有效果

所以之後我也會繼續研究學習演算法的內容,好繼續和大家分享,感謝各位了,騷年們!

相關文章