二叉树 深度优先搜索 广度优先搜索 完全二叉树的节点个数
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
难度:简单
思路:递归
如果当前节点存在则加1
然后对当前节点 的左右节点执行当前函数
1 2 3 4 5 function countNodes (root ) { if (!root) return 0 ; return 1 + countNodes(root.left) + countNodes(root.right); }
二叉树的最大深度
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
难度:简单
思路:递归
当前节点存在则加1
再加上左右节点 中的最大值
1 2 3 4 5 function maxDepth (root ) { if (!root) return 0 ; return 1 + Math .max(maxDepth(root.left), maxDepth(root.right)); }
二叉搜索树的最近公共祖先 题目链接
重点关注
难度:简单
思路:两侧遍历,参考题解
二叉搜索树的值是数值,并且当前节点的值大于左子树的值小于右子树的值(重要)
遍历两次二叉树,分别找出根节点到这两个节点的路径,路径中需要包括目标节点
两个路径从后向前数,最近的相同节点就是最近的公共节点
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 function lowestCommonAncestor (root, p, q ) { function getRoute (root, num ) { const route = []; let currNode = root; while (currNode) { route.push(currNode); if (num > currNode.val) { currNode = currNode.right; } else if (num < currNode.val) { currNode = currNode.left; } else { return route; } } } const pRoute = getRoute(root, p.val); const qRoute = getRoute(root, q.val); for (let i = pRoute.length - 1 ; i >= 0 ; i--) { if (qRoute.includes(pRoute[i])) { return pRoute[i]; } } }
思路:一次遍历,参考题解
跟两次遍历方法类似,只不过这次是同时对 p 和 q 进行比较
如果当前节点大于 p 和 q,则 p 和 q 在左子树
如果当前节点小于 p 和 q,则 p 和 q 在右子树
如果不满足 2 和 3 条件,则表示 p 和 q 分别在左子树和右子树,即再次分叉,即当前节点是最近的公共节点
此方法省去了存储路径的空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function lowestCommonAncestor (root, p, q ) { let currNode = root; while (currNode) { const value = currNode.val; if (value > p.val && value > q.val) { currNode = currNode.left; } else if (value < p.val && value < q.val) { currNode = currNode.right; } else { return currNode; } } return currNode; }
链表 删除排序链表中的重复元素
存在一个按升序排列 的链表,给你这个链表的头节点 head
,请你删除所有重复的元素,使每个元素 只出现一次 。
难度:简单
思路
链表是按升序 进行排列的,因此值相同的节点会排列在一起
将具有相同值的第一个节点 记做uniqueNode
,将当前节点记做currNode
遍历链表,当uniqueNode
的值和currNode
的值不相等 的时候,让uniqueNode.next = currNode
这样即可将与uniqueNode.val
相同的值跳过、去重
需要注意的是最后需要单独让uniqueNode.next = null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function deleteDuplicates (head ) { if (head === null ) return head; let currNode = head; let uniqueNode = head; while (currNode) { if (uniqueNode.val !== currNode.val) { uniqueNode.next = currNode; uniqueNode = currNode; } currNode = currNode.next; } uniqueNode.next = null ; return head; }
删除排序链表中的重复元素 II
存在一个按升序排列的链表,给你这个链表的头节点 head
,请你删除链表中所有存在数字重复情况的节点 ,只保留原始链表中 没有重复出现 的数字。
重点关注
难度:中等
思路
由题意可知,head
节点也可能会被删除,因此需要创建一个dummy
节点指向head
节点,作用就是删除head
节点
判断当前节点currNode
的后两个节点 的值是否相等。如果相等则表示重复,通过while
循环将其删除。如果不相等,则直接将currNode
节点向后移动一位即可
最后返回dummy.next
即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function deleteDuplicates (head ) { if (!head) return head; const dummy = new ListNode(0 , head); let currNode = dummy; while (currNode.next && currNode.next.next) { const val = currNode.next.val; if (currNode.next.next.val === val) { while (currNode.next && currNode.next.val === val) { currNode.next = currNode.next.next; } } else { currNode = currNode.next; } } return dummy.next; }
从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)
难度:简单
思路:栈
创建一个栈
遍历链表,将节点push
入栈
遍历结束后在将栈 中的节点pop
出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function reversePrint (head ) { const stack = []; const result = []; let currNode = head; while (currNode) { stack.push(currNode.val); currNode = currNode.next; } while (stack.length) { result.push(stack.pop()); } return result; }
动态规划 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意: 给定 n 是一个正整数。
重点关注
难度:简单
思路
有动态转移方程 f(n) = f(n - 1) + f(n - 2)
因此只需要计算f(n - 1)
和f(n - 2)
即可
1 2 3 4 5 6 7 8 9 function climbStairs (n ) { const dp = [0 , 1 , 2 ]; for (let i = 3 ; i <= n; i++) { dp[i] = dp[i - 1 ] + dp[i - 2 ]; } return dp[n]; }
最大子数组和(关注) 题目链接
难度:简单
思路:两层循环,肯定超时,不写了
思路:
用 f(i)表示以第 i 个数结尾的[最大子数组和],最终只需要求出最大的 f(i)即可
重点:关键在于如何求 f(i),f(i) = Math.max(f(i - 1) + nums[i], nums[i])
1 2 3 4 5 6 7 8 9 10 11 12 13 function maxSubArray (nums ) { const dp = [...nums]; for (let i = 1 ; i < nums.length; i++) { dp[i] = Math .max(dp[i - 1 ] + nums[i], nums[i]); } return Math .max(...dp); }
递归 比赛中的配对次数 题目链接
难度:简单
思路:递归
每次都执行相同的操作,故使用递归
确认递归的出口条件,当决出获胜队伍后return
,即当剩余队伍数为 1
接下来就是判断奇偶,再根据题意去计算即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function numberOfMatches (n ) { if (n === 1 ) return 0 ; let count = 0 ; const isOdd = n & 1 ; if (isOdd) { count = (n - 1 ) / 2 ; } else { count = n / 2 ; } return count + numberOfMatches(count + isOdd); }
思路:遍历
创建一个变量存储当前比赛次数,取代递归函数的return
结束条件是一致的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function numberOfMatches (n ) { let total = 0 ; let count = 0 ; while (n > 1 ) { const isOdd = n & 1 ; if (isOdd) { count = (n - 1 ) / 2 ; } else { count = n / 2 ; } n = count + isOdd; total += count; } return total; }
二分查找 二分查找
给定一个 n 个元素有序的(升序) 整型数组 nums
和一个目标值 target
,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1
难度:简单
思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function search (nums, target ) { let start = 0 ; let end = nums.length - 1 ; let index = 0 ; let middle = 0 ; while (start <= end) { index = Math .floor((start + end) / 2 ); middle = nums[index]; if (middle > target) { end = index - 1 ; } else if (middle < target) { start = index + 1 ; } else { return index; } } return -1 ; }
sqrt(x)
计算 x 算数平方根的整数部分值,0 <= x < 231 - 1
难度:简单
思路
如果 x 的算数平方根整数部分为 k,那么根据数学知识 可以得到k2 <= x 并且(k + 1)2 > x ,
上述两个条件可以作为二分查找 的上下界
首指针start = 0
,尾指针end = x
,因为k <= x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function mySqrt (x ) { let start = 0 ; let end = x; let k = 0 ; while (start <= end) { k = Math .floor((start + end) / 2 ); if (k ** 2 > x) { end = k - 1 ; } else if ((k + 1 ) ** 2 <= x) { start = k + 1 ; } else { return k; } } }
字符串 一年中的第几天
给你一个字符串 date ,按 YYYY-MM-DD 格式表示一个 现行公元纪年法 日期。请你计算并返回该日期是当年的第几天
题目链接
难度:简单
思路:使用Date
计算日期的毫秒值
将年的值 year
从日期 date
中截取出来
获取year-1-1
的毫秒值
获取date
的毫秒值
两者相减,再除一天的毫秒值,最后加 1
1 2 3 4 5 6 function dayOfYear (date ) { const year = date.split("-" )[0 ]; const now = new Date (date); const beginning = new Date (`${year} -1-1` ); return (now - beginning) / (24 * 60 * 60 * 1000 ) + 1 ; }
替换所有的问号 题目链接
难度:简单
思路:枚举 + 随机值
因为字符串不可修改只能被赋值,所以将字符串转成数组
遍历数组,判断当前项是不是问号 ,如果是则使用string.charCodeAt()
方法获取前后字符的ASCII码值
小写字母的ASCII码
范围为97 - 122
封装一个函数返回这个范围的ASCII码
随机值所对应的字符,并且这个随机值不能等于传入的参数,如果等于则再次执行改函数
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 function modifyString (s ) { const arr = s.split("" ); let preStrCode = "" ; let nextStrCode = "" ; function random (pre, next ) { const val = Math .floor(Math .random() * (122 - 97 + 1 ) + 97 ); if (val !== pre && val !== next) { return String .fromCharCode(val); } return random(pre, next); } for (let i = 0 ; i < arr.length; i++) { if (arr[i] === "?" ) { preStrCode = arr[i - 1 ] ? arr[i - 1 ].charCodeAt() : "" ; if (arr[i + 1 ] && arr[i + 1 ] !== "?" ) { nextStrCode = arr[i + 1 ].charCodeAt(); } else { nextStrCode = "" ; } arr[i] = random(preStrCode, nextStrCode); } } return arr.join("" ); }
简化路径 题目链接
难度:中等
思路:栈
首先创建一个栈stack
用来存储目录和文件名,表示当前的目录结构
再将路径字符串path
以/
为标识进行分割,得到一个由目录
、文件
、..
组成的数组
遍历这个数组,判断当前项是不是目录或者文件(由字母、数字、_组成),如果是将其push
入栈,如果是..
表示回到上一层目录,则将stack
的栈顶 元素pop
出来
最后将stack
以/
为标识进行拼接,得到的就是简化后的路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function simplifyPath (path ) { const stack = []; const pathArr = path.split("/" ); const pathReg = /[\w\_]+/ ; pathArr.forEach((item ) => { if (pathReg.test(item) || /[\.]{3,}/ .test(item)) { stack.push(item); } else if (item === ".." ) { stack.pop(); } }); return "/" + stack.join("/" ); }
一次编辑 题目链接
难度:中等
思路:普通
题目要求编辑次数小于等于 1,因此可以得出两个字符串的长度差 不能大于 1
如果两个字符串的长度相等 ,
仅仅翻转字母 题目链接
难度:简单
思路:双指针
字符串只能被赋值,不能被修改,因此先将字符串转为数组
创建两个指针,分别是 start 和 end。当两个指针所指的元素都是字母时进行交换,否则移动指针
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 function reverseOnlyLetters (s ) { const arr = s.split("" ); const length = arr.length; const lettersReg = /[a-zA-Z]/ ; let start = 0 ; let end = length - 1 ; let startIsLetter = true ; let endIsLetter = true ; while (start < end) { startIsLetter = lettersReg.test(arr[start]); endIsLetter = lettersReg.test(arr[end]); if (startIsLetter && endIsLetter) { [arr[start], arr[end]] = [arr[end], arr[start]]; start++; end--; } else { if (!startIsLetter) { start++; } if (!endIsLetter) { end--; } } } return arr.join("" ); }
思路:字母栈,参考题解
创建一个栈用来存储所有的字母
遍历 s 字符串,将遇到的字母 push 入栈
再次遍历 s 字符串,当遇到字母时,将栈顶元素 pop 出来,替代当前字母,这样就实现了字母的翻转
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 function reverseOnlyLetters (s ) { const arr = s.split("" ); const length = arr.length; const lettersReg = /[a-zA-Z]/ ; const stack = []; for (let i = 0 ; i < length; i++) { if (lettersReg.test(arr[i])) { stack.push(arr[i]); } } for (let i = 0 ; i < length; i++) { if (lettersReg.test(arr[i])) { arr[i] = stack.pop(); } } return arr.join("" ); }
删除回文子序列 题目链接
难度:简单
思路:直接判断,参考题解
根据题意,子序列可以不是连续的
字符串中只有两种字符 a 和 b,因此最多需要删两次,最少删一次,因为字符串本身就是回文的3. 理解错题意了,以为是删除连续子序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function removePalindromeSub (s ) { const length = s.length; for (let i = 0 ; i < Math .floor(length / 2 ); i++) { if (s[i] !== s[length - 1 - i]) { return 2 ; } } return 1 ; }
有效的括号 题目链接
难度:简单
思路:栈
创建栈
遍历字符串,将所有的左括号push
入栈
当遍历到右括号时,判断当前括号是否和栈顶元素配对,如果配对则继续遍历,不过不是直接 return false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function isValid (s ) { const stack = []; const map = { "(" : ")" , "{" : "}" , "[" : "]" , }; for (let i = 0 ; i < s.length; i++) { if (map[s[i]]) { stack.push(s[i]); } else if (map[stack.pop()] !== s[i]) return false ; } return stack.length <= 0 ; }
句子中的有效单词数 题目链接
难度:简单
思路:正则
首先需要以空格 为分隔符分隔句子,得到tokens
数组
遍历tokens
数组,使用正则 判断token
是否符合条件的难点在于token
中是否存在连字符
因此创建两个正则表达式,分别匹配有连字符和无连字符的token
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 function countValidWords (sentence ) { let count = 0 ; const tokens = sentence.split(" " ); const tokenReg1 = /^[a-z]+\-[a-z]+(!|\.|,)?$/ ; const tokenReg2 = /^[a-z]*(!|\.|,)?$/ ; tokens.forEach((token ) => { if (!token) return ; if (token.includes("-" )) { tokenReg1.test(token) && count++; } else { tokenReg2.test(token) && count++; } }); return count; }
数组 适龄的朋友 题目链接
难度:中等
思路:暴力法
从题中可以知道年龄低的用户不能向比自己年龄大的用户发起请求
对ages
进行降序排序 ,保证发起请求的人比被请求的人年龄大或者相等
最后再判断被请求用户 是否满足ages[y] > 0.5 * ages[x] + 7
即可
需要注意的是年龄相同的用户 可以相互通信,需要特殊处理
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 function numFriendRequests (ages ) { let sum = 0 ; ages.sort((i, j ) => j - i); for (let x = 0 ; x < ages.length; x++) { let index = x; if ( ages[index] === ages[index - 1 ] && ages[index] > 0.5 * ages[index] + 7 ) { while (index > 0 && ages[index] === ages[index - 1 ]) { sum++; index--; } } for (let y = x + 1 ; y < ages.length; y++) { if (ages[y] <= 0.5 * ages[x] + 7 ) break ; sum++; } } return sum; }
统计特殊四元组 题目链接
难度:简单
思路:暴力法,直接枚举
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function countQuadruplets (nums ) { let count = 0 ; for (let a = 0 ; a < nums.length - 3 ; a++) { for (let b = a + 1 ; b < nums.length; b++) { for (let c = b + 1 ; c < nums.length; c++) { const target = nums[a] + nums[b] + nums[c]; for (let d = c + 1 ; d < nums.length; d++) { if (nums[d] === target) { count++; } } } } } return count; }
思路:hashMap
[3, n]
这个区间的值都可以当作nums[d]
从后向前 遍历数组,记录相同值的个数
nums[1] + nums[b] + nums[c] = nums[d]
的个数即为nums[d]
出现的次数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function countQuadruplets (nums ) { const map = new Map (); let count = 0 ; for (let c = nums.length - 2 ; c >= 2 ; c--) { map.set(nums[c + 1 ], (map.get(nums[c + 1 ]) || 0 ) + 1 ); for (let a = 0 ; a < c; a++) { for (b = a + 1 ; b < c; b++) { const sum = nums[a] + nums[b] + nums[c]; count += map.get(sum) || 0 ; } } } return count; }
一手顺子 题目链接
难度:中等
思路:排序 + splice
因为题目要求每一组都是有序的 ,因此先对数组进行排序
每一组的长度为groupSize
,我们可以从下标为 0 的元素开始将连续的值 组合在一起,如果不存在下一个连续的值,则表示当前hand
数组不满足顺子 条件
匹配到满足条件的值后,将其从数组中删掉 ,防止相同元素出现在不同的组中
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 function isNStraightHand (hand, groupSize ) { if (hand.length % groupSize !== 0 ) return false ; hand.sort((i, j ) => i - j); while (hand.length) { let size = groupSize - 1 ; let val = hand[0 ]; let index; hand.shift(); while (size) { index = hand.indexOf(val + 1 ); if (index === -1 ) return false ; hand.splice(index, 1 ); val++; size--; } } return true ; }
思路:排序 + hashMap
首先排序,原因与方法一 一样
方法一 使用了splice
方法导致时间复杂度 增加,想办法使用O(1)
的复杂度代替splice
方法,这里使用map
实现
方法一 使用splice
是为了将已经使用过的元素 从数组中删除,避免重复使用 。可以使用map
记录相同元素的个数 来实现相同的功能,每当元素被使用一次后,元素的可用次数 减一,当下一个连续的元素 的个数为 0 时,表示hand
数组不满足顺子的条件
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 function isNStraightHand (hand, groupSize ) { const length = hand.length; if (length % groupSize !== 0 ) return false ; const map = new Map (); hand.sort((i, j ) => i - j); for (let i = 0 ; i < length; i++) { const count = map.get(hand[i]); map.set(hand[i], (count || 0 ) + 1 ); } for (let i = 0 ; i < length; i++) { let size = groupSize; let val = hand[i]; let count; if (!map.get(val)) continue ; while (size) { count = map.get(val); if (!count) return false ; map.set(val, count - 1 ); val++; size--; } } return true ; }
一维数组转二维数组 题目链接
难度:简单
思路:两层循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function construct2DArray (original, m, n ) { const result = []; if (original.length / n !== m) { return result; } for (let i = 0 ; i < m; i++) { const arr = []; for (let j = 0 ; j < n; j++) { arr.push(original[i * n + j]); } result.push(arr); } return result; }
思路:一层循环
两种方法的时间复杂度 是一样的
第一种方法的时间复杂度是O(mn)
第二种方法的时间复杂度是O(original.length)
,然而original.length === m * n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function construct2DArray (original, m, n ) { const result = []; const length = original.length; if (length / n !== m) { return result; } let arr = []; for (let i = 0 ; i < length; i++) { arr.push(original[i]); if (arr.length === n) { result.push(arr); arr = []; } } return result; }
至少是其他数字两倍的最大数 题目链接
难度:简单
思路
整体思路就是找出nums
中的最大值max
,再找出第二大值secondMax
判断max >= secondMax * 2
是否成立,成立则存在满足条件的最大数
对secondMax
满足以上条件,则对其他数一定满足以上条件
找max
和secondMax
值的方式还有很多,例如使用排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function dominantIndex (nums ) { let max = -1 ; let secondMax = -1 ; let index = -1 ; for (let i = 0 ; i < nums.length; i++) { if (nums[i] > max) { secondMax = max; max = nums[i]; index = i; } else if (nums[i] > secondMax) { secondMax = nums[i]; } } return max >= secondMax * 2 ? index : -1 ; }
最小时间差 题目链接
难度:中等
思路:暴力法,两层循环
当前时间和之后的所有时间进行比较,找出最小差
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function findMinDifference (timePoints ) { const length = timePoints.length; const formatTime = (time ) => { const [hour, minute] = time.split(":" ); return +hour * 60 + +minute; }; const wholeTime = formatTime("24:00" ); let min = Infinity ; for (let i = 0 ; i < length - 1 ; i++) { const currTime = formatTime(timePoints[i]); for (let j = i + 1 ; j < length; j++) { const nextTime = formatTime(timePoints[j]); const minus = Math .abs(currTime - nextTime); min = Math .min(Math .min(wholeTime - minus, minus), min); } } return min; }
思路:排序 + 一次循环
从小到大对时间进行排序
排序后最小时间差 可以通过相邻时间比较 和首尾时间比较 得到
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 function findMinDifference (timePoints ) { const formatTime = (time ) => { const [hour, minute] = time.split(":" ); return +hour * 60 + +minute; }; let min = Infinity ; timePoints.sort(); const firstTime = formatTime(timePoints[0 ]); let preTime = firstTime; for (let i = 1 ; i < timePoints.length; i++) { const currTime = formatTime(timePoints[i]); min = Math .min(currTime - preTime, min); preTime = currTime; } const minus = Math .abs(firstTime - preTime); min = Math .min(min, Math .min(1440 - minus, minus)); return min; }
存在重复元素 II 题目链接
重点关注
难度:简单
思路:暴力法
让下标为i
的元素和i + 1
到i + k
这个范围内的所有元素进行比较,判断是否满足题目的条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function containsNearbyDuplicate (nums, k ) { for (let i = 0 ; i < nums.length; i++) { for (let j = i + 1 ; j <= i + k; j++) { if (nums[i] === nums[j]) { return true ; } } } return false ; }
思路:哈希表
创建 HashMap,用来存储元素对应的下标
遍历数组,判断当前元素在 map 中是否存在,如果存在取出上一个相同元素的下标,判断与当前下标的差是否小于等于 k。如果不存在则存入 map
利用 hashMap 减少一次循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function containsNearbyDuplicate (nums, k ) { const map = new Map (); for (let i = 0 ; i < nums.length; i++) { const index = map.get(nums[i]); if (typeof index !== "undefined" ) { if (Math .abs(index - i) <= k) return true ; } map.set(nums[i], i); } return false ; }
单调数列 题目链接
难度:简单
思路:一次遍历
假设 nums 是单调数列
根据首尾元素判断数组单调的方向
遍历数组,判断数组是否满足该方向上的单调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function isMonotomic (nums ) { const length = nums.length; if (nums[0 ] >= nums[length - 1 ]) { for (let i = 0 ; i < length; i++) { if (nums[i] < nums[i + 1 ]) return false ; } } else { for (let i = 0 ; i < length; i++) { if (nums[i] > nums[i + 1 ]) return false ; } } return true ; }
思路:一次遍历,参考题解
定义变量 dec 和 inc 表示是否为递减或者递增数列,默认为 true
遍历数组,如果当前项大于下一项,让 inc 为 false; 如果当前项小于下一项,dec 为 false
最后只要有一个变量为 true 则表示是单调数列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function isMonotomic (nums ) { let dec = true ; let inc = true ; for (let i = 0 ; i < nums.length - 1 ; i++) { if (nums[i] > nums[i + 1 ]) { inc = false ; } if (nums[i] < nums[i + 1 ]) { dec = false ; } } return dec || inc; }
排序数组 题目链接
难度:中等
思路:递归快排
在数组中取一个中间值,然后遍历数组
将大于这个中间值的值放在右侧,小于的放在左侧
然后在对左右两侧的值进行相同的操作
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 function sortArray (nums ) { if (nums.length <= 1 ) { return nums; } const index = Math .floor(nums.length / 2 ); const middle = nums[index]; const left = []; const right = []; nums.forEach((num, i ) => { if (i === index) return ; if (num > middle) { right.push(num); } else if (num <= middle) { left.push(num); } }); return sortArray(left).concat(middle).concat(sortArray(right)); }
两数之和 题目链接
难度:简单
思路:哈希表
创建哈希 map,key 为数组的某个值,value 是这个值在数组中对应的下标
遍历数组,判断 map 中是否存在 key 为 target - nums[i]的值
存在则找到了和为 target 的值,返回其下标,不存在则将当前值和下标存入 map
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function twoSum (nums, target ) { const map = new Map (); for (let i = 0 ; i < nums.length; i++) { const value = map.get(target - nums[i]); if (typeof value === "undefined" ) { map.set(nums[i], i); } else { return [value, i]; } } }
游戏中弱角色的数量 题目链接
难度:中等
思路:排序
按照攻击力从大到小进行排序,当遇到攻击力相等的角色时,让防御力大的角色在后面
创建一个变量用来表示当前最大的防御力
遍历数组,因为攻击力是从大到小排序的,因此只需要比较防御力即可
记录目前防御力的最大值,如果当前角色的防御力小于最大值,则表示当前角色是弱角色
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 function numberOfWeakCharacters (properties ) { properties.sort((a, b ) => (a[0 ] === b[0 ] ? a[1 ] - b[1 ] : b[0 ] - a[0 ])); let count = 0 ; let maxDef = 0 ; for (let p of properties) { if (p[1 ] > maxDef) { maxDef = p[1 ]; } else if (p[1 ] < maxDef) { count++; } } return count; }
买卖股票的最佳时机(关注) 题目链接
难点:简单
思路:一次遍历
要想获得最大利润,肯定是在最低点买入,最高点卖出,关键在于如何找到最低点和最高点(肯定是在最低点后面)
创建min
代表当前遇到的最低股票,profit
代表当前的最大利润
难点在于需要动态的更新min
最小值,但是更新了并不代表最大利润也会更新,这个需要注意
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 function maxProfit (prices ) { let min = Infinity ; let profit = 0 ; for (let i = 0 ; i < prices.length - 1 ; i++) { if (prices[i] < min) { min = prices[i]; } if (prices[i] > prices[i + 1 ]) { continue ; } if (min < prices[i + 1 ]) { profit = Math .max(prices[i + 1 ] - min, profit); } } return profit; }
二进制 位运算符
&
,与运算符: 运算符两侧的值都是 1 才返回 1,否则返回 0
|
,或运算符: 运算符两侧只要有一个 1 就返回 1,两个全是 0 返回 0
~
,非运算符: 按位取反
^
,按位异或运算符: 不同值异或返回 1,相同值异或返回 0
<<
,左移运算符: 将二进制值向左移动,右侧补 0
>>
,有符号右移: 将二进制向右移动,左侧的值补符号位
>>>
,无符号右移: 将二进制向右移动,左侧补 0
二进制中 1 的个数 题目链接
难度:简单
思路:位运算
1 & 1 => 1,1 & 0 = 0
,让二进制的每一位 &
1,即可知道当前位是不是 1
>>>
无符号右移位操作符 可以向右移动二进制
1 2 3 4 5 6 7 8 9 function hammingWeight (n ) { let count = 0 ; for (let i = 0 ; i < 32 ; i++) { if ((n >>> i) & 1 ) { count++; } } return count; }
其他 完美数 题目链接
难度:简单
思路:暴力枚举
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function checkPerfectNumber (num ) { let sum = 0 ; for (let i = 1 ; i < num; i++) { const val = num / i; if (val === Math .floor(val)) { sum += i; } if (sum > num) return false ; } return sum === num; }
LRU 缓存(关注) 题目链接
难度:中等
思路:哈希表 + 双向链表,参考题解
LRU(least recently used)最近最少次使用
LRU 数据结构涉及到了查询 和删除 ,并且要求O(1)
的时间复杂度
查询操作 肯定会使用O(1)
的哈希表
删除操作 有多种方法,但是满足时间复杂度为0(1)
的只有双向链表
数组 的删除和插入等操作都是O(n)
单向链表 的删除操作需要从头节点 开始遍历,也是O(n)
双向链表 存有上一个节点 ,因此删除当前节点只需要O(1)
key-value
存在链表节点中,被使用的key
更新到链表头部,那么链表尾部就是最久未使用的值 。哈希表存储key-Node
来和链表建立引用关系
get
和put
操作都会更新链表的顺序
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 class DoubleListNode { constructor (key, value ) { this .key = key; this .value = value; this .pre = null ; this .next = null ; } } class LRUCache { constructor (capacity ) { this .capacity = capacity; this .usage = 0 ; this .cache = new Map (); this .dummyHead = new DoubleListNode(); this .dummyTail = new DoubleListNode(); this .dummyHead.next = this .dummyTail; this .dummyTail.pre = this .dummyHead; } isExist (key ) { return this .cache.has(key); } get (key ) { const node = this .cache.get(key); if (!this .isExist(key)) return -1 ; this .moveToHead(node); return node.value; } put (key, value ) { if (this .isExist(key)) { const node = this .cache.get(key); node && (node.value = value); this .moveToHead(node); } else { const newNode = new DoubleListNode(key, value); if (this .usage >= this .capacity) { const deleteNode = this .dummyTail.pre; this .removeNode(deleteNode); this .cache.delete(deleteNode.key); this .usage--; } this .addNewNode(newNode); this .cache.set(key, newNode); this .usage++; } } moveToHead (node ) { this .removeNode(node); this .addNewNode(node); } addNewNode (node ) { node.next = this .dummyHead.next; this .dummyHead.next.pre = node; this .dummyHead.next = node; node.pre = this .dummyHead; } removeNode (node ) { node.next.pre = node.pre; node.pre.next = node.next; } } const LRU = new LRUCache(2 );