前言 如题,打算使用JS从头开始刷leetcode,大致地记录并讲解一下自己的解题思路吧,由于自己的水平原因,所以思路与解法都会比较偏新手向吧,同时不会只关注这一个题的思路,会有一些整体的思路与延伸吧。做这个记录的原因主要是想以新手的角度讲下自己解题遇到的弯路,同时也给自己加深印象吧。开始时可能注释会有些啰嗦,后续会减少不必要的代码解释。打算先写完并理解前150题吧,也并不打算分类的,因为前百题的思路都是经典解题思路,都需要慢慢体会吧。
1、两数之和 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。你可以按任意顺序返回答案。转载自两数之和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const twoSum = (nums, target ) => { let res = []; let map = new Map (); for (let i = 0 ; i < nums.length; i++) { map.set(nums[i], i); } for (let i = 0 ; i < nums.length; i++) { let newTarget = target - nums[i]; if (map.has(newTarget) && (map.get(newTarget) !== i)) { res.push(i, map.get(newTarget)); return res; } } }
2、两数相加 给你两个非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。转载至两数之和
例:输入:l1 = [2,4,3], l2 = [5,6,4];输出:[7,0,8];解释:342 + 465 = 807。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const addTwoNumbers = function (l1, l2 ) { let carry = 0 ; let sum = new ListNode(0 ); let head = sum; while (l1 || l2 || carry) { let value1 = ((l1 === null ) ? 0 : l1.val); let value2 = ((l2 === null ) ? 0 : l2.val); sum.next = new ListNode((value1 + value2 + carry) % 10 ); carry = ((value1 + value2 + carry) >= 10 ? 1 : 0 ); sum = sum.next; if (l1) l1 = l1.next; if (l2) l2 = l2.next; } return head.next; }
一般情况下,除了用链表来存储大数以外,在JS中更常用的是用字符串来保存,因此下面我们写一个字符串类型的大数相加,基本的逻辑和链表是一样的,主要是处理的方式不同,这里我就不再赘述原理了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const add = function add (a, b ) { let m = "" ; let n = "" ; let res = []; let sum = 0 ; if (a.length > b.length){ a = '0' + a; for (let i = b.length; i < a.length; i++){ b = '0' + b; } m = a.split('' ); n = b.split('' ); } else { b = '0' + b; for (let i = a.length; i < b.length; i++){ a = '0' + a; } a = a.split('' ); b = b.split('' ); } for (let i = a.length - 1 ; i >= 0 ; i--) { let k = (parseInt (a[i]) + parseInt (b[i]) + sum) % 10 ; res.unshift(k); if ((parseInt (a[i]) + parseInt (b[i]) + sum) >= 10 ) { if ((m === n) && (i = t)) { res.unshift("1" ); } sum = 1 } else { sum = 0 ; } } let ans = res.join("" ); return ans; }
3、无重复字符的最长子串 题目:给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
例如:输入: s = “abcabcbb”。输出: 3 。解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
思路:像这种子串的问题,最小覆盖子串、字符串的排列、找到字符串中所有字母异位词、无重复字符的最长子串。其实思路基本一致,子串的问题基本都能用滑动窗口的思想来解决。滑动窗口就是双指针的进阶版吧,即维护一个窗口,不断滑动并更新答案。
1、 我们在字符串S
中使用双指针中的左右指针技巧,初始化left = right = 0
,把索引左闭右开区间[left, right)
称为一个「窗口」。
2、 我们先不断地增加right
指针扩大窗口[left, right)
,直到窗口中的字符串符合要求(包含了T
中的所有字符)。
3、 此时,我们停止增加right
,转而不断增加left
指针缩小窗口[left, right)
,直到窗口中的字符串不再符合要求(不包含T
中的所有字符了)。同时,每次增加left
,我们都要更新一轮结果。
4、 重复第 2 和第 3 步,直到right
到达字符串S
的尽头
第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解, 也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。下面画图理解一下,needs
和window
相当于计数器,分别记录T
中字符出现次数和「窗口」中的相应字符的出现次数。v
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const lengthOfLongestSubstring = function (s ) { let right = 0 , left = 0 ; let length = 0 ; let arr = []; while (right < s.length) { let index = arr.indexOf(s[right]); if (index !== -1 ) { arr.splice(0 , index + 1 ); } arr.push(s[right]); length = Math .max(length, arr.length); right++; } return length; };
4、寻找正序数组的两个中位数 题目:给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?
思路:暴力求解的话极其简单,直接for循环即可,第一时间会想到就是归并排序最后一步。按照 O(log (m+n)) 时间复杂度的话,面对有序数组的排序,首先想到二分查找,见下方,但是对于两个数组的二分查找该如何实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function binary_search (arr,low, high, key ) { if (low > high){ return -1 ; } var mid = parseInt ((high + low) / 2 ); if (arr[mid] == key){ return mid; }else if (arr[mid] > key){ high = mid - 1 ; return binary_search(arr, low, high, key); }else if (arr[mid] < key){ low = mid + 1 ; return binary_search(arr, low, high, key); } };
该题的本质可扩展为寻找两个有序数组的第k小数,不一定是中位数,因此二分查找的本质是partition,每次都剔除k/2个数,且保证这些数都在第k小数左边,即都比第k小数小,然后k = k/2;递归处理后,得到第k小数。
二分查找,关键点在于要partition两个排好序的数组成左右两等份,partition需要满足len(Aleft)+len(Bleft)=(m+n+1)/2 - m是数组A的长度, n是数组B的长度并且partition后 A左边最大(maxLeftA), A右边最小(minRightA), B左边最大(maxLeftB), B右边最小(minRightB) 满足(maxLeftA <= minRightB && maxLeftB <= minRightA)有了这两个条件,那么median就在这四个数中,根据奇数或者是偶数。
奇数:median = max(maxLeftA, maxLeftB);偶数:median = (max(maxLeftA, maxLeftB) + min(minRightA, minRightB)) / 2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 var findMedianSortedArrays = function (nums1, nums2 ) { if (nums1.length > nums2.length) { return findMedianSortedArrays(nums2, nums1); } const m = nums1.length const n = nums2.length let low = 0 let high = m while (low <= high) { const i = low + Math .floor((high - low) / 2 ) const j = Math .floor((m + n + 1 ) / 2 ) - i const maxLeftA = i === 0 ? -Infinity : nums1[i-1 ] const minRightA = i === m ? Infinity : nums1[i] const maxLeftB = j === 0 ? -Infinity : nums2[j-1 ] const minRightB = j === n ? Infinity : nums2[j] if (maxLeftA <= minRightB && minRightA >= maxLeftB) { return (m + n) % 2 === 1 ? Math .max(maxLeftA, maxLeftB) : (Math .max(maxLeftA, maxLeftB) + Math .min(minRightA, minRightB)) / 2 } else if (maxLeftA > minRightB) { high = i - 1 } else { low = low + 1 } } };
5、最长回文子串 题目:给你一个字符串 s,找到 s中最长的回文子串。
思路:最长回文子串的关键,其实通过双指针法也可以来处理,根据上题所述的思路很好进行解决,关键在于如何判定是否是回文子串的函数。但回文子串以及后序的回文子序列等问题,由于前后文联系比较紧密,且多次判断是否符合时间复杂度较大,通常使用动态规划来进行求解。
动态转移方程:dp[i] [j] = dp[i+1] [j-1] && (dp[i] === dp[j]);dp[i] [j]为true指的是:从i到j是回文串。
初始条件:即字符串长度仅为0、1时,必为回文子串,由于有这样的初始条件,初始项比较多,动态转移方程需要修改成:dp[i] [j] = (dp[i] === dp[j] && (j - i < 2 || dp[i+1] dp[j - 1]))。同样,由于初始条件必然有j > i,像这种初始条件的dp,一般都会使用斜向遍历。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const longestPalindrome = function (s ) { let res = "" ; let dp = Array .from(new Array (s.length), () => new Array (s.length).fill(0 )); for (let i = s.length - 1 ; i >= 0 ; i--) { for (let j = i; j < s.length; j++) { dp[i][j] = ((s[i] === s[j]) && (j - i < 2 || dp[i+1 ][j-1 ])); if (dp[i][j] && (j - i + 1 > res.length)) { res = s.substring(i, j+1 ); } } } return res; }
6、Z字形变换 题目:将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。比如输入字符串为 “PAYPALISHIRING” 行数为 3 时,排列如下:之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:”PAHNAPLSIIGYIR”。请你实现这个将字符串进行指定行数变换的函数:let convert(string s, int numRows)。
P A H N A P L S I I G Y I R
思路:关键在于找规律,中间列每列一个,且列数为numsRows-2;因此将一个第一列与后续的中间列作为一个循环,个数为numsRows + numsRows - 2。这样一个循环就能找出来了,再根据每个字符在循环中的位置,分别将其置入不同行。其中仅循环的第一位为第一行,V形排列。
1 2 3 4 5 6 7 8 9 10 11 12 13 const convert = function (s, numRows ) { if (numRows === 1 ) { return s; } let rows = new Array (numRows).fill("" ); let circle = (2 * numRows - 2 ); for (let i = 0 ; i < s.length; i++) { let x = i % circle; rows[Math .min(x, circle - x)] += s[i]; } let ans = rows.join("" ); return ans; }
7、整数反转 题目:给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
思路:在先判断正负号之后,用转字符串再转数组后,使用reverse()方法可以简单实现,但可以思考下用数学方法要如何处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 let reverse = function (x ) { let res = x.toString().split("" ); if (res[0 ] == "-" ) { res.push("-" ); } let ans = parseInt (res.reverse().join("" )); if (ans >= Math .pow(2 , 31 ) || ans <= -Math .pow(2 , 31 )) { ans = 0 ; }ue return ans; } let reverse = function (x ) { let result = 0 ; while (x !== 0 ) { result = x % 10 + result * 10 ; x = (x / 10 ) | 0 ; } return (result | 0 ) === result ? result : 0 ; }
8、字符串转整数 题目:请你来实现一个 atoi 函数,使其能将字符串转换成整数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。接下来的转化规则如下:
如果第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字字符组合起来,形成一个有符号整数。
假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成一个整数。
该字符串在有效的整数部分之后也可能会存在多余的字符,那么这些字符可以被忽略,它们对函数不应该造成影响。
假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换,即无法进行有效转换。在任何情况下,若函数不能进行有效的转换时,请返回 0 。
注意:本题中的空白字符只包括空格字符 ‘ ‘ 。假设我们的环境只能存储 32 位大小的有符号整数。
1 2 3 4 5 6 7 8 9 10 11 var myAtoi = function (s ) { const re = new RegExp (/^(-|\+)?\d+/ ); let str = s.trim().match(re); let res = str ? Number (str[0 ]) : 0 ; return res >= 0 ? Math .min(res, 2 **31 - 1 ) : Math .max(res, -(2 **31 )) };
9、回文数 题目:判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
思路:跟前面的整数反转一样,简单的思路的话,直接变数组之后,使用reverse()方法之后判断即可;同样也有数学方法来解决这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var isPalindrome = function (x ) { return x === Number (x.toString().split('' ).reverse().join('' )) }; var isPalindrome = function (x ) { if (x < 0 ) { return false ; } let result = 0 ; let value = x; while (x !== 0 ) { result = result * 10 + x % 10 ; x = (x / 10 ) | 0 ; } if (value = result) { return true ; } else { return false ; } }
10、正则表达式匹配 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const isMatch = (s, p ) => { if (s == null || p == null ) return false ; const sLen = s.length, pLen = p.length; const dp = new Array (sLen + 1 ); for (let i = 0 ; i < dp.length; i++) { dp[i] = new Array (pLen + 1 ).fill(false ); } dp[0 ][0 ] = true ; for (let j = 1 ; j < pLen + 1 ; j++) { if (p[j - 1 ] == "*" ) dp[0 ][j] = dp[0 ][j - 2 ]; } for (let i = 1 ; i < sLen + 1 ; i++) { for (let j = 1 ; j < pLen + 1 ; j++) { if (s[i - 1 ] == p[j - 1 ] || p[j - 1 ] == "." ) { dp[i][j] = dp[i - 1 ][j - 1 ]; } else if (p[j - 1 ] == "*" ) { if (s[i - 1 ] == p[j - 2 ] || p[j - 2 ] == "." ) { dp[i][j] = dp[i][j - 2 ] || dp[i - 1 ][j - 2 ] || dp[i - 1 ][j]; } else { dp[i][j] = dp[i][j - 2 ]; } } } } return dp[sLen][pLen]; };
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 ;解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
思路:其实是一个滑动窗口(双指针)类型的题目,暴力法:即穷举所有可能,分别计算面积并保存最大值。
双指针法:从左右两侧开始,将较矮柱子的指针进行移动,而先不移动高柱子的指针,原因:矮柱子选取后如果移动高柱子的话面积是一定会减小的,因为长度距离在变小的时候,此时高度只能小于或等于矮的柱子。因此只能移动矮的柱子这边才有可能使得高度比矮柱子大。所以,每次都移动的是高柱子的指针。这种方法其可以看作是暴力法的剪枝,而不是传统的滑动窗口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const maxArea = function (height ) { let left = 0 ; let right = height.length - 1 ; let ans = 0 ; while (left < right) { let height1 = Math .min(height[left], height[right]); let aquare = height1 * (right - left); ans = Math .max(ans, aquare); if (height[left] < height[right]) { left++; } else { right--; } } return ans; }
12、整数转罗马数字 思路:整数转罗马数字,比罗马数字转整数要简洁一些,同样关键在于求余的操作。
1 2 3 4 5 6 7 8 9 10 11 12 var intToRoman = function (num ) { var Q = ["" , "M" , "MM" , "MMM" ]; var B = ["" , "C" , "CC" , "CCC" , "CD" , "D" , "DC" , "DCC" , "DCCC" , "CM" ]; var S = ["" , "X" , "XX" , "XXX" , "XL" , "L" , "LX" , "LXX" , "LXXX" , "XC" ]; var G = ["" , "I" , "II" , "III" , "IV" , "V" , "VI" , "VII" , "VIII" , "IX" ]; return Q[Math .floor(num/1000 )] + B[Math .floor((num%1000 )/100 )] + S[Math .floor((num%100 )/10 )] + G[num%10 ]; };
13、罗马数字转整数 思路:罗马数字转整数,首先将所有的组合可能性列出并添加到哈希表中
然后对字符串进行遍历,由于组合只有两种,一种是 1 个字符,一种是 2 个字符,其中 2 个字符优先于 1 个字符
先判断两个字符的组合在哈希表中是否存在,存在则将值取出加到结果 ans 中,并向后移2个字符。不存在则将判断当前 1 个字符是否存在,存在则将值取出加到结果 ans 中,并向后移 1 个字符,遍历结束返回结果 ans。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const romanToInt = function (s ) { const map = { I : 1 , IV: 4 , V: 5 , IX: 9 , X: 10 , XL: 40 , L: 50 , XC: 90 , C: 100 , CD: 400 , D: 500 , CM: 900 , M: 1000 }; let ans = 0 ; for (let i = 0 ;i < s.length;) { if (i + 1 < s.length && map[s.substring(i, i+2 )]) { ans += map[s.substring(i, i+2 )]; i += 2 ; } else { ans += map[s.substring(i, i+1 )]; i ++; } } return ans; };
14、最长公共前缀 题目:编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""
。
思路:很简单的题目,一个个的依次找最长公共前缀,先比较前两个,再用得出的公共前缀来匹配下一个;因此需要两层for循环,第一层,用于获取各个字符串,第二层对比每个字符串的各个字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 var longestCommonPrefix = function (strs ) { if (strs.length == 0 ) { return "" ; } let res = strs[0 ]; for (let i = 1 ; i < strs.length; i++) { let compare = res; res = "" ; for (let j = 0 ; (j < strs[i].length) && (j < compare.length); j++) { if (strs[i][j] === compare[j]) { res = res + compare[j]; } else { break ; } } if (res == "" ) { return "" ; } } return res; }
15、三数之和 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。
思路:关键在于如何保证不重复,用常规思路的话,先用三层for循环,之后再用哈希表去重;换一个思路,我们保持三重循环的大框架不变,只需要保证:第二重循环枚举到的元素不小于当前第一重循环枚举到的元素;第三重循环枚举到的元素不小于当前第二重循环枚举到的元素。即这样就只有一种顺序被枚举了,可以先排序,然后再查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const threeSum = function (nums ) { nums.sort((a, b ) => { return (a - b); }); let res = []; for (let i = 0 ; i < nums.length; i++) { if (nums[i] > 0 ) { break ; } let target = -nums[i]; if ((target + nums[i - 1 ] == 0 ) && i > 0 ) { continue ; } let left = i + 1 ; let right = nums.length - 1 ; while (left < right) { let n2 = nums[left]; let n3 = nums[right]; if (n2 + n3 === target) { res.push([nums[i], n2, n3]); while (left < right && nums[left] === n2) left++; while (left < right && nums[right] === n3) right++; } else if (n2 + n3 < target) { left++; } else { right--; } } } return res; }
16、最接近的三数之和 题目:给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
思路:同样的思路,排序加双指针的做法能够很好地判断该如何去判断怎么去移动;同样,本题中不需要去考虑重复的问题了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const threeSumClosest = function (nums, target ) { nums.sort((a, b ) => { return (a - b); }); let ans = 0 ; let compare = Infinity ; for (let i = 0 ; i < nums.length; i++) { let left = i + 1 ; let right = nums.length - 1 ; while (left < right) { let compare2 = target - nums[left] - nums[right] - nums[i]; if (Math .abs(compare2) < compare) { ans = nums[i] + nums[right] + nums[left]; compare = Math .abs(compare2) } if (compare2 < 0 ) { right--; } else { left++; } } } return ans; }
17、电话号码的组合 给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
思路:看上去十分容易,本质是一个三叉树的遍历方法,可以用BFS或者DFS。由这个题,我们可以先引申一下深度遍历和广度遍历的思路逻辑与基本方法。
1、DFS回溯(回溯是一种算法思想,一般可以用递归来实现。通俗点讲回溯就是一种试探,类似于穷举,但回溯有“剪枝”功能,)
回溯本质是暴力搜索,在问题的解空间树中,用 DFS 的方式,从根节点出发搜索整个解空间。如果要找出所有的解,则要搜索整个子树,如果只用找出一个解,则搜到一个解就可以结束搜索。类似「找出所有可能的组合」的问题,适合回溯算法。
回溯类题目,有三个关键点:
(1).选择:决定了你每个节点有哪些分支,可以帮助你构建出解的空间树。
(2).约束条件:用来剪枝,剪去不满足约束条件的子树,避免无效的搜索。
(3).目标:决定了何时捕获解,或者剪去得不到解的子树,提前回溯。
回溯法实质:它的求解过程实质上是先序遍历一棵“状态树”的过程。只不过,这棵树不是遍历前预先建立的,而是隐含在遍历过程中。如果认识到这点,很多问题的递归过程设计也就迎刃而解了。【回溯与递归的区别】回溯这个算法思想可以由递归这个算法结构来实现
我们构建一个递归来实现DFS,。递归的关键:递归关系与递归终止条件。(其他的扔给递归)
1、找整个递归的终止条件:递归应该在什么时候结束?2、找返回值:应该给下一级返回什么信息?3、本级递归应该做什么:在这一级递归中,应该完成什么任务?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const letterCombinations = (digits ) => { if (digits.length === 0 ) { return []; } const res = []; const map = new Map ([['2' ,'abc' ], ['3' ,'def' ],['4' ,'ghi' ],['5' ,'jkl' ],['6' ,'mno' ],['7' ,'pqrs' ],['8' ,'tuv' ],['9' ,'wxyz' ]]); const dfs = (curStr, i ) => { if (i > digits.length - 1 ) { res.push(curStr); return ; } let letters = map.get(digits[i]); for (j of letters) { dfs(curStr, i+1 ); } } dfs("" , 0 ); return res; }
2、BFS广度搜索的方法
BFS通常是维护一个队列,即一层层地进行遍历,每次将对应层数的叶子加到之前层上,这里可以使用队列先进先出来解决,先进先出,依次地每次更新对应的下一层的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const letterCombinations = (digits ) => { if (digits.length == 0 ) { return []; } let res = []; let map = new Map ([['2' ,'abc' ], ['3' ,'def' ],['4' ,'ghi' ],['5' ,'jkl' ],['6' ,'mno' ],['7' ,'pqrs' ],['8' ,'tuv' ],['9' ,'wxyz' ]]); let queue = []; queue.push('' ); for (let i = 0 ; i < digits.length; i++) { let levelSize = queue.length; for (let j = 0 ; j < levelSize; j++) { let curStr = queue.shift(); let letters = map.get(digits[i]); for (let k of letters) { queue.push(curStr + k); } } } return queue; }
18、四数之和 题目:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组
思路:其实跟前面的三数之和思路一样,后面两个数可以用双指针法来进行枚举;而前面两个数只能通过两层的for循环来实现。同样,先对数组进行排序,且添加每个数时都要进行重复判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 const fourSum = function (nums, target ) { const quadruplets = []; if (nums.length < 4 ) { return quadruplets; } nums.sort((x, y ) => x - y); const length = nums.length; for (let i = 0 ; i < length - 3 ; i++) { if (i > 0 && nums[i] === nums[i - 1 ]) { continue ; } if (nums[i] + nums[i + 1 ] + nums[i + 2 ] + nums[i + 3 ] > target) { break ; } if (nums[i] + nums[length - 3 ] + nums[length - 2 ] + nums[length - 1 ] < target) { continue ; } for (let j = i + 1 ; j < length - 2 ; j++) { if (j > i + 1 && nums[j] === nums[j - 1 ]) { continue ; } if (nums[i] + nums[j] + nums[j + 1 ] + nums[j + 2 ] > target) { break ; } if (nums[i] + nums[j] + nums[length - 2 ] + nums[length - 1 ] < target) { continue ; } let left = j + 1 , right = length - 1 ; while (left < right) { const sum = nums[i] + nums[j] + nums[left] + nums[right]; if (sum === target) { quadruplets.push([nums[i], nums[j], nums[left], nums[right]]); while (left < right && nums[left] === nums[left + 1 ]) { left++; } left++; while (left < right && nums[right] === nums[right - 1 ]) { right--; } right--; } else if (sum < target) { left++; } else { right--; } } } } return quadruplets; };
19、删除链表的倒数第N个结点 题目:给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。进阶:你能尝试使用一趟扫描实现吗?
思路:不要求时间复杂度的话,先扫一遍来确定链表的长度,而后再根据长度找到倒数第N个结点;要使用一趟扫描实现的话,可以使用双指针来进行。第一个指针比第二个快n个,这样第二个指向尾节点时,则第一个指向要删除的结点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const removeNthFromEnd = function (head, n ) { let start = new ListNode; start.next = head; let left = start; let right = start; for (let i = 0 ; i < n; i++) { right = right.next; } while (right.next != null ) { left = left.next; right = right.next; } left.next = left.next.next; return start.next; };
20、有效的括号 题目:给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。注意空字符串可被认为是有效字符串。
思路:类似于栈的操作,判断很简单,这里就不赘述了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 var isValid = function (s ) { if (s.length == 0 ) { return true ; } let compare = []; for (let i = 0 ; i < s.length; i++) { if (s[i] === "(" || s[i] === "[" || s[i] === "{" ) { compare.push(s[i]); } else if (s[i] === ")" ) { let a = compare.pop(); if (a !== "(" ) { return false ; } } else if (s[i] === "]" ) { let a = compare.pop(); if (a !== "[" ) { return false ; } } else if (s[i] === "}" ) { let a = compare.pop(); if (a !== "{" ) { return false ; } } } if (compare.length == 0 ) { return true ; } else { return false ; } };