笔试刷题

155 阅读5分钟

1. 题目类型

1.1 动态规划

需要明确:

  1. 需要记录的状态是什么
  2. 如何从一个状态转移到下一个状态(状态转移方程)

1.2 双指针

1.3 快慢指针

思路:快指针先前进N步,随后一起移动,直到快指针先到尾部,此时慢指针位置就是目标节点的前驱节点

关键字:倒数第N个

模式识别:

  • 涉及列表的特殊位置,考虑快慢指针
  • 要删除列表节点,找到它的前驱

相关题目:10

1.4 二分搜索及其变种

关键字:排序,搜索

模式识别:

  • 有序或者部分有序

2. 力扣top100

力扣top100:leetcode-cn.com/problem-lis…

以下编号是top100从上往下的编号,并非题目实际的编号

1. 两数之和

思路一:直接找 target-nums[i] 的值

var twoSum = function(nums, target) {
    const result = [];
    for (let i = 0; i < nums.length; i++) {
        let idx = nums.indexOf(target - nums[i], i + 1);
        if (idx > 0) {
            result.push(i, idx);
            break;
        }
    }
    return result;
};

思路二:利用 Map 结构(边判断,边存储,循环一遍过)

var twoSum = function(nums, target) {
    const map = new Map();
    for (let i = 0; i < nums.length; i++) {
        const num = target - nums[i];
        if (map.has(num)) {
            return [map.get(num), i];
        }
        map.set(nums[i], i);
    }
    return [];
};

2. 两数相加

思路一:直接处理,通过创建默认头结点、已结束列表对应位置值设置为0使代码更简洁

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
var addTwoNumbers = function(l1, l2) {
    let node = new ListNode();
    const head = node;
    let addOne = 0;
    while (addOne || l1 || l2) {
        let num1 = l1 ? l1.val : 0; // l1?.val??0;
        let num2 = l2 ? l2.val : 0;
        let sum = num1 + num2 + addOne;
        node.next = new ListNode(sum % 10);
        node = node.next;

        addOne = sum >= 10 ? 1 : 0;
        if (l1) l1 = l1.next;
        if (l2) l2 = l2.next;
    }

    return head.next;
};

3. 无重复字符的最长子串长度

思路:滑动窗口,注意左下标位置不能回退,i必须通过Math.max()进行判断(案例:'abba')

var lengthOfLongestSubstring = function(s) {
    if (s.length === 0) return 0;
    let i = 0, max = 0;
    const map = new Map();
    for (let j = 0; j < s.length; j++) {
        if (map.has(s[j])) {
            i = Math.max(i, map.get(s[j]) + 1);
        }
        map.set(s[j], j);
        max = Math.max(max, j - i + 1);
    }

    return max;
};

4. 寻找两个有序数组的中位数

思路:二分查找、数组分割线

分割线

function findMedianSortedArrays(nums1: number[], nums2: number[]): number {
    // 约定nums1长度小于nums2
    if (nums1.length > nums2.length) {
        return findMedianSortedArrays(nums2, nums1);
    }

    // m、n分别存储nums1、nums2的长度
    let m = nums1.length, n = nums2.length;
    // 分割线左边的所有元素需要满足个数 (m + n + 1) / 2【向下取整】
    let totalLeft = Math.floor((m + n + 1) / 2);

    // 在nums1的区间[0, m]中查找恰当的分割线,第二条分割线可根据totalLeft计算位置
    // 约定i、j分别位于两个分割线的右边,也等于左侧被分割数组长度
    // 分割线需要满足nums1[i - 1] <= nums2[j] && nums[j - 1] <= nums[i]
    // 以较短的数组进行查找(第一个数组)
    let left = 0, right = m;

    while (left < right) {
        let i = left + (right - left + 1) / 2;
        
    }
};

5. 最长回文子串

思路一:暴力破解,最长子串为本身、小一个字符子串……进行查找,力扣会超时

function longestPalindrome(s: string): string {
    // 判断字符串是否是回文字符串
    const palindrome = (str: string): boolean => {
        const reverseStr = str.split('').reverse().join('');
        return str === reverseStr;
    };

    let maxLen = s.length;
    while (maxLen > 0) {
        for (let i = 0; i <= s.length - maxLen; i++) {
            const subStr = s.slice(i, i + maxLen);
            if (palindrome(subStr)) {
                return subStr;
            }
        }

        maxLen--;
    }

    return '';
};

思路二:动态规划(dp[i][j] = s[i] === s[j] && dp[i + 1][j - 1]

// 动态规划:dp[i][j] = s[i] === s[j] && dp[i+1][j-1]
function longestPalindrome(s: string): string {
    const length = s.length;
    if (length < 2) {
        return s;
    }

    // 初始化
    let dp = [];
    for (let i = 0; i < length; i++) {
        dp.push([]);
        // dp[i][j]表示s[i...j]是否是回文串
        dp[i][i] = true;
    }
    let maxLen = 1, begin = 0;
    const charArr = s.split('');

    // 从右向左按列填写
    for (let j = 1; j < length; j++) {
        // i,j分别表示串的左右下标
        for (let i = 0; i < j; i++) {
            if (charArr[i] !== charArr[j]) {
                dp[i][j] = false;
            }
            else {
                // 转移的状态子串长度小于2
                if (j - i < 3) {
                    dp[i][j] = true;
                }
                else {
                    dp[i][j] = dp[i + 1][j - 1];
                }
            }

            // 只要dp[i][j]===true成立,就表示子串s[i...j]是回文字符串,记录此时的长度和起始位置
            let currentLen = j - i + 1;
            if (dp[i][j] && currentLen > maxLen) {
                maxLen = currentLen;
                begin = i;
            }
        }
    }

    return s.substring(begin, begin + maxLen);
};

6. 正则表达式匹配

状态转移方程:

function isMatch(s: string, p: string): boolean {
    let sLen = s.length, pLen = p.length;
    let dp = [];
    for (let i = 0; i <= sLen; i++) {
        dp.push([]);
    }
    dp[0][0] = true;

    // 计算机并不知道输入字符串和匹配串位置会随着走,而是遍历所有情况,存储状态
    for (let i = 0; i <= sLen; i++) {
        for (let j = 1; j <= pLen; j++) {
            if (p.charAt(j - 1) === '*') {
                dp[i][j] = dp[i][j - 2];
                if (matches(s, p, i, j - 1)) {
                    dp[i][j] = dp[i][j] || dp[i - 1][j];
                }
            }
            else {
                if (matches(s, p, i, j)) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
            }
        }
    }

    return !!dp[sLen][pLen];
};

// i, j分别是s, p的下标
function matches(s: string, p: string, i: number, j: number): boolean {
    if (i === 0) {
        return false;
    }
    if (p.charAt(j - 1) === '.') {
        return true;
    }
    return s.charAt(i - 1) === p.charAt(j - 1);
}

7. 盛最多水的容器

模式识别:需要移动左右两头的问题可以考虑双指针

// 双指针:从两边出发记录面积,每次移动短的一边,一遍过
function maxArea(heights: number[]): number {
    let max = 0, left = 0, right = heights.length - 1;
    while (left < right) {
        const width = right - left;
        const height = Math.min(heights[left], heights[right]);
        max = Math.max(max, width * height);
        heights[left] < heights[right] ? left++ : right--;
    }
    return max;
};

8. 三数之和

// 排序 + 双指针
// 固定一个数,剩下两个可通过双指针进行查找
function threeSum(nums: number[]): number[][] {
    const res: number[][] = [], length = nums.length;
    nums.sort((a, b) => a - b);

    for (let first = 0; first < length; first++) {
        // 需要和上次枚举的数不同
        if (first > 0 && nums[first] === nums[first - 1]) {
            continue;
        }

        // 起始位置为最后一个元素,target可以为任意元素此处为0
        let third = length - 1, target = 0 - nums[first];
        for (let second = first + 1; second < length; second++) {
            // 需要和上次枚举的数不同
            if (second > first + 1 && nums[second] === nums[second - 1]) {
                continue;
            }

            while (second < third && (nums[second] + nums[third]) > target) {
                third--;
            }

            if (second === third) {
                break;
            }
            if ((nums[second] + nums[third]) === target) {
                res.push([nums[first], nums[second], nums[third]]);
            }
        }
    }

    return res;
};

js数组默认排序方式是按照字符串进行排序的,可传入回调函数按照指定规则排序

[11, 1, 2].sort(); // [1, 11, 2]
[11, 1, 2].sort((a, b) => a - b); // [1, 2, 11]

9. 电话号码的字母组合

回朔算法:

function letterCombinations(digits: string): string[] {
    const combinations: string[] = [];
    if (digits.length === 0) {
        return combinations;
    }

    const keyMap = new Map();
    keyMap.set('2', 'abc');
    keyMap.set('3', 'def');
    keyMap.set('4', 'ghi');
    keyMap.set('5', 'jkl');
    keyMap.set('6', 'mno');
    keyMap.set('7', 'pqrs');
    keyMap.set('8', 'tuv');
    keyMap.set('9', 'wxyz');

    backtrack(combinations, digits, keyMap, 0, []);
    return combinations;
};

function backtrack(combinations: string[], digits: string, keyMap: Map<string, string>, index: number, buffer: string[]): void {
    if (index === digits.length) {
        combinations.push(buffer.join(''));
    }
    else {
        const digit = digits.charAt(index);
        const letters = keyMap.get(digit);
        const lettersLen = letters.length;
        for (let i = 0; i < lettersLen; i++) {
            buffer.push(letters.charAt(i));
            backtrack(combinations, digits, keyMap, index + 1, buffer);
            buffer.splice(index, 1);
        }
    }
}

10. 删除链表的倒数第 N 个结点

直接分情况讨论:list.length=1(n=1), n=list.length, 1<n<list.length

function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
    if (!head) {
        return head;
    }

    const nodeList: Array<ListNode> = [];
    let temp = head;
    while (temp) {
        nodeList.push(temp);
        temp = temp.next;
    }

    if (nodeList.length === 1) {
        head = null;
    }
    else if (n === nodeList.length) {
        head.next = null;
        head = nodeList[1];
    }
    else {
        if (n === 1) {
            const preNode = nodeList[nodeList.length - n - 1];
            preNode.next = null;
        }
        else {
            const preNode = nodeList[nodeList.length - n - 1];
            let node: ListNode | null = nodeList[nodeList.length - n];
            const nexNode = nodeList[nodeList.length - n + 1];
            preNode.next = nexNode;
            node.next = null;
            // 释放引用
            node = null;
        }
    }

    // 释放引用
    nodeList.length = 0;
    return head;
};

快慢指针

function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
    if (head) {
        let runner = head, chaser = head;

        // 快指针先移动N步
        while(n > 0) {
            runner = runner.next;
            n--;
        }
        if (runner === null) {
            return head.next;
        }

        while (runner.next !== null) {
            runner = runner.next;
            chaser = chaser.next;
        }
        chaser.next = chaser.next.next;
    }

    return head;
};

11. 有效的括号

栈:

function isValid(s: string): boolean {
    const map = new Map([['(', ')'], ['[', ']'], ['{', '}']]);
    const stack = [];
    for (const c of s) {
        if (stack.length === 0) stack.push(c);
        else if (map.get(stack[stack.length - 1]) === c) stack.pop();
        else stack.push(c)
    }
    return stack.length === 0;
};

12. 合并两个有序链表

思路:使用一个假节点作为头节点减少了很多判断

function mergeTwoLists(list1: ListNode | null, list2: ListNode | null): ListNode | null {
    if (list1 === null || list2 === null) return list1 ? list1 : list2;
    const head = new ListNode();
    let node = head;
    while (list1 && list2) {
        if (list1.val < list2.val) {
            node.next = list1;
            list1 = list1.next;
        }
        else {
            node.next = list2;
            list2 = list2.next;
        }
        node = node.next;
    }
    node.next = list1 ? list1 : list2;
    return head.next;
};

13. 括号生成

思路:深度优先遍历(回溯算法)+剪枝

function generateParenthesis(n: number): string[] {
    const res: string[] = [];
    dfs('', n, n, res);
    return res;
}

// curStr->当前字符串,left->剩余左括号数量,right->剩余右括号数量,res->存储的结果集
function dfs(curStr, left, right, res): void {
    if (left === 0 && right === 0) { // 到达叶子节点
        res.push(curStr);
        return;
    }

    // 左括号剩余数量严格大于右括号剩余数量,剪枝
    if (left > right) return;
    // 产生分支(左分支添加左括号,右分支添加右括号)
    if (left > 0) dfs(curStr + '(', left - 1, right, res);
    if (right > 0) dfs(curStr + ')', left, right - 1, res);
}

14. 合并K个升序链表

思路一:每次循环找到数组中值最小的节点,进行列表拼接(耗时较长)

function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
    let node = new ListNode();
    const head = node;
    while (lists.some(list => list)) {
        let minNode = new ListNode(1 * 10 ** 4 + 1), index = '';
        // 遍历找到最小节点
        for (let i in lists) {
            if (lists[i] && lists[i].val < minNode.val) {
                minNode = lists[i];
                index = i;
            }
        }
        node.next = minNode;
        node = node.next;
        minNode = minNode.next;
        // 替换数组中的元素
        lists[index] = minNode;
    }

    return head.next;
};

思路二:在合并二个列表的基础上合并多个

function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
    let res = null;
    for (const list of lists) {
        res = mergeTwoList(res, list);
    }
    return res;
}

function mergeTwoList(list1: ListNode | null, list2: ListNode | null): ListNode | null {
    if (!list1 || !list2) return list1 ? list1 : list2;
    const head = new ListNode();
    let tail = head;
    while (list1 && list2) {
        if (list1.val < list2.val) {
            tail.next = list1;
            list1 = list1.next;
        }
        else {
            tail.next = list2;
            list2 = list2.next;
        }
        tail = tail.next;
    }
    tail.next = list1 ? list1 : list2;
    return head.next;
}

思路三:优化思路二,分而治之

function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
    return merge(lists, 0, lists.length - 1);
}

function merge(lists: Array<ListNode | null>, l: number, r: number): ListNode | null {
    if (l === r) return lists[l];
    if (l > r) return null;
    const mid = (l + r) >> 1;
    return mergeTwoList(merge(lists, l, mid), merge(lists, mid + 1, r));
}

function mergeTwoList(list1: ListNode | null, list2: ListNode | null): ListNode | null {
    if (!list1 || !list2) return list1 ? list1 : list2;
    const head = new ListNode();
    let tail = head;
    while (list1 && list2) {
        if (list1.val < list2.val) {
            tail.next = list1;
            list1 = list1.next;
        }
        else {
            tail.next = list2;
            list2 = list2.next;
        }
        tail = tail.next;
    }
    tail.next = list1 ? list1 : list2;
    return head.next;
}

15. 下一个排列

思路:两遍扫描(比较巧妙,包括倒序数组)

function nextPermutation(nums: number[]): void {
    if (nums.length === 1) return;
    let i = nums.length - 2;
    while (i >= 0 && nums[i] >= nums[i + 1]) {
        i--;
    }
    if (i >= 0) {
        let j = nums.length - 1;
        while (j > i && nums[j] <= nums[i]) {
            j--;
        }
        swap(nums, i, j);
    }
    reverse(nums, i + 1);
};

function swap(nums: number[], i: number, j:number): void {
    nums[i] = nums[i] + nums[j];
    nums[j] = nums[i] - nums[j];
    nums[i] = nums[i] - nums[j];
}

function reverse(nums: number[], start: number) : void {
    let left = start, right = nums.length - 1;
    while (left < right) {
        swap(nums, left, right);
        left++;
        right--;
    }
}

补充:交换两数

  1. 添加第三方临时变量
let a = 3, b = 5;
const temp = a;
a = b;
b = temp;
  1. 不使用临时变量(很巧妙)
let a = 3, b = 5;
a = a + b;
b = a - b;
a = a - b;
  1. 采用或运算特性,安全
let a = 3, b = 5;
a = a ^ b;
b = a ^ b;
a = a ^ b;

16. 最长有效括号

思路一:动态规划(时间复杂度->O(n), 空间复杂度O(n))

  • 记录状态:dp[i] 表示以该位元素结尾的最长有效子字符串长度
  • 确定状态转移方程
function longestValidParentheses(s: string): number {
    let max = 0;
    const length = s.length, dp = new Array(length).fill(0);
    for (let i = 1; i < length; i++) {
        if (s.charAt(i) === ')') {
            if (s.charAt(i - 1) === '(') {
                dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
            }
            else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) === '(') {
                dp[i] = dp[i - 1] + (i - dp[i - 1] >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
            }
            max = Math.max(dp[i], max);
        }
    }
    return max;
};

思路二:栈(时间复杂度->O(n), 空间复杂度O(n))

为了保持一致性,栈初始值放入 -1,表示子串是从0开始的

function longestValidParentheses(s: string): number {
    let max = 0;
    const stack = [-1];
    for (let i = 0; i < s.length; i++) {
        if (s.charAt(i) === '(') stack.push(i);
        else {
            stack.pop();
            if (stack.length === 0) stack.push(i);
            else max = Math.max(max, i - stack[stack.length - 1]);
        }
    }
    return max;
};

思路三:正反向遍历(时间复杂度->O(n), 空间复杂度O(1))

function longestValidParentheses(s: string): number {
    let left = 0, right = 0, max = 0;
    const length = s.length;
    // 正向遍历
    for (let i = 0; i < length; i++) {
        if (s.charAt(i) === '(') left++;
        else right++;
        if (left === right) max = Math.max(max, left * 2);
        else if (right > left) left = right = 0;
    }
    // 反向遍历
    left = right = 0;
    for (let i = length - 1; i >= 0; i--) {
        if (s.charAt(i) === '(') left++;
        else right++;
        if (left === right) max = Math.max(max, right * 2);
        else if (left > right) left = right = 0;
    }
    return max;
};

17. 搜索旋转排序数组

类别:二分搜索变种

function search(nums: number[], target: number): number {
    const length = nums.length;
    if (length === 0) return -1;
    if (length === 1) return nums[0] === target ? 0 : -1;
    let left = 0, right = length - 1;
    while (left <= right) {
        let mid = (left + right) >> 1;
        if (nums[mid] === target) return mid;
        if (nums[0] <= nums[mid]) {
            if (nums[0] <= target && target < nums[mid]) right = mid - 1;
            else left = mid + 1;
        }
        else {
            if (nums[mid] < target && target <= nums[length - 1]) left = mid + 1;
            else right = mid - 1;
        }
    }
    return -1;
};

3. 力扣others

24. 两两交换链表中的节点

思路:变量存储前后节点,一遍过

function swapPairs(head: ListNode | null): ListNode | null {
    let prev: ListNode | null, curr: ListNode | null;
    prev = new ListNode(), prev.next = head, curr = head, head = prev;
    while (curr && curr.next !== null) {
        const next = curr.next;
        // 重整
        curr.next = next.next;
        next.next = curr;
        prev.next = next;
        // 移动
        prev = curr;
        curr = curr.next;
    }
    return head.next;
};

206. 反转链表

思路:通过变量,一遍过

function reverseList(head: ListNode | null): ListNode | null {
    let curr = head, prev = null;
    while (curr !== null) {
        const next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
};

904. 水果成篮

思路:滑动窗口

关键点:counter记录篮子中存放水果的种类,records数组记录每种水果的数量

function totalFruit(fruits: number[]): number {
    const length = fruits.length;
    if (length <= 2) return length;
    let left = 0, right = 0, max = 2, counter = 0;
    // 记录每种水果的数目(下标->水果种类,值->水果数量)
    const records = new Array(length).fill(0);
    while (right < length) {
        records[fruits[right]]++;
        if (records[fruits[right]] === 1) counter++;
        while (counter > 2) {
            records[fruits[left]]--;
            if (records[fruits[left]] === 0) counter--;
            left++;
        }

        right++;
        max = Math.max(max, right - left);
    }

    return max;
};