leetcode Hot 100 题目整理

845 阅读24分钟

前言: leetcode 上的 hot100,解法借鉴了论坛里的一些常规解法以及优化方式,按类型整理,包括一些题目的变种题等。

往期文章:

高频面试题整理

前端常见手写题

一、数组

1、双指针

1、盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

var maxArea = function(height) {
    let l = 0, r = height.length - 1;
    let max = 0;
    while(l < r) {
        cur = Math.min(height[l], height[r]) * (r - l);
        max = Math.max(cur, max);
        if(height[l] < height[r]) {
            l++;
        } else {
            r--;
        }
    }
    return max;
};

2、三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

var threeSum = function(nums) {
    if(nums.length < 3) return [];
    let res = [];
    nums.sort((a,b) => a - b);
    for(let i = 0; i < nums.length - 2; i++) {
        while(nums[i] === nums[i - 1]) i++;  
        let l = i + 1, r = nums.length - 1;
        while(l < r) {
            if(nums[i] + nums[l] + nums[r]  === 0) {
                res.push([nums[i], nums[l],nums[r]]);
                l++; r--; 
                while(nums[l] === nums[l-1]) l++;
                while(nums[r] === nums[r+1]) r--;
            } else if(nums[i] + nums[l] + nums[r]  < 0) {
                l++;
            } else {
                r--;
            }
        }
    }
    return res;
};

2.1、最接近的三数之和

给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。

返回这三个数的和。

假定每组输入只存在恰好一个解。

var threeSumClosest = function(nums, target) {
    const len = nums.length;
    if(len < 3) return null;
    nums.sort((a, b) => a - b);
    let res = target - (nums[0] + nums[1] + nums[2]);

    for(let i = 0; i < len - 2; i++) {
        let left = i + 1, right = len - 1;
        while(left < right) {
            const sum = nums[i] + nums[left] + nums[right];
            if(sum === target) {
                return sum;
            } else if(sum < target) { // sum < target 时,left++
                while (nums[left] === nums[++left]);
            } else {  // sum > target时,right--
                while (nums[right] === nums[--right]);
            }
            if(Math.abs(sum - target) < Math.abs(res)) {
                res = target - sum; // 存储与 target 最近的值
            }
        }
    }
    return target - res;
};

3、下一个排列

整数数组的一个 排列  就是将其所有成员以序列或线性顺序排列。

例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。

类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。

而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

var nextPermutation = function(nums) {
    if(nums.length < 2) return nums;
    let i = nums.length - 2;        // 先从后往前找前一个数是 t 是否小于自己,                 
    while(i >= 0 && nums[i] >= nums[i + 1])  i--;   // 此时 t 后的数为降序,
    if(i >= 0) {
        let j = nums.length - 1;
        while(nums[i] >= nums[j]) j--; // t 再从后往前,与后面的第一个大于 t 的数交换
        [nums[i], nums[j]] = [nums[j], nums[i]];
    }
    let l = i + 1, r = nums.length - 1;  // 最后用双指针部分倒叙成升序即可
    while(l < r) {
        [nums[l], nums[r]] = [nums[r], nums[l]];
        l++; r--;
    }
    return nums;
};

4、接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

var trap = function(height) {
    // 当左边最大挡板<右边最大挡板,左边向前挺近,
    // 最终值加上当前左最大挡板-当前左指针所指值
    //(相当于左边只要不超过右边,右边最大挡板稳定兜底,左边无脑挺近累加)大于则反之
    // 对于每一个柱子接的水,那么它能接的水=min(左右两边最高柱子)-当前柱子高度
    let res = 0; 
    let l = 0, r = height.length - 1;
    let maxl = 0, maxr = 0;
    while(l < r) {
        maxl = Math.max(height[l], maxl);
        maxr = Math.max(height[r], maxr);
        if(maxl < maxr) {
            res += maxl - height[l];
            l++;
        } else {
            res += maxr - height[r];
            r--;
        }
    }
    return res;
};

5、颜色分类

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库的sort函数的情况下解决这个问题。

var sortColors = function(nums) {
    let l = 0, r = nums.length - 1;
    let cur = 0;
    function swap(i ,j) {
        [nums[i], nums[j]] = [nums[j], [nums[i]]];
    }
    while(cur <= r) {
        while(cur >= l && cur <= r && nums[cur] !== 1) {
            if(nums[cur] === 0) {
                swap(cur, l++);
            } else if(nums[cur] === 2) {
                swap(cur, r--);
            }
        }
        cur++;
    }
    return nums;
};

6、移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

var moveZeroes = function(nums) {
    let l = 0, r = 0;
    for(let i = 0; i < nums.length; i++) {
        if(nums[i] !== 0) {
            temp = nums[l];
            nums[l] = nums[r];
            nums[r] = temp;
            l++
        }
        r++;
    }
};

7、长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

var minSubArrayLen = function(target, nums) {
    let l = 0, sum = 0, len = Infinity;
    for(let r = 0; r < nums.length; r++) {
        sum += nums[r];
        while(l <= r && sum >= target) {
            len = Math.min(len, r - l + 1);
            sum -= nums[l++];
        }
    }
    return len === Infinity ? 0 : len;
};

8、最长重复子数组

给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

var findLength = function(nums1, nums2) {
    // 遍历两个数组,遍历的长度是从 0 到 数组的长度 - 得到的最大子数组的长度
    // 在两个数组中找到第一个相同数字的位置的下标,再从该位置往后寻找最大相同子数组
    let max = 0;
    for(let i = 0; i < nums1.length - max; i++) {
        for(let j = 0; j < nums2.length - max; j++) {
            if(nums1[i] === nums2[j]) {
                // 再从相同位置开始,找出最长的相同子数组的长度,和最大长度比较取最大值
                max = Math.max(max, getMax(nums1, nums2, i, j));
            }
        }
    }
    return max;
};

function getMax(nums1, nums2, i, j) { // 双指针寻找两个数组的最大相同子数组的长度
    let count = 0;
    while(i < nums1.length && j < nums2.length) {
        if(nums1[i] === nums2[j]) {
            count++;
            i++; j++;
        } else {
            break;
        }
    }
    return count;
}

2、二分查找

1、搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

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

2、在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

var searchRange = function(nums, target) {
    let l = 0, r = nums.length - 1, mid;
     while(l <= r) {
         mid = l + ((r - l) >> 1);
         if(target === nums[mid]) {
             l = mid - 1;
             r = mid + 1;
             while(true) {
                 if(nums[l] === target) {
                     l--;
                 } else if(nums[r] === target) {
                     r++;
                 } else {
                     return [l + 1, r - 1];
                 }
             }
         }
         if(target < nums[mid]) {
             r = mid - 1;
         } else {
             l = mid + 1;
         }
     }
     return [-1, -1];
};

3、寻找重复数

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

var findDuplicate = function(nums) {
    let left = 0, right = nums.length;
    while(left < right) {
        let mid = left + ((right - left) >> 1);
        let count = 0;
        for(let num of nums) {
            if(num <= mid) count++;
        }
        if(count <= mid) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return left;
};

4、最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

var lengthOfLIS = function(nums) {
    // let dp = new Array(nums.length).fill(1); 
    // for(let i= 0; i < nums.length; i++) {
    //     for(let j = 0; j < i; j++) {
    //         if(nums[j] < nums[i]) {
    //             dp[i] = Math.max(dp[i], dp[j] + 1);
    //         }
    //     }
    // }
    // return Math.max(...dp);

    let tail = new Array(nums.length).fill(1);
    let res = 0;
    for(let val of nums) {
        let l = 0, r = res;
        while(l < r) {
            let mid = l + ((r - l) >> 1);
            if(val > tail[mid]) {
                l = mid + 1;
            } else {
                r = mid;
            }
        }
        tail[r] = val;
        if(r === res) res++;
    }
    return res;
};

5、数组中的第K个最大元素(快排)

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

var findKthLargest = function (nums, k) {
    const target = nums.length - k;
    let left = 0, right = nums.length - 1;
    while(left < right) {
        const mid = partition(nums, left, right);
        if(target === mid) {
            return nums[mid];
        } else if(target > mid) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return nums[left];
};

function partition(nums, start, end) {
   // 位于基准值之前的元素都要小于基准值,位于基准值之后的元素都要大于等于基准值。
   // 如果它的位置小于排序之后第 K 个最大元素的位置,就去它之后寻找第 K 个最大元素;
   // 如果它的位置大于排序之后第 K 个最大元素的位置,就去它之前寻找第 K 个最大元素;
  const pivot = nums[start];
    while(start < end) {
        while(start < end && nums[end] >= pivot) end--;
        nums[start] = nums[end];
        while(start < end && nums[start] < pivot) start++;
        nums[end] = nums[start];
    }
    nums[start] = pivot;
    return start;
}

3、二维数组

1、旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

var rotate = function(matrix) {
    for(let i = 0; i < matrix.length; i++) {
        for(let j = i; j < matrix.length; j++) {
            [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]// 沿矩阵主轴交换值
        }    
    }
    matrix.map(item => item.reverse());    // 交换之后再让每层逆序即可得正确结果       
    return matrix;
};

2、搜索二维矩阵 II

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

每行的元素从左到右升序排列。

每列的元素从上到下升序排列。

var searchMatrix = function(matrix, target) {
    // 从矩阵的右上角出发,右上角的数在该行为最大,在该列为最小
    // 若target大于右上角数,则target不可能在该行,压缩该行
    // 若target小于右上角数,则target不能在该列,压缩该列
    // 若相等,返回true
    let i = matrix.length - 1, j = 0;
    while(true) {
        if(i < 0 || j > matrix[0].length - 1) return false;
        if(matrix[i][j] === target) return true;
        matrix[i][j] < target ? j++ : i--;
    }
};

3、螺旋矩阵

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

var spiralOrder = function(matrix) {
    let columns = matrix.length, rows = matrix[0].length;
    if(columns <= 0 || rows <= 0) return [];
    let start = 0;
    let res = [];
    while(columns > start * 2 && rows > start * 2) {
        printMartixInCircle(matrix, columns, rows, start, res);
        ++start;
    }
};

function printMartixInCircle(matrix, columns, rows, start, res) {
    let endX = columns - 1 - start;
    let endY = rows - 1 - start;
    // 从左到右打印一行
    for(let i = start; i <= endX; ++i) {
        res.push(matrix[start][i]);
    }
    // 从上到下打印一列
    if(start < endY) {
        for(let i = start + 1; i <= endY; ++i) {
            res.push(matrix[i][endX]);
        }
    }
    // 从右到左打印一行
    if(start < endX && start < endY) {
        for(let i = endX - 1; i >= start; --i) {
            res.push(matrix[endY][i]);
        }
    }
    // 从下到上打印一列
    if(start < endX && start < endY - 1){
        for(let i = endY - 1; i >= start + 1; --i) {
            res.push(matrix[i][start]);
        }
    }
    return res;
}

4、数组基础操作

1、两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

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

2、寻找两个正序数组的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。

算法的时间复杂度应该为 O(log (m+n)) 。

var findMedianSortedArrays = function(nums1, nums2) {
  const arr = [...nums1, ...nums2];
  arr.sort((a, b) => a - b);
  const r = Math.floor(arr.length / 2);
  if(r === arr.length / 2) {
      return (arr[r-1] + arr[r]) /2;
  } else {
      return arr[r];
  }
};

3、最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

var maxSubArray = function(nums) {
    let max = nums[0];
    for(let i = 1; i < nums.length; i++) {
        nums[i] += Math.max(nums[i - 1], 0);
        max = Math.max(max,nums[i]);
    }
    return max;
};

4、跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

var canJump = function(nums) {
    let max = nums[0];
    if(nums.length === 1) return true;
    for(let i = 1; i < nums.length; i++) {
        if(i <= max) {
            max = Math.max(max, nums[i] + i);
            if(max >= nums.length - 1) return true;
        }
    }
    return false;
};

5、除自身以外数组的乘积

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内。

请不要使用除法,且在 O(n) 时间复杂度内完成此题。

var productExceptSelf = function(nums) {
    // 分别计算当前数前后乘积之后相乘
    // 例如:nums = [1,2,3,4,5]
    // 假设当前数是 3 即nums[2]
    // left(2) = nums[0]*nums[1];
    // right(2) = nums[4]*nums[5];
    // 实现:循环递增得到left(n)、递减得到right(n)
    // left[0]默认1、right[nums.length]默认1

  let result = [1], rightMiddleValue = 1;
	for(let i = 1; i < nums.length; i++) {
		result[i] = nums[i - 1] * result[i - 1];
	}
	for(let i = nums.length - 1; i >= 0; i--) {
		result[i] = result[i] * rightMiddleValue;
		rightMiddleValue *= nums[i]
	}
    return result;
};

6、前 K 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

var topKFrequent = function(nums, k) {
    const map = new Map();
    nums.forEach(i => {
        map.set(i, map.has(i) ? map.get(i) + 1 : 1);
    })
    const list = Array.from(map).sort((a, b) => b[1] - a[1]);
    return list.slice(0,k).map(i => i[0]);
};

7、根据身高重建队列

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

var reconstructQueue = function(people) {
    // 身高相同,按照个数升序排序;身高不同,按照身高降序排序
    // 第二个数字作为索引位置,把数组放在目标索引位置上,
    // 如果原来有数了,会往后移(前面的都是高的,不怕后移)
    let res = [];
    people.sort((a, b) => a[0] === b[0] ? a[1] - b[1] : b[0] -a[0]);
    people.forEach(i => res.splice(i[1], 0, i));
    return res;
};

8、找到所有数组中消失的数字

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

var findDisappearedNumbers = function(nums) {
    const a = new Set(nums);
    let res = [];
    for(let i = 1; i <= nums.length; i++) {
        if(!a.has(i)) {
            res.push(i);
        }
    }
    return res;
};

9、和为 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k ****的连续子数组的个数。

var subarraySum = function(nums, k) {
    const map = new Map();
    map.set(0, 1);
    let sum = 0, count = 0;
    for(let num of nums) {
        sum += num;
        if(map.has(sum - k)) count += map.get(sum - k);
        map.set(sum, (map.has(sum) ? map.get(sum) : 0) + 1);    
    }
    return count;
};

10、缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

var firstMissingPositive = function(nums) {
    // 思路:就把 1 这个数放到下标为 0 的位置, 2 这个数放到下标为 1 的位置,
    // 按照这种思路整理一遍数组。
    // 然后我们再遍历一次数组,第 1 个遇到的它的值不等于下标的那个数,
    // 就是我们要找的缺失的第一个正数。
    let len = nums.length;
    for (let i = 0; i < len; i++) {
        while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] !== nums[i]) {
            swap(nums, nums[i] - 1, i);
        }
    }
    // [1, -1, 3, 4]
    for (let i = 0; i < len; i++) {
        if (nums[i] !== i + 1) {
            return i + 1;
        }
    }
    return len + 1; // 都正确则返回数组长度 + 1
};

function swap(nums, l, r) {
    let temp = nums[l];
    nums[l] = nums[r];
    nums[r] = temp;
}

二、字符串

1、双指针

1、最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。

如果 s 中存在这样的子串,我们保证它是唯一的答案。

var minWindow = function(s, t) {
    const map = new Map();
    for(let str of t) {
        map.set(str, (map.has(str) ? map.get(str) : 0) + 1 );
    }
    let count = map.size;
    let start = 0, end = 0, minStart = 0, minEnd = 0, minLength = Infinity;
    while(end < s.length || (count ===0 && end === s.length)) {
        if(count > 0) {
            if(map.has(s[end])) {
                map.set(s[end], map.get(s[end]) - 1);
                if(map.get(s[end]) === 0) {
                    count--;
                }
            }
            end++;
        } else {
            if(end - start < minLength) {
                minLength = end - start;
                minStart = start;
                minEnd = end;
            }
            if(map.has(s[start])) {
                map.set(s[start], map.get(s[start]) + 1);
                if(map.get(s[start]) === 1) {
                    count++;
                }
            }
            start++;
        }
    }
    return minLength < Infinity ? s.substring(minStart, minEnd) : "";
};

2、找到字符串中所有字母异位词

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

var findAnagrams = function(s, p) {
   // O(m+n) 时间复杂度  O(1)空间复杂度
   let indices = [];
   if(s.length < p.length) return indices;
   let counts = new Array(26).fill(0);
   for(let i = 0; i < p.length; ++i) {
       // js不能直接两个字符相减,转ASCII码
       counts[p.charCodeAt(i) - 'a'.charCodeAt()]++;  // p遍历到的加1
       counts[s.charCodeAt(i) - 'a'.charCodeAt()]--;  // s遍历到的减1
   }
   if(areAllZero(counts)) indices.push(0);
   // p遍历结束,遍历s剩余字符
   for(let i = p.length; i < s.length; ++i) {
       counts[s.charCodeAt(i) - 'a'.charCodeAt()]--;  // 右指针
       counts[s.charCodeAt(i - p.length) - 'a'.charCodeAt()]++; // 左指针
       if(areAllZero(counts)) {
           indices.push(i - p.length + 1);
       }
   }
   return indices;
};

function areAllZero(counts) {
    for(let count of counts) {
        if(count !== 0 ) return false;
    }
    return true;
}

2、回文字符串

1、最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

var longestPalindrome = function(s) {
    if(s.length < 2) return s;
    let str = '';
    for(let i = 0; i < s.length; i++) {
        const str1 = getStr(s, i, i)   // 奇数回文的对称中心
        const str2 = getStr(s, i, i + 1) // 偶数回文的对称中心
        if(str1.length > str.length || str2.length > str.length) {
            str = str1.length > str2.length ? str1 : str2;
        }
    }
	  return str;
}

function getStr(s, L, R) {
	  if(s[L] != s[R]) return '';
    while(L > 0 && R < s.length && s[L - 1] === s[R + 1]) {
        L--;
        R++;
    }
    return s.substring(L, R + 1);
}

2、回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

var countSubstrings = function(s) {
    let count = 0;
    for (let i = 0; i < s.length; i++) {
        for (let l = i, r = i; l >= 0 && s[l] === s[r]; l--, r++) count++;
        for (let l = i, r = i + 1; l >= 0 && s[l] === s[r]; l--, r++) count++;
    }
    return count;
};

3、字符串基础操作

1、无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

var lengthOfLongestSubstring = function(s) {
  let res = [], max = 0;
  for(let str of s) {
      while(res.includes(str)) res.shift();
      res.push(str);
      max = Math.max(max, res.length);
  }
  return max;
}

2、字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。

var groupAnagrams = function(strs) {
    let map = new Map();
    for(let i = 0; i < strs.length; i++) {
        let str = strs[i].split('').sort().join();
        if(map.has(str)) {
            let temp = map.get(str);
            temp.push(strs[i]);
            map.set(str, temp);
        } else {
            map.set(str, [strs[i]]);
        }
    }
    return [...map.values()];
};

3、比较版本号

给你两个版本号 version1 和 version2 ,请你比较它们。

版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.33 和 0.1 都是有效的版本号。

比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。

返回规则如下:

如果 version1 > version2 返回 1,

如果 version1 < version2 返回 -1,

除此之外返回 0。

var compareVersion = function(version1, version2) {
    const n1 = version1.split('.');
    const n2 = version2.split('.');
    let i = 0;
    while(i < n1.length || i < n2.length) {
        let x = 0;
        let y = 0;
        if(i < n1.length) x = parseInt(n1[i]);
        if(i < n2.length) y = parseInt(n2[i]);
        if(x > y) {
            return 1;
        } else if(x < y) {
            return -1;
        }
        i++;
    }
    return 0;
};

4、字符串相加

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。

你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。

var addStrings = function(num1, num2) {
    let res = '';
    let i1 = num1.length - 1;
    let i2 = num2.length - 1;
    let carry = 0;
    while (i1 >= 0 || i2 >= 0) {
        const x = i1 >= 0 ? num1[i1] - '0' : 0;
        const y = i2 >= 0 ? num2[i2] - '0' : 0;
        const sum = x + y + carry;
        res += (sum % 10);
        carry = Math.floor(sum / 10);
        i1--;
        i2--;
    }
    if (carry) res += carry;
    return res.split("").reverse().join("");
};

5、最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""。

var longestCommonPrefix = function(strs) {
    if(strs.length <= 1) return strs[0];
    strs.sort();
    let i;
    for(i = 0; i < strs[0].length; i++) {
        if(strs[strs.length - 1][i] !== strs[0][i]) break;
    }
    if(i < 1) {
        return "";
    } else {
        return strs[0].substring(0, i);
    }
};

三、链表

1、反转链表

1、反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

var reverseList = function(head) {
    let pre = null, cur = head;
    while (cur) {
        const next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
};

2、K 个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:

你可以设计一个只使用常数额外空间的算法来解决此问题吗?

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

var reverseKGroup = function(head, k) {
  if(!head) return null;
  let a = head, b = head;
  for(let i = 0; i < k; i++) {  // 区间[a,b)包含k个元素
    if(b === null) {  //剩余的结点数不足k个,反转结束
      return head;
    } else {
      b = b.next;
    }
  }
  let newHead = reverseList(a, b);
  a.next = reverseKGroup(b,k);
  return newHead;
};

function reverseList(a, b) {
    let pre = null, cur = a;
    while (cur !== b) {
        const next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
};

2、环

1、环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

var hasCycle = function(head) {
    let fast = head, slow = head;
    while(fast && slow && fast.next) {
        fast = fast.next.next;
        slow = slow.next;
        if(fast === slow) return true;
    }
    return false;
};

2、环形链表 II

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

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

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var detectCycle = function(head) {
    if(head === null || head.next === null) return null;
    let fast = head.next.next, slow = head.next, p = head;
    while(fast && fast.next) {
        if(fast === slow) {
            fast = p;
            while(fast !== slow) {
                fast = fast.next;
                slow = slow.next;
            }
            return slow;
        } else {
            fast = fast.next.next;
            slow = slow.next;
        }
    }
    return null;
};

3、链表基础操作

1、两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

var addTwoNumbers = function(l1, l2) {
    let sum = new ListNode('0');
    let head = sum, carry = 0;
    while(carry || l1 || l2) {
        let val1 = l1 === null ? 0 : l1.val;
        let val2 = l2 === null ? 0 : l2.val;
        let res = val1 + val2 + carry;
        carry = res >= 10 ? 1 : 0;
        sum.next = new ListNode(res % 10);
        sum = sum.next;
        if(l1) l1 = l1.next;
        if(l2) l2 = l2.next;
    }
    return head.next;
};

2、删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n **个结点,并且返回链表的头结点。

var removeNthFromEnd = function(head, n) {
    const dummy = new ListNode(0, head);
    let fast = dummy, slow = dummy;
    while(n--) {
        fast = fast.next;
    }
    while(fast.next) {
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return dummy.next;
};

2.1、链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

var getKthFromEnd = function(head, k) {
    const dummy = new ListNode(0, head);
    let fast = dummy, slow = dummy;
    while(k--) {
        fast = fast.next;
    }
    while(fast.next) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow.next;
};

3、合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

var mergeTwoLists = function(l1, l2) {
    let res = new ListNode();
    let l3 = res;
    while(l1 && l2) {
        if(l1.val < l2.val) {
            l3.next = l1;
            l1 = l1.next;
        } else {
            l3.next = l2;
            l2 = l2.next;
        }
        l3 = l3.next;
    }
    l3.next = (l1 ? l1 : l2);
    return res.next;
};

4、合并K个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

var mergeKLists = function(lists) {
    let res = null;
    for(let i = 0; i < lists.length; i++) {
        res = merge(res, lists[i]);
    }
    return res;
};

function merge(l1, l2) {
    const res = new ListNode();
    let l3 = res;
    while(l1 && l2) {
        if(l1.val < l2.val) {
            l3.next = l1;
            l1 = l1.next;
        } else {
            l3.next = l2;
            l2 = l2.next;
        }
        l3 = l3.next;
    }
    l3.next = (l1 ? l1 : l2);
    return res.next;
}

5、排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

var sortList = function(head) {
// 归并排序,将链表从中间分成两段,合并排序
    if(!head || !head.next) return head;
    let fast = head, slow = head, preSlow = null;
    while(fast && fast.next){
        preSlow = slow;
        fast = fast.next.next;
        slow = slow.next;
    }
    // slow指向mid,或者mid线左边一个; 设置preSlow用于切断
    preSlow.next = null;
    return merge(sortList(head), sortList(slow));
};

function merge(l, r){
    // 合并两个有序数组
    const dummy = new ListNode(0);
    let p = dummy;
    while(l && r){
        if(l.val > r.val){
            p.next = r;
            r = r.next;
        }else{
            p.next = l;
            l = l.next;
        }
        p = p.next;
    }
    if(l) p.next = l;
    if(r) p.next = r;
    return dummy.next;
}

6、相交链表

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

var getIntersectionNode = function(headA, headB) {
   /**
    定义两个指针, 第一轮让两个到达末尾的节点指向另一个链表的头部, 最后如果相遇则为交点(在第一轮
    移动中恰好抹除了长度差)两个指针等于移动了相同的距离, 有交点就返回, 无交点就是各走了两条指针
    的长度
    **/
    let p1 = headA, p2 = headB;
    while(p1 !== p2) {
        p1 = p1 ? p1.next : headB;
        p2 = p2 ? p2.next : headA;
    }
    return p1;
};

7、回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

var isPalindrome = function(head) {
   // O(n) 时间复杂度,O(1)空间复杂度
   if(head === null || head.next === null) return true;
   let slow = head, fast = head.next;
   while(fast.next && fast.next.next) {
       fast = fast.next.next;
       slow = slow.next;
   }
   let secondHaif = slow.next;
   if(fast.next) {
       secondHaif = slow.next.next;
   }
   slow.next = null;
   return equals(secondHaif,reverseList(head));
};

function equals (head1,head2) {
    while(head1 && head2) {
        if(head1.val !== head2.val) {
            return false;
        }
        head1 = head1.next;
        head2 = head2.next;
    }
    return head1 === null && head2 === null;
}

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

\

四、二叉树

1、搜索树

1、把二叉搜索树转换为累加树

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

提醒一下,二叉搜索树满足下列约束条件:

节点的左子树仅包含键 小于 节点键的节点。

节点的右子树仅包含键 大于 节点键的节点。

左右子树也必须是二叉搜索树。

var convertBST = function(root) {
    let sum = 0
    function traverse(root) {  // 反向的中序遍历,从大到小
        if(!root) return
        traverse(root.right)   // 右
        sum += root.val
        root.val = sum         // 根
        traverse(root.left)    // 左
    }
    traverse(root)
    return root
};

2、二叉搜索树中第K小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k ****个最小元素(从 1 开始计数)。

var kthSmallest = function(root, k) {
    const res = [];
    function dfs(node) {
        if(!node) return;
        dfs(node.left);
        res.push(node.val);
        dfs(node.right);
    }
    dfs(root);
    return res[k - 1];
};

2、树转链表

1、二叉树展开为链表

给你二叉树的根结点 root ,请你将它展开为一个单链表:

展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。

展开后的单链表应该与二叉树 先序遍历 顺序相同。

var flatten = function(root) {
    let res = [];
    function dfs(node) {
        if(!node) return;
        res.push(node);
        deep(node.left);
        deep(node.right); 
    }
    dfs(root);
    for(let i = 1; i < res.length; i++) {
        res[i - 1].left = null;
        res[i - 1].right = res[i];
    }
};

3、树的基础操作

1、二叉树的中序遍历

给定一个二叉树的根节点 root ,返回它的 中序 遍历。

var inorderTraversal = function(root) {
    let res = [];
    function dfs(node) {
        if(!node) return ;
        dfs(node.left);
        res.push(node.val);
        dfs(node.right);
    }
    dfs(root);
    return res;
};

2、二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

var levelOrder = function(root) {
    if(!root) return [];
    const q = [root], res = [];
    while(q.length) {
        let len = q.length;
        res.push([]);
        while(len--) {
            const node = q.shift();
            res[res.length - 1].push(node.val);
            if(node.left) q.push(node.left);
            if(node.right) q.push(node.right);
        }
    }
    return res;
};

2.0、二叉树的右视图

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

var rightSideView = function(root) {
    if(!root) return [];
    const q = [root], res = [];
    while(q.length) {
        let len = q.length;
        while(len--) {
            const node = q.shift();
            if(!len) res.push(node.val);
            if(node.left) q.push(node.left);
            if(node.right) q.push(node.right);
        }
    }
    return res;
};

2.1、N 叉树的层序遍历

给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。

树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。

var levelOrder = function(root) {
    if(!root) return [];
    let q = [root], res = [];
    while(q.length) {
        let len = q.length;
        res.push([]);
        while(len--) {
            const node = q.shift();
            res[res.length - 1].push(node.val);
            for(let child of node.children) {
                q.push(child);
            }
        }
    }
    return res;
};

2.2、二叉树的锯齿形层序遍历

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

var zigzagLevelOrder = function(root) {
    const res = [];
    function dfs(i, node) {
        if(!node) return;
        if(!Array.isArray(res[i])) res[i] = [];
        if(i & 1) {
            res[i].unshift(node.val); // 奇数层从前加
        } else {
            res[i].push(node.val); // 偶数层从后加
        }
        dfs(i + 1, node.left);
        dfs(i + 1, node.right);
    }
    dfs(0, root);
    return res;
};

3、从前序与中序遍历序列构造二叉树

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

var buildTree = function(preorder, inorder) {
    // 根据前序确定根节点在中序数组中的位置,以此划分左右子树
    if (!preorder.length || !inorder.length) return null
    const rootVal = preorder[0]
    const node = new TreeNode(rootVal)
    const i = inorder.indexOf(rootVal)
    node.left = buildTree(preorder.slice(1, i+1), inorder.slice(0,i))
    node.right = buildTree(preorder.slice(i+1), inorder.slice(i+1))
    return node
};

4、二叉树的最大深度

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:

给定二叉树 [3,9,20,null,null,15,7],

var maxDepth = function(root) {
    return root === null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1
};

5、不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

var numTrees = function(n) {
    let dp = new Array(n+1).fill(0);
    dp[0] = 1;
    for(let i = 1;i<=n;i++){
        for(let j = 1;j<=i;j++){
            dp[i] += dp[j-1] * dp[i-j];
        }
    }
    return dp[n];
};

6、验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

节点的左子树只包含 小于 当前节点的数。

节点的右子树只包含 大于 当前节点的数。

所有左子树和右子树自身必须也是二叉搜索树。

var isValidBST = function(root, min = -Infinity, max = Infinity) {
    if (!root) return true;
    if (root.val <= min || root.val >= max) return false;
    return isValidBST(root.left, min, root.val) && isValidBST(root.right, root.val, max);
};

7、对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

var isSymmetric = function(root) {
    if(!root) return true;
    function dfs(l, r) {
        if(!l && !r) return true;
        if(l && r && l.val === r.val && dfs(l.left,r.right) && dfs(l.right, r.left)) {
            return true;
        }
        return false;
    }
    return dfs(root.left, root.right);
};

8、二叉树中的最大路径和

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

var maxPathSum = function(root) {
    let max = root.val;
    function dfs(node) {
        if(node === null) return 0;
        const left = Math.max(0, deep(node.left));
        const right = Math.max(0,deep(node.right));
        max = Math.max(max, left + right + node.val);
        return Math.max(left, right) + node.val;
    }
    dfs(root);
    return max;
};

9、翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

var invertTree = function(root) {
    if(!root) return root;
    const left = invertTree(root.left);
    const right = invertTree(root.right);
    root.left = right;
    root.right = left;
    return root;
};

10、二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

var lowestCommonAncestor = function(root, p, q) {
    if(!root || root === p || root === q) return root;
    const left = lowestCommonAncestor(root.left, p, q);
    const right = lowestCommonAncestor(root.right, p, q);
    if(left && right) {
        return root;
    } else {
        return left ? left : right;
    }
};

11、二叉树的序列化与反序列化

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构

var serialize = function(root) {
    function dfs(root) {
        if(!root) return "#,";
        return dfs(root.left) + dfs(root.right) + root.val + ","
    }
    return dfs(root);
};

var deserialize = function(data) {
    const arr = data.split(",");
    arr.pop();
    function dfs(arr) {
        const data = arr.pop();
        if(data === "#") return null;
        const root = new TreeNode(data);
        root.right = dfs(arr);
        root.left = dfs(arr);
        return root;
    }
    return dfs(arr);
};

12、二叉树的直径

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

示例 :

给定二叉树

注意: 两结点之间的路径长度是以它们之间边的数目表示。

var diameterOfBinaryTree = function(root) {
    let res = 0;
    function dfs (node) {
        if (!node) return 0; 
        let left = depth(node.left); 
        let right = depth(node.right);
        res = Math.max(left + right, res); // 计算l+r 更新res
        return Math.max(left, right) + 1; // 返回该节点为根的子树的深度
    }
    dfs(root);
    return res;
};

13、合并二叉树

给你两棵二叉树: root1 和 root2 。

想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。

返回合并后的二叉树。

注意: 合并过程必须从两个树的根节点开始。

var mergeTrees = function(root1, root2) {
    if(!root1) return root2;
    if(!root2) return root1;
    root1.val = root1.val + root2.val;
    root1.left = mergeTrees(root1.left, root2.left);
    root1.right = mergeTrees(root1.right, root2.right);
    return root1;
};

14、打家劫舍 III(树形dp)

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

var rob = function(root) {
    const checkMoney = (root) => {
        if (!root) return [0,0];
        const left = checkMoney(root.left);
        const right = checkMoney(root.right);
        const rob = root.val + left[1] + right[1] // 当前偷,左右必不能偷
        const not_rob = Math.max(left[0], left[1]) + Math.max(right[0], right[1]) 
        // 当前不偷,左右可偷可不偷
        return [rob,not_rob];
    }
    const result = checkMoney(root);
    return Math.max(...result);
};

15、路径总和 III

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

var pathSum = function(root, targetSum) {
    let cnt = 0;
    function dfs(node, sum, flag) {
        if (!node) return;
        sum += node.val;
        if (sum === targetSum) cnt++;
        dfs(node.left, sum, true);
        dfs(node.right, sum, true);
        if (flag) return;
        dfs(node.left, 0, false);
        dfs(node.right, 0, false);
    }
    dfs(root, 0, false);
    return cnt;
};

15.1、路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

叶子节点 是指没有子节点的节点。

var hasPathSum = function(root, targetSum) {
    if(!root) return false;
    let res = false;
    function dfs(node, sum) {
        if(!node.left && !node.right && sum === targetSum) {
            res = true
        }
        if(node.left) dfs(node.left, sum + node.left.val)
        if(node.right) dfs(node.right, sum + node.right.val)
    }
    dfs(root, root.val);
    return res;
};

15.2、路径总和 II

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点

var pathSum = function(root, targetSum) {
    let res = [];
    function dfs(node, sum, path) {
        if(!node) return; 
        sum += node.val; 
        path.push(node.val); 
        if(!node.left && !node.right && sum === targetSum) {  
            res.push(path.slice()); 
        } else {
            dfs(node.left, sum, path);  
            dfs(node.right, sum, path);  
        }
        path.pop(); 
    }
    dfs(root, 0, []);
    return res;
};

五、回溯

1、组合

1、组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

var combinationSum = function(candidates, target) {
    // 迭代
    let res = [];
    function dfs(index, path, target) {
        if(target=== 0) {
            res.push([...path])
        } else if(target > 0) {        // 剪枝
            for(let i = index; i < candidates.length; i++) {
                path.push(candidates[i]);
                dfs(i, path, target - candidates[i]);
                path.pop();
            }
        }
    }
    dfs(0, [], target);
    return res;
  
   // 递归 
   let result = [], combination = [];
   function dfs(nums, target, index, combination, result) {
        if(target === 0) {
            result.push([...combination])
        } else if(target > 0 && index < nums.length) {  // 剪枝
            dfs(nums, target, index + 1, combination, result);
            combination.push(nums[index]);
            dfs(nums, target - nums[index], index, combination, result);
            combination.pop();
        }
    };
  dfs(candidates, target, 0, combination, result);
  return result;
};

1.1、组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。 

var combinationSum2 = function(candidates, target) {
   candidates.sort();
   let result = [], combination = [];
   function dfs(nums, target, index, combination, result) {
        if(target === 0) {
            result.push([...combination])
        } else if(target > 0 && index < nums.length) {  // 剪枝
            dfs(nums, target, getNext(nums, index), combination, result);
            combination.push(nums[index]);
            dfs(nums, target - nums[index], index + 1, combination, result);
            combination.pop();
        }
    };
    dfs(candidates, target, 0, combination, result);
    return result;
};

function getNext(nums, index) {
    let next = index;
    while(next < nums.length && nums[next] === nums[index]) {
        next++;
    }
    return next;
}

2、子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

var subsets = function(nums) {
   const result = [];
   if(nums.length === 0) return result;
   function dfs(nums, index, path, result) {
       if(index === nums.length) {
           result.push([...path]);
       } else if (index < nums.length) {
           dfs(nums, index + 1, path, result);
           path.push(nums[index]);
           dfs(nums, index + 1, path, result);
           path.pop();
       }
   }
   dfs(nums, 0, [], result);
   return result;
};

3、电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

var letterCombinations = function(digits) {
  if (digits.length === 0) return [];
  let numMap = new Map([
    ['0', ''],
    ['1', ''],
    ['2', 'abc'],
    ['3', 'def'],
    ['4', 'ghi'],
    ['5', 'jkl'],
    ['6', 'mno'],
    ['7', 'pqrs'],
    ['8', 'tuv'],
    ['9', 'wxyz']
  ])
  let res = [];
  function dfs(str, digit) {
    if (digit.length === 0) {
        res.push(str);
    } else {
      let numstr = numMap.get(digit[0]);  // 拿到字符串第一个字符,拿到其对应的数字
      for (let i = 0; i < numstr.length; i++) {
        str += numstr[i];
        dfs(str, digit.slice(1));
        str = str.slice(0, -1);
      }
    }
  }
  dfs("", digits);
  return res;
};

2、排列

1、全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

var permute = function(nums) {
    const result = [];
    dfs(nums, 0, result);
    return result;
};

function dfs(nums, i, result) {
    if(i === nums.length) {
        const path = [];
        for(let num of nums) {
            path.push(num);
        }
        result.push(path);
    } else {
        for(let j = i; j < nums.length; j++) {
            swap(nums, i, j);
            dfs(nums, i + 1, result);
            swap(nums, i, j);
        }
    }
}

function swap(nums, i, j) {
    if(i !== j) {
        let temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

2、全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

var permuteUnique = function(nums) {
    const result = [];
    dfs(nums, 0, result);
    return result;
};

function dfs(nums, i, result) {
    if(i === nums.length) {
        const path = [];
        for(let num of nums) {
            path.push(num);
        }
        result.push(path);
    } else {
        const hash = new Map();
        for(let j = i; j < nums.length; j++) {
            if(!hash.has(nums[j])) {
                hash.set(nums[j], j);
                swap(nums, i, j);
                dfs(nums, i + 1, result);
                swap(nums, i, j);
            }
        }
    }
}

function swap(nums, i, j) {
    if(i !== j) {
        let temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

3、回溯基础操作

1、括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

var generateParenthesis = function(n) {
    let res = [];
    function dfs(cur, l, r) {
        if(cur.length === 2 * n) {
            res.push(cur);
            return;
        }
        if(l < n) {
            dfs(cur + "(", l + 1, r);
        }
        if(r < l) {
            dfs(cur + ")", l, r + 1);
        }
    }
    dfs("", 0, 0);
    return res;
};

2、岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

var numIslands = function(grid) {
    const row = grid.length, col = grid[0].length;
    let res = 0;
    for(let i = 0; i < row; i ++) {
        for(let j = 0; j < col; j ++) {
            if(grid[i][j] === '1') {
                res++;
                dfs(i, j, grid)
            }
        }
    }
    return res;
};

function dfs(row, col, grid) {
    const rowMax = grid.length, colMax = grid[0].length;
    //是否超过边界
    if(row >= rowMax || col >= colMax || row < 0 || col < 0) {
        return;
    }
    if(grid[row][col] === '0') { // 非陆地 / 已经遍历过
        return;
    }
    grid[row][col] = '0';
    dfs(row + 1, col, grid);
    dfs(row - 1, col, grid);
    dfs(row, col + 1, grid);
    dfs(row, col - 1, grid);
}

2.1、岛屿的最大面积

给你一个大小为 m x n 的二进制矩阵 grid 。

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

岛屿的面积是岛上值为 1 的单元格的数目。

计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

var maxAreaOfIsland = function (grid) {
    let max = -1;
    for (let i = 0; i < grid.length; i++)
        for (let j = 0; j < grid[0].length; j++)
            max = Math.max(max, dfs(grid, i, j));
    return max;
};

function dfs(grid, row, col) {
    // 越界
    if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length) {
        return 0;
    }
    if (grid[row][col] === -1 || grid[row][col] === 0) { // 已访问过, 或者不是岛屿
        return 0;
    }
    grid[row][col] = -1 // 标记已经访问
    let area = 1;
    area += dfs(grid, row - 1, col); // 上
    area += dfs(grid, row, col + 1); // 右
    area += dfs(grid, row + 1, col); // 下
    area += dfs(grid, row, col - 1); // 左
    return area;
}

3、复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

var restoreIpAddresses = function(s) {
    let result = [];
    dfs(s, 0, 0, "", "", result);
    return result;
};

function dfs(s, i, segI, seg, ip, result) {
    if(i === s.length && segI === 3 && isValid(seg)) {
        result.push(ip + seg);
    } else if(i < s.length && segI <= 3) {
        if(isValid(seg + s[i])) {
            dfs(s, i + 1, segI, seg + s[i], ip, result);
        }
        if(seg.length > 0 && segI < 3) {
            dfs(s, i + 1, segI + 1, "" + s[i], ip + seg + ".", result);
        }
    }
}

function isValid(seg) {
    return seg <= 255 && (seg === "0" || seg[0] !== '0');
}

六、动态规划

1、单序列

1、打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

var rob = function(nums) {
    if(nums.length === 0) return 0;
    let dp = new Array(nums.length);
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0], nums[1]);
    for(let i = 2; i < nums.length; i++) {
        dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
    }
    return dp[nums.length - 1];
};

2、最佳买卖股票时机含冷冻期

给定一个整数数组prices,其中第  prices[i] 表示第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

var maxProfit = function(prices) {
    if(prices.length < 2) return 0;
    let dp = new Array(prices.length).fill(0).map(()=> new Array(2).fill(0));
    dp[0][0] = 0; // 无股票
    dp[0][1] = -prices[0];  // 有股票
    dp[1][0] = Math.max(dp[0][0], dp[0][1] + prices[1]);
    dp[1][1] = Math.max(dp[0][1], dp[0][0] - prices[1]);
    for(let i = 2; i < prices.length; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); // 无股票
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i]); // 持有股票
    }
    return dp[prices.length-1][0]; // 最后一天无股票
};

2.1、买卖股票的最佳时机 II

给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。

在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

var maxProfit = function(prices) {
    let dp = new Array(prices.length).fill(0).map(() => new Array(2).fill(0));
    dp[0][0] = 0, dp[0][1] = -prices[0];
    for (let i = 1; i < prices.length; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); // 没股票
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); // 有股票
    }
    return dp[prices.length - 1][0];

    // 贪心
    let res = 0;
    for (let i = 1; i < prices.length; ++i) {
        res += Math.max(0, prices[i] - prices[i - 1]);
    }
    return res;

};

2.2、买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

var maxProfit = function(prices) {
    // let curMin = prices[0];
    // let sum = 0;
    // for (let i = 0; i < prices.length; i++) {
    //     if (prices[i] < curMin) {
    //         curMin = prices[i];
    //         continue;
    //     }
    //     sum = Math.max(prices[i] - curMin, sum);
    // }
    // return sum;
    let result = 0, pre = prices[0];
    // 思路,选出索引值1后面的最大值,和前面的最小值做运算
    for(let i = 1; i < prices.length; i++) {
        pre = Math.min(pre, prices[i - 1]);
        result = Math.max(result, prices[i] - pre);
    }
    return result;
};

3、戳气球

有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

var maxCoins = function(nums) {  
  let points = [1, ...nums, 1];  // 添加两侧的虚拟气球
  let dp = new Array(nums.length + 2).fill(0).map(()=> new Array(nums.length + 2).fill(0));
  for (let i = nums.length; i >= 0; i--) {  // 最后一行开始遍历,从下往上
    for (let j = i + 1; j < nums.length + 2; j++) {  // 从左往右
      for (let k = i + 1; k < j; k++) {
        dp[i][j] = Math.max(dp[i][j], points[j] * points[k] * points[i] + dp[i][k] + dp[k][j]);
      }
    }
  }
  return dp[0][nums.length + 1];
};

2、双序列

1、编辑距离

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。

你可以对一个单词进行如下三种操作:

插入一个字符

删除一个字符

替换一个字符

var minDistance = function(word1, word2) {
    let m = word1.length, n = word2.length;
    let dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
    for(let i = 0; i <= m; i++) dp[i][0] = i;
    for(let j = 0; j <= n; j++) dp[0][j] = j;
    for(let i = 1; i <= m; i++) {
        for(let j = 1; j <= n; j++) {
            if(word1[i - 1] === word2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1];
            } else { // 删除 插入 替换
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1;
            }
        }
    }
    return dp[m][n];
};

2、最大正方形

在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。

var maximalSquare = function(matrix) {
    if(!matrix.length) return 0;
    let dp = new Array(matrix.length).fill(0).map(()=> new Array(matrix[0].length).fill(0));
    let maxLen = 0;
    for(let i = 0; i < dp.length; i++){
        for(let j = 0; j < dp[0].length; j++){
            if(matrix[i][j] == '1'){
                if(i === 0 || j === 0)  {
                   dp[i][j] = 1
                } else {
                   dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1; 
                } 
                maxLen = Math.max(maxLen, dp[i][j]);    
            }   
        }
    }
    return maxLen * maxLen;
};

3、矩阵路径

1、不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

var uniquePaths = function(m, n) {
    let dp = new Array(n).fill(1);
    for(let i = 1; i < m; i++) {
        for(let j = 1; j < n; j++) {
            dp[j] += dp[j - 1];
        }
    }
    return dp[n - 1];
};

2、最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明: 每次只能向下或者向右移动一步。

var minPathSum = function(grid) {
    let dp = new Array(grid[0].length);
    dp[0] = grid[0][0];
    for(let j = 1; j < grid[0].length; j++) {
        dp[j] = grid[0][j] + dp[j - 1];
    }
    for(let i = 1; i < grid.length; i++) {
        dp[0] += grid[i][0];
        for(let j = 1; j < grid[0].length; j++) {
            dp[j] = grid[i][j] + Math.min(dp[j], dp[j - 1]);
        }
    }
    return dp[grid[0].length - 1];
};

4、背包

// 01背包
for (int i = 0; i < n; i++) {
    for (int j = m; j >= V[i]; j--) {
        f[j] = max(f[j], f[j-V[i]] + W[i]);
    }
}
// 完全背包
for (int i = 0; i < n; i++) {
    for (int j = V[i]; j <= m; j++) {
        f[j] = max(f[j], f[j-V[i]] + W[i]);
    }
}

f[j] 代表当前背包容量为j的时候,可以获取的最大价值。完全背包是从左向右遍历,f[j-V[i]]取到的是拿第i个物品时的值,是新值,可以重复无限的拿,f[j]的值也会随之增加。01背包从右向左遍历,不能重复拿。
V:商品的体积
W:商品的价值

1、完全平方数(完全背包)

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

var numSquares = function(n) {
    const dp = new Array(n+1).fill(Infinity);
    dp[0] = 0;
    for(let i = 1; i <= Math.sqrt(n); i++) {
        for(let j = i * i; j <= n; j++) {
            dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
        }
    }
    return dp[n];
};

2、零钱兑换(完全背包)

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

var coinChange = function(coins, amount) {
  let dp = new Array(amount + 1).fill(amount + 1);
  dp[0] = 0;
  for(let i = 1; i <= amount; i++) {
    for(let coin of coins) {
      if(i - coin >= 0) {
        dp[i] = Math.min(dp[i], dp[i - coin] + 1);
      }
    }
  }
  return dp[amount] === amount + 1 ? -1 : dp[amount];
};

3、单词拆分(完全背包)

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

var wordBreak = function(s, wordDict) {
    let dp = new Array(s.length + 1).fill(false);
    dp[0] = true;
    for(let i = 0; i <= s.length; i++) {
        for(let j = 0; j < wordDict.length; j++) {
            if(i >= wordDict[j].length
            && dp[i - wordDict[j].length]
            && s.slice(i - wordDict[j].length, i) === wordDict[j]
            ) {
                dp[i] = true;
            }
        }
    }
    return dp[s.length];
};

4、分割等和子集(01背包)

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

var canPartition = function(nums) {
    const sum = nums.reduce((a,b) => a + b);
    if(sum % 2 === 1) return false;
    const target = sum / 2;
    const dp = new Array(target + 1).fill(0);
    for(let i = 0; i < nums.length; i++) {
        for(let j = target; j >= nums[i]; j--){   //01背包,物品不可重复,得从后往前遍历背包
            dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
        }
    }
    return dp[target] === target;
};

5、目标和(01背包)

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

var findTargetSumWays = function(nums, target) {
    const sum = nums.reduce((a,b) => a + b);
    if((sum - target) % 2 === 1 || sum - target < 0) return 0;
    const result = (sum - target) / 2;
    const dp = new Array(result + 1).fill(0);
    dp[0] = 1;
    for(let i = 0; i < nums.length; i++) {
        for(let j = result;j >= nums[i]; j--) {
            dp[j] = dp[j] + dp[j - nums[i]];
        }
    }
    return dp[result];
};

5、dp基础操作

1、爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

var climbStairs = function(n) {
    const dp = [];
    dp[0] = 1;
    dp[1] = 2;
    for (let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n - 1];
};

2、乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

子数组 是数组的连续子序列。

var maxProduct = function(nums) {
    if(nums.length === 0) return 0;
    let dpMax = [], dpMin = [];
    dpMax[0] = nums[0];
    dpMin[0] = nums[0];
    max = dpMax[0]
    for(let i = 1; i < nums.length; i++){
        dpMax[i] = Math.max(nums[i], Math.max(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i]))
        dpMin[i] = Math.min(nums[i], Math.min(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i]))
        max = Math.max(max, dpMax[i]);
    }
    return max;
};

3、让字符串成为回文串的最少插入次数

给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。

请你返回让 s 成为回文串的 最少操作次数 。

「回文串」是正读和反读都相同的字符串。

var minInsertions = function(s) {
    // 判断 s[i] 和 s[j]是否相等
    // 如果相等则不用插入,看 s[i+1...j-1] 的情况
    // 如果不相等,则看
    // 1. 在 s[j] 后面插入 s[i],次数为 1 + f(i + 1, j)
    // 2. 在 s[i] 前面插入 s[j],次数为 1 + f(i, j - 1)
    // 这两种情况哪种的插入次数更小
    // 左下到右上,保证计算dp[i][j]时,dp[i + 1][j]、dp[i][j - 1]均已计算过
    let len = s.length;
    let dp = new Array(len).fill(0).map(() => new Array(len).fill(0));
    for(let i = len - 1; i >= 0; i--) {
        for(let j = i + 1; j <= len - 1; j++) {  
            if(s[i] === s[j]) {
                dp[i][j] = dp[i + 1][j - 1];
            } else {
                dp[i][j] = Math.min(dp[i][j - 1], dp[i + 1][j]) + 1;
            }
        }
    }
    return dp[0][len - 1];
};

七、贪心

1、区间

1、合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

var merge = function(intervals) {
    if(!intervals) return [];
    intervals.sort((a, b) => a[0] - b[0]);
    let res = [intervals[0]];
    for(let i = 1; i < intervals.length; i++) {
        if (res[res.length - 1][1] >= intervals[i][0]) {
            res[res.length - 1][1] = Math.max(res[res.length - 1][1], intervals[i][1])
        } else {
            res.push(intervals[i])
        }
    }
    return res;
};

2、不相交区间

有许多[start, end]的闭区间, 请设计⼀个算法, 算出这些区间中, 最多有⼏个互不相交的区间

⽐如intvs = [[1,3], [2,4], [3,6]]

这些区间最多有两个区间互不相交, 即 [1,3], [3,6], intervalSchedule函数此时应该返回2

var eraseOverlapIntervals = function(intervals) {
  if (intervals.length === 0) return 0;
  let sortArray = intervals.sort((a,b) => a[1] - b[1]);
  let count = 1;
  let xEnd = sortArray[0][1];
  for (let item of intervals) {
  // 题⽬说了区间 [1,2][2,3] 的边界相互“接触”,但没有相互重叠, 所以应该是item[0] >= xEnd
    if (item[0] >= xEnd) {
      xEnd = item[1];
      count++;
    }
  }
  return intervals.length - count;
}

八、其他

1、LRU

LRU 缓存

请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存

int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。

void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

/**
 * @param {number} capacity
 */
var LRUCache = function(capacity) {
    this.capacity = capacity;
    this.map = new Map();
};

/** 
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function(key) {
    if(this.map.has(key)) {
        let temp = this.map.get(key);
        this.map.delete(key);
        this.map.set(key, temp);
        return temp;
    } else {
        return -1;
    }
};

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function(key, value) {
    if(this.map.has(key)) {
        this.map.delete(key);
    }
    this.map.set(key, value);
    if(this.map.size > this.capacity) {
        this.map.delete(this.map.keys().next().value);
    }
};


/**
 * 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)
 */

2、字典树

实现 Trie (前缀树)

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

Trie() 初始化前缀树对象。

void insert(String word) 向前缀树中插入字符串 word 。

boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。

boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。

var Trie = function() {
    this.children = {};
};

/** 
 * @param {string} word
 * @return {void}
 */
Trie.prototype.insert = function(word) {
    let node = this.children;
    for(let ch of word) {
        if(!node[ch]) {
            node[ch] = {};
        }
        node = node[ch];
    }
    node.isEnd = true;
};

/** 
 * @param {string} word
 * @return {boolean}
 */
Trie.prototype.search = function(word) {
    let node = this.children;
    for(let ch of word) {
        if(!node[ch]) {
            return false;
        }
        node = node[ch];
    }
    return !!node.isEnd;
};

/** 
 * @param {string} prefix
 * @return {boolean}
 */
Trie.prototype.startsWith = function(prefix) {
    let node = this.children;
    for(let ch of prefix) {
        if(!node[ch]) {
            return false;
        }
        node = node[ch];
    }
    return true;
};


/**
 * Your Trie object will be instantiated and called as such:
 * var obj = new Trie()
 * obj.insert(word)
 * var param_2 = obj.search(word)
 * var param_3 = obj.startsWith(prefix)
 */

3、栈

1、有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。

左括号必须以正确的顺序闭合。

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    let map = new Map([
        ['(', ')'],
        ['{', '}'],
        ['[', ']'],
    ])
    let stack = [];
    for(let i of s) {
        if(map.has(i)) {
            stack.push(map.get(i));
        } else {
            if(stack.length === 0 || stack.pop() !== i) return  false;
        }
    }
    return !stack.length;
};

2、最长有效括号

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

/**
 * @param {string} s
 * @return {number}
 */
var longestValidParentheses = function(s) {
    let stack = [-1], res = 0;
    for(let i = 0; i < s.length; i++) {
        if(s[i] === '('){
            stack.push(i);
        } else {
            stack.pop();
            if(stack.length === 0) {
                stack.push(i);
            } else {
                res = Math.max(res, i - stack[stack.length - 1]);
            }
        }
    }
    return res;
};

3、最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

MinStack() 初始化堆栈对象。

void push(int val) 将元素val推入堆栈。

void pop() 删除堆栈顶部的元素。

int top() 获取堆栈顶部的元素。

int getMin() 获取堆栈中的最小元素。

/**
 * initialize your data structure here.
 */
var MinStack = function () {
    this.item = [];
    this.min = Infinity;
    return this;
};

/** 
 * @param {number} x
 * @return {void}
 */
MinStack.prototype.push = function (x) {
    this.item.push(x);
    this.min = Math.min(this.min, x);
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function () {
    if(!this.item.length) return 0;
    let s = this.item.pop();
    if(this.min === s) this.min = Math.min(...this.item);
};

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

/**
 * @return {number}
 */
MinStack.prototype.getMin = function () {
    return this.min;
};

/** 
 * Your MinStack object will be instantiated and called as such:
 * var obj = new MinStack()
 * obj.push(x)
 * obj.pop()
 * var param_3 = obj.top()
 * var param_4 = obj.getMin()
 */

4、简化路径

给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//')都被视为单个斜杠 '/' 。 对于此问题,任何其他格式的点(例如,'...')均被视为文件/目录名称。

请注意,返回的 规范路径 必须遵循下述格式:

始终以斜杠 '/' 开头。

两个目录名之间必须只有一个斜杠 '/' 。

最后一个目录名(如果存在)不能 以 '/' 结尾。

此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '.' 或 '..')。

返回简化后得到的 规范路径 。

/**
 * @param {string} path
 * @return {string}
 */
var simplifyPath = function(path) {
    // path'/'分割成数组,如 /a/./b/../../c/分割成[ '', 'a', '.', 'b', '..', '..', 'c', '']   
    // 新建一个栈stack为当前的路径,遍历path分割后的数组元素
    // 遇到斜杠就处理、遇到.不管、遇到.. 回退、遇到其他就入栈
    const stack = [];
    const pathArr = path.split('/');
    for (let item of pathArr) {
        if (item === '' || item === '.') {
            continue;
        } else if (item === '..') {
            stack.pop();
        } else {
            stack.push(item);
        }
    }
    return '/' + stack.join('/');
};