用JavaScript刷LeetCode的正確姿勢

雖然很多人都覺得前端算法弱,但其實 JavaScript 也可以刷題啊!最近兩個月斷斷續續刷完了 leetcode 前 200 的 middle + hard ,總結了一些刷題常用的模板代碼。走過路過發現 bug 請指出,拯救一個辣雞(但很帥)的少年就靠您啦!

常用函數

包括打印函數和一些數學函數。

const _max = Math.max.bind(Math);
const _min = Math.min.bind(Math);
const _pow = Math.pow.bind(Math);
const _floor = Math.floor.bind(Math);
const _round = Math.round.bind(Math);
const _ceil = Math.ceil.bind(Math);
const log = console.log.bind(console);
//const log = _ => {}

 log 在提交的代碼中當然是用不到的,不過在調試時十分有用。但是當代碼裏面加了很多 log 的時候,提交時還需要一個個註釋掉就相當麻煩了,只要將 log 賦值爲空函數就可以了。

舉一個簡單的例子,下面的代碼是可以直接提交的。

// 計算 1+2+...+n
// const log = console.log.bind(console);
const log = _ => {}

function sumOneToN(n) {
    let sum = 0;
    for (let i = 1; i <= n; i++) {
        sum += i;
        log(`i=${i}: sum=${sum}`);
    }
    return sum;
}

sumOneToN(10);

 

位運算的一些小技巧

判斷一個整數 x 的奇偶性: x & 1 = 1  (奇數) ,  x & 1 = 0  (偶數)

求一個浮點數 x 的整數部分: ~~x ,對於正數相當於  floor(x)  對於負數相當於  ceil(-x) 

計算  2 ^ n : 1 << n  相當於  pow(2, n) 

計算一個數 x 除以 2 的 n 倍: x >> n  相當於  ~~(x / pow(2, n)) 

判斷一個數 x 是 2 的整數冪(即  x = 2 ^ n ):  x & (x - 1) = 0 

※注意※:上面的位運算只對32位帶符號的整數有效,如果使用的話,一定要注意數!據!範!圍!

記住這些技巧的作用:

提升運行速度 ❌

提升逼格 ✅

舉一個實用的例子,快速冪(原理自行google)

// 計算x^n n爲整數
function qPow(x, n) {
    let result = 1;
    while (n) {
        if (n & 1) result *= x; // 同 if(n%2)
        x = x * x;
        n >>= 1; // 同 n=floor(n/2)
    }
    return result;
}

 

鏈表

剛開始做 LeetCode 的題就遇到了很多鏈表的題。噁心心。最麻煩的不是寫題,是調試啊!!於是總結了一些鏈表的輔助函數。

/**
 * 鏈表節點
 * @param {*} val
 * @param {ListNode} next
 */
function ListNode(val, next = null) {
    this.val = val;
    this.next = next;
}
/**
 * 將一個數組轉爲鏈表
 * @param {array} a
 * @return {ListNode}
 */
const getListFromArray = (a) => {
    let dummy = new ListNode()
    let pre = dummy;
    a.forEach(x => pre = pre.next = new ListNode(x));
    return dummy.next;
}
/**
 * 將一個鏈表轉爲數組
 * @param {ListNode} node
 * @return {array}
 */
const getArrayFromList = (node) => {
    let a = [];
    while (node) {
        a.push(node.val);
        node = node.next;
    }
    return a;
}
/**
 * 打印一個鏈表
 * @param {ListNode} node 
 */
const logList = (node) => {
    let str = 'list: ';
    while (node) {
        str += node.val + '->';
        node = node.next;
    }
    str += 'end';
    log(str);
}

還有一個常用小技巧,每次寫鏈表的操作,都要注意判斷表頭,如果創建一個空表頭來進行操作會方便很多。

let dummy = new ListNode();
// 返回
return dummy.next;

使用起來超爽噠~舉個例子。@leetcode 82。題意就是刪除鏈表中連續相同值的節點。

/*
 * @lc app=leetcode id=82 lang=javascript
 *
 * [82] Remove Duplicates from Sorted List II
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    // 空指針或者只有一個節點不需要處理
    if (head === null || head.next === null) return head;

    let dummy = new ListNode();
    let oldLinkCurrent = head;
    let newLinkCurrent = dummy;

    while (oldLinkCurrent) {
        let next = oldLinkCurrent.next;
        // 如果當前節點和下一個節點的值相同 就要一直向前直到出現不同的值
        if (next && oldLinkCurrent.val === next.val) {
            while (next && oldLinkCurrent.val === next.val) {
                next = next.next;
            }
            oldLinkCurrent = next;
        } else {
            newLinkCurrent = newLinkCurrent.next = oldLinkCurrent;
            oldLinkCurrent = oldLinkCurrent.next;
        }
    }
    newLinkCurrent.next = null; // 記得結尾置空~
    logList(dummy.next);
    return dummy.next;
};

deleteDuplicates(getListFromArray([1,2,3,3,4,4,5]));
deleteDuplicates(getListFromArray([1,1,2,2,3,3,4,4,5]));
deleteDuplicates(getListFromArray([1,1]));
deleteDuplicates(getListFromArray([1,2,2,3,3]));

本地運行結果

list: 1->2->5->end
list: 5->end
list: end
list: 1->end

是不是很方便!

矩陣(二維數組)

矩陣的題目也有很多,基本每一個需要用到二維數組的題,都涉及到初始化,求行數列數,遍歷的代碼。於是簡單提取出來幾個函數。

/**
 * 初始化一個二維數組
 * @param {number} r 行數
 * @param {number} c 列數
 * @param {*} init 初始值
 */
const initMatrix = (r, c, init = 0) => new Array(r).fill().map(_ => new Array(c).fill(init));
/**
 * 獲取一個二維數組的行數和列數
 * @param {any[][]} matrix
 * @return [row, col]
 */
const getMatrixRowAndCol = (matrix) => matrix.length === 0 ? [0, 0] : [matrix.length, matrix[0].length];
/**
 * 遍歷一個二維數組
 * @param {any[][]} matrix 
 * @param {Function} func 
 */
const matrixFor = (matrix, func) => {
    matrix.forEach((row, i) => {
        row.forEach((item, j) => {
            func(item, i, j, row, matrix);
        });
    })
}
/**
 * 獲取矩陣第index個元素 從0開始
 * @param {any[][]} matrix 
 * @param {number} index 
 */
function getMatrix(matrix, index) {
    let col = matrix[0].length;
    let i = ~~(index / col);
    let j = index - i * col;
    return matrix[i][j];
}
/**
 * 設置矩陣第index個元素 從0開始
 * @param {any[][]} matrix 
 * @param {number} index 
 */
function setMatrix(matrix, index, value) {
    let col = matrix[0].length;
    let i = ~~(index / col);
    let j = index - i * col;
    return matrix[i][j] = value;
}

找一個簡單的矩陣的題示範一下用法。@leetcode 566。題意就是將一個矩陣重新排列爲r行c列。

/*
 * @lc app=leetcode id=566 lang=javascript
 *
 * [566] Reshape the Matrix
 */
/**
 * @param {number[][]} nums
 * @param {number} r
 * @param {number} c
 * @return {number[][]}
 */
var matrixReshape = function(nums, r, c) {
    // 將一個矩陣重新排列爲r行c列
    // 首先獲取原來的行數和列數
    let [r1, c1] = getMatrixRowAndCol(nums);
    log(r1, c1);
    // 不合法的話就返回原矩陣
    if (!r1 || r1 * c1 !== r * c) return nums;
    // 初始化新矩陣
    let matrix = initMatrix(r, c);
    // 遍歷原矩陣生成新矩陣
    matrixFor(nums, (val, i, j) => {
        let index = i * c1 + j; // 計算是第幾個元素
        log(index);
        setMatrix(matrix, index, val); // 在新矩陣的對應位置賦值
    });
    return matrix;
};

let x = matrixReshape([[1],[2],[3],[4]], 2, 2);
log(x)

 

二叉樹

當我做到二叉樹相關的題目,我發現,我錯怪鏈表了,嗚嗚嗚這個更噁心。

當然對於二叉樹,只要你掌握先序遍歷,後序遍歷,中序遍歷,層序遍歷,遞歸以及非遞歸版,先序中序求二叉樹,先序後序求二叉樹,基本就能AC大部分二叉樹的題目了(我瞎說的)。

二叉樹的題目 input 一般都是層序遍歷的數組,所以寫了層序遍歷數組和二叉樹的轉換,方便調試。

function TreeNode(val, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
}
/**
 * 通過一個層次遍歷的數組生成一棵二叉樹
 * @param {any[]} array
 * @return {TreeNode}
 */
function getTreeFromLayerOrderArray(array) {
    let n = array.length;
    if (!n) return null;
    let index = 0;
    let root = new TreeNode(array[index++]);
    let queue = [root];
    while(index < n) {
        let top = queue.shift();
        let v = array[index++];
        top.left = v == null ? null : new TreeNode(v);
        if (index < n) {
            let v = array[index++];
            top.right = v == null ? null : new TreeNode(v);
        }
        if (top.left) queue.push(top.left);
        if (top.right) queue.push(top.right);
    }
    return root;
}
/**
 * 層序遍歷一棵二叉樹 生成一個數組
 * @param {TreeNode} root 
 * @return {any[]}
 */
function getLayerOrderArrayFromTree(root) {
    let res = [];
    let que = [root];
    while (que.length) {
        let len = que.length;
        for (let i = 0; i < len; i++) {
            let cur = que.shift();
            if (cur) {
                res.push(cur.val);
                que.push(cur.left, cur.right);
            } else {
                res.push(null);
            }
        }
    }
    while (res.length > 1 && !res[res.length - 1]) res.pop(); // 刪掉結尾的 null
    return res;
}

一個例子,@leetcode 110,判斷一棵二叉樹是不是平衡二叉樹。

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isBalanced = function(root) {
    if (!root) return true; // 認爲空指針也是平衡樹吧

    // 獲取一個二叉樹的深度
    const d = (root) => {
        if (!root) return 0;
        return _max(d(root.left), d(root.right)) + 1;
    }

    let leftDepth = d(root.left);
    let rightDepth = d(root.right);

    // 深度差不超過 1 且子樹都是平衡樹
    if (_min(leftDepth, rightDepth) + 1 >= _max(leftDepth, rightDepth)
        && isBalanced(root.left) && isBalanced(root.right)) return true;

    return false;
};

log(isBalanced(getTreeFromLayerOrderArray([3,9,20,null,null,15,7])));
log(isBalanced(getTreeFromLayerOrderArray([1,2,2,3,3,null,null,4,4])));

 

二分查找

參考 C++ STL 中的 lower_bound  和 upper_bound 。這兩個函數真的很好用的!

/**
 * 尋找>=target的最小下標
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
function lower_bound(nums, target) {
    let first = 0;
    let len = nums.length;

    while (len > 0) {
        let half = len >> 1;
        let middle = first + half;
        if (nums[middle] < target) {
            first = middle + 1;
            len = len - half - 1;
        } else {
            len = half;
        }
    }
    return first;
}

/**
 * 尋找>target的最小下標
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
function upper_bound(nums, target) {
    let first = 0;
    let len = nums.length;

    while (len > 0) {
        let half = len >> 1;
        let middle = first + half;
        if (nums[middle] > target) {
            len = half;
        } else {
            first = middle + 1;
            len = len - half - 1;
        }
    }
    return first;
}

照例,舉個例子,@leetcode 34。題意是給一個排好序的數組和一個目標數字,求數組中等於目標數字的元素最小下標和最大下標。不存在就返回 -1。

/*
 * @lc app=leetcode id=34 lang=javascript
 *
 * [34] Find First and Last Position of Element in Sorted Array
 */
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
  let lower = lower_bound(nums, target);
  let upper = upper_bound(nums, target);
  let size = nums.length;
  // 不存在返回 [-1, -1]
  if (lower >= size || nums[lower] !== target) return [-1, -1];
  return [lower, upper - 1];
};

 

在 VS Code 中刷 LeetCode

前面說的那些模板,難道每一次打開新的一道題都要複製一遍麼?當然不用啦。

首先配置代碼片段 選擇 Code -> Preferences -> User Snippets ,然後選擇 JavaScript

 

 

然後把文件替換爲下面的代碼:

{
  "leetcode template": {
    "prefix": "@lc",
    "body": [
      "const _max = Math.max.bind(Math);","const _min = Math.min.bind(Math);","const _pow = Math.pow.bind(Math);","const _floor = Math.floor.bind(Math);","const _round = Math.round.bind(Math);","const _ceil = Math.ceil.bind(Math);","const log = console.log.bind(console);","// const log = _ => {}","/**************** 鏈表 ****************/","/**"," * 鏈表節點"," * @param {*} val"," * @param {ListNode} next"," */","function ListNode(val, next = null) {","    this.val = val;","    this.next = next;","}","/**"," * 將一個數組轉爲鏈表"," * @param {array} array"," * @return {ListNode}"," */","const getListFromArray = (array) => {","    let dummy = new ListNode()","    let pre = dummy;","    array.forEach(x => pre = pre.next = new ListNode(x));","    return dummy.next;","}","/**"," * 將一個鏈表轉爲數組"," * @param {ListNode} list"," * @return {array}"," */","const getArrayFromList = (list) => {","    let a = [];","    while (list) {","        a.push(list.val);","        list = list.next;","    }","    return a;","}","/**"," * 打印一個鏈表"," * @param {ListNode} list "," */","const logList = (list) => {","    let str = 'list: ';","    while (list) {","        str += list.val + '->';","        list = list.next;","    }","    str += 'end';","    log(str);","}","/**************** 矩陣(二維數組) ****************/","/**"," * 初始化一個二維數組"," * @param {number} r 行數"," * @param {number} c 列數"," * @param {*} init 初始值"," */","const initMatrix = (r, c, init = 0) => new Array(r).fill().map(_ => new Array(c).fill(init));","/**"," * 獲取一個二維數組的行數和列數"," * @param {any[][]} matrix"," * @return [row, col]"," */","const getMatrixRowAndCol = (matrix) => matrix.length === 0 ? [0, 0] : [matrix.length, matrix[0].length];","/**"," * 遍歷一個二維數組"," * @param {any[][]} matrix "," * @param {Function} func "," */","const matrixFor = (matrix, func) => {","    matrix.forEach((row, i) => {","        row.forEach((item, j) => {","            func(item, i, j, row, matrix);","        });","    })","}","/**"," * 獲取矩陣第index個元素 從0開始"," * @param {any[][]} matrix "," * @param {number} index "," */","function getMatrix(matrix, index) {","    let col = matrix[0].length;","    let i = ~~(index / col);","    let j = index - i * col;","    return matrix[i][j];","}","/**"," * 設置矩陣第index個元素 從0開始"," * @param {any[][]} matrix "," * @param {number} index "," */","function setMatrix(matrix, index, value) {","    let col = matrix[0].length;","    let i = ~~(index / col);","    let j = index - i * col;","    return matrix[i][j] = value;","}","/**************** 二叉樹 ****************/","/**"," * 二叉樹節點"," * @param {*} val"," * @param {TreeNode} left"," * @param {TreeNode} right"," */","function TreeNode(val, left = null, right = null) {","    this.val = val;","    this.left = left;","    this.right = right;","}","/**"," * 通過一個層次遍歷的數組生成一棵二叉樹"," * @param {any[]} array"," * @return {TreeNode}"," */","function getTreeFromLayerOrderArray(array) {","    let n = array.length;","    if (!n) return null;","    let index = 0;","    let root = new TreeNode(array[index++]);","    let queue = [root];","    while(index < n) {","        let top = queue.shift();","        let v = array[index++];","        top.left = v == null ? null : new TreeNode(v);","        if (index < n) {","            let v = array[index++];","            top.right = v == null ? null : new TreeNode(v);","        }","        if (top.left) queue.push(top.left);","        if (top.right) queue.push(top.right);","    }","    return root;","}","/**"," * 層序遍歷一棵二叉樹 生成一個數組"," * @param {TreeNode} root "," * @return {any[]}"," */","function getLayerOrderArrayFromTree(root) {","    let res = [];","    let que = [root];","    while (que.length) {","        let len = que.length;","        for (let i = 0; i < len; i++) {","            let cur = que.shift();","            if (cur) {","                res.push(cur.val);","                que.push(cur.left, cur.right);","            } else {","                res.push(null);","            }","        }","    }","    while (res.length > 1 && !res[res.length - 1]) res.pop(); // 刪掉結尾的 null","    return res;","}","/**************** 二分查找 ****************/","/**"," * 尋找>=target的最小下標"," * @param {number[]} nums"," * @param {number} target"," * @return {number}"," */","function lower_bound(nums, target) {","    let first = 0;","    let len = nums.length;","","    while (len > 0) {","        let half = len >> 1;","        let middle = first + half;","        if (nums[middle] < target) {","            first = middle + 1;","            len = len - half - 1;","        } else {","            len = half;","        }","    }","    return first;","}","","/**"," * 尋找>target的最小下標"," * @param {number[]} nums"," * @param {number} target"," * @return {number}"," */","function upper_bound(nums, target) {","    let first = 0;","    let len = nums.length;","","    while (len > 0) {","        let half = len >> 1;","        let middle = first + half;","        if (nums[middle] > target) {","            len = half;","        } else {","            first = middle + 1;","            len = len - half - 1;","        }","    }","    return first;","}",
      "$1"
    ],
    "description": "LeetCode常用代碼模板"
  }
}

以後每一次寫題之前,鍵入 @lc 就會出現提示,輕鬆加入代碼模板。 

 

當然,必須推薦刷題神器,vscode 中的一款插件 vscode-leetcode

最後我要大聲說,前端真的有機會用到算法的(不只面試)!來一起快樂刷題!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章