一些常用的演算法技巧

野生純情的小獅子發表於2019-03-01

1、巧用陣列下標

例:在統計一個字串中字幕出現的次數時,我們就可以把這些字母當做陣列下標,在遍歷的時候,如果字母A遍歷到,則arr[`a`]就可以加1了。

法一:利用物件的key值不重複

var str = `hajshdjldjf`;
function count(str){
    var obj = {};
    for(var i = 0; i < str.length; i++){
        if(obj[str[i]]){
            obj[str[i]]++;
        }else{
            obj[str[i]] = 1;
        }
    }
    console.log(obj);
    return obj;
}
count(str);複製程式碼

法二:利用陣列的下標

var str = `hajshdjldjf`;
function count(str){
    var arr = [];
    for(var i = 0; i < str.length; i++){
        if(arr[str[i]]){
            arr[str[i]]++;
        }else{
            arr[str[i]] = 1;
        }
    }
}
count(str);複製程式碼

其實這兩種方法的思想是一致的。

例:給你n個無序的int整型陣列arr,並且這些整數的取值範圍都在0-20之間,要你在 O(n) 的時間複雜度中把這 n 個數按照從小到大的順序列印出來。

對於這道題,如果你是先把這 n 個數先排序,再列印,是不可能O(n)的時間列印出來的。但是數值範圍在 0-20。我們就可以巧用陣列下標了。把對應的數值作為陣列下標,如果這個數出現過,則對應的陣列加1。

利用物件:

var arr = [1,2,3,4,5,4,5,6,6,7,6,9,17,16,15,14,12,11];
//常規解法  利用物件的key值不能重複去計算次數
//res去記錄數字出現的順序
function fn(arr){
    arr.sort(function(a,b){
        return a - b;
    });
    var res = [];
    var resdetail = [];
    for(var i = 0; i < arr.length; i++){
        if(res.length === 0 || res[res.length-1] !== arr[i]){
            res.push(arr[i]);
            var obj = {
                key:arr[i],
                value:1
            };
            resdetail.push(obj);
        }else{
            resdetail[resdetail.length-1].value++;
        }
    }
    console.log(resdetail);
    return resdetail;


}
fn(arr);複製程式碼

利用陣列下標

var arr = [1,2,3,4,5,4,5,6,6,7,6,9,17,16,15,14,12,11];
//利用陣列下標  時間複雜度為O(n)
function fn(arr){
    var temp = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
    for(var i = 0; i < arr.length; i++){
        temp[arr[i]]++;
    }
    for(var j = 0; j < 21; j++){
        for(var k = 0; k < temp[j]; k++){
            console.log(j);
        }
    }
}
fn(arr);複製程式碼

2、巧用取餘

有時候我們在遍歷陣列的時候,會進行越界判斷,如果下標差不多要越界了,我們就把它置為0重新遍歷。特別是在一些環形的陣列中,例如用陣列實現的佇列。往往會寫出這樣的程式碼:

for (int i = 0; i < N; i++)
{
    if (pos < N) {
        //沒有越界
        // 使用陣列arr[pos]
    else
        {
            pos = 0;//置為0再使用陣列
            //使用arr[pos]
        }
        pos++;
    }
}複製程式碼

實際上我們可以通過取餘的方法來簡化程式碼

for (int i = 0; i < N; i++) {
    //使用陣列arr[pos]   (我們假設剛開始的時候pos < N)
    pos = (pos + 1) % N;
}複製程式碼

3、巧用雙指標

對於雙指標,在做關於單連結串列的題是特別有用,比如“判斷單連結串列是否有環”、“如何一次遍歷就找到連結串列中間位置節點”、“單連結串列中倒數第 k 個節點”等問題。對於這種問題,我們就可以使用雙指標了,會方便很多。

(1)判斷單連結串列中是否有環

我們可以設定一個快指標和一個慢指標,慢的一次移動一個節點,快的一次移動兩個節點。如果存在環,快指標會在第二次遍歷時和慢指標相遇。

(2)如何一次遍歷就找到連結串列中間位置節點

一樣是設定一個快指標和慢指標。慢的一次移動一個節點,快的一次移動兩個。當快指標遍歷完成時,慢指標剛好到達中點。

(3)單連結串列中倒數第 k 個節點

設定兩個指標,其中一個指標先移動k-1步,從第k步開始,兩個指標以相同的速度移動。當那個先移動的指標遍歷完成時,第二個指標指向的位置即倒數第k個位置。

4、巧用位移

有時候我們在進行除數或乘數運算的時候,例如n / 2,n / 4, n / 8這些運算的時候,我們就可以用移位的方法來運算了,這樣會快很多。

例如:

n / 2 等價於 n >> 1

n / 4 等價於 n >> 2

n / 8 等價於 n >> 3。

這樣通過移位的運算在執行速度上是會比較快的,也可以顯的你很厲害的樣子,哈哈。

還有一些 &(與)、|(或)的運算,也可以加快運算的速度。例如判斷一個數是否是奇數,你可能會這樣做

if(n % 2 == 1){
    dosomething();
}複製程式碼

不過我們用與或運算的話會快很多。例如判斷是否是奇數,我們就可以把n和1相與了,如果結果為1,則是奇數,否則就不會。即

if(n & 1 == 1){
    dosomething();
)複製程式碼

5、設定哨兵位

在連結串列的相關問題中,我們經常會設定一個頭指標,而且這個頭指標是不存任何有效資料的,只是為了操作方便,這個頭指標我們就可以稱之為哨兵位了。

例如我們要刪除頭第一個節點是時候,如果沒有設定一個哨兵位,那麼在操作上,它會與刪除第二個節點的操作有所不同。但是我們設定了哨兵,那麼刪除第一個節點和刪除第二個節點那麼在操作上就一樣了,不用做額外的判斷。當然,插入節點的時候也一樣。

有時候我們在運算元組的時候,也是可以設定一個哨兵的,把arr[0]作為哨兵。例如,要判斷兩個相鄰的元素是否相等時,設定了哨兵就不怕越界等問題了,可以直接arr[i] == arr[i-1]?了。不用怕i = 0時出現越界。

6、與遞迴相關的一些優化

(1)對於可以遞迴的問題考慮狀態儲存。

當我們使用遞迴來解決一個問題時,很容易產生重複去算同一個子問題,這個時候我們要考慮狀態儲存以防止重複計算。

例:斐波那契數列

function fn(n){
    if(n <= 2){
        return 1;
    }else{
        return fn(n-1) + fn(n-2);
    }
}
console.log(fn(10));複製程式碼

不過對於可以使用遞迴解決的問題,我們一定要考慮是否有很多重複計算。顯然對於 f(n) = f(n-1) + f(n-2) 的遞迴,是有很多重複計算的。如

一些常用的演算法技巧

就有很多重複計算了。這個時候我們要考慮狀態儲存。並且可以自底向上。

function fn(n){
    var res = [];
    res[0] = 1;
    res[1] = 1;
    for(var i = 2; i < n; i++){
        res[i] = res[i-1] + res[i-2];
    }
    console.log(res[n-1]);
    return res[n-1];
}
fn(10);複製程式碼

進一步優化:使用兩個變數。

function fn(n){
    var a = 1;
    var b = 1;
    for(var i = 3; i <= n; i++){
        a = a + b;
        b = a - b;
    }
    return a;
}
fn(10);複製程式碼

相關文章