简单
1. 两数之和
序号:1
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9输出:[0,1]解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6输出:[0,1]
哈希表
维护一个哈希表,key是数字,value为下标,遍历数组,判断(目标值-当前数)的值是否存在哈希表里,存在则返回对应的下标,不存在则把当前数字放入哈希表
var tomSum = function(nums, target) {// 存 数字 => 下表 键值对let hashMap = new Map();for (let i = 0; i < nums.length; i++) {const another = target - nums[i];const isHas = hashMap.has(another);if (isHas) {return [hashMap.get(another), i];}hashMap.set(nums[i], i);}return [];}
或者
var tomSum = function(nums, target) {let obj = {};for (let i = 0; i < nums.length; i++) {let another = target - nums[i];if (another in obj) {return [obj[another], i];}obj[nums[i]] = i;}return [];}
复杂度:
- 时间复杂度O(N)
-
双重for循环
var tomSum = function(nums, target) {for (let i = 0; i < nums.length; i++) {for (let j = i + 1; j < nums.length; j++) {if ((nums[i) + nums[j] === target) && (i !== j)) {return [i, j];}}}return [];}
复杂度:
时间复杂度O(N^2)
- 空间复杂度O(1)
4. 寻找两个正序数组的中位数
序号:4
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]输出:2.00000解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]输出:2.50000解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
合并数组排序
/*** @param {number[]} nums1* @param {number[]} nums2* @return {number}*/var findMedianSortedArrays = function(nums1, nums2) {const newArray = nums1.concat(nums2).sort(function(a, b){return a - b});const length = newArray.length;if (length % 2 === 0) {const num = length / 2;return (newArray[num -1] + newArray[num]) / 2;}let num = Math.ceil(length / 2) - 1;return newArray[num];};
14. 最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 “”。
示例 1:
输入:strs = ["flower","flow","flight"]输出:"fl"
双重循环
/*** @param {string[]} strs* @return {string}*/var longestCommonPrefix = function(strs) {if (strs.length === 0) {return "";}let ans = strs[0];for (let i = 1; i < strs.length; i++) {let j = 0;for (; j < ans.length && j < strs[i].length; j++) {if (ans[j] != strs[i][j]) {break;}}ans = ans.substr(0, j);if (ans === "") {return ans;}}return ans;};
20. 有效的括号
给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
示例 1:
输入:s = "()"输出:true
示例 2:
输入:s = "()[]{}"输出:true
示例 3:
输入:s = "(]"输出:false
示例 4:
输入:s = "([)]"输出:false
示例 5:
输入:s = "{[]}"输出:true
提示:
- 1 <= s.length <= 104
- s 仅由括号 ‘()[]{}’ 组成
栈
// 执行用时 64ms 内存消耗 41.3MB/*** @param {string} s* @return {boolean}*/var isValid = function(s) {const n = s.length;if (n % 2 === 1) {return false;}// const pairs = {// "}": "{",// "]": "[",// ")": "("// }const pairs = new Map([[')', '('],[']', '['],['}', '{']]);const stk = [];for (let ch of s){if (pairs.has(ch)) { // // pairs.has(ch) => obj[ch]if (!stk.length || stk[stk.length - 1] !== pairs.get(ch)) { // // pairs.get(ch) => obj[ch]return false;}stk.pop();}else {stk.push(ch);}};return !stk.length;};
或者 类似的写法:
var isValid = function(s) {const stack = [];for (let i = 0; i < s.length; i++) {if (['(', '{', '['].includes(s[i])) {stack.unshift(s[i]);} else if (['()', '{}', '[]'].includes(stack[0] + s[i])) {stack.shift();} else {return false;}}return stack.length == 0;};
复杂度分析
- 时间复杂度:O(n),其中 nn 是字符串 ss 的长度。
- 空间复杂度:O(n+∣Σ∣),其中 Σ 表示字符集,本题中字符串只包含 6 种括号,∣Σ∣=6。栈中的字符数量为 O(n),而哈希表使用的空间为 O(∣Σ∣),相加即可得到总空间复杂度。
26. 删除有序数组中的重复项
序号:26
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
输入:nums = [1,1,2]输出:2, nums = [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]解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 3 * 104-104 <= nums[i] <= 104nums 已按升序排列
双指针
如果数组nums 的长度为 0,则数组不包含任何元素,因此返回 0。
当数组 nums 的长度大于 0 时,数组中至少包含一个元素,在删除重复元素之后也至少剩下一个元素,因此nums[0] 保持原状即可,从下标 1 开始删除重复元素。
定义两个指针 fast 和 slow 分别为快指针和慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置,初始时两个指针都指向下标 1。
假设数组nums 的长度为 n。将快指针fast 依次遍历从 1到 n-1的每个位置,对于每个位置,如果 nums[fast] / nums[fast−1],说明 nums[fast] 和之前的元素都不同,因此将 nums[fast] 的值复制到 nums[slow],然后将 slow 的值加 1,即指向下一个位置。
遍历结束之后,从 nums[0] 到nums[slow−1] 的每个元素都不相同且包含原数组中的每个不同的元素,因此新的长度即为 slow,返回 slow 即可。
题解:
/*** @param {number[]} nums* @return {number}*/var removeDuplicates = function(nums) {const n = nums.length;if (n === 0) {return 0;}let fast = 1, slow = 1;while (fast < n) {if (nums[fast] !== nums[fast -1]) {nums[slow] = nums[fast];++slow;}++fast;}return slow;}
复杂度分析
- 时间复杂度:O(n),其中 n 是数组的长度。快指针和慢指针最多各移动 n 次。
- 空间复杂度:O(1)。只需要使用常数的额外空间。
53. 最大子数组和
序号:53
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]输出:6解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]输出:1
示例 3:
输入:nums = [5,4,-1,7,8]输出:23
提示:
- 1 <= nums.length <= 105
- -104 <= nums[i] <= 104
动态规划
若前一个元素大于0,则将其加到当前元素上。
原数组:[-2, 1, -3, 4, -1, 2, 1, -5, 4];新数组:[-2, 1, -2, 4, 3, 5, 6, 1, 5];取新数组的最大值
/*** @param {number[]} nums* @return {number}*/// 判断前一个值是否大于0,形成新数组// 执行用时:68 ms 内存消耗:49.7 MBvar maxSubArray = function(nums) {const n = nums.length;for (let i = 1; i < n; i++) {if (nums[i - 1] > 0) {nums[i] += nums[i -1];}}return Math.max(...nums);};
复杂度分析
- 时间复杂度:O(n),其中 n 为 nums 数组的长度。
- 空间复杂度:O(1)。我们只需要常数空间存放若干变量。
贪心算法
若当前指针所指元素之前的和小于0.则丢弃当前元素之前的数列 ```javascript 旧数组:[-2, 1, -3, 4, -1, 2, 1, -5, 4];
- -2 + 0 < 0; 丢弃
- 1 + 0 > 0; 保留 当前和为1
- 1 + -3 < 0; 丢弃
- 4 + 0 > 0; 保留
…
复杂度分析```javascriptvar maxSubArray = function(nums) {let pre = 0, maxAns = nums[0];nums.forEach((x) => {pre = Math.max(pre + x, x);maxAns = Math.max(maxAns, pre);});return maxAns;};
- 时间复杂度:O(n),其中 n 为 nums 数组的长度。
- 空间复杂度:O(1)。我们只需要常数空间存放若干变量。
71. 简化路径
题号:71
给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 ‘/‘ 开头),请你将其转化为更加简洁的规范路径。
在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,’//‘)都被视为单个斜杠 ‘/‘ 。 对于此问题,任何其他格式的点(例如,’…’)均被视为文件/目录名称。
请注意,返回的 规范路径 必须遵循下述格式:
始终以斜杠 ‘/‘ 开头。
两个目录名之间必须只有一个斜杠 ‘/‘ 。
最后一个目录名(如果存在)不能 以 ‘/‘ 结尾。
此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 ‘.’ 或 ‘..’)。
返回简化后得到的 规范路径 。
示例 1:
输入:path = "/home/"输出:"/home"解释:注意,最后一个目录名后面没有斜杠。
示例 2:
输入:path = "/../"输出:"/"解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。
示例 3:
输入:path = "/home//foo/"输出:"/home/foo"解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
示例 4:
输入:path = "/a/./b/../../c/"输出:"/c"
栈
/*** @param {string} path* @return {string}*/var simplifyPath = function(path) {const names = path.split("/");const stack = [];for (const name of names) {if (name === "..") {if (stack.length) {stack.pop();}} else if (name.length && name !== ".") {stack.push(name);}}return "/" + stack.join("/");};
复杂度分析
- 时间复杂度:O(n),其中 n 是字符串 path 的长度。
- 空间复杂度:O(n)。我们需要 O(n)的空间存储 names 中的所有字符串。
第二种:
var simplifyPath = function(path) {const dir = path.split('/'), stack = []for(let name of dir) {if(name === '.' || name === '') continue;if(name === '..') {stack.length && stack.pop();continue;}stack.push(name)}return '/' + stack.join('/')};
88. 合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3输出:[1,2,2,3,5,6]解释:需要合并 [1,2,3] 和 [2,5,6] 。合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0输出:[1]解释:需要合并 [1] 和 [] 。合并结果是 [1] 。
示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1输出:[1]解释:需要合并的数组是 [] 和 [1] 。合并结果是 [1] 。注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
提示:
- nums1.length == m + n
- nums2.length == n
- 0 <= m, n <= 200
- 1 <= m + n <= 200
- -109 <= nums1[i], nums2[j] <= 109
直接合并后排序
/*** @param {number[]} nums1* @param {number} m* @param {number[]} nums2* @param {number} n* @return {void} Do not return anything, modify nums1 in-place instead.*/var merge = function(nums1, m, nums2, n) {nums1.splice(m, nums1.length - m, ...nums2);nums1.sort((a, b) => a -b);};
复杂度分析
- 时间复杂度:O((m+n)log(m+n))。排序序列长度为 m+n,套用快速排序的时间复杂度即可,平均情况为 O((m+n)log(m+n))。
空间复杂度:O(log(m+n))。排序序列长度为 m+nm+n,套用快速排序的空间复杂度即可,平均情况为 O(log(m+n))。
双指针
讲解
首先判断ptr1是否走完nums1,或者ptr2是否走完nums2,如果一个数组走完则将剩余数组全部接在sorted后面;
- 当nums1[ptr1] <= nums2[ptr2]时,将nums1[ptr1]赋值给临时变量cur, ptr1++;
- 否则,将nums2[ptr2]赋值给临时变量cur, ptr2++;
- sorted数组当前的位置是ptr1 + ptr2 - 1, 因为上一步将指针++了,但是并未赋值,所以sorted[ptr1 + ptr2 - 1] = cur;
最后将sorted每一个位置一一赋值给nums1;
var merge = function(nums1, m, nums2, n) {let p1 = 0, p2 = 0;const sorted = new Array(m + n).fill(0);var cur;while (p1 < m || p2 < n) {if (p1 === m) {cur = nums2[p2++];} else if (p2 === n) {cur = nums1[p1++];} else if (nums1[p1] < nums2[p2]) {cur = nums1[p1++];} else {cur = nums2[p2++];}sorted[p1 + p2 - 1] = cur;}for (let i = 0; i != m + n; ++i) {nums1[i] = sorted[i];}};
复杂度分析
时间复杂度:O(m+n)。指针移动单调递增,最多移动 m+n 次,因此时间复杂度为 O(m+n)。
空间复杂度:O(m+n)。需要建立长度为 m+n 的中间数组 sorted。
逆向双指针
var merge = function(nums1, m, nums2, n) {let p1 = m - 1, p2 = n - 1;let tail = m + n - 1;var cur;while (p1 >= 0 || p2 >= 0) {if (p1 === -1) {cur = nums2[p2--];} else if (p2 === -1) {cur = nums1[p1--];} else if (nums1[p1] > nums2[p2]) {cur = nums1[p1--];} else {cur = nums2[p2--];}nums1[tail--] = cur;}};
复杂度分析
时间复杂度:O(m+n)。指针移动单调递减,最多移动 m+n 次,因此时间复杂度为 O(m+n)。
- 空间复杂度:O(1)。直接对数组 nums1 原地修改,不需要额外空间。
439. 两个数组的交集
序号:439
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]输出:[9,4]解释:[4,9] 也是可通过的
提示:
- 1 <= nums1.length, nums2.length <= 1000
- 0 <= nums1[i], nums2[i] <= 1000
两个集合
/*** @param {number[]} nums1* @param {number[]} nums2* @return {number[]}*/var intersection = function(nums1, nums2) {const set1 = new Set(nums1);const set2 = new Set(nums2);if (set1.size > set2.size) {[set1, set2] = [set2, set1];}const intersetion = new Set();for (const num of set1) {if (set2.has(num)) {intersetion.add(num);}}return [...intersetion];};
复杂度分析
- 时间复杂度:O(m+n),其中 m 和 n 分别是两个数组的长度。使用两个集合分别存储两个数组中的元素需要 O(m+n)的时间,遍历较小的集合并判断元素是否在另一个集合中需要 O(min(m,n)) 的时间,因此总时间复杂度是 O(m+n)。
- 空间复杂度:O(m+n),其中 m 和 n 分别是两个数组的长度。空间复杂度主要取决于两个集合。
排序 + 双指针
如果两个数组是有序的,则可以使用双指针的方法得到两个数组的交集。
首先对两个数组进行排序,然后使用两个指针遍历两个数组。可以预见的是加入答案的数组的元素一定是递增的,为了保证加入元素的唯一性,我们需要额外记录变量 pre 表示上一次加入答案数组的元素。
初始时,两个指针分别指向两个数组的头部。每次比较两个指针指向的两个数组中的数字,如果两个数字不相等,则将指向较小数字的指针右移一位,如果两个数字相等,且该数字不等于pre ,将该数字添加到答案并更新 pre 变量,同时将两个指针都右移一位。当至少有一个指针超出数组范围时,遍历结束。
var intersection = function(nums1, nums2) {nums1.sort((x, y) => x - y);nums2.sort((x, y) => x - y);const length1 = nums1.length, length2 = nums2.length;let index1 = 0, index2 = 0;const intersection = [];while (index1 < length1 && index2 < length2) {const num1 = nums1[index1], num2 = nums2[index2];if (num1 === num2) {// 保证加入元素的唯一性if (!intersection.length || num1 !== intersection[intersection.length - 1]) {intersection.push(num1);}index1++;index2++;} else if (num1 < num2) {index1++;} else {index2++;}}return intersection;};
复杂度分析
- 时间复杂度:O(mlogm+nlogn),其中 m 和 n 分别是两个数组的长度。对两个数组排序的时间复杂度分别是 O(mlogm) 和 O(nlogn),双指针寻找交集元素的时间复杂度是 O(m+n),因此总时间复杂度是 O(mlogm+nlogn)。
- 空间复杂度:O(logm+logn),其中 m 和 n 分别是两个数组的长度。空间复杂度主要取决于排序使用的额外空间。
509. 斐波那契数
序号:509
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
给定 n ,请计算 F(n) 。F(0) = 0,F(1) = 1F(n) = F(n - 1) + F(n - 2),其中 n > 1
示例1:
输入:n = 2输出:1解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例2:
输入:n = 3输出:2解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例3:
输入:n = 4输出:3解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
- 0 <= n <= 30
动态规划
var fib = function(n) {if (n < 2) {return n;}let p = 0, q = 0, r= 1;for (let i = 2; i <= n; i++) {p = 1;q = r;r = p + q;}return r;}
复杂度分析
- 时间复杂度:O(n)。
-
暴力递归
var fib = function(n) {if (n < 2) {return n;}return fib(n-1) + fib(n-2);}
递推
var fib = function(n) {let array = [];for (let i = 0; i <= n; i++) {if (n < 2) {array[i] = i;} else {array[i] = array[i - 1] + array[i - 2];}}return array[n];}
复杂度分析
时间复杂度:O(n)。
- 空间复杂度:O(1)。
