剑指offer---Typescript版本

140 阅读42分钟

数组

面试题6:排序数组中的两个数字之和

Leetcode: LCR 006. 两数之和 II - 输入有序数组

题目:输入一个递增排序的数组和一个值k,请问如何在数组中找出两个和为k的数字并返回它们的下标?假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。例如,输入数组[1,2,4,6,10],k的值为8,数组中的数字2与6的和为8,它们的下标分别为1与3。

// 1.双指针
function twoSum(numbers: number[], target: number): number[] {
    if (!numbers.length) return [];
    let left = 0, right = numbers.length - 1;
    while(left < right) {
       if (numbers[left] + numbers[right] === target) {
         return [left, right];
       } else if (numbers[left] + numbers[right] > target) {
         right--;
       } else {
        left++;
       }
    }
    return [];
}; 

// 2. 二分查找(适合已排序数组)

面试题7:数组中和为0的3个数字

Leetcode: 15. 三数之和

题目:输入一个数组,如何找出数组中所有和为0的3个数字的三元组?需要注意的是,返回值中不得包含重复的三元组。例如,在数组[-1,0,1,2,-1,-4]中有两个三元组的和为0,它们分别是[-1,0,1]和[-1,-1,2]​。

function threeSum(nums: number[]): number[][] {
    if (nums.length < 3) return [];
    const res = [];
    // 将数组排序后,可以使用方向相反的两个指针求和
    nums.sort((a, b) => a - b);
    for (let i = 0; i < nums.length - 1; i++) {
        // for 循环中固定一个值,然后在排序数组[i+1, nums.length-1]范围内,用双指针寻找三和为0的组合
        if (i > 0 && nums[i] === nums[i - 1]) continue; // 若遍历到的固定数字出现过,则跳出当前循环
        let left = i + 1, right = nums.length - 1; // 定义两个指针,每次寻找的最大数组范围
        while (left < right) {
            if (nums[i] + nums[left] + nums[right] === 0) {
                res.push([nums[i], nums[left], nums[right]]);
                // left指针去重
                while (left < right && nums[left] == nums[left + 1]) {
                    left++;
                }
                // right指针去重
                while (left < right && nums[right] == nums[right - 1]) {
                    right--;
                }
                // 找到一个组合后,缩小范围继续寻找是否存在下一个组合
                left++;
                right--;
            } else if (nums[i] + nums[left] + nums[right] > 0) {
                // 和比0大,需要从排序数组的右边缩小范围,right指针值会减小,从而和减小
                right--;
            } else {
                // 和比0小
                left++;
            }
        }
    }
    return res;
};

面试题8:和大于或等于k的最短子数组

Leetcode: LCR 008. 长度最小的子数组

题目:输入一个正整数组成的数组和一个正整数k,请问数组中和大于或等于k的连续子数组的最短长度是多少?如果不存在所有数字之和大于或等于k的子数组,则返回0。例如,输入数组[5,1,4,3]​,k的值为7,和大于或等于7的最短连续子数组是[4,3]​,因此输出它的长度2。

function minSubArrayLen(target: number, nums: number[]): number {
    // left, right将组成一个滑动窗口,两个指针都往同一个方向移动
    // left指针永远不会走到right指针的右边
    let left = 0, right = 0; // 两个指针初始化都指向数组的第1个数字
    let minLen = Number.MAX_VALUE; // 记录和大于等于target的最小子数组长度
    let sum = 0; // 记录窗口子数组和
    // right右移会增大子数组长度且增大sum值
    while (right < nums.length) {
        sum += nums[right];
        while (left <= right && sum >= target) {
            // 右移left,减短数组长度,若剪短后还能大于目标值,则找出一个更短的满足需求的子数组
            // 若小于目标值,则跳出while循环,去外层循环右移right,加长数组,也就增加sum
            minLen = Math.min(minLen, right - left + 1);
            sum -= nums[left];
            left++;
        }
        right++;
    }
    return minLen === Number.MAX_VALUE ? 0 : minLen;
};

面试题9:乘积小于k的子数组

Leetcode: LCR 009. 乘积小于 K 的子数组

题目:输入一个由正整数组成的数组和一个正整数k,请问数组中有多少个数字乘积小于k的连续子数组?例如,输入数组[10,5,2,6]​,k的值为100,有8个子数组的所有数字的乘积小于100,它们分别是[10]​、​[5]​、​[2]​、​[6]​、​[10,5]​、​[5,2]​、​[2,6]和[5,2,6]​。

function numSubarrayProductLessThanK(nums: number[], k: number): number {
    let res = 0; // 记录满足条件的子数组个数
    let prod = 1; // 子数组乘积
    let left = 0, right = 0; // 两个指针形成一个滑动窗口
    while(right < nums.length) {
     // 第一层循环,右指针从0向右移动,遍历所有数组元素,且作为子数组右边界
      prod *= nums[right];
      while (left <= right && prod >= k) {
        // 第二层循环,根据条件移动左指针,缩小子数组范围
         prod /= nums[left];
         left++;
      }
      res = res + right - left + 1; //两个指针之间有多少个数字,就找到了多少个数字乘积小于k的子数组。
      right++;
    }
    return res;
};

数组求子数组之和解法总结

  1. 滑动窗口(双指针):使用双指针解决子数组之和的面试题有一个前提条件—数组中的所有数字都是正数。
  2. 累加数组:数组中的数字有正数、负数和零都可以用。

面试题10:和为k的子数组

Leetcode: LCR 010. 和为 K 的子数组

题目:输入一个整数数组和一个整数k,请问数组中有多少个数字之和等于k的连续子数组?例如,输入数组[1,1,1]​,k的值为2,有2个连续子数组之和等于2。

function subarraySum(nums: number[], k: number): number {
    const map = new Map(); // 哈希表存放 --> key: 前缀和,value: 前缀和出现次数
    map.set(0, 1); // 处理特殊场景,遍历的第一个数字就满足要求。假设 nums = [3, 1, 2, -1, 1],k = 3
    let preSum = 0; // 记录前缀和
    let res = 0; // 记录满足要求的数组个数
    for (let i = 0; i < nums.length; i++) {
        preSum += nums[i];
        if (map.has(preSum - k)) {
            res += map.get(preSum - k);
        }
        map.set(preSum, (map.get(preSum) || 0) + 1);
    }
    return res;
};

面试题11:0和1个数相同的子数组

Leetcode: LCR 011. 连续数组

题目:输入一个只包含0和1的数组,请问如何求0和1的个数相同的最长连续子数组的长度?例如,在数组[0,1,0]中有两个子数组包含相同个数的0和1,分别是[0,1]和[1,0]​,它们的长度都是2,因此输出2。

// 首先把输入数组中所有的0都替换成-1,那么题目就变成求包含相同数目的-1和1的最长子数组的长度。
function findMaxLength(nums: number[]): number {
   let maxLength = 0;
   const map = new Map();
   map.set(0, -1);
   let sum = 0;
   for (let i = 0; i < nums.length; i++) {
      sum += nums[i] === 0 ? -1 : 1;
      if (map.has(sum)) {
        maxLength = Math.max(maxLength, i - map.get(sum));
      } else {
        map.set(sum, i)
      }

   }
   return maxLength;
};

面试题12:左右两边子数组的和相等

Leetcode: LCR 012. 寻找数组的中心下标

题目:输入一个整数数组,如果一个数字左边的子数组的数字之和等于右边的子数组的数字之和,那么返回该数字的下标。如果存在多个这样的数字,则返回最左边一个数字的下标。如果不存在这样的数字,则返回-1。例如,在数组[1,7,3,6,2,9]中,下标为3的数字(值为6)的左边3个数字1、7、3的和与右边两个数字2和9的和相等,都是11,因此正确的输出值是3。

function pivotIndex(nums: number[]): number {
    const total = nums.reduce((prev, curr) => prev + curr, 0);
    let sum = 0;
    for (let i = 0; i < nums.length; i++) {
        if (sum === total - nums[i] - sum) {
            return i;
        }
        sum += nums[i]
    }
    return -1;
};

面试题13:二维子矩阵的数字之和

Leetcode: LCR 013. 二维区域和检索 - 矩阵不可变

题目:输入一个二维矩阵,如何计算给定左上角坐标和右下角坐标的子矩阵的数字之和?对于同一个二维矩阵,计算子矩阵的数字之和的函数可能由于输入不同的坐标而被反复调用多次。例如,输入图2.1中的二维矩阵,以及左上角坐标为(2,1)和右下角坐标为(4,3)的子矩阵,该函数输出8。

class NumMatrix {
    public sums = [];
    constructor(matrix: number[][]) {
        this.numMatrix(matrix);
    }

    numMatrix(matrix: number[][]) {
        if (matrix.length == 0 || matrix[0].length == 0) {
            return;
        }
        // 由于坐标值r1或c1有可能等于0,因此r1-1或c1-1可能是负数,不再是有效的数组下标
        // 如果在矩阵的最上面增加一行,最左面增加一列,这样就不必担心出现数组下标为-1的情形
        // 初始化一个行列比原始二元数组多一行一列的sum二维数组,并且每一项初始化value为0
        this.sums = new Array(matrix.length + 1).fill(0).map(() => new Array(matrix[0].length + 1).fill(0));
        for (let i = 0; i < matrix.length; i++) {
            let rowSum = 0;
            for (let j = 0; j < matrix[0].length; j++) {
                rowSum += matrix[i][j];
                this.sums[i + 1][j + 1] = rowSum + this.sums[i][j + 1];
            }
        }
    }

    sumRegion(row1: number, col1: number, row2: number, col2: number): number {
        //该子矩阵的数字之和等于sums[r2]​[c2]+sums[r1-1]​[c2]-sums[r2]​[c1-1]+sums[r1-1]​[c1-1]​
        return this.sums[row2 + 1][col2 + 1]
            - this.sums[row1][col2 + 1]
            - this.sums[row2 + 1][col1]
            + this.sums[row1][col1]
    }
}

/**
 * Your NumMatrix object will be instantiated and called as such:
 * var obj = new NumMatrix(matrix)
 * var param_1 = obj.sumRegion(row1,col1,row2,col2)
 */

字符串

1. 双指针(滑动窗口)

面试题14:字符串中的变位词

Leetcode: LCR 014. 字符串的排列

题目:输入字符串s1和s2,如何判断字符串s2中是否包含字符串s1的某个变位词?如果字符串s2中包含字符串s1的某个变位词,则字符串s1至少有一个变位词是字符串s2的子字符串。假设两个字符串中只包含英文小写字母。例如,字符串s1为"ac",字符串s2为"dgcaf",由于字符串s2中包含字符串s1的变位词"ca",因此输出为true。如果字符串s1为"ab",字符串s2为"dgcaf",则输出为false。

function checkInclusion(s1: string, s2: string): boolean {
    const window = new Map();
    const needs = new Map();
    for (let char of s1) {
        needs.set(char, (needs.get(char) || 0) + 1);
    }
    let left = 0, right = 0;
    let valid = 0;
    while (right < s2.length) {
        const char = s2[right];
        right++;
        window.set(char, (window.get(char) || 0) + 1);
        //判断当前字符的数量是否与needs哈希表中一致
        if (window.get(char) === needs.get(char)) {
            valid++;
        }

        while (right - left >= s1.length) {
            if (valid === needs.size) {
                return true;
            }
            const removedChar = s2[left];
            left++;
            if (needs.has(removedChar)) {
                if (window.get(removedChar) === needs.get(removedChar)) {
                    valid--;
                }
                window.set(removedChar, window.get(removedChar) - 1);
            }
        }
    }
    return false;
};

面试题15:字符串中的所有变位词

Leetcode: LCR 015. 找到字符串中所有字母异位词

题目:输入字符串s1和s2,如何找出字符串s2的所有变位词在字符串s1中的起始下标?假设两个字符串中只包含英文小写字母。例如,字符串s1为"cbadabacg",字符串s2为"abc",字符串s2的两个变位词"cba"和"bac"是字符串s1中的子字符串,输出它们在字符串s1中的起始下标0和5。

function findAnagrams(s: string, p: string): number[] {
    const window = new Map();
    const needs = new Map();
    for (let char of p) {
        needs.set(char, (needs.get(char) || 0) + 1);
    }
    let left = 0, right = 0;
    let valid = 0;
    const res = [];
    while (right < s.length) {
        const char = s[right];
        right++;
        window.set(char, (window.get(char) || 0) + 1);
        //判断当前字符的数量是否与needs哈希表中一致
        if (window.get(char) === needs.get(char)) {
            valid++;
        }

        while (right - left >= p.length) {
            if (valid === needs.size) {
                res.push(left);
            }
            const removedChar = s[left];
            left++;
            if (needs.has(removedChar)) {
                if (window.get(removedChar) === needs.get(removedChar)) {
                    valid--;
                }
                window.set(removedChar, window.get(removedChar) - 1);
            }
        }
    }
    return res;
};

面试题16:不含重复字符的最长子字符串

Leetcode: LCR 016. 无重复字符的最长子串

题目:输入一个字符串,求该字符串中不含重复字符的最长子字符串的长度。例如,输入字符串"babcca",其最长的不含重复字符的子字符串是"abc",长度为3。

function lengthOfLongestSubstring(s: string): number {
    const window = new Map();//滑动窗口
    let left = 0, right = 0; // 窗口范围[left, right)
    let res = 0; // 记录结果
    while (right < s.length) {
        const char = s[right]; // 移入窗口的字符
        // 更新窗口内数据
        window.set(char, (window.get(char) || 0) + 1);// key:字符,value:字符出现次数
        right++;
        // 判断左侧窗口收缩的条件:如果字符出现次数大于1,说明窗口中存在重复字符,不符合条件,需要左移left
        while (window.get(char) > 1) {
            const removedChar = s[left]; // 即将移除窗口的字符
            // 进行窗口内数据更新
            window.set(removedChar, window.get(removedChar) - 1);
            left++;
        }
        res = Math.max(res, right - left);
    }
    return res;
};

面试题17:包含所有字符的最短字符串

Leetcode: LCR 017. 最小覆盖子串

题目:输入两个字符串s和t,请找出字符串s中包含字符串t的所有字符的最短子字符串。例如,输入的字符串s为"ADDBANCAD",字符串t为"ABC",则字符串s中包含字符'A'、'B'和'C'的最短子字符串是"BANC"。如果不存在符合条件的子字符串,则返回空字符串""。如果存在多个符合条件的子字符串,则返回任意一个。

function minWindow(s: string, t: string): string {
    const window = new Map(); // 记录窗口中的字符
    const need = new Map(); // 记录需要的凑齐的字符

    for (let char of t) { // 遍历短字符串,统计其包含所有字符出现的次数
        need.set(char, (need.get(char) || 0) + 1);
    }

    let left = 0, right = 0; // 初始化滑动窗口的两端,注意区间左闭右开[left, right),初始化的窗口不包含任何元素
    let valid = 0; // 记录窗口中满足条件的字符个数:valid == need.size
    // 记录最小覆盖子串的起始索引及长度
    let start = 0, len = Number.MAX_VALUE; // 用于截取最终返回的符合条件的子串
    while (right < s.length) {
        // char是即将移入窗口的字符
        const char = s[right];
        // 进行窗口内数据的一系列更新
        if (need.has(char)) {
            window.set(char, (window.get(char) || 0) + 1);
            if (window.get(char) === need.get(char)) {
                valid++;
            }
        }
        // 右移窗口
        // 在判断左侧是否需要更新前先right++,那么在统计长度的时候无须len = right - left + 1, 直接如下面所示,len = right - left.
        right++;       
        // console.log('窗口信息:', left, right);     
        // 判断左侧窗口是否要收缩
        while (valid === need.size) {
            if (len > right - left) {
                len = right - left;
                start = left;
            }
            // 将移除窗口的字符
            const removedChar = s[left];
            // 进行窗口内数据更新
            if (need.has(removedChar)) {
                if (window.get(removedChar) === need.get(removedChar)) {
                    valid--;
                }
                window.set(removedChar, window.get(removedChar) - 1);
            }
            // 左移窗口
            left++;
        }
    }
    return len < Number.MAX_VALUE ? s.substr(start, len) : ''; // start起始位置,len为截取长度
};

滑动窗口算法思路:

  1. 我们在字符串S中使用双指针中的左右指针技巧,初始化left=right=0,把索引左闭右开区间【left,right)称为一个“窗口”。
  2. 我们先 不断增加right指针扩大窗口【left, right),直到窗口中的字符串符合要求(如包含T中所有字符)。
  3. 此时我们停止增加right,转而不断增加left指针缩小窗口,直到窗口中的字符不再符合要求。同时,每次增加left,我们都要更新一轮结果。
  4. 重复2和3,直到right到达字符串S的尽头。

2. 回文字符串

面试题18:有效的回文

Leetcode: LCR 018. 验证回文串

题目:给定一个字符串,请判断它是不是回文。假设只需要考虑字母和数字字符,并忽略大小写。例如,"Was it a cat I saw?"是一个回文字符串,而"race a car"不是回文字符串。

function isPalindrome(s: string): boolean {
    // 定义双指针,相向移动
    let left = 0, right = s.length - 1;
    while (left < right) {
        // 判断左指针指向字符是否为字母和数字,不是就右移left
        if (!isLetterOrDigit(s[left])) {
          left++;
        } else if (!isLetterOrDigit(s[right])) {  // 判断右指针指向字符是否为字母和数字
          right--;
        } else {
            // 两个指针指向字符都转成小写比较
            if (s[left].toLowerCase() !== s[right].toLowerCase()) {
              return false;
            }
            // 两个指针指向字符相同,则同时移动指针
            left++;
            right--;
        }
    }
    return true;
};
// 工具函数:使用正则表达式判断字符是否为字母或数字
function isLetterOrDigit(char: string) {
    return /[a-zA-Z0-9]/.test(char);
}

面试题19:最多删除一个字符得到回文

Leetcode: LCR 019. 验证回文串 II

题目:给定一个字符串,请判断如果最多从字符串中删除一个字符能不能得到一个回文字符串。例如,如果输入字符串"abca",由于删除字符'b'或'c'就能得到一个回文字符串,因此输出为true。

function validPalindrome(s: string): boolean {
   let left = 0, right = s.length - 1;
   while(left <= right) {
     if (s[left] != s[right]) {
         // 当出现左右指针指向字符不一致时,分别继续尝试[left + 1, right],[left, right -1]这两个范围,
         // 其中只要一个是回文,就返回true
         return isPalindrome(s, left + 1, right) || isPalindrome(s, left, right - 1);
     }
     left++;
     right--;
   }
   return true; // s本身是回文
};

function isPalindrome(s: string, left: number, right: number) {
  while (left < right) {
     if (s[left] != s[right]) {
         return false;
     }
     left++;
     right--;
  }
  return true;
}

面试题20:回文子字符串的个数

Leetcode: LCR 020. 回文子串

题目:给定一个字符串,请问该字符串中有多少个回文连续子字符串?例如,字符串"abc"有3个回文子字符串,分别为"a"、"b"和"c";而字符串"aaa"有6个回文子字符串,分别为"a"、"a"、"a"、"aa"、"aa"和"aaa"。

function countSubstrings(s: string): number {
    if (!s || !s.length) {
        return 0;
    }
    let count = 0; // 计算回文子串个数
    //字符串的每个字符都作为回文中心进行判断,中心是一个字符或两个字符
    // 以中心点向两端延申,判断是否存在回文
    for (let i = 0; i < s.length; i++) {
       count += countPalindrome(s, i, i); // 奇数回文子串中心点相同
       count += countPalindrome(s, i, i + 1); // 偶数回文子串中心点为相邻两个数
    }
    return count;
};

function countPalindrome(s: string, start: number, end: number): number {
    let count = 0;
    while (start >= 0 && end < s.length && s[start] == s[end]) {
        count++;
        start--;
        end++;
    }
    return count;
}

链表

面试题21:删除倒数第k个节点

Leetcode: LCR 021. 删除链表的倒数第 N 个结点

题目:如果给定一个链表,请问如何删除链表中的倒数第k个节点?假设链表中节点的总数为n,那么1≤k≤n。要求只能遍历链表一次。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
     const dummy = new ListNode(-1);
     dummy.next = head;
     // 定义快慢指针,指向虚拟头节点,避免判空
     let fast = dummy, slow = dummy;
     // 快指针先走n步
     while (n) {
       fast = fast.next;
       n--;
     }
     // 此时快慢指针相隔n
     // fast多走一步,接下来快慢指针同频移动直到fast = null,此时慢指针刚好指向要被删除的节点的上一个节点
     fast = fast.next;
     while (fast) {
        fast = fast.next;
        slow = slow.next;
     }
     // 删除倒数第n个节点
     slow.next = slow.next.next;
     return dummy.next;
};

面试题22:链表中环的入口节点

LCR 022. 环形链表 II

题目:如果一个链表中包含环,那么应该如何找出环的入口节点?从链表的头节点开始顺着next指针方向进入环的第1个节点为环的入口节点。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function detectCycle(head: ListNode | null): ListNode | null {
    if (!head || !head.next) return null;
    // 定义快慢指针
    let slow = head, fast = head;
    // 慢指针每次走一步,快指针每次走两步,若存在环,快指针肯定会在环内追上慢指针
    while (fast.next && fast.next.next) {
        slow = slow.next;
        fast = fast.next.next;
        // 快慢指针相遇,证明有环
        if (slow == fast) {
            // 第一次相遇后,让快指针从头开始与慢指针同频移动
            fast = head;
            while (slow != fast) {
                slow = slow.next;
                fast = fast.next;
            }
            // 快慢指针二次相遇,就是环的入口节点
            return slow;
        }
    }
    // 无法相遇,返回null
    return null;
};

面试题23:两个链表的第1个重合节点

Leetcode LCR 023. 相交链表

题目:输入两个单向链表,请问如何找出它们的第1个重合节点。

image.png

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function getIntersectionNode(headA: ListNode | null, headB: ListNode | null): ListNode | null {
    let nodeA = headA, nodeB = headB;
    while (nodeA != nodeB) {
        // 如果走到 headA 链表末尾,转到 headB 链表
        nodeA = nodeA == null ? headB : nodeA.next;
        // 如果走到 headB 链表末尾,转到 headA 链表
        nodeB = nodeB == null ? headA : nodeB.next;
    }
    return nodeA;
};

面试题24:反转链表

Leetcode: LCR 024. 反转链表

题目:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

思路概述:

  1. 主要用到三个指针实现链表反转,current, prev, next。
  2. current指针在链表上一次遍历,指向当前正准备反转指向的节点。
  3. next在current原本指向断开前,暂存current的下一个节点。
  4. prev保留current前一个节点,反转就是断开原本的next,重新指向current前一个节点。prev初始化为null,因为链表反转后,初始化时的null刚好成为其结束节点。
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function reverseList(head: ListNode | null): ListNode | null {
  let prev = null; // 初始化prev指针,赋值为null,最后会成为反转链表的尾部
  let cur = head; // 当前指针指向头节点
  // cur不为空,说明没到尾节点,就继续遍历
  while (cur) {
    // 暂存后继节点
    let next = cur.next;
    // 当前节点指向其上一个节点,即反转当前节点指向
    cur.next = prev;
    // prev指向当前节点
    prev = cur;
    // 访问下一节点
    cur = next;
  }
  return prev;
};

面试题25:链表中的数字相加

Leetcode: LCR 025. 两数相加 II

题目:题目:给定两个表示非负整数的单向链表,请问如何实现这两个整数的相加并且把它们的和仍然用单向链表表示?链表中的每个节点表示整数十进制的一位,并且头节点对应整数的最高位数而尾节点对应整数的个位数。

思路概述:

  1. 将两个链表反转,因为数学计算从低位往高位加。
  2. 反转后相加,记录相加和、进位数。
  3. 两个链表全部遍历完成后,检查进位数是否大于0。
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function addTwoNumbers(l1: ListNode | null, l2: ListNode | null): ListNode | null {
   let node1 = revertList(l1); // 反转链表
   let node2 = revertList(l2); // 反转链表
   let dummy = new ListNode(-1); // 哨兵节点,dummy.next就是和链表的头节点
   let sumNode = dummy; // sum节点
   let carry = 0; // 记录进位数字
   // 两个相加链表长度不一定一样
   while (node1 || node2) {
     let currentNodeSum = 0; // 当前两位数字和
     if (node1 && node2) {
       currentNodeSum = node1.val + node2.val + carry;
       node1 = node1.next;
       node2 = node2.next;
     } else if (node1) {
        currentNodeSum = node1.val + carry;
        node1 = node1.next;
     } else {
        currentNodeSum = node2.val + carry;
        node2 = node2.next;
     }
     carry = currentNodeSum < 10 ? 0 : 1; // 当前两数和大于10,说明有进位数字1
     const value = currentNodeSum < 10 ? currentNodeSum : currentNodeSum - 10; // 和节点的数字
     sumNode.next = new ListNode(value);
     sumNode = sumNode.next;
   }
   // 判断计算完毕后,是否还有进位数,有则继续追加高位节点
   if (carry > 0) {
      sumNode.next = new ListNode(carry);
   }
   // 和链表反转后才是最终结果
   return revertList(dummy.next);
};

function revertList(head: ListNode): ListNode | null {
   let prev = null;
   let cur = head;
   while (cur) {
     const next = cur.next;
     cur.next = prev;
     prev = cur;
     cur = next;
   }
   return prev;
}

面试题26:重排链表

Leetcode: LCR 026. 重排链表

问题:给定一个链表,链表中节点的顺序是L0→L1→L2→…→Ln-1→Ln,请问如何重排链表使节点的顺序变成L0→Ln→L1→Ln-1→L2→Ln-2→…?

思路概述:

  1. 快慢指针找链表中点,奇数时注意前半段大于等于后半段
  2. 反转第二节链表
  3. 交替合并两链表,利用一个prev节点指向合并后链表的最后一个节点
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

/**
 Do not return anything, modify head in-place instead.
 */
function reorderList(head: ListNode | null): void {
    const dummy = new ListNode(0);
    dummy.next = head;
    let fast = dummy, slow = dummy; // 定义快慢指针,快指针一次走两步,慢指针走一步
    // 当快的指针走到链表的尾节点时慢的指针刚好走到链表的中间节点
    // 当链表的节点总数是奇数时,就要确保链表的前半段比后半段多一个节点
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next;
        if (fast.next) {
            fast = fast.next;
        }
    }
    const node = slow.next; // slow.next是第二节链表开头
    slow.next = null; // 第一节链表最后一个节点指向null
    // 将第二节链表反转
    let node2 = revertList(node);
    // 重新连接两节链表
    linkList(head, node2, dummy);
};

// 注意:确保node1 >= node2
function linkList(node1: ListNode, node2: ListNode, head: ListNode) {
    let prev = head;// prev节点在合并后链表中移动,prev.next永远指向即将加入合并链表的下一个节点
    while (node1 && node2) {
        // 暂时保留node1下一个节点,因为node1下一个节点将会指向node2
        let next1 = node1.next; // 断开node1现有next前,先暂存它的下一个节点
        
        prev.next = node1;
        // node1下一个节点指向node2
        node1.next = node2;
        prev = node2;
        
        // 移动n1,n2
        node1 = next1;
        node2 = node2.next;
    }
    // 处理node1比node2长的情况
    if (node1) {
        prev.next = node1;
    }
}

function revertList(head: ListNode) {
    let prev = null;
    let cur = head;
    let next = null;
    while (cur) {
        next = cur.next;
        cur.next = prev;
        prev = cur;
        cur = next;
    }
    return prev;
}

面试题27:回文链表

Leetcode: LCR 027. 回文链表

问题:如何判断一个链表是不是回文?要求解法的时间复杂度是O(n)​,并且不得使用超过O(1)的辅助空间。如果一个链表是回文,那么链表的节点序列从前往后看和从后往前看是相同的。

思路概述:

  1. 利用快慢指针将链表分成两段
  2. 奇数情况,可以把多余的节点放在前一段,但是可以不用对比
  3. 反转第二段链表进行对比
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function isPalindrome(head: ListNode | null): boolean {
   let dummy = new ListNode(0);
   dummy.next = head;
   let fast = dummy, slow = dummy;
   while (fast && fast.next) {
      slow = slow.next;
      fast = fast.next;
      if (fast.next) {
        fast = fast.next;
      }
   }
   let node2 = revertList(slow.next);
   let node1 = dummy.nextwhile (node1 && node2) {
     if (node1.val != node2.val) {
       return false;
     }
     node1 = node1.next;
     node2 = node2.next;
   }
   return true;
};

function revertList(head: ListNode) {
   let prev = null;
   let curr = head;
   let next = null;
   while (curr) {
     next = curr.next;
     curr.next = prev;
     prev = curr;
     curr = next;
   }
   return prev;
}

面试题28:展平多级双向链表

Leetcode: LCR 028. 扁平化多级双向链表

问题:在一个多级双向链表中,节点除了有两个指针分别指向前后两个节点,还有一个指针指向它的子链表,并且子链表也是一个双向链表,它的节点也有指向子链表的指针。请将这样的多级双向链表展平成普通的双向链表,即所有节点都没有子链表。例如,图(a)所示是一个多级双向链表,它展平之后如图(b)所示。

image.png

/**
 * Definition for node.
 * class Node {
 *     val: number
 *     prev: Node | null
 *     next: Node | null
 *     child: Node | null
 *     constructor(val?: number, prev? : Node, next? : Node, child? : Node) {
 *         this.val = (val===undefined ? 0 : val);
 *         this.prev = (prev===undefined ? null : prev);
 *         this.next = (next===undefined ? null : next);
 *         this.child = (child===undefined ? null : child);
 *     }
 * }
 */

function flatten(head: Node | null): Node | null {
    dfs(head);
    return head;
};
// 深度递归函数
function dfs(head: Node) {
    let node = head;
    // 指向展平后的链表的尾节点
    let tail = null;
    while (node) {
        // 保留当前节点的下一个节点,若存在子链表,next将成为子链表的尾部节点的next指向
        let next = node.next;
        if (node.child) {
            // 展开链表的头部节点
            let childHead = node.child;
            let childTail = dfs(node.child);

            // 连接展平后的子链表
            node.child = null;
            node.next = childHead;
            childHead.prev = node;
            childTail.next = next;
            // next可能为空,若为空就不存在prev指针了
            if (next) {
                next.prev = childTail;
            }
            tail = childTail;
        } else {
            tail = node;
        }
        node = next;
    }
    return tail;
}

面试题29:排序的循环链表

Leetcode: LCR 029. 循环有序列表的插入

问题:在一个循环链表中节点的值递增排序,请设计一个算法在该循环链表中插入节点,并保证插入节点之后的循环链表仍然是排序的。例如,图(a)所示是一个排序的循环链表,插入一个值为4的节点之后的链表如图(b)所示。

image.png

/**
 * Definition for node.
 * class Node {
 *     val: number
 *     next: Node | null
 *     constructor(val?: number, next?: Node) {
 *         this.val = (val===undefined ? 0 : val);
 *         this.next = (next===undefined ? null : next);
 *     }
 * }
 */

function insert(head: Node | null, insertVal: number): Node | null {
    // 即将要插入循环链表的节点
    const node = new Node(insertVal);
    // case1: 循环链表为空,则该链表就只有一个新加入的节点,并将next指向自己
    if (!head) {
        node.next = node;
        return node;
    }
    // case2: 循环链表就只有一个节点,新节点直接插入,只有两个节点,肯定有序
    if (head.next === head) {
        head.next = node;
        node.next = head;
        return head;
    }
    // case3: 循环链表中的节点个数大于1,
    // 则需要从头节点开始遍历循环链表,寻找插入新节点的位置,使得插入新节点之后的循环链表仍然保持有序
    // 此时又存在三种情况
    // case3.1: insertVal刚好介于循环链表中某两个节点之间
    // case3.2: insertVal比循环链表中已有最大值还大
    // case3.3: insertVal比循环链表中已有最小值还小

    // 定义两个相邻节点同时在循环链表中移动
    let cur = head, next = head.next;
    while (next !== head) {
        // case3.1: insertVal刚好介于循环链表中某两个节点之间
        if (insertVal >= cur.val && insertVal <= next.val) {
            break;
        }
        // 因为循环链表是有序的,如果遍历到cur.val > next.val,说明此时next已经到了头节点,否则应该cur.val小于等于next.val
        // case3.2: insertVal比循环链表中已有最大值还大
        if (cur.val > next.val && insertVal > cur.val) {
            break;
        }
        // case3.3: insertVal比循环链表中已有最小值还小
        if (cur.val > next.val && insertVal < next.val) {
            break;
        }
        cur = cur.next;
        next = next.next;
    }
    cur.next = node;
    node.next = next;
    return head;
}

哈希表

面试题30:插入、删除和随机访问都是O(1)的容器

Leetcode: LCR 030. O(1) 时间插入、删除和获取随机元素

题目:设计一个数据结构,使如下3个操作的时间复杂度都是O(1)。

  • insert(value)​:如果数据集中不包含一个数值,则把它添加到数据集中。
  • remove(value)​:如果数据集中包含一个数值,则把它删除。
  • getRandom():随机返回数据集中的一个数值,要求数据集中每个数字被返回的概率都相同。
class RandomizedSet {
    // map的键是数值,对应的值为它在数组中的位置。
    map: Map<number, number>; // 插入和删除时间复杂度是O(1),只有map满足
    valArr: Array<number>; // 等概率地返回其中的每个数值,数组满足
    
    constructor() {
        this.map = new Map();
        this.valArr = [];
    }

    insert(val: number): boolean {
        if (this.map.has(val)) {
            return false;
        }
        this.map.set(val, this.valArr.length);
        this.valArr.push(val);
        return true;
    }

    remove(val: number): boolean {
        if (!this.map.has(val)) {
            return false;
        }
        // 要删除的数字可能在数组的中间位置
        const index = this.map.get(val);
        // 数组最后一个数字
        const lastVal = this.valArr[this.valArr.length - 1];

        // 避免被删除的数字后面的所有数字都需要移动,否则删除的时间复杂度就是O(n)​
        // 将数组最后一位数字和将要删除的数字互换位置
        this.valArr[index] = lastVal;
        this.map.set(lastVal, index);

        // 减少一位数组长度
        this.valArr.length--;
        // 将目标数字从map中删除
        this.map.delete(val);
        return true;

    }

    getRandom(): number {
        const randomIndex = Math.floor(Math.random() * this.valArr.length);
        return this.valArr[randomIndex];
    }
}

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * var obj = new RandomizedSet()
 * var param_1 = obj.insert(val)
 * var param_2 = obj.remove(val)
 * var param_3 = obj.getRandom()
 */

面试题31:最近最少使用缓存

Leetcode: LCR 031. LRU 缓存

题目:请设计实现一个最近最少使用(Least Recently Used,LRU)缓存,要求如下两个操作的时间复杂度都是O(1)。

  • get(key)​:如果缓存中存在键key,则返回它对应的值;否则返回-1。
  • put(key,value)​:如果缓存中之前包含键key,则它的值设为value;否则添加键key及对应的值value。在添加一个键时,如果缓存容量已经满了,则在添加新键之前删除最近最少使用的键(缓存中最长时间没有被使用过的元素)。

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

  1. 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
  2. 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
// 定义双向链表结构
class Node {
    key: number;
    value: number;
    prev: Node | null;
    next: Node | null;
    constructor(key: number, value: number) {
        this.key = key;
        this.value = value;
        this.prev = null;
        this.next = null;
    }
}

class LRUCache {
    size: number;
    map: Map<number, Node>;
    // node将在dummyHead和dummyTail节点之间更新
    dummyHead: Node | null; // dummyHead.next 指向最近最少使用的节点,将从表头移除使用最少的
    dummyTail: Node | null; // dummyTail是双向链表的尾部节点,节点操作过,就移动到表尾
    constructor(capacity: number) {
        this.size = capacity; // 缓存容量
        this.map = new Map();
        this.dummyHead = new Node(-1, -1);
        this.dummyTail = new Node(-1, -1);
        this.dummyHead.next = this.dummyTail;
        this.dummyTail.prev = this.dummyHead;
    }

    get(key: number): number {
        if (!this.map.has(key)) {
            return -1;
        }
        const node = this.map.get(key);
        // 从该位置删除并移动节点至链表末尾
        this.delete(node);
        // 将节点移到链表末尾
        this.moveTodummyTail(node);

        return node.value;
    }

    put(key: number, value: number): void {
        if (this.map.has(key)) { // 单纯更新
            const node = this.map.get(key);
            // 如果更新的节点已经存在,改变value值
            node.value = value;
            // 节点从当前位置移除
            this.delete(node);
            // 因为被操作过,所以移到表尾
            this.moveTodummyTail(node);
        } else { // 需要插入,插入时要判断是否到达容量
            const node = new Node(key, value);
            // 达到容量上限,删除头节点后插入链表尾部
            if (this.map.size === this.size) {
                // 从表头删除一个节点
                const nodeToDelete = this.dummyHead.next;
                // 从链表移除
                this.delete(nodeToDelete);
                // 从map中移除
                this.map.delete(nodeToDelete.key);

                // 从表尾插入新增节点
                this.moveTodummyTail(node);
                this.map.set(key, node);
            } else {
                // 直接在尾部插入
                this.moveTodummyTail(node);
                this.map.set(key, node)
            }
        }
    }

    // 将node从双向链表中删除
    private delete(node: Node) {
        // 要被删除的节点,将其前一个节点的next指向其next节点
        node.prev.next = node.next;
        // 被删节点next节点的prev指向被删节点的前一个节点
        node.next.prev = node.prev;
    }

    // 将node插入双向链表尾部
    private moveTodummyTail(node: Node) {
        this.dummyTail.prev.next = node;
        node.prev = this.dummyTail.prev;
        node.next = this.dummyTail;
        this.dummyTail.prev = node;
    }
}



/**
 * Your LRUCache object will be instantiated and called as such:
 * var obj = new LRUCache(capacity)
 * var param_1 = obj.get(key)
 * obj.put(key,value)
 */

面试题32:有效的变位词

Leetcode:LCR 032. 有效的字母异位词

题目:给定两个字符串s和t,请判断它们是不是一组变位词。在一组变位词中,它们中的字符及每个字符出现的次数都相同,但字符的顺序不能相同。例如,"anagram"和"nagaram"就是一组变位词。

function isAnagram(s: string, t: string): boolean {
    if (s.length !== t.length || s === t) {
        return false;
    }
    // 只考虑英文字母,用数组模拟哈希表, 如果包含其他字符,就用真正的Map

    // 英文小写字母只有26个,因此可以用一个数组来模拟哈希表
    // 不管输入的字符串的长度如何,这个辅助数组的长度都是固定的,因此空间复杂度是O(1)​
    const arr = new Array(26).fill(0);

    //遍历记录字符串 s 中字符出现的频次
    for (let i = 0; i < s.length; i++) {
        arr[s.charCodeAt(i) - 'a'.charCodeAt(0)]++;
    }
    // 遍历字符串 t
    for (let i = 0; i < t.length; i++) {
        // 减去 数组 中对应的频次
        arr[t.charCodeAt(i) - 'a'.charCodeAt(0)]--;
        // 如果该字符在数组中的位置出现value<0,则说明 t 包含一个不在 s 中的额外字符
        if (arr[t.charCodeAt(i) - 'a'.charCodeAt(0)] < 0) {
            return false;
        }
    }
    return true;
};

面试题33:变位词组

Leetcode:LCR 033. 字母异位词分组

题目:给定一组单词,请将它们按照变位词分组。例如,输入一组单词["eat","tea","tan","ate","nat","bat"]​,这组单词可以分成3组,分别是["eat","tea","ate"]​、​["tan","nat"]和["bat"]​。假设单词中只包含英文小写字母。

function groupAnagrams(strs: string[]): string[][] {
    // key是排序字符串,value是该key变位词数组
    const map = new Map();
    for (let str of strs) {
        // 将遍历到的字符串变成数组排序
        const key = Array.from(str).sort().join();;
        if (map.has(key)) {
            // 若key以存在,直接往数组里添加变位词
            map.get(key).push(str);
        } else {
            map.set(key, [str]);
        }
    }
    return Array.from(map.values());
};

面试题34:外星语言是否排序

Leetcode:LCR 034. 验证外星语词典

题目:有一门外星语言,它的字母表刚好包含所有的英文小写字母,只是字母表的顺序不同。给定一组单词和字母表顺序,请判断这些单词是否按照字母表的顺序排序。例如,输入一组单词["offer","is","coming"]​,以及字母表顺序"zyxwvutsrqponmlkjihgfedcba",由于字母'o'在字母表中位于'i'的前面,因此单词"offer"排在"is"的前面;同样,由于字母'i'在字母表中位于'c'的前面,因此单词"is"排在"coming"的前面。因此,这一组单词是按照字母表顺序排序的,应该输出true。


const A_CODE = 'a'.charCodeAt(0);

function isAlienSorted(words: string[], order: string): boolean {
    const referOrders = [];
    for (let i = 0; i < order.length; i++) {
        referOrders[order.charCodeAt(i) - A_CODE] = i;
    }

    for (let i = 0; i < words.length - 1; i++) {
        let valid = false;
        // 相邻两个字符串按位对比字符
        for (let j = 0; j < words[i].length && j < words[i + 1].length; j++) {
            const curr = words[i].charCodeAt(j);
            const next = words[i + 1].charCodeAt(j);

            if (referOrders[curr - A_CODE] < referOrders[next - A_CODE]) {
                valid = true;
                break;
            }

            if (referOrders[curr - A_CODE] > referOrders[next - A_CODE]) {
                return false;
            }
        }

        // 若两个字符串其中之一字符串遍历完成后,对比过的字符都相等
        // 则认为字符更多的比少的大
        if (!valid) {
            // 字符更多的排在前面,不满足升序
            if (words[i].length > words[i + 1].length) {
                return false;
            }
        }
    }
    return true;
};

面试题35:最小时间差

Leetcode:LCR 035. 最小时间差

题目:给定一组范围在00:00至23:59的时间,求任意两个时间之间的最小时间差。例如,输入时间数组["23:50","23:59","00:00"]​,"23:59"和"00:00"之间只有1分钟的间隔,是最小的时间差。

function findMinDifference(timePoints: string[]): number {
    // 一天有1440分钟,如果数组大于这个数字,说明有相同的时间点,故而最小时间差为0
    if (timePoints.length > 1440) {
        return 0;
    }
    // 将 timePoints 排序后,最小时间差必然出现在 timePoints 的两个相邻时间,或者 timePoints 的两个首尾时间中。
    timePoints.sort();
    // 记录最小时差
    let res = Number.MAX_VALUE;
    let t0Minutes = getMinutes(timePoints[0]);
    let prev = t0Minutes;
    for (let i = 1; i < timePoints.length; i++) {
       const minutes = getMinutes(timePoints[i]);
       // 相邻时间的时间差,保留最小值
       res = Math.min(res, minutes - prev);
       prev = minutes
    }
    // 在计算最小时间差时,需要把排序之后的第1个时间当作第2天的时间(即加上24小时)与最后一个时间之间的间隔也考虑进去
    res = Math.min(res, t0Minutes + 1440 - prev); // 首尾时间的时间差
    return res;

};

function getMinutes(minute: string): number {
    const timeArr = minute.split(':');
    return parseInt(timeArr[0]) * 60 + parseInt(timeArr[1]);
}

面试题36:后缀表达式

leetcode: LCR 036. 逆波兰表达式求值

题目:后缀表达式是一种算术表达式,它的操作符在操作数的后面。输入一个用字符串数组表示的后缀表达式,请输出该后缀表达式的计算结果。假设输入的一定是有效的后缀表达式。例如,后缀表达式["2","1","3","","+"]对应的算术表达式是“2+13”​,因此输出它的计算结果5。

function evalRPN(tokens: string[]): number {
    const signs = ['+', '-', '*', '/'];
    const stack = [];
    for (let token of tokens) {
        if (signs.includes(token)) {
            // 注意右操作数先出来
            const right = stack.pop();
            const left = stack.pop();
            const res = calculate(left, right, token);
            stack.push(res);
        } else {
            stack.push(parseInt(token, 10));
        }
    }
    return stack[stack.length - 1];
};

function calculate(left: number, right: number, sign: string): number {
    let res: number = 0;
    switch (sign) {
        case '+':
            res = left + right;
            break;
        case '-':
            res = left - right;
            break;
        case '*':
            res = left * right;
            break;
        case '/':
            res = parseInt('' + left / right, 10);
            break;
    }
    return res;
}

面试题37:小行星碰撞

leetcode: LCR 037. 行星碰撞

题目:输入一个表示小行星的数组,数组中每个数字的绝对值表示小行星的大小,数字的正负号表示小行星运动的方向,正号表示向右飞行,负号表示向左飞行。如果两颗小行星相撞,那么体积较小的小行星将会爆炸最终消失,体积较大的小行星不受影响。如果相撞的两颗小行星大小相同,那么它们都会爆炸消失。飞行方向相同的小行星永远不会相撞。求最终剩下的小行星。例如,有6颗小行星[4,5,-6,4,8,-5],如图6.2所示(箭头表示飞行的方向)​,它们相撞之后最终剩下3颗小行星[-6,4,8]。

image.png

function asteroidCollision(asteroids: number[]): number[] {
    const stack: number[] = [];

    for (const asteroid of asteroids) {
        let current = asteroid;

        // 处理碰撞:当前小行星向左且栈顶向右
        while (
            stack.length > 0 &&
            current < 0 &&
            stack[stack.length - 1] > 0
        ) {
            const top = stack[stack.length - 1];
            const currentSize = Math.abs(current);
            const topSize = Math.abs(top);

            if (currentSize > topSize) {
                stack.pop(); // 栈顶爆炸
            } else if (currentSize === topSize) {
                stack.pop(); // 两个都爆炸
                current = 0; // 标记当前小行星爆炸
                break;
            } else {
                current = 0; // 当前小行星爆炸
                break;
            }
        }

        // 当前小行星未爆炸则入栈
        if (current !== 0) {
            stack.push(current);
        }
    }

    return stack;
}
function asteroidCollision(asteroids: number[]): number[] {
    const stack = [];
    for (let asteroid of asteroids) {
        // 几个条件只要满足一个就不会发生碰撞,可把当前行星压入栈:
        //  1.栈为空,不管当前行星是正是负(往左还是往右)都要压入栈;
        //  2.当前行星和栈顶行星同号说明同向移动不会碰撞;
        //  3.当前行星往右移动,栈顶行星向左移动也不会碰撞;

        if (stack.length == 0 || asteroid * stack[stack.length - 1] > 0 || (asteroid > 0 && stack[stack.length - 1] < 0)) {
            stack.push(asteroid);
        }
        //只有一种情况会发生碰撞,那就是当前行星往左,栈顶行星往右
        else if (asteroid < 0 && stack[stack.length - 1] > 0) {
            //若当前向左移动的行星比栈顶行星的体积小,则当前行星爆炸,不用进行任何栈操作
            if (Math.abs(asteroid) < stack[stack.length - 1]) {
                continue;
            }
            //若当前向左移动的行星和栈顶行星的体积相等,则双方都会爆炸,此时需要将栈顶行星推出栈
            else if (Math.abs(asteroid) == stack[stack.length - 1]) {
                stack.pop();
                continue;
            }
            //若当前向左移动的行星比栈顶行星的体积大,则栈顶行星会爆炸,而且当前行星可能会持续向左移动继续引发栈中下面的行星接连爆炸
            else if (Math.abs(asteroid) > stack[stack.length - 1]) {
                //若在出栈(栈顶星球爆炸)操作后,当前栈顶行星体积都比当前行星要小且双方反向移动,当前星球正在面对的栈顶星球都会持续爆炸
                while (stack.length && stack[stack.length - 1] < Math.abs(asteroid) && asteroid * stack[stack.length - 1] < 0) {
                    stack.pop();
                }
                //如果栈空了说明当前行星让栈内行星都炸完,则它会持续向左移动且前方已无反向移动行星,此时可以将它压入栈
                if (!stack.length) {
                    stack.push(asteroid);
                }
                //如果栈不空,且栈顶行星和当前行星同向,则说明当前栈顶行星不会与当前行星发生碰撞爆炸,此时也可以将它压入栈
                else if (stack.length && stack[stack.length - 1] * asteroid > 0) {
                    stack.push(asteroid);
                }
                //如果栈不空,栈顶行星和当前行星反向且双方体积相同,都会爆炸,将栈顶星球推出栈
                else if (stack.length && stack[stack.length - 1] + asteroid == 0) {
                    stack.pop();
                }
                //如果栈不空,且当前行星比栈顶行星体积小又反向运动(同向情况前面已经判断过),则当前行星肯定爆炸,无需进行栈操作,直接进行下一个行星的遍历
                else if (stack.length && stack[stack.length - 1] > Math.abs(asteroid)) {
                    continue;
                }
            }
        }
    }
    return stack;
};

面试题38:每日温度

leetcode:LCR 038. 每日温度

题目:输入一个数组,它的每个数字是某天的温度。请计算每天需要等几天才会出现更高的温度。例如,如果输入数组[35,31,33,36,34]​,那么输出为[3,1,1,0,0]​。由于第1天的温度是35℃,要等3天才会出现更高的温度36℃,因此对应的输出为3。第4天的温度是36℃,后面没有更高的温度,它对应的输出是0。其他的以此类推。

function dailyTemperatures(temperatures: number[]): number[] {
    const stack = []; // 存储索引的栈
    const res = new Array(temperatures.length).fill(0);

    for (let i = 0; i < temperatures.length; i++) {
        const current = temperatures[i];
        // 当栈不为空且当前温度大于栈顶温度时
        while (stack.length && current > temperatures[stack[stack.length - 1]]) {
            const topIndex = stack.pop(); // 弹出栈顶索引
            res[topIndex] = i - topIndex; // 计算等待天数
        }
        stack.push(i); // 当前索引入栈
    }
    return res;
};

面试题39:直方图最大矩形面积

LCR 039. 柱状图中最大的矩形 --- Hard

题目:直方图是由排列在同一基线上的相邻柱子组成的图形。输入一个由非负数组成的数组,数组中的数字是直方图中柱子的高。求直方图中最大矩形面积。假设直方图中柱子的宽都为1。例如,输入数组[3,2,5,4,6,1,4,2]​,其对应的直方图如图6.3所示,该直方图中最大矩形面积为12,如阴影部分所示。

function largestRectangleArea(heights: number[]): number {
    // 添加哨兵值0到数组末尾,确保所有柱子都能被处理
    const newHeights = [...heights, 0];
    const stack: number[] = [-1]; // 栈底放入哨兵索引-1
    let maxArea = 0;
    
    for (let i = 0; i < newHeights.length; i++) {
        // 当当前柱子高度小于栈顶柱子高度时
        while (stack.length > 1 && newHeights[i] < newHeights[stack[stack.length - 1]]) {
            // 弹出栈顶柱子并计算面积
            const height = newHeights[stack.pop()!];
            const width = i - stack[stack.length - 1] - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        stack.push(i);
    }
    
    return maxArea;
}

面试题40:矩阵中的最大矩形

LCR 040. 最大矩形 --- Hard

题目:请在一个由0、1组成的矩阵中找出最大的只包含1的矩形并输出它的面积。例如,在图6.6的矩阵中,最大的只包含1的矩阵如阴影部分所示,它的面积是6。

function maximalRectangle(matrix: string[]): number {
    if (matrix.length === 0 || matrix[0].length === 0) return 0;
    
    const rows = matrix.length;
    const cols = matrix[0].length;
    // 初始化高度数组(多一列用于哨兵)
    const heights = new Array(cols + 1).fill(0);
    let maxArea = 0;

    for (const row of matrix) {
        // 更新当前行的高度
        for (let j = 0; j < cols; j++) {
            heights[j] = row[j] === '1' ? heights[j] + 1 : 0;
        }
        // 计算当前直方图的最大矩形面积
        maxArea = Math.max(maxArea, largestRectangleArea([...heights]));
    }
    
    return maxArea;
}

// 计算直方图最大矩形面积(带哨兵)
function largestRectangleArea(heights: number[]): number {
    // 添加哨兵(尾部已添加,只需在头部添加0)
    heights.unshift(0);
    const stack = [0];  // 存储索引的栈(初始放入哨兵)
    let maxArea = 0;

    for (let i = 1; i < heights.length; i++) {
        // 当前高度小于栈顶高度时循环处理
        while (heights[i] < heights[stack[stack.length - 1]]) {
            const h = heights[stack.pop()!];  // 弹出栈顶高度
            const w = i - stack[stack.length - 1] - 1;  // 计算宽度
            maxArea = Math.max(maxArea, h * w);  // 更新最大面积
        }
        stack.push(i);
    }
    
    return maxArea;
}

面试题41:滑动窗口的平均值

LCR 041. 数据流中的移动平均值

题目:请实现如下类型MovingAverage,计算滑动窗口中所有数字的平均值,该类型构造函数的参数确定滑动窗口的大小,每次调用成员函数next时都会在滑动窗口中添加一个整数,并返回滑动窗口中所有数字的平均值。

class MovingAverage {
    left: number = 0;
    right: number = 0;
    queue: Record<number, number> = {};
    sum: number = 0;
    size: number;
    constructor(size: number) {
        this.size = size;
    }

    next(val: number): number {
        if (this.right - this.left >= this.size) {
            const removedVal = this.queue[this.left];
            this.sum -= removedVal;
            this.left++;
        }
        this.queue[this.right] = val;
        this.sum += val;
        this.right++;
        return this.sum / (this.right - this.left);
    }
}

/**
 * Your MovingAverage object will be instantiated and called as such:
 * var obj = new MovingAverage(size)
 * var param_1 = obj.next(val)
 */

面试题42:最近请求次数

LCR 042. 最近的请求次数

题目:请实现如下类型RecentCounter,它是统计过去3000ms内的请求次数的计数器。该类型的构造函数RecentCounter初始化计数器,请求数初始化为0;函数ping(int t)在时间t添加一个新请求(t表示以毫秒为单位的时间)​,并返回过去3000ms内(时间范围为[t-3000,t]​)发生的所有请求数。假设每次调用函数ping的参数t都比之前调用的参数值大。

class RecentCounter {
    left: number = 0;
    right: number = 0;
    queue: Record<number, number> = {0 : 0};
    constructor() {

    }

    ping(t: number): number {
      this.queue[this.right++] = t;
      let count = 0;
      while (this.left < this.right) {
         if (t - this.queue[this.left] <= 3000) {
           count = this.right - this.left;
           break;
         } else {
            this.left++;
         }
      }
      return count;
    }
}

/**
 * Your RecentCounter object will be instantiated and called as such:
 * var obj = new RecentCounter()
 * var param_1 = obj.ping(t)
 */

面试题43:在完全二叉树中添加节点

LCR 043. 完全二叉树插入器

题目:在完全二叉树中,除最后一层之外其他层的节点都是满的(第n层有2n-1个节点)​。最后一层的节点可能不满,该层所有的节点尽可能向左边靠拢。例如,图7.3中的4棵二叉树均为完全二叉树。实现数据结构CBTInserter有如下3种方法。

  • 构造函数CBTInserter(TreeNode root)​,用一棵完全二叉树的根节点初始化该数据结构。
  • 函数insert(int v)在完全二叉树中添加一个值为v的节点,并返回被插入节点的父节点。例如,在如图7.3(a)所示的完全二叉树中添加一个值为7的节点之后,二叉树如图7.3(b)所示,并返回节点3。在如图7.3(b)所示的完全二叉树中添加一个值为8的节点之后,二叉树如图7.3(c)所示,并返回节点4。在如图7.3(c)所示的完全二叉树中添加节点9会得到如图7.3(d)所示的二叉树并返回节点4。
  • 函数get_root()返回完全二叉树的根节点。

image.png

class CBTInserter {
    root: TreeNode | null;
    constructor(root: TreeNode | null) {
        this.root = root;
    }

    insert(v: number): number {
        const newNode = new TreeNode(v);
        const q = [this.get_root()];
        while (q.length) {
            const size = q.length;
            for (let i = 0; i < size; i++) {
                const node = q.shift();
                if (node.left) {
                    q.push(node.left);
                } else {
                    node.left = newNode;
                    return node.val;
                }
                if (node.right) {
                    q.push(node.right);
                } else {
                    node.right = newNode;
                    return node.val;
                }
            }
        }
    }

    get_root(): TreeNode | null {
        return this.root;
    }
}

面试题44:二叉树中每层的最大值

LCR 044. 在每个树行中找最大值

题目:输入一棵二叉树,请找出二叉树中每层的最大值。例如,输入图7.4中的二叉树,返回各层节点的最大值[3,4,9]​。

image.png

function largestValues(root: TreeNode | null): number[] {
    if (!root) return [];
    const queue = [root];
    const res = [];
    while (queue.length) {
       const size = queue.length;
       let max = Number.MIN_SAFE_INTEGER;
       for (let i = 0; i < size; i++) {
         const node = queue.shift();
         max = Math.max(node.val, max);
         if (node.left) queue.push(node.left);
         if (node.right) queue.push(node.right);
       }
       res.push(max);
    }
    return res;
};

面试题45:二叉树最低层最左边的值

LCR 045. 找树左下角的值

题目:如何在一棵二叉树中找出它最低层最左边节点的值?假设二叉树中最少有一个节点。例如,在如图7.5所示的二叉树中最低层最左边一个节点的值是5。

image.png

function findBottomLeftValue(root: TreeNode | null): number {
    const q = [root];
    let lastLeft: number = root.val;
    while (q.length) {
        let currentSize = q.length;
        lastLeft = q[0].val;
        for (let i = 0; i < currentSize; i++) {
            const node = q.shift();
            if (node.left) q.push(node.left);
            if (node.right) q.push(node.right);
        }
    }
    return lastLeft;
};

面试题46:二叉树的右侧视图

LCR 046. 二叉树的右视图

题目:给定一棵二叉树,如果站在该二叉树的右侧,那么从上到下看到的节点构成二叉树的右侧视图。例如,图7.6中二叉树的右侧视图包含节点8、节点10和节点7。请写一个函数返回二叉树的右侧视图节点的值。

image.png

function rightSideView(root: TreeNode | null): number[] {
   if (!root) return [];
   const q = [root];
   const res = [];
   while (q.length) {
      const currentSize = q.length;
      res.push(q[currentSize - 1].val);
      for (let i = 0; i < currentSize; i++) {
         const node = q.shift();
         if (node.left) q.push(node.left);
         if (node.right) q.push(node.right);
      }
   }
   return res;
};