基礎演算法

寒東設計師發表於2018-03-25

起手詩

從今天起,做一個牛X的人,早起,健身,修煉演算法
從今天起,關心程式碼質量,我有一個夢想,朝九晚五,年薪百萬
從今天起,和每一個親人通訊,告訴他們我的決心
那成功的天使告訴我的
我想告訴每一個人
給每一個檔案、每一個變數取一個溫暖的名字
陌生人,我也為你祝福
願你有一個燦爛的前程
願你的頭髮不再減少
願你明天仍是公司棟樑
而我,只願朝九晚五,年薪百萬

第一天 【2018-03-25】

寫一個函式判斷一個括號表示式是否平衡,例如:balance('[()') = false,balance('[()()[]]') = true。

function balance(str) {
    let [first, ...others] = str;
    let stack = [first]
    while(others.length) {
        let stact_last = stack[stack.length-1];
        let others_first = others.shift();
        if(!if_match(stact_last,others_first)) {
            stack.push(others_first);
        }else{
            stack.pop();
        }
        return stack.length?false:true;
    }
}
function if_match(left,right) {
    return (left === '[' && right === ']') || (left === '(' && right === ')');
}
複製程式碼

思路:
      這個解法基於棧(後進先出)。首先,如果只有一個字元,則必然不平衡。
      如果大於一個字元。我們把字串中的每一個字元取出並依次放入中,假如最新放入中的字元與的最後一個字元匹配(match方法返回true),則刪掉中的最後一個。
      就這樣把字串迴圈一遍後,如果中的字元被刪光,則說明這個字串是平衡的。

基礎演算法
      如圖,我們可以把它想象成“消消樂”,字串中的字元依次放到中,如果相鄰兩個不匹配,則放在的頂端;如果匹配,則“消掉”。

第二天 【2018-03-26】

實現一個斐波那契數列生成函式,fibonacci(10) = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

遞迴解法:

      斐波那契數列的前兩項是1,後面的每一項都等於前兩項之和,所以用遞迴很容易實現,不過需要注意效能問題。
      如果直接遞迴的話會出現大量重複計算,試想遞迴數字10的時候會依次遞迴數字9、8、7、6……,但是當再遞迴數字9的時候會在遞迴數字8、7、6……,其中8、7、6……被多次遞迴。所以宣告瞭一個catch陣列用來快取,遞迴之前判斷如果該項沒有值才會進行遞迴。

function fibonacciCatch(num) {
    let cache = [1, 1];
    (function fibonacci(n) {
        return typeof cache[n] == 'number' ? cache[n] : (cache[n] = fibonacci(n - 1) + fibonacci(n - 2))
    })(num - 1);
    return cache;
}
console.log(fibonacciCatch(10))   //[ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ]
複製程式碼
Generator解法:

      下面是另一種解法,這種解法不存在多次計算問題,思路是:
      首先記錄前兩個值,之後的每一個yeild都返回前兩個值之和,當然還要更新前兩個值為最新的。
      對generator不瞭解的同學可以參考阮一峰老師的書籍:es6入門-generator

function* fibonacci(n) {
    let a = 1, b = 1
    yield a
    yield b
    while (true) {
        const t = b
        b = a + b
        a = t
        yield b
    }
}
let it = fibonacci()
let result = Array.from(Array(10), it.next, it).map(x => x.value)
console.log(result)  //[ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ]
複製程式碼

第三天 【2018-03-27】

寫一個方法,求任意長度陣列的笛卡爾積。例如:cartesian([[1,2],['a','b']]) = [[1,'a'],[1,'b'],[2,'a'],[2,'b']]

笛卡爾乘積是指在數學中,兩個集合X和Y的笛卡尓積(Cartesianproduct)又稱直積,表示為X×Y,第一個物件是X的成員而第二個物件是Y的所有可能有序對的其中一個成員

function cartesion(...arys) {
    if (arys.length == 0) return []
    if (arys.length == 1) return arys
    return arys.reduce((prev, item) => {
        let temp = []
        for (let i = 0; i < prev.length; i++) {
            for (let j = 0; j < item.length; j++) {
                temp.push(Array.isArray(prev[i]) ? [...prev[i], item[j]] : [prev[i], item[j]])
            }
        }
        return temp
    })
}
console.log(cartesion([1, 2, 3], ['a', 'b'], ['x', 'y']))
//[[1, 'a', 'x'],[1, 'a', 'y'],[1, 'b', 'x'],[1, 'b', 'y'],[2, 'a', 'x'],[2, 'a', 'y'],[2, 'b', 'x'],[2, 'b', 'y'],[3, 'a', 'x'],[3, 'a', 'y'],[3, 'b', 'x'],[3, 'b', 'y']]
複製程式碼

      思路:
      這道題主要是訓練reduce的用法,我們依次組合兩個陣列中的每一項,然後把結果作為一個整體繼續和接下來的陣列進行組合。
      這裡還需要注意es6擴充套件符...的用法,大家可以把這個符號去掉再執行一遍程式,看看有什麼不同。

第四天 【2018-03-28】

假設有一個陣列,它的第 i 個元素是一個給定的股票在第i 天的價格。設計一個演算法來找到最大的利潤。你可以完成儘可能多的交易(多次買賣股票)。然而,你不能同時參與多個交易(你必須在再次購買前出售股票)。

var maxProfit = function (prices) {
    if (!prices.length) return 0 //越界判斷
    var max = 0
    for (var i = 0; i < prices.length; i++) {
        var diff = prices[i] - prices[i - 1]
        if (diff > 0) {
            max += diff
        }
    }
    return max
};
複製程式碼

      思路是:
      1、宣告一個max記錄利潤
      2、從第二項開始迴圈陣列,判斷明天的價格是否高於今天
      3、如果高於前一天,為了利潤最大,今天應該買入,所以最大利潤max增加相應的差價

第五天 【2018-03-29】

今天是一個二叉查詢樹,地址在這裡:二叉查詢樹的概念及js實現,分兩天寫。

第六天 【2018-03-30】

今天還是寫二叉樹,地址在這裡:二叉查詢樹的概念及js實現


第七天 【2018-04-01】

今天寫一個超級簡單的氣泡排序:

function bubbleSort(ary) {
    for (let i = 0; i < ary.length; i++) {
        for (let j = i + 1; j < ary.length; j++) {
            if (ary[i] > ary[j]) {
                //交換位置
                let temp = ary[i]
                ary[i] = ary[j]
                ary[j] = temp
            }
        }
    }
    return ary
}
console.log(bubbleSort([1, 9, 4, 2, 55, 3, 0, -14]))   //[ -14, 0, 1, 2, 3, 4, 9, 55 ]
複製程式碼

      思路:氣泡排序的思路就是兩個迴圈,就是讓陣列中的每一項都與後面所有的數比較,如果這一項比後面的數字大,則交換位置。
      氣泡排序的思路簡單易於理解,但是它的時間複雜度是所有排序中最高的O(n²),這種排序方法基本不會在專案中使用。

第八天 【2018-03-31】

今天再寫一個簡單的選擇排序:

function slectionSort(ary) {
    let min
    for (var i = 0; i < ary.length - 1; i++) {
        min = i
        for (var j = i + 1; j < ary.length; j++) {
            if (ary[j] < ary[min]) {
                min = j
            }
        }
        let temp = ary[min]
        ary[min] = ary[i]
        ary[i] = temp
    }
    return ary
}
console.log(slectionSort([1, 9, 4, 2, 55, 3, 0, -14]))   //[ -14, 0, 1, 2, 3, 4, 9, 55 ]
複製程式碼

      思路:
      選擇排序的思路跟冒泡相似,都是兩個for迴圈。
      冒泡是比較前後兩個交換位置,而選擇排序是先找到後面所有數字最小的,再與當前數字交換位置。
      程式碼中min記錄了內層迴圈的最小值,迴圈結束,交換外層迴圈當前項與min項

第九天 【2018-04-02】

今天接著寫插入排序:

function insertSort(ary) {
    for(let i=1;i<ary.length;i++){
        let j = i;
        let temp = ary[i];
        //迴圈找到比它小的是第j-1項
        while(j>0 && ary[j - 1]>temp){
            ary[j] = ary[j-1]
            j--
        }
        //把第j項賦值為temp,相當於把temp插入到第j-1項後面了
        ary[j] = temp
    }
    return ary
}

複製程式碼

      哎,昨天雖然把插入排序寫了,但是沒有時間寫分析了,這是第二天補上的,罪過罪過。
      插入排序也屬於比較慢的排序,因為也是用了兩個迴圈。它的思路就是從第二項開始拿數字,然後用這個數字逐個與前面的數字比較,找到比它小的然後放到它後面。

第十天 【2018-04-03】

基本排序終於寫完了 --> 進化了 --> 今天寫一個高階的歸併排序:

      歸併排序是一個效能很好的排序演算法,Firefox瀏覽器的Array.prototype.sort就是歸併排序,Chrome瀏覽器用的是快速排序。

function mergeSort(ary) {
    if (ary.length == 1) {
        return ary
    }
    let mindle = Math.floor(ary.length / 2)
    let left = ary.slice(0, mindle)
    let right = ary.slice(mindle)
    return merge(mergeSort(left), mergeSort(right))
}
function merge(left, right) {
    let result = []
    let il = 0
    let ir = 0
    while (il < left.length && ir < right.length) {
        if (left[il] < right[ir]) {
            result.push(left[il++])
        } else {
            result.push(right[ir++])
        }
    }
    while (il < left.length) {
        result.push(left[il++])
    }
    while (ir < right.length) {
        result.push(right[ir++])
    }
    return result
}
console.log(mergeSort([1, 9, 4, 2, 55, 3, 0, -14]))   //[ -14, 0, 1, 2, 3, 4, 9, 55 ]
複製程式碼

      思路:歸併排序的思想是分而治之。我理解就是把一個陣列從中間無限分割,直到只剩一個元素,然後把這些陣列的item取出來放入最後的陣列中。
      放的過程就涉及到比較,也就是上面程式碼的第一個while迴圈,這裡面的程式碼保證了永遠把最小的先放入陣列中。
      之前有一個問題困擾這我,如果兩個陣列中有一個先被取光,那那個剩下的豈不是沒有排序?
      其實剩下的那個已經是排序好的了,因為遞迴的關係,陣列是從小逐漸變大的,所一之前的已經排好順序了。

第十一天 【2018-04-04】

今天寫快速排序:

      快排是處理大資料集最快的演算法之一,它和歸併排序一樣也是一種分而治之的演算法。

function fastSort(ary) {
    if (ary.length == 0) {
        return []
    }
    let pivot = ary[0]
    let left = [], right = []
    for (let i = 1; i < ary.length; i++) {
        let current = ary[i]
        if (current < pivot) {
            left.push(current)
        } else {
            right.push(current)
        }
    }
    return fastSort(left).concat(pivot, fastSort(right))

}
console.log(fastSort([1, 9, 4, 2, 55, 3, 0, -14]))   //[ -14, 0, 1, 2, 3, 4, 9, 55 ]
複製程式碼

      思路:
      快排採用的是遞迴的方法,將資料依次分解為較小的資料集合(left)和較大的資料集合(right),最後把這左中右三個陣列組合在一起。
      快排應用廣泛,但是我的一個演算法大牛兄弟曾經偷偷告訴我,當資料少的時候快排相對慢,可以使用歸併排序進行優化。

第十二天 【2018-04-05】

寫一個簡單的只能搜尋:根據“80-20”原則,我們常用的資料其實只佔20%,所以希望隨著搜說次數的增加能夠使我們能夠更快的搜到常用資料:

function seqSearch(ary, data){
    for(let i = 0; i< ary.length; i++){
        if(ary[i] == data){
            if(i > 0){
                let temp = ary[i]
                ary[i] = ary[i-1]
                ary[i-1] = temp
            }
            return true
        }
    }
    return false
}
複製程式碼

      思路:
      這個演算法很簡單,如果搜到這個資料返回true,否則返回false。只不過如果搜到的話會把這個資料向前移動一位,這樣隨著搜尋次數的增加,常用的資料會一直往前移動,從而逐漸減少搜尋次數。

第十三天 【2018-04-06】

今天寫一個二分搜尋。

function binSearch(ary, data) {
    let up = ary.length - 1
    let down = 0
    while (down <= up) {
        let min = Math.floor((up + down) / 2)
        if (ary[min] < data) {
            down = min + 1
        } else if (ary[min] > data) {
            up = min - 1
        } else {
            return data
        }
    }
    return -1
}

複製程式碼

      思路:
      二分搜尋的前提條件是資料必須有序,起初設定一個上限和下限,然後通過迴圈比較中間的數字和資料的大小來縮小查詢範圍。

  • 如果中間的數字比資料小,那麼就把新的下限設定為中間的索引+1。
  • 大的話就把上限設定為索引-1。
  • 如果相等則返回資料。

第十四天 【2018-04-07】

給定一個整數陣列,除了某個元素外其餘元素均出現兩次。請找出這個只出現一次的元素。你的演算法應該是一個線性時間複雜度。 你可以不用額外空間來實現它嗎?

var singleNumber = function(nums) {
    nums = nums.sort()
    for (var i = 0; i < nums.length; i = i + 2) {
        var prev = nums[i]
        var next = nums[i + 1]
        if (prev != next) {
            return prev
        }
    }
};
console.log(singleNumber([1, 2, 1, 3, 2, 222, 4, 222, 4, 3, 8]))  //8
複製程式碼

      這是letCode上的一道題,思路是:
      先對陣列排序,然後一次兩項迴圈陣列,比較這兩項如果不同,則返回第一項。

第十五天 【2018-04-08】

實現一個任意物件的深拷貝:

function deepClone(obj){
    if(obj == null || typeof obj != 'object') return obj
    let newObj = obj.constructor()

    for(let key in obj.getOwnPropertyDescriptors()){
      newObj[key] = deepClone(obj[key])
    }
    return newObj
}
複製程式碼

      這是一個遞迴實現,思路:

  • 首先設遞迴終止條件,遞迴到不是物件為止
  • 通過constructor建立一個新的物件
  • 迴圈老物件的所有私有屬性,遞迴賦值給新的物件

第十六天 【2018-04-09】

寫一個截流函式:

函式截流:就是防止短時間內的多次函式觸發。例如,我們想在滑鼠滾動的時候呼叫某個介面,滑鼠滾動事件觸發很快會造成短時間內多次呼叫介面造成效能損耗。

function throttle(fn, interval){
    let prevTime = 0
    return function(){
        let thisTime = new Date()
        if(thisTime - prevTime >= interval){
            fn.apply(null, arguments)
            prevTime = thisTime
        }
    }
}
let throttleFn = throttle(fn, 100)
複製程式碼

      思路:
      函式截流使用了一個高階函式實現,在閉包裡儲存一個上次函式執行的時間,返回一個新的函式。
      判斷上次的時間和這次時間差是否大於傳入的間隔,大於的話執行函式並更新記錄時間。

第十七天 【2018-04-10】

寫一個防抖函式:

函式防抖:與截流相似,防抖也是防止短時間內函式多次觸發,只不過截流的目的是過濾掉不符合要求的操作,而防抖是符合要求之後才會呼叫函式。例如,我們監聽輸入框的change事件,但是想在使用者輸入完成時(也就是他停止輸入一小段時間後)呼叫介面。

function shake(fn, interval) {
    let timer = null;
    return function(){
        clearTimeout(timer);
        let t_fn = fn.call(this)
        timer = setTimeout(t_fn, interval) 
    }
}
複製程式碼

第十八天 - 第二十五天 【2018-04-11】- 【2018-04-18】

最沒有每天更新,但是也沒有偷懶,用了一週時間寫了domDiff演算法,地址在這裡 淺入淺出圖解domDIff

第二十六天 【2018-04-19】

給定兩個陣列,寫一個方法來計算它們的交集。例如:給定 nums1 = [1, 2, 2, 1], nums2 = [2, 2], 返回 [2, 2].

var intersect = function(nums1, nums2) {
    let result = []
    if(nums1.length>nums2.length){
        let temp = nums1
        nums1 = nums2
        nums2 = temp
    }
    for (let i = 0; i < nums1.length; i++) {
        let item = nums1[i]
        let index = nums2.indexOf(item)
        if (index > -1) {
            let eve = nums2.splice(index, 1)[0]
            result.push(eve)
        }
    }
    return result
};
複製程式碼

      思路:
      首先,我們找到比較短的那個陣列(當然這不是必須的),然後迴圈它,如果其中一項在另一個陣列中也存在,那麼把它記錄下來,同時刪掉另一項裡面的這一項。最後返回記錄的陣列。

第二十七天 【2018-04-20】

給定一個非負整陣列成的非空陣列,在該數的基礎上加一,返回一個新的陣列。最高位數字存放在陣列的首位,陣列中每個元素只儲存一個數字。你可以假設除了整數0之外,這個整數不會以零開頭。

例如:輸入: [1,2,3]; 輸出: [1,2,4]; 解釋: 輸入陣列表示數字 123。
輸入: [1,2,9]; 輸出: [1,3,0]; 解釋: 輸入陣列表示數字 130

function plusOne(digits) {
    let result = []
    function next(currentAry) {
        if (currentAry.length) {
            let l = currentAry.pop()
            //如果小於9,l+1入列,否則0入列,再遞迴上一位
            if (l > 8) {
                result.unshift(0)
                //判斷上一位有沒有值,如果沒有,給他建立一位,值為1
                !currentAry.length && result.unshift(1)
                next(currentAry)
            } else {
                result.unshift(l + 1)
            }
        }
    }
    next(digits)
    return digits.concat(result)
}
複製程式碼

      思路:
      這個問題不難,就是把最後的一個數字+1,只不過涉及到如果最後一個是9,那麼就需要進一位+1。
      還需要注意一個問題,就是如果第一位是9的情況,例如92,9這一位的上一位已經沒有值了,所以要自己新建一位。我在程式碼中已經標註。

第二十八天 【2018-04-21】

letCode上的一道題目,判斷一個 9x9 的數獨是否有效。

基礎演算法

function isValidSudoku(board) {
    let columns = []
    let box = {}
    //迴圈每一列
    for (let i = 0; i < board.length; i++) {
        let row = board[i]
        let boxRowIndex = Math.floor(i / 3)
        let tempAry = []
        //迴圈每一行
        for (let j = 0; j < row.length; j++) {
            let item = row[j]
            //判斷每一行的重複
            if (item != '.' && tempAry.indexOf(item) > -1) {
                return false
            } else {
                tempAry.push(item)
            }
            //判斷每一列的重複
            if (typeof columns[j] != 'undefined') {
                if (item != '.' && columns[j].indexOf(item) > -1) {
                    return false
                } else {
                    columns[j].push(item)
                }
            } else {
                columns[j] = []
                columns[j].push(item)
            }
            //判斷每一個九宮格的重複
            let boxColumnsIndex = Math.floor(j / 3) + ''
            let boxIndex = boxRowIndex + boxColumnsIndex
            if (typeof box[boxIndex] != 'undefined') {
                if (item != '.' && box[boxIndex].indexOf(item) > -1) {
                    return false
                } else {
                    box[boxIndex].push(item)
                }
            } else {
                box[boxIndex] = []
                box[boxIndex].push(item)
            }

        }
    }
    return true
}
複製程式碼

      思路:
      首先,這是個二維陣列,至少需要兩個迴圈遍歷。我們需要判斷三種情況:

  • 每一行是否又重複
  • 每一列是否有重複
  • 每一個九宮格是否有重複

      每一行好說,我們只需要維護一個陣列tempAry,把每一行的每一個元素放進去,發現相同就返回false。並且在迴圈下一行的時候清空tempAry
      下面看每一列,和每一行思路差不多,我們維護一個二維陣列columns,它記錄著每一列的元素,同樣是遇到重複返回false
      九宮格稍微麻煩一些,我們需要維護一個物件box,因為一個九宮格佔用三行雜湊,所有我們使用了boxRowIndexboxColumnsIndex的拼接來標記每一個九宮格,後面的思路還是一樣,遇見相同的返回false

第二十九天 【2018-04-22】

請編寫一個函式,其功能是將輸入的字串反轉過來。

var reverseString = function(s) {
    return s.split('').reverse().join('')
};
複製程式碼

      這幾天有些忙,落了幾天,只能在letcode上找些簡單的追上進度了,不用解釋了,轉成陣列,反轉後再轉回來……

第三十一天 【2018-04-23】

給定一個 32 位有符號整數,將整數中的數字進行反轉。

例如:輸入: 123,輸出: 321; 輸入: -123,輸出: -321; 輸入: 120,輸出: 21。
假設我們的環境只能儲存 32 位有符號整數,其數值範圍是 [−231, 231 − 1]。根據這個假設,如果反轉後的整數溢位,則返回 0。

var reverse = function(x) {
    x = x.toString()
    let minus = x[0] === '-' ? '-' : ''
    let result = x.split('').reverse().join('').replace(/^0+|-$/g, '')
    result = Number(minus + result)
    return Math.abs(result) < Math.pow(2,31)?result:0
};
複製程式碼

      思路:
      這個很簡單,跟前一天的思路一樣,只不過需要處理“-”和“0”,同時,判斷是否反轉後的數值溢位。

第三十二天 【2018-04-24】

給定一個字串,找到它的第一個不重複的字元,並返回它的索引。如果不存在,則返回 -1。

var firstUniqChar = function(s) {
    for (let i = 0; i < s.length; i++) {
        let ifEqual = false
        for (let j = 0; j < s.length; j++) {
            if (i != j && s[i] === s[j]) {
                ifEqual = true
            }
        }
        if (!ifEqual) {
            return i
        }
    }
    return -1
}
複製程式碼
var firstUniqChar = function(s) {
    let obj = {}
    for (let i = 0; i < s.length; i++) {
        let item = s[i]
        if (obj[item]) {
            obj[item].num++
        } else {
            obj[item] = {
                index: i,
                num: 1
            }
        }
    }
    for (let key in obj) {
        if (obj[key].num == 1) {
            return obj[key].index
        }
    }
    return -1
};
複製程式碼

      思路:
      這道題有兩種解法,第一種不需要額外空間,但是時間複雜度為O(n2),第二種時間複雜度為O(n)但是需要申請額外空間。具體用哪一種應該根據實際的場景選擇,例如字串長度較短,可以選擇第一種,如果太長,那麼應該選擇第二種。

第三十三天 【2018-05-22】

實現一個方法getKeys,返回字串str在data中所有上級節點的名稱,例如:

var data = {
    key1: 'str1',
    key2: {
        key3: 'str3',
        key4: 'str4',
        key5: {
            key6: 'str6'
        }
    }
}
getKeys(data, 'str1')  //'key1'
getKeys(data, 'str3')  //'key2 key3'
getKeys(data, 'str6')  //'key2 key5 key6'
複製程式碼

實現:

function getKeys(data, str) {
    let result = []
    function search(d) {
        for (let key in d) {
            if (d[key] == str) {
                result.push(key)
                return
            }
            if (typeof d[key] == 'object') {
                result.push(key)
                search(d[key])
            }
        }
    }
    search(data)
    return result.join(' ')
}
複製程式碼

第三十四天 【2018-05-23】

實現一個陣列隨機打亂方法shuffle

效能差但是簡單的解法

function shuffle2(a) {
    return a.sort(function (a, b) {
        return Math.random() > .5 ? 1 : -1
    })
}
複製程式碼

效能好的解法:

function shuffle(a) {
    let len = a.length;
    for (let i = 0; i < len - 1; i++) {
        let index = parseInt(Math.random() * (len - i));
        let top = len - i - 1;
        [a[index], a[top]] = [a[top], a[index]]
    }
}
複製程式碼

第三十五天 【2018-05-25】

給定兩個字串 s 和 t ,編寫一個函式來判斷 t 是否是 s 的一個字母異位詞。

例如:
輸入: s = "anagram", t = "nagaram";輸出: true
輸入: s = "rat", t = "car";輸出: false

var isAnagram = function(s, t) {
    if (s.length != t.length) return false;
    for (let i = 0; i < s.length; i++) {
        let item = s[i]
        t = t.replace(new RegExp(item), '')
    }
    return t == ''
};
複製程式碼

結語

      堅持!堅持!還是堅持!

相關文章