找到好工作之 LeetcodeTop100(Easy) by JavaScript

眷你發表於2019-03-04

記錄一下 leetcode top100

該部分只記錄 easy 難度,由於為 easy 難度,故基本直接放解答

1. two sum

兩數和 - 找到無序陣列中和為定值的兩個數,返回下標

因為需要返回下標,因此先排序後用兩個指標掃(前->後, 後->前)的方式(NlogN)不行。 故選用類似 hash 的形式解; key 為陣列值, value 為下標

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    const map = {};
    nums.forEach((num, index) => map[num] = index);
    const len = nums.length;
    for(let i = 0; i < len; i++) {
        const otherIndex = map[target - nums[i]];
        if(otherIndex && otherIndex != i) {
            return [Math.min(i, otherIndex), Math.max(i, otherIndex)]
        }
    }
};
複製程式碼

20. Valid Parentheses

判斷輸入的字串是不是隻包含(){}[ and ]

  • 必須使用相同型別的括號關閉左括號。
  • 必須以正確的順序關閉左括號。
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    const stack = [];
    const helpMap = {
        '(': ')',
        '[': ']',
        '{': '}'
    }
    for(let i = 0; i < s.length; i++) {
        const char = s[i];
        if(char in helpMap) {
            stack.push(helpMap[char]);
        } else if(char !== stack.pop()){
            return false;
        }
    }
    return stack.length === 0;
};

複製程式碼

21. Merge Two Sorted Lists

合併兩個已排序的連結串列並將其作為新連結串列返回。新連結串列應該通過拼接前兩個連結串列的節點來完成。

  • 解法一:遞迴
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function (l1, l2) {
    if (!l1) return l2;
    if (!l2) return l1;
    if (l1.val <= l2.val) {
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
};
複製程式碼
  • 解法二:迴圈
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function (l1, l2) {
    if (!l1) return l2;
    if (!l2) return l1;
    const newList = new ListNode(null);
    let newPointer = newList;
    while (l1 && l2) {
        if (l1.val < l2.val) {
            newPointer.next = l1;
            l1 = l1.next;
        } else {
            newPointer.next = l2;
            l2 = l2.next;
        }
        newPointer = newPointer.next;
    }
    if (l1) newPointer.next = l1;
    if (l2) newPointer.next = l2;
    return newList.next;
};
複製程式碼

35. Search Insert Position

給定排序陣列和目標值,如果找到目標,則返回索引。如果沒有,請返回索引按順序插入的索引。

類似於二分查詢的變種題

平時二分查詢遞迴的寫多了,這裡來個不使用遞迴的版本

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var searchInsert = function(nums, target) {
    let start = 0, end = nums.length - 1;
    let mid = Math.ceil((end + start) / 2);
    while(nums[mid] !== target) {
        if(end <= start) {
            // 如果沒有找到要看看插哪裡
            return nums[end] < target ? end + 1 : end;
        }
        if(nums[mid] > target) {
            // 保護一下不要變成負的
            end = Math.max(mid - 1, 0);
        } else {
            // 保護一下不要越界
            start = Math.min(mid + 1, nums.length - 1);
        }
        mid = Math.floor((end + start) / 2);
    }
    return mid;
};
複製程式碼

52. Maximum Subarray

經典題:找一個陣列中最大子序列和

解法:從頭到尾遍歷每一個陣列元素,如何前面元素的和為正,則加上本元素的值繼續搜尋;如何前面元素的和為負,則此元素開始新的和計數。以及整個過程中要注意更新和的最大值。

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    let cache = 0;
    let max = -Infinity;
    nums.forEach(num => {
        cache += num;
        if(cache > max) max = cache;
        if(cache < 0) cache = 0;
    })
    return max;
};
複製程式碼

70. Climbing Stairs

跳臺階,經典DP

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    const dp = [0, 1, 2];
    for(let i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n]
};
複製程式碼

100. Same Tree

判斷倆二叉樹是不是完全相同 不好的寫法:利用短路等特性把程式碼寫在一行,注意做取值保護

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {boolean}
 */
var isSameTree = function(p, q) {
    return (p || q) ? (p || {}).val === (q || {}).val && isSameTree((p || {}).left, (q || {}).left) && isSameTree((p || {}).right, (q || {}).right) : true
};
複製程式碼

101. Symmetric Tree

判斷一顆二叉樹是不是映象 解法一:繼續一行寫完

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
    return childIsSymmetric((root || {}).left, (root || {}).right);
};

function childIsSymmetric (left, right) {
    return (left || right) ? (left || {}).val === (right || {}).val && childIsSymmetric((left || {}).left, (right || {}).right) && childIsSymmetric((left || {}).right, (right || {}).left) : true;
}
複製程式碼

解法二:

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
    if(!root) return true;
    function areMirror(left, right) {
        if(!left && !right) return true;
        if((left && !right) || (right && !left)) return false;
        if(left.val != right.val) return false;
        return areMirror(left.left, right.right) && areMirror(left.right, right.left);
    }
    return areMirror(root.left, root.right);
};
複製程式碼

104. Maximum Depth of Binary Tree

找二叉樹深度

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root, deep = 0) {
    if(!root) return deep;
    return Math.max(maxDepth(root.left) + 1, maxDepth(root.right) + 1);
};
複製程式碼

121. Best Time to Buy and Sell Stock

給一個陣列,其中第i個元素是第i天給定股票的價格。只能進行一次買進和賣出,求最大利潤

首先設定最大利潤和最低價格:

  • 如果當前這一天的股票價格比最低價格還小,那就把最低價格設定為這一天的股票價格。
  • 如果最大利潤比當天價格減掉最低價格還要低,那就把最大利潤設定成當天價格減去最低的價格。
/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if(!prices.length) return 0;
    let minPrice = Infinity, maxProfit = -Infinity;
    prices.forEach(price => {
        if(price < minPrice) {
            minPrice = price;
        }
        if(maxProfit < (price - minPrice)) {
            maxProfit = (price - minPrice);
        }
    });
    return maxProfit;
};
複製程式碼

136. Single Number

經典題,一行程式碼異或解決

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums){
    return nums.reduce((a,b) => { return a ^ b});
}
複製程式碼

141. Linked List Cycle

判斷連結串列是否有環,不能使用額外空間 解法:用兩個指標,快指標每次走兩步,慢指標每次走一步。這樣每走一次快指標比慢指標多一步,如果有環最終能夠相遇。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function (head) {
    let slow = head;
    let fast = head;

    while (fast) {
        slow = slow.next;
        fast = fast.next && fast.next.next;
        if (slow === fast && fast !== null) {
            return true;
        }
    }

    return false;
};
複製程式碼

155. Min Stack

設計一個支援pushpoptop和在恆定時間內檢索最小元素的堆疊。

  • push(x) -- 將元素x推入堆疊
  • pop() --  刪除堆疊頂部的元素
  • top() -- 獲取頂部元素
  • getMin() -- 檢索堆疊中的最小元素

最優解 - O(1):

該題要求的是實現一個棧,棧的修改元素的操作只有 pop()push(x),因此我們可以在 push 的時候維護一個作用類似於 快取 的,記錄這個時候最小元素是什麼的棧 minStack。在每次 push 的時候,更新一下我們的快取,這個時候只需要對比一下當前入棧元素和 minStack 棧頂元素,然後取小的推入 minStack 即可,代表著這個時候最小值應該是 之前棧中最小的 和 新來的 中最小的一個元素。

/**
 * initialize your data structure here.
 */
var MinStack = function() {
    this.stack = [];
    this.minStack = [];
};

/** 
 * @param {number} x
 * @return {void}
 */
MinStack.prototype.push = function(x) {
    this.stack.push(x);
    const minStackTop = this.minStack[this.minStack.length - 1];
    this.minStack.push(Math.min(x, minStackTop === undefined ? Infinity : minStackTop));
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function() {
    this.stack.pop();
    this.minStack.pop();
};

/**
 * @return {number}
 */
MinStack.prototype.top = function() {
    return this.stack[this.stack.length - 1];
};

/**
 * @return {number}
 */
MinStack.prototype.getMin = function() {
    return this.minStack[this.minStack.length - 1];
};

/** 
 * Your MinStack object will be instantiated and called as such:
 * var obj = Object.create(MinStack).createNew()
 * obj.push(x)
 * obj.pop()
 * var param_3 = obj.top()
 * var param_4 = obj.getMin()
 */
複製程式碼

160. Intersection of Two Linked Lists

找到兩個單連結串列公共開頭的節點。

  • 果兩個連結列表根本沒有交集,則返回null
  • 函式返回後,連結列表必須保留其原始結構。
  • 可以假設整個連結結構中沒有任何迴圈。
  • 程式碼在O(n)時間內執行,並且只使用O(1)記憶體。

image.png | left | 488x165
由上圖可以得知,如果要讓遍歷用的指標在連結串列公共節點入口處相遇,即兩個指標走的節點一樣長,只需要在 A 連結串列遍歷完以後換到 B 連結串列上(走了 a + c + b 個節點),然後 B 連結串列出發走完了換到 A 連結串列上(走了 b + c + a 個節點)即可。

同時我們還要處理兩個連結串列沒有公共節點的情況:

image.png | left | 244x161

如上圖,從 A 出發的指標在走了 a + b 個節點後,從 B 出發的指標也走了 b + a 個節點,因此他們此時再走一步以後就都是 undefined, 也就是說兩個連結串列沒有公共節點的話,只要判斷兩個指標都是 undefined 就可以知道了。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    let nodeA = headA, nodeB = headB;
    while(nodeA !== nodeB) {
        nodeA = nodeA ? nodeA.next : headB;
        nodeB = nodeB ? nodeB.next : headA;
    }
    return nodeA || null;
};
複製程式碼

169. Majority Element

找出陣列中出現次數超過 `⌊ n/2 ⌋ 的數 這個題比較有意思一點,給出如下幾種方案:

  • 由於多於一半,因此可以直接排序後看中間位置上的數
  • Moore voting algorithm:`每次都找出一對不同的元素,從陣列中刪掉,直到陣列為空或只有一種元素
  • Boyer-Moore Algorithm(多數投票演算法):記錄一個當前過半數變數 A 及其個數 numA ,在遍歷過程中,如果當前元素和記錄元素 A 相等,則 numA 加 1;如果不相等,則 numA 減 1。如果 numA 為零,則更新 A 和重置 numA 。本質是:在遍歷陣列時,如果numA為0,表示當前並沒有候選元素,也就是說之前的遍歷過程中並沒有找到超過半數的元素。那麼,如果超過半數的元素A存在,那麼A在剩下的子陣列中,出現次數也一定超過半數。因此我們可以將原始問題轉化為它的子問題。這裡有一個視覺化的流程
/**
 * @param {number[]} nums
 * @return {number}
 */
var majorityElement = function(nums) {
    let result, count = 0
    nums.forEach(num => {
        if(result !== num) {
            if(count === 0) {
                count = 1;
                result = num;
            } else {
                count--;
            }
        } else {
            count++
        }
    });
    return result;
};
複製程式碼

198. House Robber

你是一名專業強盜,計劃沿著一條街打家劫舍。每間房屋都儲存有一定數量的金錢,唯一能阻止你打劫的約束條件是:由於房屋之間有安全系統相連,如果同一個晚上有兩間相鄰的房屋被闖入,它們就會自動聯絡警察,因此不可以打劫相鄰的房屋。給定一列非負整數,代表每間房屋的金錢數,計算出在不驚動警察的前提下一晚上最多可以打劫到的金錢數。

DP:對於第i個房間我們的選擇是偷和不偷

  • 如果決定是偷,則第 i-1 個房間必須不偷,那麼這一步的就是 dp[i] = nums[i-1] + dp[i -2] , 假設dp[i]表示打劫到第 i 間房屋時累計取得的金錢最大值
  • 如果是不偷, 那麼上一步就無所謂是不是已經偷過, dp[i] = dp[i -1]
  • 因此 dp[i] = max(dp[i - 2] + nums[i - 1], dp[i - 1] )
/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    if(!nums.length) return 0;
    const dp = [nums[0]];
    for(let i = 1; i < nums.length; i++){
        dp[i] = Math.max(dp[i - 1], (dp[i - 2] || 0) + nums[i]);
    }
    return dp[nums.length - 1];
};
複製程式碼

206. Reverse Linked List

反轉連結串列

可以老老實實的迴圈/遞迴:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function (head) {
    if (!head) return head;
    let start = head;
    let end = head
    while (end.next) {
        const node = end.next;
        end.next = node.next;
        node.next = start;
        start = node;
    }
    return start;
}
複製程式碼

也可以騷操作一下:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function (head, pre) {
    if(!head) return head
    const next = head.next;
    head.next = pre || null
    return next ? reverseList(next, head) : head;
};
複製程式碼

226. Invert Binary Tree

反轉二叉樹

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {
    if(!root) return [];
    [root.left, root.right] = [root.right, root.left];
    invertTree(root.left);
    invertTree(root.right);
    return root
};
複製程式碼

234. Palindrome Linked List

判斷一個連結串列是不是迴文,要求 O(n) 時間, O(1) 空間

本題暴力解法就是遍歷完連結串列後轉為字串,然後看是不是迴文,符合時間複雜度要求,但是不符合空間複雜度要求

要求 O(1) 的空間,那就只能從連結串列本身動手了。首先判斷迴文無非就是從兩邊到中間或者從中間到兩邊。由於我們可以對連結串列本身動手,那就考慮讓連結串列能夠倒著訪問(因為要求O(1)空間,所以不能直接改造為雙向連結串列)。由於我們只能讓連結串列順著一個方向走,所以可以想到選擇從中間到兩邊的方式,左邊的向前(pre),右邊的向後(next)。

那麼我們如何找到中間的節點呢 - 中間節點即為連結串列的一半,那我們使用一個快指標一次走兩步,一個慢指標一次走一步,那麼快指標走到尾時,慢指標應該走到連結串列中間。同時要注意區分連結串列長度是奇數還是偶數:如果是奇數的話,正中間的節點不需要做判斷,應該用它前後兩個節點開始比較。

最後的程式碼如下:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {boolean}
 */
var isPalindrome = function(head) {
    if(!head) return true;
    if(!head.next) return true;
    let fast = head.next, slow = head;
    let pair = null;
    while(fast != null && fast.next != null) {
        slow.next.pre = slow;
        slow = slow.next;
        fast = fast.next.next;
    }
    if(!fast || fast.next) {
        // 奇數
        pair = slow.next;
        slow = slow.pre;
    } else {
        // 偶數
        pair = slow.next;
    }
    while(pair) {
        if(pair.val !== slow.val) return false;
        pair = pair.next;
        slow = slow.pre;
    }
    return true;
};
複製程式碼

283. Move Zeroes

給定一個陣列nums,寫一個函式將所有0移動到它的末尾,同時保持非零元素的相對順序。 額外要求:

  • 您必須在不製作陣列副本的情況下就地執行此操作
  • 最小化操作總數。 思路:
  • 最小化操作:遍歷一遍的過程中操作完,且不需要額外移動操作
  • 記 zero 的個數為 zeroesNums ,然後將每一個非零的數向前移動 zeroesNums ,最後在陣列末尾填上 zero
/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var moveZeroes = function(nums) {
    let zeroesNums = 0;
    nums.forEach((num, index) => {
        if(num === 0) {
            zeroesNums++;
        } else {
            nums[index - zeroesNums] = num;
        }
    });
    for(let i = nums.length - 1; zeroesNums > 0; i--) {
        nums[i] = 0;
        zeroesNums--;
    }
};
複製程式碼

437. Path Sum III

給一顆每個節點都是整數(可正可負)的二叉樹,求有多少條路徑加起來等於一個給定值。注意,路徑不需要在根或葉子處開始或結束,但必須向下(即僅從父節點行進到子節點)。

解法一: 暴力解,使用 BFS 和遞迴來搜尋符合條件的路徑。需要注意的是這種方法沒有利用任何快取,即計算每條路徑和的時候都重新遍歷了所有路徑節點。時間複雜度為 O(n²)

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} sum
 * @return {number}
 */
var pathSum = function(root, sum) {
    if(!root) return 0;
    let result = 0;
    const queue = [root];
    while(stack.length) {
        const node = queue.shift();
        result += reslove(node, sum);
        node.left && queue.push(node.left);
        node.right && queue.push(node.right);
    }
    return result
};

function reslove(root, sum) {
    if(!root) return 0;
    let result = 0;
    if(sum === root.val) result++;
    return result + reslove(root.left, sum - root.val) + reslove(root.right, sum - root.val);
}
複製程式碼

解法二: 如果不想用 queue,那也可以直接用遞迴來做搜尋

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} sum
 * @return {number}
 */
var pathSum = function(root, sum) {
    if(!root) return 0;
    return reslove(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum);
};

function reslove(root, sum) {
    if(!root) return 0;
    return ((sum === root.val) ? 1 : 0) + reslove(root.left, sum - root.val) + reslove(root.right, sum - root.val);
}
複製程式碼

解法三(O(n)): 在前兩種解法中,我們自頂而下重複遍歷了每層節點(第一層被遍歷一次,第二層被遍歷兩次,……)。這個時候我們就該想辦法利用快取來減少遍歷次數。

因此便有了如下 O(n) 複雜度的解法(思路寫在了註釋中了)

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} sum
 * @return {number}
 */
var pathSum = function(root, sum) {
    // 快取
    const hashMap = {};
    let currentSum = 0;
    return pathSumRecursive(root, currentSum, hashMap, sum);
};

function pathSumRecursive(node, currentSum, hashMap, target) {
    if (!node) {
        return 0;
    }

    const newCurrentSum = currentSum + node.val;
    // 看一看能不能利用之前的快取,巧妙的在一次遍歷中算出了所有線段
    // 當前路徑和 - 目標值 —— 本質是看 中間有沒有一段路徑和 等於 目標值
    // 比如 2 - 5 - 3 的路徑, 目標為 8,那麼在 3 這個節點時,路徑和為 10 , 減去目標值8 後為 2, 之前路徑上有1條路線和為 2,因此中間有一段和為目標值 8
    let totalPaths = hashMap[newCurrentSum - target] || 0;
    
    if (newCurrentSum === target) {
        totalPaths++;
    }
    // 更新一下快取
    if (hashMap[newCurrentSum]) {
        hashMap[newCurrentSum]++;
    } else {
        hashMap[newCurrentSum] = 1;
    }

    totalPaths += pathSumRecursive(node.left, newCurrentSum, hashMap, target);
    totalPaths += pathSumRecursive(node.right, newCurrentSum, hashMap, target);
    
    // 由於是共用一個快取,因此遍歷完後續節點後,要在退回上一層的時候把自身從快取中刪掉,來保證快取資料的正確性(只應該有之前路徑的)
    hashMap[newCurrentSum]--;
    return totalPaths;
}
複製程式碼

438. Find All Anagrams in a String

在字串中尋找同構體:給定一個字串s和一個非空字串p,找到s中p的同構體的所有起始索引。什麼是同構體:兩個字串字母一樣,字母在字串中的順序可能不一樣,比如 ab 和 ba 是同構的

解法一:

對於這個首先想到的是利用快取來提高效率,這裡我們先才用 Map 的形式做對映。同時使用滑動視窗 - 兩個指標(下標)來指向當前子串。然後從前往後掃,來通過 Map 看是不是匹配。如果

  • 當前字元在我們的 Map 中有,但是已經被匹配完了,則需要把指向開頭的指標向後掃,直到遇見同樣的字元。
  • 當前字元在我們的 Map 中根本沒有,則意味著到這個字元為止都不會有符合條件的字串,因此需要將 Map 恢復後,從當前字元的下一個開始重新尋找
/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
var findAnagrams = function(s, p) {
    const targetMap = {};
    // 構建map
    for(let char of p) {
        if(targetMap[char]) {
            targetMap[char]++;
        } else {
            targetMap[char] = 1;
        }
    }
    let start = 0;
    let cacheLen = p.length;
    const result = [];
    for(let i = 0; i < s.length; i++) {
        const char = s[i];
        // 如果 char 還有
        if(targetMap[char]) {
            targetMap[char]--;
            cacheLen--;
            // 如果都匹配上了
            if(cacheLen === 0) {
                result.push(start); // 推進去
                // 所有的向前移動一位
                targetMap[s[start]]++; 
                start++;
                cacheLen++;
            }
        } else if(targetMap[char] === 0) {
            // char 有,但是超過個數了,就要向前走把char去掉一個
            while(s[start] !== char) {
                targetMap[s[start]]++;
                start++;
                cacheLen++;
            }
            start++;
        } else {
            // char 根本沒有,就跳過之前這段
            while(start < i) {
                targetMap[s[start]]++;
                start++;
            }
            start++;
            cacheLen = p.length;
        }
    }
    return result;
};
複製程式碼

解法二: 上圖中用 Obj 做 Mapping,我們也可以用陣列結合字元下標來做 Mapping

/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
var findAnagrams = function (s2, s1) {
    const map = Array(128).fill(0);
    let start = 0,
        end = 0,
        counter = s1.length;
    const res = [];
    for (let i = 0; i < s1.length; ++i) {
        map[s1.charCodeAt(i)]++;
    }
    while (end < s2.length) {
        if (map[s2.charCodeAt(end)] > 0) {
            counter--;
        }
        map[s2.charCodeAt(end)]--;
        end++;
        while (counter == 0) {
            if (end - start == s1.length) {
                res.push(start);
            }
            if (map[s2.charCodeAt(start)] == 0) {
                counter++;
            }
            map[s2.charCodeAt(start)]++;
            start++;
        }
    }
    return res;
};
複製程式碼

448. Find All Numbers Disappeared in an Array

常規解法:因為題目上給出條件說陣列裡的數字都在 [1, n],且要求不適用額外空間,因此可以想到該題為套路題:對原位置上的數字移動/加減/位運算等解法。 此題常規可以選用反轉對應位置上數字的方法:把出現的數字的對應位上的數字變為負數,然後遍歷找出那些正數,其下標+1則為沒有出現過的數字

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var findDisappearedNumbers = function(nums) {
    nums.forEach(num => {
        num = Math.abs(num);
        if(nums[num - 1] > 0) {
            nums[num - 1] = -nums[num - 1]
        }
    });
    const result = [];
    nums.forEach((num, index) => {
        if(num > 0) {
            result.push(index + 1);
        }
    });
    return result;
};
複製程式碼

位運算騷操作版: 首先需要簡單理解幾個位運算是幹什麼的:

  • JavaScript 中位運算將其運算元(operands)當作32位的位元序列,有符號數最左位元位為1
  • 1 << 31:變成 10000000000000000000000000000000
  • 1 << 31 - 1 則變成 01111111111111111111111111111111
  • 與1 << 31 進行 | 運算,則會把一個數(無論正負)變成負數(只修改符號位)
  • 與1 << 31 - 1 進行 & 運算,則會把一個數(無論正負)變成正數(只修改符號位) 該解法用以上方式避開對符號的判斷
/**
 * @param {number[]} nums
 * @return {number[]}
 */
var findDisappearedNumbers = function(nums) {
    for (var i = 0; i < nums.length; i++) {
        nums[(nums[i] & ((1 << 31) - 1)) - 1] |= (1 << 31);
        //                 統一正負數運算            變成負數                               
    }
    
    ans = [];
    
    for (var i = 0; i < nums.length; i++) {
        // 如果不是負數,就推進去
        if ((nums[i] & (1 << 31)) != (1 << 31))
            ans.push(i+1);
    }
    
    return ans;
};
複製程式碼

461. Hamming Distance

Hamming Distance 表示兩個等長字串在對應位置上不同字元的數目,也度量了通過替換字元的方式將字串x變成y所需要的最小的替換次數。

1.常規解法:轉成二進位制以後一位一位的算,需要手動補位或手動判斷 undefined

/**
 * @param {number} x
 * @param {number} y
 * @return {number}
 */
var hammingDistance = function (x, y) {
    let binaryX = x.toString(2);
    let binaryY = y.toString(2);
    const len = Math.max(binaryX.length, binaryY.length);
    if (binaryX.length < len) {
        binaryX = binaryX.padStart(len, '0')
    } else {
        binaryY = binaryY.padStart(len, '0')
    };
    let result = 0;
    for (let i = len - 1; i >= 0; i--) {
        if (binaryX[i] !== (binaryY[i])) {
            result++
        }
    }
    return result;
};

複製程式碼

2.位運算:按位異或,不需要考慮補長度,更簡潔

/**
 * @param {number} x
 * @param {number} y
 * @return {number}
 */
var hammingDistance = function (x, y) {
    var res = x ^ y;
    var count = 0;
    while (res != 0) {
        if (res % 2 == 1) count++;
        res = Math.floor(res / 2); // res = res >> 1;
    }
    return count;
};
複製程式碼

538. Convert BST to Greater Tree

二叉搜尋樹上的每一個節點要加上所有大於他的節點的值:原始BST的每個 key 都更改為原始 key 加上大於BST中原始 key 的所有 key 的總和。

解法一:

  • BST的性質如下
    • 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
    • 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
    • 它的左、右子樹也分別為二叉排序樹。
  • 使用 右->中->左的順序從大到小遍歷,並利用 cacheVal 來快取比當前節點大的值來達到 O(n) 的時間複雜度
  • 在遞迴中進行 cacheVal 的傳遞而不是在外層儲存該值(麻煩一點,因為需要處理右子樹最左節點:程式碼29行
  • 因為右子樹最左節點的只是除了當前節點以外最小的,所以右子樹最左節點的值為最大的(所有比當前節點大的節點加起來的值)。因此當前節點只需要加上右子樹最左節點的值即可
/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var convertBST = function(root) {
    if(root) {
        converVal(root);
        return root;
    } else {
        return [];
    }
};

function converVal(root, cacheVal = 0) {
    if(root.right) {
        cacheVal = converVal(root.right, cacheVal);
    }
    root.val += cacheVal;
    cacheVal = root.val;
    if(root.left) {
        // 處理右子樹最左節點,返回給上一層遞迴來使用(此時右子樹最左節點為上一層節點需要加的值)
        return converVal(root.left, cacheVal);
    } 
    return root.val;
}
複製程式碼

解法二:把解法一中的 cacheVal 提出來放在外圍搞一個閉包,然後就不用每次遞迴傳進去了,這樣只需要從大到小遍歷即可,簡單易懂。

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var convertBST = function(root) {
    let sum = 0;
    
    return function inner(root) {
        if (root == null) return null;
        inner(root.right);
        root.val += sum;
        sum = root.val;
        inner(root.left);
        return root;
    }(root);
};
複製程式碼

543. Diameter of Binary Tree

給定二叉樹,計算樹的直徑長度 - 二叉樹的直徑是樹中任意兩個節點之間最長路徑的長度。此路徑可能會也可能不會通過根節點。

遞迴,因為是尋找一條最長的路徑,因此分成兩個情況考慮:

  • 尋找當前子樹左子樹和右子樹單側最長路徑,並返回給上一層使用
  • 返回當前子樹最長路徑(左子樹最長路徑 + 當前根節點(1) + 右子樹最長路徑給上一層使用

最後找出這兩者更大的一個即可

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var diameterOfBinaryTree = function(root) {
    if(!root) return 0;
    // 返回一個最大的
    return Math.max(...diameterOfSubtree(root)) - 1;
};
function diameterOfSubtree(root) {
    if(!root.left && !root.right) return [1, 1];
    let left = 0, leftBig = 0, right = 0, rightBig = 0;
    if(root.left) [left, leftBig] = diameterOfSubtree(root.left);
    if(root.right) [right, rightBig] = diameterOfSubtree(root.right);
    // 當前子樹最長路徑
    const cacheBig = Math.max(leftBig, rightBig, left + right + 1);
    return [1 + Math.max(left, right), cacheBig];
}

複製程式碼

572. Subtree of Another Tree

判斷一棵樹是不是另一顆樹的子結構

解法一:直接遞迴看一下是不是子樹,但這樣有重複遍歷

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} s
 * @param {TreeNode} t
 * @return {boolean}
 */
var isSubtree = function (s, t) {
    return !!(subtree(s, t) || 
            (s.left && isSubtree(s.left, t)) || 
            (s.right && isSubtree(s.right, t)));
};
function subtree(s, t) {
    if (!s && !t) return true;
    return ((s || {}).val === (t || {}).val) && 
            subtree(s.left, t.left) && 
            subtree(s.right, t.right);
}
複製程式碼

解法二:前序遍歷樹,存成字串,然後看看 source 裡面是不是包含 target 即可

var isSubtree = function(s, t) {
  let string1 = {str: ""};
  let string2 = {str: ""};
  treeString(s, string1);
  treeString(t, string2);

  return string1.str.includes(string2.str);
}

function treeString(root, string) {
  if (!root) {
    string.str += "X";
    return;
  }
  string.str += `,${root.val},`
  treeString(root.left, string);
  treeString(root.right, string);
}
複製程式碼

581. Shortest Unsorted Continuous Subarray

最短未排序連續子陣列:給定一個整數陣列,您需要找到一個最短的連續的子陣列,要求是如果序對此子陣列進行升序排序後,整個陣列也將按升序排序。

第一種簡單的方法是把陣列進行排序,那麼原陣列和新陣列不一樣的個數即為界限,但是這種的複雜度為 O(nlgn) (排序後遍歷)

或者我們可以用兩個指標,一個從前向後,一個從後向前

  • 從前向後的尋找最後一個不為最大值的索引
  • 從後向前的尋找第一個不為最小值的索引

然後就能得出哪一段是沒有按照升序排序的了

/**
 * @param {number[]} nums
 * @return {number}
 */
var findUnsortedSubarray = function(nums) {
    let last = 0, first = -1, max = -Infinity, min = Infinity;
    for(let i = 0, j = nums.length - 1; j >= 0; j--, i++){
        max = Math.max(max, nums[i]);
        if(nums[i] !== max) first = i;
        
        min = Math.min(min, nums[j]);
        if(nums[j] !== min) last = j;
    }
    return first - last + 1;
};
複製程式碼

617. Merge Two Binary Trees

該題有疑似有惡性 bug - testcase:

Input: [] []
Expected: []
Output: null
複製程式碼

遞迴:

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} t1
 * @param {TreeNode} t2
 * @return {TreeNode}
 */
var mergeTrees = function(t1, t2) {
  if (!t1) {
    return t2;
  }
  if (!t2) {
    return t1;
  }
  t1.val += t2.val;
  t1.left = mergeTrees(t1.left, t2.left);
  t1.right = mergeTrees(t1.right, t2.right);
  return t1;
};
複製程式碼

非遞迴:利用棧加樹的層次遍歷寫法

var mergeTrees = function (t1, t2) {
    if (t1 === null) {
        return t2;
    }
    const stack = [];
    stack.push([t1, t2]);
    while (stack.length !== 0) {
        const t = stack.pop();
        if (t[0] === null || t[1] === null) {
            continue;
        }
        t[0].val += t[1].val;
        if (t[0].left === null) {
            t[0].left = t[1].left;
        } else {
            stack.push([t[0].left, t[1].left]);
        }
        if (t[0].right === null) {
            t[0].right = t[1].right;
        } else {
            stack.push([t[0].right, t[1].right]);
        }
    }
    return t1;
};
複製程式碼

相關文章