被 leetcode 干翻的一个月...

572 阅读18分钟

前端时间群里有大佬组织刷算法,每周一个主题,定了奖惩机制,本着一起学习的心思加进来了...想着以前没正儿八经的玩过算法,正好可以补充一下短板

心路历程

  • 你虽然能写业务,能搭框架,建得起基础设施...真玩起算法来,还真是不行...
  • 刚开始从简单级别的做,每个专题每周至少3题,发现有些简单的题都还得看看官方题解...
  • 刷题的过程中能发现自己语法上的一些问题,比如时间、空间复杂度的不注重(因为leetcode提交时会检测代码耗时以及实现是否过于复杂);
  • 甚至于对之前常用的语法或者 api 的性能损耗理解都有偏差;
  • 过程中夯实了对基础逻辑的理解、对算法理论的补充、对很多 api 的重新认识

题解

数组

1. 三数之和

  • 题目
//给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复
//的三元组。
//
// 注意:答案中不可以包含重复的三元组。
//
//
//
// 示例:
//
// 给定数组 nums = [-1, 0, 1, 2, -1, -4],
//
//满足要求的三元组集合为:
//[
//  [-1, 0, 1],
//  [-1, -1, 2]
//]
//
// Related Topics 数组 双指针

//leetcode submit region begin(Prohibit modification and deletion)
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function (nums) {
  
};
  • 思路1: 常规思维,通过多层循环定位
if (!nums || nums.length < 3) return [];
  
// 从小到大排序
// 这里有一个坑,sort 默认排序不是按照数值比较
nums.sort(function(a, b) {return a - b;});

// 判断当前数组是否同符号
if (nums[0] > 0 || nums[nums.length - 1] < 0) return [];
  
// 傻瓜循环,结论不通过,复杂度太高
for (var i = 0; i < nums.length - 2; i++) {
      for (var j = i + 1; j < nums.length - 1; j++) {
           result = 0 - nums[i] - nums[j];
        
           var idx = nums.lastIndexOf(result);
           if (idx > j) {
             var arr = [nums[i], nums[j], result];
             var str = arr.toString();
        
             !mapping[str] && buffer.push(arr);
             mapping[str] = true;
           }
      }
}
  
return buffer;
  • 思路2: 高效思路,通过双向指针方式进行扫描
if (!nums || nums.length < 3) return [];

// 从小到大排序
// 这里有一个坑,sort 默认排序不是按照数值比较
nums.sort((a, b) => a - b);

// 判断当前数组是否同符号
if (nums[0] > 0 || nums[nums.length - 1] < 0) return [];

var len = nums.length, buffer = [], mapping = {}, l, r, result, arr, str;
for (var i = 0; i < len; i++) {
    // // 判断后续数值是否同符号
    if (nums[i] > 0) return buffer;
    
    // 左探针
    l = i + 1;
    // 右探针
    r = len - 1;
    
    while (l < r) {
      result = nums[i] + nums[l] + nums[r];
    
      if (result === 0) {
        arr = [nums[i], nums[l], nums[r]];
        str = arr.toString();
    
        if (!mapping[str]) {
          mapping[str] = true;
          buffer.push(arr);
        }
    
        l++;
        r--;
      } else if (result > 0) {
        // 结果集大于0则向小的值方向移动
        r--;
      } else {
        // 结果集小于0则向大的值方向移动
        l++;
      }
    }
}

return buffer;
  • 小结

关于sort排序的坑:没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。
双向指针方式检索效率更高,复杂度低

2. 下一个排列

  • 题目
//实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
//
// 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
//
// 必须原地修改,只允许使用额外常数空间。
//
// 以下是一些例子,输入位于左侧列,其相应输出位于右侧列。
//1,2,3 → 1,3,2
//3,2,1 → 1,2,3
//1,1,5 → 1,5,1
//1,3,2 → 2,1,3
//2,3,1 → 3,1,2
//5,4,7,5,3,2 → 5,5,2,3,4,7
// Related Topics 数组


//leetcode submit region begin(Prohibit modification and deletion)
/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var nextPermutation = function (nums) {

};

  • 思路
    • 逆向寻找 nums[i-1] > nums[i] 的特征索引;
    • 定位到索引为 i-1 的起始点,再逆向查找最后位符合 nums[i-1] > nums[j] 特征索引,这里交换索引;
    • 交换值后,再以特征索引 i-1 之后的数据进行从小到大排序,即从索引为 i 开始;
    • 排序,这里有一个逻辑点,再经过上述检索后会发现,其实 i 之后的数据其实是从大到小的规则,这里其实只需要逆序 i 起始的值即可;
var nextPermutation = function (nums) {
  // 常规判空
  if (!nums || !nums.length) return [];

  // 索引值交换
  const swap = (i, j, nums) => {
    var temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
  };

  var i = len - 1, j, len = nums.length;
  // 逆序寻找特征索引,即存在 nums[i-1] > nums[i],即索引为 i-1
  while (i > 0 && nums[i - 1] >= nums[i]) {
    i--;
  }

  if (i > 0) {
    // 检索到特征值索引

    j = len - 1;
    // 逆序检索 i-1 之后的最后一位符合特征值
    while (j >= 0 && nums[i - 1] >= nums[j]) {
      j--;
    }

    // 定位到索引,即交换标记值
    swap(i - 1, j, nums);
  }

  // 从索引处开始从小到大排序
  j = len - 1;
  // 从索引处向后的数据正好是从大向小的顺序,故这里只需逆序即可
  while (i < j) {
    swap(i, j, nums);
    i++;
    j--;
  }

  return nums;
}
  • 提炼:reverse 逆序交换算法
function reverse(start, nums) {
    var end = nums.length; 
    while (start < end) {
      swap(start, end, nums);
      start++;
      end--;
    }
}
  • 复杂度分析

时间复杂度:O(n),在最坏的情况下,只需要对整个数组进行两次扫描。
空间复杂度:O(1),没有使用额外的空间,原地替换足以做到。

  • 小结

指针检索极为便捷;
此题映射出数值大小排列的隐含规则,很有象征性;

3. 两数之和

  • 题目
// 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
//
// 你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
//
// 示例:
//
// 给定 nums = [2, 7, 11, 15], target = 9
//
// 因为 nums[0] + nums[1] = 2 + 7 = 9
// 所以返回 [0, 1]
//
// Related Topics 数组 哈希表


//leetcode submit region begin(Prohibit modification and deletion)
/**
 * 思路,遍历,计算 target - nums[idx] 答案存在的索引值,找到答案即退出循环
 *
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function (nums, target) {
};
  • 思路1:一次遍历,通过 lastIndexOf 检索结果索引定位,运行时间 168ms,内存消耗34.6M
var twoSum = function (nums, target) {
  // tip1: target 可能为0
  if (!nums.length) return [];

  var _resultIdx;
  for (var i = 0; i < nums.length; i++) {
    // tip2: 两个数字可能相同,需要从后进行索引,并且需要索引比较异同
    _resultIdx = nums.lastIndexOf(target - nums[i]);
    if (_resultIdx > -1 && _resultIdx !== i) {
       buffer.push(i, _resultIdx);
       break;
    }
  }

  return [];
};
  • 思路2:2次循环查找,运行时间 108ms,内存消耗35M
var twoSum = function (nums, target) {
  // tip1: target 可能为0
  if (!nums.length) return [];

  for (var i = 0; i < nums.length; i++) {
     // tip3: lastIndexOf or indexOf 方法比循环运行更慢,但是循环内存消耗更多,
     for (var j = i + 1; j < nums.length; j++) {
       if (target - nums[i] === nums[j]) {
         return [i, j];
       }
     }
  }

  return [];
};
  • 思路3:对象映射,一次循环检索,运行时间 68ms,内存消耗35.6M
var twoSum = function (nums, target) {
  // tip1: target 可能为0
  if (!nums.length) return [];

  var map = {};
  for (var i = 0; i < nums.length; i++) {
    // tip4 采用 map 映射的模式检索值
    var it = nums[i];
    var result = target - it;
    if (map[result] !== undefined) {
      return [map[result], i]
    }

    map[it] === undefined && (map[it] = i);
  }

  return [];
};
  • 小结

lastIndexOf or indexOf 方法比循环运行更慢,但是循环内存消耗更多;
循环消耗性能和内存,故一次循环采用 map 映射值与索引反向定位方式最优;

4. 删除排序数组中的重复项

  • 题目
//给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
//
// 不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
//
//
//
// 示例 1:
//
// 给定数组 nums = [1,1,2],
//
//函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
//
//你不需要考虑数组中超出新长度后面的元素。
//
// 示例 2:
//
// 给定 nums = [0,0,1,1,1,2,2,3,3,4],
//
//函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
//
//你不需要考虑数组中超出新长度后面的元素。
//
//
//
//
// 说明:
//
// 为什么返回数值是整数,但输出的答案是数组呢?
//
// 请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
//
// 你可以想象内部操作如下:
//
// // nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
//int len = removeDuplicates(nums);
//
//// 在函数里修改输入数组对于调用者是可见的。
//// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
//for (int i = 0; i < len; i++) {
//    print(nums[i]);
//}
//
// Related Topics 数组 双指针


//leetcode submit region begin(Prohibit modification and deletion)
/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function (nums) {
  
};

//leetcode submit region end(Prohibit modification and deletion)
  • 思路:遍历,使用对象缓存之前值的映射,然后校验是否已存在,则删除
var removeDuplicates = function (nums) {
  // 判空 好习惯
  if (!nums || !nums.length) return 0;

  var mapping = {};
  for (var i = 0; i < nums.length; i++) {
    // 校验值是否存在
    if (mapping[nums[i]]) {
      // 删除原数组数据
      nums.splice(i, 1);
      // 索引减 1
      i--;
    }
    // 缓存值的映射
    mapping[nums[i]] = true;
  }

  return nums.length;
};
  • 小结

对象值映射往往便于查找

动态规划

1. 最长回文子串

  • 题目
//给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。 
//
// 示例 1: 
//
// 输入: "babad"
//输出: "bab"
//注意: "aba" 也是一个有效答案。
// 
//
// 示例 2: 
//
// 输入: "cbbd"
//输出: "bb"
// 
// Related Topics 字符串 动态规划


//leetcode submit region begin(Prohibit modification and deletion)
/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {

};
//leetcode submit region end(Prohibit modification and deletion)
  • 回文算法

首先需要解决回文字符串的判断算法

// 校验是否回文
function isLoopString(a, start, end) {
  var i = start, j = end;
  // 双指针索引方式对称比较
  while (i >= 0 && j < a.length && i < j && a[i] === a[j]) {
    i++;
    j--;
  }

  // 偶数位索引为 0;
  // 奇数位索引差为1
  return (end - start + 1) % 2 !== 0 ? i - j === 0 : i - j === 1;
}
  • 思路1: 粗暴循环,穷举判断,复杂度较高,Time Limit Exceeded
var longestPalindrome = function(s) {
  // 1位
  if (s.length === 1) return s;
  // 2位
  if (s.length === 2) return s[0] === s[1] ? s : s[0];

  // 粗暴解法, 确定复杂度太高,耗时,Time Limit Exceeded
  var maxStr = s[0];
  for (var i = 0; i < s.length - 1; i++) {
    for (var j = s.length - 1; j > i; j--) {
      if (isLoopString(s, i, j)) {
        maxStr = !maxStr || maxStr.length < j - i + 1 ? s.substring(i, j + 1) : maxStr;
        continue;
      }
    }
  }
  
  return maxStr;
};
  • 思路2:指针索引方式比较

起始索引 A 指针从头向后定位 A++
终点索引 B 指针从后向前缩进 B--,不匹配回文,则起始索引 A++ 向后, B = end
当前回文串长度若大于等于索引区字符串 B - A + 1 则当前回文串即最长串,跳出循环,程序结束

var longestPalindrome = function(s) {
    var pos = 0, // 起始指针索引
      end = s.length - 1, // 终点指针索引
      maxStr = s[0];
  
    // 双指针移动
    while (pos <= end) {
      // 从后向前进行移动,最长的如果
      if (isLoopString(s, pos, end)) {
        // 找到当前索引最长子串
        maxStr = maxStr.length < end - pos + 1 ? s.substring(pos, end + 1) : maxStr;
  
        // 当前串为回文串即结束循环
        if (maxStr.length === s.length) return maxStr;
  
        // 当前索引最长串已找到,向后移动指针索引
        pos++;
        end = s.length - 1;
  
        // 当前最长串长度比后面索引起始区长度长,则当前回文串为最长,跳出循环
        if (maxStr.length >= end - pos + 1) return maxStr;
      } else {
        end--;
      }
    }
  
    return maxStr;
};
  • 小结

检索类别惯性思维往往是多层循环,而循环复杂度反而较高,故尽量避免此类方式;
指针索引方式效率则较高,复杂度低

2. 爬楼梯

  • 题目
//假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
//
// 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
//
// 注意:给定 n 是一个正整数。
//
// 示例 1:
//
// 输入: 2
//输出: 2
//解释: 有两种方法可以爬到楼顶。
//1.  1 阶 + 1 阶
//2.  2 阶
//
// 示例 2:
//
// 输入: 3
//输出: 3
//解释: 有三种方法可以爬到楼顶。
//1.  1 阶 + 1 阶 + 1 阶
//2.  1 阶 + 2 阶
//3.  2 阶 + 1 阶
//
// Related Topics 动态规划

//leetcode submit region begin(Prohibit modification and deletion)
/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function (n) {
  var buffer = {};

  // 方法1 递归法
  function calcute(i, n) {
    if (i > n) return 0;
    if (i == n) return 1;

    // 校验缓存
    if (buffer[i] > 0) return buffer[i];
    // 缓存
    buffer[i] = calcute(i + 1, n) + calcute(i + 2, n);
    return buffer[i];
  }

  return calcute(0, n);
};
//eetcode submit region end(Prohibit modification and deletion)
  • 思路1: 递归穷举法
// 时间复杂度:O(2^n),树形递归的大小为 2^n
// 空间复杂度:O(n),递归树的深度可以达到 n 
var climbStairs = function (n) {
  function calcute(i, n) {
    if (i > n) return 0;
    if (i == n) return 1;

    return calcute(i + 1, n) + calcute(i + 2, n);
  }

  return calcute(0, n);
};
  • 思路2: 递归缓存法,减少递归计算
// 时间复杂度:O(n),树形递归的大小可以达到 n。
// 空间复杂度:O(n),递归树的深度可以达到 n。
var climbStairs = function (n) {
  var buffer = {};

  function calcute(i, n) {
    if (i > n) return 0;
    if (i == n) return 1;

    // 校验缓存
    if (buffer[i] > 0) return buffer[i];
    // 缓存
    buffer[i] = calcute(i + 1, n) + calcute(i + 2, n);
    return buffer[i];
  }

  return calcute(0, n);
};
  • 思路3: 动态规划
// 时间复杂度:O(n),单循环到 n 。
// 空间复杂度:O(n),dp 数组用了 n 的空间。
var climbStairs = function (n) {
  // 第 ii 阶可以由以下两种方法得到:
  //
  // 在第 (i-1) 阶后向上爬一阶。
  //
  // 在第 (i-2) 阶后向上爬 22 阶。
  //
  // 所以到达第 i 阶的方法总数就是到第 (i-1) 阶和第 (i−2) 阶的方法数之和。

  // dp[i] = dp[i−1] + dp[i−2]
  if (n == 0) return 0;

  var dp = [], i = 0;

  // i = 1、2
  dp[1] = 1;
  dp[2] = 2;
  for (var i = 3; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }

  return dp[n];
};
  • 思路4: 斐波那契数
// 时间复杂度:O(n),单循环到 n,需要计算第 n 个斐波那契数。
// 空间复杂度:O(1),使用常量级空间。
var climbStairs = function (n) {
// 在上述方法中,我们使用 dp 数组,其中 dp[i]=dp[i-1]+dp[i-2]。可以很容易通过分析得出 dp[i] 其实就是第 i 个斐波那契数。
//
// Fib(n)=Fib(n-1)+Fib(n-2)
//
// 现在我们必须找出以 1 和 2 作为第一项和第二项的斐波那契数列中的第 n 个数,也就是说 Fib(1)=1 且 Fib(2)=2

  if (n <= 2) return n;

  var first = 1, second = 2;
  for (var i = 3; i <= n; i++) {
    // 常量级空间,空间复杂度低
    var third = first + second;
    first = second;
    second = third;
  }

  return second;
}
  • 小结

12组合常规处理递归法,然后递归消耗时间2^n和空间n
基于递归优化,可增加递归结果缓存的做法,优化时间复杂度为 n;
dp[i] = dp[i-1] + dp[i-2] 循环递推,dp[1] = 1dp[2] = 2,单次循环,时间空间均为 n;
斐波那契数 使用常量级别空间大大减少空间复杂度为1,更像变量接力:third = first + second; first = second; second = third; ...

3. 解码方法

  • 题目
//一条包含字母 A-Z 的消息通过以下方式进行了编码:
//
// 'A' -> 1
//'B' -> 2
//...
//'Z' -> 26
//
//
// 给定一个只包含数字的非空字符串,请计算解码方法的总数。
//
// 示例 1:
//
// 输入: "12"
//输出: 2
//解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。
//
//
// 示例 2:
//
// 输入: "226"
//输出: 3
//解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
//
// Related Topics 字符串 动态规划


//leetcode submit region begin(Prohibit modification and deletion)
/**
 * @param {string} s
 * @return {number}
 */
var numDecodings = function (s) {
}
//leetcode submit region end(Prohibit modification and deletion)
  • 思路: 同爬楼梯中算法一致,只不过增加了楼层的判断,某种情况只能1某种情况只能2
var numDecodings = function(s){
  if (!s || s <= 0 || s[0] === '0') return 0;
  if (s.length === 1 && s > 0) return 1;

  var first = 1, second = 1;
  for (var i = 1; i < s.length; i++) {
    var temp = second;

    if (s[i] === '0') {
      // 当 s[i] == '0'时候,若 s[i - 1] 为 1 or 2 时,则 dp[i] = dp[i - 2], 即不计数;
      if (s[i - 1] === '1' || s[i - 1] === '2')
        second = first;
      else
        // 否则为 0
        return 0;
    } else if (s[i - 1] === '1' || (s[i - 1] === '2' && s[i] >= '1' && s[i] <= '6')) {
      // 当 s[i - 1] = 1 时,dp[i] = dp[i-1] + dp[i-2], s[i] 和 s[i-1] 分开译码为 dp[i-1]; 合并 则为 dp[i-2]
      // 当 s[i - 1] = 2 并且大于等于1小于等于6 时,同上
      second = second + first;
    }

    first = temp;
  }

  return second;
}
  • 复杂度分析

时间复杂度,O(n)
空间复杂度,O(1)

4. 最大子序和

  • 题目
//给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
//
// 示例:
//
// 输入: [-2,1,-3,4,-1,2,1,-5,4],
//输出: 6
//解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
//
//
// 进阶:
//
// 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
// Related Topics 数组 分治算法 动态规划

//leetcode submit region begin(Prohibit modification and deletion)
/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function (nums) {
};

//leetcode submit region end(Prohibit modification and deletion)
  • 思路: 贪心算法

使用单个数组作为输入来查找最大(或最小)元素(或总和)的问题,贪心算法是可以在线性时间解决的方法之一。
每一步都选择最佳方案,到最后就是全局最优的方案。\

复杂度分析
时间复杂度:O(N),只遍历一次数组。
空间复杂度:O(1),只使用了常数空间。

var maxSubArray = function (nums) {
  if (!nums || !nums.length) return 0;

  if (nums.length === 1) return nums[0];

  var max = current = nums[0];
  for (var i = 1; i < nums.length; i++) {
    // 计算当前位置的最大和,否则舍弃从新计算
    current = Math.max(nums[i], current + nums[i]);
    // 记录最大和
    max = Math.max(current, max);
  }

  return max;
};
  • 思路2: 动态规划

在整个数组或在固定大小的滑动窗口中找到总和或最大值或最小值的问题可以通过动态规划(DP)在线性时间内解决。\

有两种标准 DP 方法适用于数组:
常数空间,沿数组移动并在原数组修改。
线性空间,首先沿 left->right 方向移动,然后再沿 right->left 方向移动。 合并结果。\

复杂度分析
时间复杂度:O(N),只遍历一次数组。
空间复杂度:O(1),只使用了常数空间。

var maxSubArray = function (nums) {
  if (!nums || !nums.length) return 0;

  if (nums.length === 1) return nums[0];

  var max = nums[0];
  for (var i = 1; i < nums.length; i++) {
      
     // 若前一个数大于0,则进行累加,并替换当前值
     if (nums[i - 1] > 0)
          nums[i] += nums[i - 1];
    
     // 取最大值
     max = Math.max(nums[i], max);
  }
  
  return max;
};

字符串

1. 有效的括号

  • 题目
//给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
//
// 有效字符串需满足:
//
//
// 左括号必须用相同类型的右括号闭合。
// 左括号必须以正确的顺序闭合。
//
//
// 注意空字符串可被认为是有效字符串。
//
// 示例 1:
//
// 输入: "()"
//输出: true
//
//
// 示例 2:
//
// 输入: "()[]{}"
//输出: true
//
//
// 示例 3:
//
// 输入: "(]"
//输出: false
//
//
// 示例 4:
//
// 输入: "([)]"
//输出: false
//
//
// 示例 5:
//
// 输入: "{[]}"
//输出: true
// Related Topics 栈 字符串


//leetcode submit region begin(Prohibit modification and deletion)
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
};
//leetcode submit region end(Prohibit modification and deletion)
  • 思路

空字符串为有效
奇数长度一定无效
左入栈,右出栈,最终栈空间为0则有效,否则无效

var isValid = function (s) {
  if (s === undefined) return false;

  var len = s.length;

  if (len === 0) return true;

  // 奇数位则为 false
  if (len % 2 === 1) return false;

  var mapping = {
    '{': '}',
    '[': ']',
    '(': ')'
  };

  var buffer = [], leftKeys = Object.keys(mapping);
  for (var i = 0; i < len; i++) {
    if (leftKeys.includes(s[i])) {
      buffer.push(s[i]);
    } else {
      if (i > 0 && buffer.length > 0 && mapping[buffer[buffer.length - 1]] === s[i]) {
        buffer.pop();
      } else {
        buffer.push(s[i])
      }
    }
  }

  return buffer.length === 0;
};

1. 相同的树

  • 题目
//给定两个二叉树,编写一个函数来检验它们是否相同。
//
// 如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
//
// 示例 1:
//
// 输入:       1         1
//          / \       / \
//         2   3     2   3
//
//        [1,2,3],   [1,2,3]
//
//输出: true
//
// 示例 2:
//
// 输入:      1          1
//          /           \
//         2             2
//
//        [1,2],     [1,null,2]
//
//输出: false
//
//
// 示例 3:
//
// 输入:       1         1
//          / \       / \
//         2   1     1   2
//
//        [1,2,1],   [1,1,2]
//
//输出: false
//
// Related Topics 树 深度优先搜索


//leetcode submit region begin(Prohibit modification and deletion)
/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {boolean}
 */
var isSameTree = function (p, q) {
};
//leetcode submit region end(Prohibit modification and deletion)
  • 思路

遍历树所有节点
先序遍历、中序遍历、后续遍历等方式

var isSameTree = function (p, q) {
  
  // 先序遍历获取树节点
  var getNodes = function (node, buffer) {
    if (!node) return buffer;

    buffer.push(node.val);

    if (node.left) {
      getNodes(node.left, buffer);
    } else {
      buffer.push(null);
    }

    if (node.right) {
      getNodes(node.right, buffer);
    } else {
      buffer.push(null);
    }

    return buffer;
  };

  var buffer1 = [], buffer2 = [];
  getNodes(p, buffer1);
  getNodes(q, buffer2);

  return buffer1.join(',') === buffer2.join(',');
};
  • 复杂度分析

时间复杂度 : O(N),其中 N 是树的结点数,因为每个结点都访问一次。
空间复杂度 : 最优情况(完全平衡二叉树)时为 O(log(N)),最坏情况下(完全不平衡二叉树)时为 O(N),用于维护递归栈。

  • 小结

树的常规操作需要掌握树的遍历,先序遍历、中序遍历、后续遍历、层次遍历等方式

总结

这个月收获丰富,下个月继续坚持,加油

坚持