Skip to content

Latest commit

 

History

History
1203 lines (1045 loc) · 28.1 KB

File metadata and controls

1203 lines (1045 loc) · 28.1 KB

1. 两数之和

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

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
  const len = nums.length
  const map = {}
  for (let i = 0; i < len; i++) {
    if (map[target - nums[i]] !== void 0) {
      return [map[target - nums[i]], i]
    } else {
      map[nums[i]] = i
    }
  }
  return []
};

2. 两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

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

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
  return add(l1, l2, 0)
};
function add (l1, l2, carry) {
  if (!l1 && !l2 && !carry) return null
  const sum = ((l1 && l1.val) || 0) + ((l2 && l2.val) || 0) + (carry || 0)
  const node = new ListNode(sum % 10)
  node.next = add(l1 && l1.next, l2 && l2.next, Math.floor(sum / 10))
  return node
}

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

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

示例 1:

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

滑动窗口

参考: 无重复字符的最长子串

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
  let map = {}
  let maxLen = 0
  let len = s.length
  let char = ''
  let left = 0
  for (let i = 0; i < len; i++) {
    char = s[i]
    if (map[char] >= left) {
      left = map[char] + 1
    }
    map[char] = i
    maxLen = Math.max(maxLen, i - left + 1)
  }
  return maxLen
};

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

给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。

请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。

你可以假设 nums1 和 nums2 不会同时为空。

示例 1:

nums1 = [1, 3]
nums2 = [2]

则中位数是 2.0

示例 2:

nums1 = [1, 2]
nums2 = [3, 4]

则中位数是 (2 + 3)/2 = 2.5

分治

参考: 【分步详解】两个有序数组中的中位数和Top K问题

var findMedianSortedArrays = function(nums1, nums2) {
  let n = nums1.length
  let m = nums2.length
  if (n > m) {
    return findMedianSortedArrays(nums2, nums1)
  }
  let L1, L2, R1, R2, c1, c2, lo = 0, hi = 2 * n
  while (lo <= hi) {
    c1 = Math.floor((lo + hi) / 2)
    c2 = m + n - c1
    L1 = (c1 === 0) ? Number.MIN_SAFE_INTEGER : nums1[Math.floor((c1 - 1) / 2)]
    R1 = (c1 === 2 * n) ? Number.MAX_SAFE_INTEGER : nums1[Math.floor(c1 / 2)]
    L2 = (c2 === 0) ? Number.MIN_SAFE_INTEGER : nums2[Math.floor((c2 - 1) / 2)]
    R2 = (c2 === 2 * m) ? Number.MAX_SAFE_INTEGER : nums2[Math.floor(c2 / 2)]

    if (L1 > R2) {
      hi = c1 - 1
    } else if (L2 > R1) {
      lo = c1 + 1
    } else {
      break
    }
  }
  return (Math.max(L1, L2) + Math.min(R1, R2)) / 2
}

5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:

输入: "cbbd"
输出: "bb"

暴力破解

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

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
  let p = []
  let range = 1
  let m = 0
  let maxId = 0
  let id = 0
  let tempStr = ''
  let maxStr = ''
	// 将字符串转化为奇数长度获取到新的字符串
  var newStr = '#' + s.split('').join('#') + '#'
  var newLen = newStr.length
	// 时间复杂度为O(n),空间复杂度为O(1)获取到所有的子回文的长度值组成的数组
  for (let i = 0; i < newLen; i++) {
    // maxId表示当前边界值最大的回文子串的边界值
    p[i] = maxId > i ? Math.min(p[2 * id - i], maxId - i) : 1
    range = p[i]
    tempStr = newStr[i]
    while (range-- > 1) {
      m = (tempStr.length - 1) / 2
      tempStr = tempStr.slice(0, m).concat(newStr[i - range], tempStr.slice(m, m + 1), newStr[i + range], tempStr.slice(m + 1))
    }
    // 超出其半径的位置再做额外判断
    while ((newStr[i + p[i]] === newStr[i - p[i]]) && newStr[i + p[i]]){
      tempStr = newStr[i + p[i]] + tempStr + newStr[i - p[i]]
      p[i]++
    }
    // 获取到边界最大的回文子串的中心位置以及边界值,以保证后续迭代可以做以上快捷处理
    if (i + p[i] > maxId) {
      id = i
      maxId = id + p[i]
    }
    if (tempStr.length > maxStr.length) {
      maxStr = tempStr
    }
    tempStr = ''
  }
	return maxStr.replace(/#/g, '')
};

Manacher算法

参考文章:

时间复杂度为 O(n)

var longestPalindrome = function(s) {
  // p[i] 表示以下标为 i 的字符为中心字符的回文子串的回文半径长度(相对于新串)
  let p = []
  // 表示当前边界值最大的回文子串的边界值(即当前已经取得的所有回文子串中所包含的所有字符中,最大的下标)(相对于新串)
  let maxIndex = 0
  // 边界最大的回文子串的中心位置(相对于新串)
  let id = 0
  // 最大的回文子串的中心位置的下标(相对于新串)
  let middleIndex = 0
  // 最大的回文子串的长度(相对于新串)
  let maxLen = 0
	// 将字符串转化为奇数长度获取到新的字符串
  let newStr = '#' + s.split('').join('#') + '#'
  const newLen = newStr.length
	// 时间复杂度为O(n),空间复杂度为O(1)获取到所有的子回文的长度值组成的数组
  for (let i = 0; i < newLen; i++) {
    p[i] = maxIndex > i ? Math.min(p[2 * id - i], maxIndex - i) : 1
    // 超出其半径的位置再做额外判断
    while ((newStr[i + p[i]] === newStr[i - p[i]]) && newStr[i + p[i]]){
      p[i]++
    }
    // 获取到边界最大的回文子串的中心位置以及边界值,以保证后续迭代可以做以上快捷处理
    if (i + p[i] > maxIndex) {
      id = i
      maxIndex = id + p[i]
    }
    if (p[i] > maxLen) {
      middleIndex = i
      maxLen = p[i]
    }
  }
  return s.substr((middleIndex + 1 - maxLen) / 2, maxLen - 1)
};

6. Z 字形变换

将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 LEETCODEISHIRING 行数为 3 时,排列如下:

L   C   I   R
E T O E S I I G
E   D   H   N

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:LCIRETOESIIGEDHN

请你实现这个将字符串进行指定行数变换的函数:

string convert(string s, int numRows);

示例 1:

输入: s = "LEETCODEISHIRING", numRows = 3
输出: "LCIRETOESIIGEDHN"

示例 2:

输入: s = "LEETCODEISHIRING", numRows = 4
输出: "LDREOEIIECIHNTSG"

解释:

L     D     R
E   O E   I I
E C   I H   N
T     S     G

参考文章:

暴力破解

/**
 * @param {string} s
 * @param {number} numRows
 * @return {string}
 */
var convert = function(s, numRows) {
  const len = s.length
  const markNum = 2 * numRows - 2
  let res = ''
  if (markNum === 0) return s
  for (let i = 0; i < numRows; i++) {
    for (let j = 0; j < len; j++) {
      if ((i + j) % markNum ===0 || (j - i) % markNum === 0) {
        res += s[j]
      }
    }
  }
  return res
}

步进优化

/**
 * @param {string} s
 * @param {number} numRows
 * @return {string}
 */
var convert = function(s, numRows) {
  const len = s.length
  const markNum = 2 * numRows - 2
  let res = ''
  let mIndex = 0
  if (markNum === 0) return s
  for (let i = 0; i < numRows; i++) {
    for (let j = i; j < len; j+=markNum) {
      res += s[j]
      mIndex = j + markNum - 2 * (j % markNum)
      if (mIndex !== j && mIndex < len && mIndex % markNum) {
        res += s[mIndex]
      }
    }
  }
  return res
};

7. 整数反转

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。

示例 1:

输入: 123
输出: 321

示例 2:

输入: -123
输出: -321

示例 3:

输入: 120
输出: 21

注意:

假设我们的环境只能存储得下 32 位的有符号整数,则其数值范围为 [−231, 231 − 1]。请根据这个假设,如果反转后整数溢出那么就返回 0

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function(x) {
    let res = Array.from(x+'');
     
    if(res[0] !== '-'){
        let num = parseInt(res.reverse().join(''),10);
         if( num> Math.pow(2,31) -1){
             return 0
         }
       return num; 
    }else{
        res.reverse().pop();
      let num = parseInt(res.join(''),10);
        if( num> Math.pow(2,31) -1){
             return 0
         }
        return  '-' + num; 
    }
};

8. 字符串转换整数 (atoi)

请你来实现一个 atoi 函数,使其能将字符串转换成整数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0

说明:

假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,qing返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。

示例 1:

输入: "42"
输出: 42

示例 2:

输入: "   -42"
输出: -42
解释: 第一个非空白字符为 '-', 它是一个负号。
     我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。

示例 3:

输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。

示例 4:

输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。
     因此无法执行有效的转换。

示例 5:

输入: "-91283472332"
输出: -2147483648
解释: 数字 "-91283472332" 超过 32 位有符号整数范围。 
     因此返回 INT_MIN (−231) 。
/**
 * @param {string} str
 * @return {number}
 */
const INT_MAX=Math.pow(2,31)-1;
const INT_MIN=Math.pow(-2,31);
var myAtoi = function(str) {
  let newStr = str.trim()
  if (!/[\d|+|-]/.test(newStr[0])) return 0
  const v = newStr.match(/^[\d|+|-]\d*/)[0]
  if (v.length === 1 && (v === '+' || v === '-')) return 0
  if (v<INT_MIN) {
      return INT_MIN
  } else if (v>INT_MAX) {
      return INT_MAX
  }
  return +v
};

9. 回文数

判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

示例 1:

输入: 121
输出: true

示例 2:

输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。

示例 3:

输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function(x) {
 if (x < 0 || (x % 10 === 0 && x !== 0)) return false
  let reverseNum = 0
  while (x > reverseNum) {
    reverseNum = reverseNum * 10 + x % 10
    x = Math.floor(x/10)
  }
  return x === reverseNum || x === Math.floor(reverseNum / 10)
};

10. 正则表达式匹配

给定一个字符串 (s) 和一个字符模式 (p)。实现支持 .* 的正则表达式匹配。

. 匹配任意单个字符。 * 匹配零个或多个前面的元素。 匹配应该覆盖整个字符串 (s) ,而不是部分字符串。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。

示例 1:

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:
s = "aa"
p = "a*"
输出: true
解释: '*' 代表可匹配零个或多个前面的元素, 即可以匹配 'a' 。因此, 重复 'a' 一次, 字符串可变为 "aa"。

示例 3:

输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。

示例 4:

输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 'c' 可以不被重复, 'a' 可以被重复一次。因此可以匹配字符串 "aab"。

示例 5:

输入:
s = "mississippi"
p = "mis*is*p*."
输出: false

递归

参考文章:

var isMatch = function(s, p) {
  if (p === '') return s === ''
  const firstMatch = s !== '' && (s[0] === p[0] || p[0] === '.')
  if (p.length >= 2 && p[1] === '*') {
    return (firstMatch && isMatch(s.substr(1), p)) || isMatch(s, p.substr(2))
  } else {
    return firstMatch && isMatch(s.substr(1), p.substr(1))
  }
};

11. 盛最多水的容器

给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai)(i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器,且 n 的值至少为 2

示例:

输入: [1,8,6,2,5,4,8,3,7]
输出: 49

翻译成数学题,即,垂直的两条线段将会与坐标轴构成一个矩形区域,较短线段的长度将会作为矩形区域的宽度,两线间距将会作为矩形区域的长度,而我们必须最大化该矩形区域的面积。

暴力破解

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function(height) {
  let max = 0
  const len = height.length
  for (let i = 0; i < len; i++) {
    for (let j = 0; j < len; j++) {
      max = Math.max(max, Math.min(height[i], height[j]) * (j - i))
    }
  }
  return max
};

双指针法

参考文章

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

12. 整数转罗马数字

罗马数字包含以下七种字符: I, V, X, L,C,DM

字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

例如, 罗马数字 2 写做 II ,即为两个并列的 112 写做 XII ,即为 X + II27 写做 XXVII, 即为 XX + V + II

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给定一个整数,将其转为罗马数字。输入确保在 1 到 3999 的范围内。

示例 1:

输入: 3
输出: "III"

示例 2:

输入: 4
输出: "IV"

示例 3:

输入: 9
输出: "IX"

示例 4:

输入: 58
输出: "LVIII"
解释: L = 50, V = 5, III = 3.

示例 5:

输入: 1994
输出: "MCMXCIV"
解释: M = 1000, CM = 900, XC = 90, IV = 4.
/**
 * @param {number} num
 * @return {string}
 */
var intToRoman = function(num) {
  const values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
  const reps = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']
  const len = values.length
  let res = ''
  for (let i = 0; i < len; i++) {
    while (num >= values[i]) {
      num -= values[i]
      res += reps[i]
    }
  }
  return res
}

13. 罗马数字转整数

与上题 12. 整数转罗马数字相反的操作

/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function(s) {
  const map = {
    'M': 1000,
    'CM': 900,
    'D': 500,
    'CD': 400,
    'C': 100,
    'XC': 90,
    'L': 50,
    'XL': 40,
    'X': 10,
    'IX': 9,
    'V': 5,
    'IV': 4,
    'I': 1
  }
  let res = 0
  while (s) {
    if (map[s.slice(0, 2)]) {
      res += map[s.slice(0, 2)]
      s= s.slice(2)
    } else {
      res += map[s.slice(0, 1)]
      s = s.slice(1)
    }
  }
  return res
};

14. 最长公共前缀

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

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

示例 1:

输入: ["flower","flow","flight"]
输出: "fl"

示例 2:

输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。

说明:

所有输入只包含小写字母 a-z 。

水平扫描法

参考文章:

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
  const len = strs.length
  if (!strs || len === 0) return ''
  let pre = strs[0]
  for (let i = 1; i < len; i++) {
    while (strs[i].indexOf(pre) !== 0) {
      pre = pre.slice(0, pre.length - 1)
      if (pre === '') return ''
    }
  }
  return pre || ''
};

水平扫描

var longestCommonPrefix = function(strs) {
  const len = strs.length
  if (!strs || len === 0) return ''
  let pre = strs[0]
  const preLen = pre.length
  let char = ''
  for (let i = 0; i < preLen; i++) {
    char = pre[i]
    for (let j = 1; j < len; j++) {
      if (strs[j][i] !== char) {
        return pre.slice(0, i)
      }
    }
  }
  return pre
};

分治法

var longestCommonPrefix = function(strs) {
  const len = strs.length
  if (!strs || len === 0) return ''
  return divideArr(strs, 0, strs.length - 1)
};

function divideArr (strs, left, right) {
  if (left === right) return strs[left]
  const mid = Math.floor((left + right) / 2)
  const lopLeft = divideArr(strs, left, mid)
  const lopRight = divideArr(strs, mid + 1, right)
  return compare(lopLeft, lopRight)
}

function compare (left, right) {
  const min = Math.min(left.length, right.length)
  for (let i = 0; i < min; i++) {
    if (left[i] !== right[i]) {
      return left.slice(0, i)
    }
  }
  return left.slice(0, min)
}

15. 三数之和

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

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

例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4]

满足要求的三元组集合为:

[
  [-1, 0, 1],
  [-1, -1, 2]
]

参考文章:

三数之和)

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
  let arrs = []
  const len = nums.length
  let target = 0
  nums = nums.sort((a, b) => a - b)
  for (let i = 0; i < len; i++) {
    if (nums[i] > 0) break
    if (nums[i] === nums[i - 1]) continue
    target = -nums[i]
    let j = i + 1, k = len - 1
    while (j < k) {
      if (nums[j] + nums[k] === target) {
        arrs.push([nums[i], nums[j], nums[k]])
        while (nums[j] === nums[j + 1]) {
          j = j + 1
        }
        while (nums[k] === nums[k - 1]) {
          k = k - 1
        }
        j = j + 1
        k = k - 1
      } else if (nums[j] + nums[k] < target) {
        j = j + 1
      } else {
        k = k - 1
      }
    }
  }
  return arrs
};

16. 最接近的三数之和

给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

例如,给定数组 nums = [-1,2,1,-4], 和 target = 1.

target 最接近的三个数的和为 2. (-1 + 2 + 1 = 2).

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var threeSumClosest = function(nums, target) {
  if (!nums || nums.length < 2) return 0
  const len = nums.length
  if (len === 3) return nums[0] + nums[1] + nums[2]
  let arrs = [], sum = 0
  let closest = Number.MAX_SAFE_INTEGER
  nums = nums.sort((a, b) => a - b)
  for (let i = 0; i < len; i++) {
    let j = i + 1, k = len - 1
    while (j < k) {
      const tempSum = nums[i] + nums[j] + nums[k]
      const abs = Math.abs(tempSum - target)
      if (tempSum < target) {
        if (abs < closest) {
          closest = abs
          sum = tempSum
        }
        j++
      } else if(tempSum > target) {
        if (abs < closest) {
          closest = abs
          sum = tempSum
        }
        k--
      } else {
        return target
      }
    }
  }
  return sum
};

17

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

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

img

示例:

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

说明: 尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

const map = {
  2: 'abc',
  3: 'def',
  4: 'ghi',
  5: 'jkl',
  6: 'mno',
  7: 'pqrs',
  8: 'tuv',
  9: 'wxyz'
}

function helper(ans,str,digits) {
  if (digits === '') {
    return ans.push(str)
  }
  const d = digits[0]
  const alphabets = map[d]
  for(let j = 0; j < alphabets.length; j++) {
    const newStr = str + alphabets[j]
    helper(ans, newStr, digits.slice(1))
  }
}

var letterCombinations = function (digits) {
  if (digits === '') return []
  const ans = []
  helper(ans, '', digits)
  return ans
}

18. 四数之和

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,cd ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

注意:

答案中不可以包含重复的四元组。

示例:

给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。

满足要求的四元组集合为:
[
  [-1,  0, 0, 1],
  [-2, -1, 1, 2],
  [-2,  0, 0, 2]
]

双指针法

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[][]}
 */
var fourSum = function(nums, target) {
  let arrs = []
  const len = nums.length
  nums = nums.sort((a, b) => a - b)
  for (let i = 0; i < len; i++) {
    if (nums[i] === nums[i - 1]) continue
    const target1 = target - nums[i]
    for (let j = i + 1; j < len; j++) {
      if (nums[j] === nums[j - 1]) continue
      const target2 = target1 - nums[j]
      let l = j + 1
      let r = len - 1
      while (l < r) {
        if (nums[l] + nums[r] === target2) {
          arrs.push([nums[i], nums[j], nums[l], nums[r]])
          while (nums[l] === nums[l + 1]) l++
          while (nums[r] === nums[r - 1]) r--
          l++;
          r--;
        } else if (nums[l] + nums[r] < target2) {
          l++
        } else {
          r--
        }
      }
    } 
  }
  return arrs
};

n 数之和通用解法

参考链接:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[][]}
 */
var fourSum = function(nums, target) {
    nums.sort((a, b) => a - b)
  let results = []
  findNsum(nums, target, 4, [], results)
  return results
};

function findNsum(nums, target, n, result, results) {
  const len = nums.length
  if (len < n || n < 2) return []
  if (n === 2) {
    let l = 0, r = len - 1
    while (l < r) {
      if (nums[l] + nums[r] === target) {
        results.push(result.concat(nums[l], nums[r]))
        l++
        r--
        while (l < r && nums[l] === nums[l - 1]) l++
        while (l < r && nums[r] === nums[r + 1]) r--
      } else if (nums[l] + nums[r] < target) {
        l++
      } else {
        r--
      }
    }
  } else {
    for (let i = 0; i < len - n + 1; i++) {
      if (target < nums[i] * n || target > nums[len - 1] * n) {
        break
      }
      if (i === 0 || nums[i-1] != nums[i]) {
        findNsum(nums.slice(i + 1), target-nums[i], n-1, result.concat(nums[i]), results)
      }
    }
  }
}

19. 删除链表的倒数第N个节点

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

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5. 说明:

给定的 n 保证是有效的。

两次遍历算法

参考链接:两次遍历算法

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
var removeNthFromEnd = function(head, n) {
  const dummy = new ListNode(0)
  dummy.next = head
  let length = 0
  let first = head
  while (first) {
    length++
    first = first.next
  }
  length -= n
  first = dummy
  while (length > 0) {
    length--
    first = first.next
  }
  first.next = first.next.next
  return dummy.next
};

一次遍历算法

参考链接:一次遍历算法

var removeNthFromEnd = function(head, n) {
  const dummy = new ListNode(0)
  dummy.next = head
  let first = dummy
  let second = dummy
  for (let i = 1; i <= n + 1; i++) {
    first = first.next
  }
  while (first) {
    first = first.next
    second = second.next
  }
  second.next = second.next.next
  return dummy.next
}