用 JavaScript 刷 LeetCode 的正確姿式【進階】

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!html

以前寫了篇文章 用JavaScript刷LeetCode的正確姿式,簡單總結一些用 JavaScript 刷力扣的基本調試技巧。最近又刷了點題,總結了些數據結構和算法,但願能對各爲 JSer 刷題提供幫助。前端

此篇文章主要想給你們一些開箱即用的 JavaScipt 版本的代碼模板,涉及到較複雜的知識點,原理部分可能會省略,有須要的話後面有時間能夠給部分知識點單獨寫一篇詳細的講解。node

走過路過發現 bug 請指出,拯救一個辣雞(但很帥)的少年就靠您啦!!!git

BigInt

衆所周知,JavaScript 只能精確表達 Number.MIN_SAFE_INTEGER(-2^53+1) ~ Number.MAX_SAFE_INTEGER(2^53-1) 的值。程序員

而在一些題目中,經常會有較大的數字計算,這時就會產生偏差。舉個栗子:在控制檯輸入下面的兩個表達式會獲得相同的結果:web

>> 123456789*123456789      // 15241578750190520
>> 123456789*123456789+1    // 15241578750190520
複製代碼

而若是使用 BigInt 則能夠精確求值:算法

>> BigInt(123456789)*BigInt(123456789)              // 15241578750190521n
>> BigInt(123456789)*BigInt(123456789)+BigInt(1)    // 15241578750190522n
複製代碼

能夠經過在一個整數字面量後面加 n 的方式定義一個 BigInt ,如:10n,或者調用函數 BigInt()。上面的表達式也能夠寫成:後端

>> 123456789n*123456789n       // 15241578750190521n
>> 123456789n*123456789n+1n    // 15241578750190522n
複製代碼

BigInt 只能與 BigInt 作運算,若是和 Number 進行計算須要先經過 BigInt() 作類型轉換。數組

BigInt 支持運算符,+*-**% 。除 >>>(無符號右移)以外的位操做也能夠支持。由於 BigInt 都是有符號的, >>>(無符號右移)不能用於 BigIntBigInt 不支持單目 (+) 運算符。markdown

BigInt 也支持 / 運算符,可是會被向上取整。

const rounded = 5n / 2n; // 2n, not 2.5n
複製代碼

取模運算

在數據較大時,通常沒有辦法直接去進行計算,一般都會給一個大質數(例如,1000000007),求對質數取模後的結果。

取模運算的經常使用性質:

(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p) % p
(a * b) % p = (a % p * b % p) % p
a ^ b % p = ((a % p) ^ b) % p
複製代碼

能夠看出,加/減/乘/乘方,均可直接在運算的時候取模,至於除法則會複雜一些,稍後再講。

舉一個例子,LeetCode 1175. 質數排列

請你幫忙給從 1n 的數設計排列方案,使得全部的「質數」都應該被放在「質數索引」(索引從 1 開始)上;你須要返回可能的方案總數。

讓咱們一塊兒來回顧一下「質數」:質數必定是大於 1 的,而且不能用兩個小於它的正整數的乘積來表示。

因爲答案可能會很大,因此請你返回答案 模 mod 10^9 + 7 以後的結果便可。

題目很簡單,先求出質數的個數 x,則答案爲 x!(n-x)!(不理解的能夠去看題解區找題解,這裏就不詳細解釋了)

因爲階乘的值很大,因此在求階乘的時候須要在運算時取模,同時這裏用到了上面所說的BigInt

/** * @param {number} n * @return {number} */
var numPrimeArrangements = function(n) {
    const mod = 1000000007n;
    // 先把100之內的質數打表(不想再寫判斷質數的代碼了
    const prime = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97];
    // 預處理階乘
    const fac = new Array(n + 1);
    fac[0] = 1n; // 要用bigint
    for (let i = 1; i <= n; i++) {
        fac[i] = fac[i - 1] * BigInt(i) % mod;
    }
    // 先求n之內的質數的個數
    const x = prime.filter(i => i <= n).length;
    // x!(n-x)!
    return fac[x] * fac[n - x] % mod;
};
複製代碼

快速冪

快速冪,顧名思義,快速求冪運算。原理也很簡單,好比咱們求 x^10 咱們能夠求 (x^5)^2 能夠減小一半的運算。

假設咱們求 (x^n)

  • 若是 n 是偶數,變爲求 (x^(n/2))^2
  • 若是 n 是奇數,則求 (x^⌊n/2⌋)^2 * x⌊⌋ 是向下取整)

由於快速冪涉及到的題目通常數據都很大,須要取模,因此加了取模運算。其中,代碼中 n>>=1 至關於 n=n/2if(n&1)是在判斷n是否爲奇數。

代碼以下:

// x ^ n % mod
function pow(x, n, mod) {
    let ans = 1;
    while (n > 0) {
        if (n & 1) ans = ans * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return ans;
}
複製代碼

乘法逆元(數論倒數)

上面說了除法的取模會複雜一些,其實就是涉及了乘法逆元

當咱們求 (a/b)%p 你覺得會是簡單的 ((a%p)/(b%p))%p?固然不是!(反例本身想去Orz

假設有 (a*x)%p=1 則稱 ax關於p互爲逆元(ax 關於 p 的逆元,xa 關於 p 的逆元)。好比:2*3%5=123 關於 5 互爲逆元。

咱們把 a 的逆元用 inv(a) 表示。那麼:

(a/b) % p
= ( (a/b) * (b*inv(b)) ) % p // 由於(b*inv(b))爲1
= (a * inv(b)) % p
= (a%p * inv(b)%p) % p
複製代碼

如今經過逆元神奇的把除法運算變沒了~~~

問題在於怎麼求乘法逆元。有兩種方式,費馬小定理擴展歐幾里德算法

不求甚解的我只記了一種解法,即費馬小定理:a^(p-1) ≡ 1 (mod p)

由費馬小定理咱們能夠推論:a^(p-2) ≡ inv(a) (mod p)

數學家的事咱們程序員就不要想那麼多啦,記結論就行了。即:

a關於p的逆元爲a^(p-2)

好了,如今能夠經過快速冪求出 a 的逆元了。

function inv(a, p) {
    return pow(a, p - 2, p); // pow是上面定義的快速冪函數
}
複製代碼

(P.S.其實我數論很爛= =,平時都是直接記結論,因此此處講解可能存在不許確的狀況。僅供參考。

二分答案

解題的時候每每會考慮枚舉答案而後檢驗枚舉的值是否正確。若知足單調性,則知足使用二分法的條件。把這裏的枚舉換成二分,就變成了「二分答案」。二分答案的時間複雜度是O(logN * (單次驗證當前值是否知足條件的複雜度))

不少同窗在邊界問題上常常出bug,也會不當心寫個死循環什麼的,我總結了一個簡單清晰不會出錯的二分模板:

// isValid 判斷某個值是否合法 根據題目要求實現
// 假設 若是x合法則大於x必定合法 若是x不合法則小於x必定不合法
// 求最小合法值
function binaryCalc() {
    let l = 0, r = 10000;   // 答案可能出現的最小值l和最大值r 根據題目設置具體值
    let ans;    // 最終答案
    while (l <= r) {
        let mid = (l + r) >> 1; // 位運算取中間值 至關於 floor((l+r)/2)
        if (isValid(mid)) {
            // 若是 mid 合法 則 [mid, r] 都是合法的
            // 咱們先把ans設置爲當前獲取的合法值的最小值 mid
            ans = mid;
            // 而後再去繼續去求[l,mid-1]裏面是否有合法值
            r = mid - 1;
        } else {
            // 若是mid不合法 則[l,mid]都是不合法的
            // 咱們去[mid+1,r]中找答案
            l = mid + 1;
        }
    }
    return ans;
}
複製代碼

舉一個簡單的例子,LeetCode 69. x 的平方根 是一個二分模板題。題目要求是,給一個數字 x 求平方小於等於 x的最大整數。此處求的是最大值,和模板中對lr的處理恰好相反。

/** * @param {number} x * @return {number} */
 var mySqrt = function(x) {
    let l = 0, r = x; // 根據題目要求 答案可能的值最小爲0 最大爲x
    let ans = 0;      // 最終答案
    
    function isValid(v) {       // 判斷一個數是否合法
        return v * v <= x;
    }

    while (l <= r) {
        let mid = (l + r) >> 1; // 取中間值
        if (isValid(mid)) {
            ans = mid;
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    return ans;
};
複製代碼

並查集

我的以爲並查集是很是精妙且簡潔優雅的數據結構,推薦學習。

並查集應用場景爲,存在一些元素,分別包含在不一樣集合中,須要快速合併兩個集合,同時可快速求出兩個元素是否處於同一集合。

簡單的理解並查集的實現,就是把每個集合都當作一棵樹,每一個節點都有一個父節點,每棵樹都有一個根節點(根節點的父節點爲其自己)。

判斷是否同一集合:咱們能夠順着節點的父節點找到該節點所在集合的根節點。當咱們肯定兩個集合擁有同一個根節點,則證實兩個節點處於同一個集合。

合併操做:分別取得兩個節點所在集合的根節點,把其中一個根節點的父節點設置爲另外一個根節點便可。

可能說的比較抽象,想詳細瞭解的同窗能夠本身深刻學習,這裏直接給出代碼模板。

class UnionFind {
    constructor(n) {
        this.n = n; // 節點個數
        // 記錄每一個節點的父節點 初始時每一個節點本身爲一個集合 即每一個節點的父節點都是其自己
        this.father = new Array(n).fill().map((v, index) => index);
    }
    // 尋找一個節點的根節點
    find(x) {
        // 若是父節點爲其自己 則證實是根節點
        if (x == this.father[x]) {
            return x;
        }
        // 遞歸查詢
        // 此處進行了路徑壓縮 即將x的父節點直接設置爲根節點 下一次查詢的時候 將減小遞歸次數
        return this.father[x] = this.find(this.father[x]);
    }
    // 合併x和y所在的兩個集合
    merge(x, y) {
        const xRoot = this.find(x); // 找到x的根節點
        const yRoot = this.find(y); // 找到y的根節點
        this.father[xRoot] = yRoot; // 將xRoot的父節點設置爲yRoot 便可將兩個集合合併
    }
    // 計算集合個數
    count() {
        // 其實就是查詢根節點的個數
        let cnt = 0;
        for (let i = 0; i < this.n; i++) {
            if (this.father[i] === i) { // 判斷是否爲根節點
                cnt++;
            }
        }
        return cnt;
    }
}
複製代碼

找一個並查集的題目,方便你們理解並查集的妙處。並查集的題目能夠出得很是靈活,可能不會輕易看出是並查集。 LeetCode 947. 移除最多的同行或同列石頭

n 塊石頭放置在二維平面中的一些整數座標點上。每一個座標點上最多隻能有一塊石頭。

若是一塊石頭的 同行或者同列 上有其餘石頭存在,那麼就能夠移除這塊石頭。

給你一個長度爲 n 的數組 stones ,其中 stones[i] = [xi, yi] 表示第 i 塊石頭的位置,返回 能夠移除的石子 的最大數量。

此處參考了官方的題解

把二維座標平面上的石頭想象成圖的頂點,若是兩個石頭橫座標相同、或者縱座標相同,在它們之間造成一條邊。

image.png

根據能夠移除石頭的規則:若是一塊石頭的 同行或者同列 上有其餘石頭存在,那麼就能夠移除這塊石頭。能夠發現:必定能夠把一個連通圖裏的全部頂點根據這個規則刪到只剩下一個頂點。

咱們遍歷全部的石頭,發現若是有兩個石頭的橫座標或者縱座標相等,則證實這兩塊石頭應該在同一個集合(即上面說的連通圖)裏。那麼最後每一個集合只留一塊石頭,剩下的則所有能夠被移除。

AC代碼:

// 定義 UnionFind 相關代碼
/** * @param {number[][]} stones * @return {number} */
 var removeStones = function(stones) {
    let n = stones.length;
    let uf = new UnionFind(n);
    for (let i = 0; i < n; i++) {
        for (let j = i + 1; j < n; j++) {
            // 有兩個石頭的橫座標或者縱座標相等 則合併
            if (stones[i][0] == stones[j][0] || stones[i][1] == stones[j][1]) {
                uf.merge(i, j);
            }
        }
    }
    // 石頭總數減去集合的個數就是答案
    return n - uf.count();
};
複製代碼

KMP

KMP 被一些算法初學者認爲是高難度數據結構,通常遇到直接放棄那種。因此我想了下幾句話應該也解釋不清,那就跳過原理直接上模板吧。:P

先簡單說一下背景,KMP 解決的是子串查找的問題。給兩個字符串ST,求T是不是S的子串。解決方法是先預處理T,求出Tnext數組,其中next[i]表明T的子串T[0...i-1](即T.substring(0, i)最長相等的前綴後綴 的長度。

嘛,最長相等的前綴後綴,就是說,好比字符串"abcuuabc"最長相等的前綴後綴就是abc,那麼其長度就應該是3

而後藉助next數組,能夠在線性時間複雜度內求出T是否爲S的子串,首次出現下標,以及出現次數。

模板代碼:

// 求字符串 s 的 next 數組
function getNext(s) {
    let len = s.length;
    let next = new Array(len + 1);
    let j = 0, k = -1;
    next[0] = -1;
    while (j < len) {
        if (k == -1 || s[j] === s[k]) next[++j] = ++k;
        else k = next[k];
    }
    return next;
}
// 求字符串 t 在字符串 s 中第一次出現的下標 不存在則返回 -1
function findIndex(s, t) {
    let i = 0, j = 0;
    let next = getNext(t);
    let slen = s.length, tlen = t.length;
    while (i < slen && j < tlen) {
        if (j === -1 || s[i] === t[j]) ++i, ++j;
        else j = next[j];
    }
    return j === tlen ? i - tlen : -1;
}
// 求字符串 t 在字符串 s 出現的次數
function findCount(s, t) {
    let ans = 0;
    let i = 0, j = 0;
    let next = getNext(t);
    let slen = s.length, tlen = t.length;
    while (i < slen && j < tlen) {
        if (j === -1 || s[i] === t[j]) ++i, ++j;
        else j = next[j];
        if (j === tlen) {
            ++ans;
            j = next[j];
        }
    }
    return ans;
}
複製代碼

若是屢次計算子串相同的話,next數組能夠預處理,不須要每次在求index時再計算。

舉個例子吧,LeetCode 1392. 最長快樂前綴

「快樂前綴」是在原字符串中既是 非空 前綴也是後綴(不包括原字符串自身)的字符串。

給你一個字符串 s,請你返回它的 最長快樂前綴

若是不存在知足題意的前綴,則返回一個空字符串。

咱們會發現這不就是 next 數組麼,因此我記得此次周賽會 KMP 的同窗直接 copy 就得分了.....

AC代碼;

// getNext 定義參考上面模板
/** * @param {string} s * @return {string} */
var longestPrefix = function(s) {
    let len = s.length;
    let next = getNext(s);
    let ansLen = next[len] == len ? len - 1 : next[len]; // 不包含原字符串 須要特殊判斷下
    return s.substring(0, ansLen);
};
複製代碼

再來一個 LeetCode 28. 實現 strStr() 求一個字符串在另外一個字符串中首次出現的位置,就是indexOf的實現,其實也就是模板中的 findIndex 函數。

AC代碼:

// findIndex 定義參考模板
/** * @param {string} haystack * @param {string} needle * @return {number} */
var strStr = function(haystack, needle) {
    return findIndex(haystack, needle);
};
複製代碼

優先隊列(堆)

優先隊列,咱們給每一個元素定義優先級,每次取隊列中的值都取的是優先級最大的數。

其餘的語言中都自帶優先隊列的實現,JSer就只能QAQ……因此我本身寫了一個優先隊列,就是經過堆來實現。(原理就不講啦,學過堆排序的應該懂~(趴

class PriorityQueue {
    /** * 構造函數 能夠傳入比較函數自定義優先級 默認是最小值排在最前 * @param {function} compareFunc 比較函數 compareFunc(a, b) 爲 true 表示 a 的優先級 > b */
    constructor(compareFunc) {
        this.queue = [];
        this.func = compareFunc || ((a, b) => a < b);
    }
    /** * 向優先隊列添加一個元素 */
    push(ele) {
        this.queue.push(ele);
        this.pushup(this.size() - 1)
    }
    /** * 彈出最小值並返回 */
    pop() {
        let { queue } = this;
        if (queue.length <= 1) return queue.pop();
        
        let min = queue[0];
        queue[0] = queue.pop();
        this.pushdown(0);
        return min;
    }
    /** * 返回最小值 */
    top() {
        return this.size() ? this.queue[0] : null;
    }
    /** * 返回隊列中元素的個數 */
    size() {
        return this.queue.length;
    }
    /** * 初始化堆 */
    setQueue(queue) {
        this.queue = queue;
        for (let i = (this.size() >> 1); i >= 0; i--) {
            this.pushdown(i);
        }
    }
    /** * 調整以保證 queue[index] 是子樹中最小的 * */
    pushdown(index) {
        let { queue, func } = this;
        let fa = index;
        let cd = index * 2 + 1;
        let size = queue.length;
        while (cd < size) {
            if (cd + 1 < size && func(queue[cd + 1], queue[cd])) cd++;
            if (func(queue[fa], queue[cd])) break;
            // 交換 queue[fa] 和 queue[cd]
            [queue[fa], queue[cd]] = [queue[cd], queue[fa]];
            // 繼續處理子樹
            fa = cd;
            cd = fa * 2 + 1;
        }
    }
    /** * 調整 index 到合法位置 */
    pushup(index) {
        let { queue, func } = this;
        while (index) {
            const fa = (index - 1) >> 1;
            if (func(queue[fa], queue[index])) {
                break;
            }
            [queue[fa], queue[index]] = [queue[index], queue[fa]];
            index = fa;
        }
    }
}
複製代碼

舉個例子,LeetCode 23. 合併K個升序鏈表 一道困難題目哦~

給你一個鏈表數組,每一個鏈表都已經按升序排列。

請你將全部鏈表合併到一個升序鏈表中,返回合併後的鏈表。

作法很簡單,把鏈表都放到優先隊列裏,每次取值最小的鏈表就行。具體實現看代碼。

/** * @param {ListNode[]} lists * @return {ListNode} */
var mergeKLists = function(lists) {
    let queue = new PriorityQueue((a, b) => a.val < b.val);

    lists.forEach(list => {
        list && queue.push(list);
    });

    const dummy = new ListNode(0);
    let cur = dummy;

    while (queue.size()) {
        let node = queue.pop();
        if (node.next) queue.push(node.next);
        cur.next = new ListNode(node.val);
        cur = cur.next;
    }

    return dummy.next;
};
複製代碼

Trie(字典樹/前綴樹)

字典樹應該算是一個比較簡單並且直觀的數據結構~字典樹模板題能夠看 LeetCode 208. 實現 Trie (前綴樹)

/** * Initialize your data structure here. */
var Trie = function() {
    this.nodes = [];
};

/** * Inserts a word into the trie. * @param {string} word * @return {void} */
Trie.prototype.insert = function(word) {
    let nodes = this.nodes;
    for (let w of word) {
        if (!nodes[w]) {
            nodes[w] = {};
        }
        nodes = nodes[w];
    }
    nodes.end = true;
};

/** * Returns if the word is in the trie. * @param {string} word * @return {boolean} */
Trie.prototype.search = function(word) {
    let nodes = this.nodes;
    for (let w of word) {
        if (!nodes[w]) {
            return false;
        }
        nodes = nodes[w];
    }
    return !!nodes.end;
};

/** * Returns if there is any word in the trie that starts with the given prefix. * @param {string} prefix * @return {boolean} */
Trie.prototype.startsWith = function(prefix) {
    let nodes = this.nodes;
    for (let w of prefix) {
        if (!nodes[w]) {
            return false;
        }
        nodes = nodes[w];
    }
    return true;
};
複製代碼

字典樹的變種應用,LeetCode 421. 數組中兩個數的最大異或值 參考:題解

咱們也能夠將數組中的元素當作長度爲 31 的字符串,字符串中只包含 01。若是咱們將字符串放入字典樹中,那麼在字典樹中查詢一個字符串的過程,剛好就是從高位開始肯定每個二進制位的過程。對於一個數求異或和的最大值,就是從最高位開始,每一位都找異或和最大的那個分支。

var Trie = function() {
    this.nodes = [];
};
Trie.prototype.insert = function(digit) {
    let nodes = this.nodes;
    for (let d of digit) {
        if (!nodes[d]) {
            nodes[d] = [];
        }
        nodes = nodes[d];
    }
};
Trie.prototype.maxXor = function(digit) {
    let xor = 0;
    let nodes = this.nodes;
    for (let i = 0; i < digit.length; i++) {
        let d = digit[i];
        if (nodes[d ^ 1]) {
            xor += 1 << (digit.length - i - 1);
            nodes = nodes[d ^ 1];
        } else {
            nodes = nodes[d];
        }
    }
    return xor;
};

/** * @param {number[]} nums * @return {number} */
var findMaximumXOR = function(nums) {
    let trie = new Trie();
    let maxXor = 0;
    for (let x of nums) {
        let binaryX = x.toString(2);
        // 由於 0 <= nums[i] <= 2^31 - 1 因此最多爲31位
        // 補前綴0統一變成31位
        binaryX = ('0'.repeat(31) + binaryX).substr(-31);
        // 插入Trie
        trie.insert(binaryX);
        maxXor = Math.max(maxXor, trie.maxXor(binaryX));
    }
    return maxXor;
};
複製代碼

總結

暫時就想到這麼多比較常見的數據結構。若是有其餘的能夠在評論區補充,若是我會的話會後續加上的。

JSer衝鴨!!!

參考資料

相關文章
相關標籤/搜索