更多文章可看专栏:juejin.cn/column/7423…
一、数组
(一) 移除元素
//时间复杂度:O(n)
//空间复杂度:O(1)
var removeElement = (nums, val) => {
let k = 0;
for(let i = 0;i < nums.length;i++){
if(nums[i] != val){
nums[k++] = nums[i]
}
}
return k;
};
(二) 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
TIP
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
var addTwoNumbers = function(l1, l2) {
let addOne = 0
// 创建一个头链表用于保存结果
let sum = new ListNode('0')
// 保存头链表的位置用于最后的链表返回
let head = sum
while (addOne || l1 || l2) {
let val1 = l1 !== null ? l1.val : 0 // 优化点
let val2 = l2 !== null ? l2.val : 0 //优化点
let r1 = val1 + val2 + addOne
addOne = r1 >= 10 ? 1 : 0
sum.next = new ListNode(r1 % 10)
sum = sum.next
if (l1) l1 = l1.next
if (l2) l2 = l2.next
}
//返回计算结果,之所以用head.next是因为head中保存的第一个节点是刚开始定义的“0”
return head.next
};
(三) 寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
TIP
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
暴力法
var findMedianSortedArrays = function(nums1, nums2) {
let n = nums1.length + nums2.length;
let nums = nums1.concat(nums2).sort((a, b) => a - b);
let result = n % 2 == 0
? (nums[n/2] + nums[n/2-1]) / 2
: nums[Math.floor(n/2)];
return result;
};
双指针法
因为两个数组有序,求中位数不需要把两个数组合并
当合并后的数组总长度len为奇数时,只要知道索引为len/2位置上的数就行了,如果数偶数,只要知道索引为len/2 - 1和len/2上的数就行,所以不管是奇数还是偶数只要遍历len/2次即可,用两个值来存遍历过程中len/2-1和len/2上的数即可
两个指针point1和point2分别指向nums1和nums2,当nums1[point1] < nums2[point2],则point1指针移动,否则point2指针移动
var findMedianSortedArrays = function(nums1, nums2) {
let n1 = nums1.length;
let n2 = nums2.length;
// 两个数组总长度
let len = n1 + n2;
// 保存当前移动的指针的值(在nums1或nums2移动),和上一个值
let preValue = -1;
let curValue = -1;
// 两个指针分别在nums1和nums2上移动
let point1 = 0;
let point2 = 0;
// 需要遍历len/2次,当len是奇数时,最后取curValue的值,是偶数时,最后取(preValue + curValue)/2的值
for (let i = 0; i <= Math.floor(len/2); i++) {
preValue = curValue;
// 需要在nums1上移动point1指针
if (point1 < n1 && (point2 >= n2 || nums1[point1] < nums2[point2])) {
curValue = nums1[point1];
point1++;
} else {
curValue = nums2[point2];
point2++;
}
}
return len % 2 === 0
? (preValue + curValue) / 2
: curValue
};
(四) 最大子数组和
// 输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
// 输出:6
// 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
var maxSubArray = function(nums) {
let pre = 0
let max = nums[0]
nums.forEach((item)=>{
pre = Math.max(pre+item,item)
max = Math.max(max, pre)
})
return max
};
(五) 最长连续递增序列
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
var findLengthOfLCIS = function(nums) {
let ans = 0;
const n = nums.length;
let start = 0;
for (let i = 0; i < n; i++) {
if (i > 0 && nums[i] <= nums[i - 1]) {
start = i;
}
ans = Math.max(ans, i - start + 1);
}
return ans;
};
(六) 有序数组中数字最后一次出现的位置
// 最简答的方式就是直接遍历然后根据有序的条件找到当前值等于目标且下一个值不等于目标的结果
// 写出来之后面试官问了时间复杂度,这个就是单层循环的 O(N),最坏情况就是刚好最后一个值是目标值
const findLast = (nums, target) => {
for (let i = 0; i < nums.length; i++) {
if (target === nums[i] && target !== nums[i + 1]) {
return i;
}
}
return -1;
};
// 二分查找,对于已经有序的数组,只需要通过双指针不断更新左右边界位置就行
// 二分法最主要的就是寻找二分结束的边界条件,这里选择所有的查找最后都只剩两个值
// 然后对这两个值再额外判断一下是否符合结果
// 二分法的时间复杂度O(logN)
// 二分查找最坏的情况是刚好第一个值或者最后一个值,或者中间值是目标值
const findLast2 = (nums, target) => {
let left = 0;
let right = nums.length - 1;
while (right > left + 1) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid;
}
}
if (nums[right] === target) {
return right;
}
if (nums[left] === target) {
return left;
}
return -1;
};
// 扩展 开始与结束为止
var searchRange = function(nums, target) {
if(nums.length === 0){
return [-1,-1]
}
if(nums.indexOf(target)===-1){
return [-1,-1]
}
return [nums.indexOf(target),nums.lastIndexOf(target)]
};
(七) 有序数组中查找元素出现的第一个和最后一个位置
const binarySearch = (nums, target, lower) => {
let left = 0, right = nums.length - 1, ans = nums.length;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
var searchRange = function(nums, target) {
let ans = [-1, -1];
const leftIdx = binarySearch(nums, target, true);
const rightIdx = binarySearch(nums, target, false) - 1;
if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] === target && nums[rightIdx] === target) {
ans = [leftIdx, rightIdx];
}
return ans;
};
二、哈希表
(一) 两数之和
function twoSum(nums, target) {
let map = {}
for(let i = 0;i<nums.length;i++){
let diff = target - nums[i]
if(diff in map){
return [map[diff],i]
}
map[nums[i]] = i
}
}
(二) 两个数组的交集
function intersection(nums1, nums2) {
let resSet = new Set();
let nums1Set = new Set(nums1);
for (let i of nums2) {
if (nums1Set.has(i)) {
resSet.add(i);
}
}
return Array.from(resSet);
};
function intersection(nums1, nums2) {
return Array.from(new Set(nums1.filter(i => nums2.includes(i))))
};
(三) 统计字符串中出现最多的字符
一边进行计数统计一遍进行大小比较,只需要 1 次 O(n) 的算法复杂度
function getFrequentChar2 (str) {
const dict = {}
let maxChar = ['', 0]
for (const char of str) {
dict[char] = (dict[char] || 0) + 1
if (dict[char] > maxChar[1]) {
maxChar = [char, dict[char]]
}
}
return maxChar
}
三、字符串
(一) 最长公共前缀
// 输入:strs = ["flower","flow","flight"]
// 输出:"fl"
/**
* @param {string[]} strs
* @return {string}
*/
var longestCommonPrefix = function(strs) {
let res = ''
// 假设最长前缀
let s = strs[0]
if(!s) return ''
for(let i = 0;i<s.length;i++){
let flag = strs.every((item)=>item[i]===s[i])
if(flag){
res += s[i]
}else {
return res
}
}
return res
};
(二) 最长公共子串
function longestCommonSubstring(strings) {
if (strings.length === 0) return '';
// 找到最短的字符串,因为最长公共子串不会比最短的字符串长
let shortestStr = strings.reduce((a, b) => a.length <= b.length ? a : b);
let longestCommonSubstr = '';
for (let i = 0; i < shortestStr.length; i++) {
for (let j = i + 1; j <= shortestStr.length; j++) {
let substr = shortestStr.slice(i, j);
if (strings.every(str => str.includes(substr)) && substr.length > longestCommonSubstr.length) {
longestCommonSubstr = substr;
}
}
}
return longestCommonSubstr;
}
// 示例
const strings = ['abc', 'eabc', 'abcd'];
console.log(longestCommonSubstring(strings)); // 输出: 'abc'
(三) 最长回文子串
本题要求在一个字符串中寻找最长的回文子串,回文大家都知道,存在单数与双数的情况,如:
单数:"ABCBA",该回文串的中心为C
双数:"DBBD",该回文串的中心为BB的中间空隙
核心思想是:遍历原字符串,每个字符或每两个字符中间,都可能被当成回文子串的中心,利用回文串的中心对称的特点,尽量往两边扩散,获取最大的“扩散面积”
var longestPalindrome = function(s) {
// s为空字符串或为长为1的字符串,返回字符串本身
if (s.length < 2) return s;
let res = '';
// 遍历每个可能的中心点位,以左右指针模拟中心点
for (let i = 0; i < s.length; i++) {
// 双数情况
getCenter(i, i);
// 单数情况
getCenter(i, i + 1);
}
// 本函数的作用为:获取最长的,以本中心点为中心的回文串
function getCenter(left, right) {
// 边界条件:左指针不小于0,右指针不超过数组的最长长度。
// 进入循环条件:满足边界条件,且当前两个指针指向的字符相等
while (left >= 0 && right < s.length && s[left] == s[right]) {
// 左侧指针左移,右侧指针右移,开启下次字符相等的判断循环。当超出系统边界或两指针指向的字符不相等,则退出
left--;
right++;
}
// 循环结束,两指针目前指向的字符串中间其实是不满足回文串
// 事实上本次while获得的回文串的左侧为left + 1,右侧为right - 1
// 所以本次获得的回文串长度为 (right - 1) - (left + 1) + 1 = right - left - 1,与res长度判断后取最长的回文子串
if (right - left - 1 > res.length) {
// 记住这里需要截取的是正确的回文子串,所以要消除while循环中,最后一次不满足条件的left、right的影响
/**
* left => left + 1
* right - 1 => right - 1 + 1 = right
**/
res = s.slice(left + 1, right);
}
}
return res
};
(四) 无重复字符的最长子串
function lengthOfLongestSubstring(s) {
let len = s.length;
let result = 0;
let set = new Set();
// 左指针用来收缩窗口
let left = 0;
// 右指针用来扩张窗口
let right = 0;
while (left < len) {
// 如果不重复,就不断扩张窗口,元素添加到set中
while (right < len && !set.has(s[right])) {
set.add(s[right]);
right++;
}
// 到这里说明有元素重复了,先记录子串长度,然后收缩窗口
result = Math.max(result, right - left);
// 收缩窗口
set.delete(s[left]);
left++;
}
return result;
}
function lengthOfLongestSubstring(s) {
let len = s.length;
let result = 0;
for (let i = 0; i < len; i++) {
let set = new Set();
let maxLen = 0;
// 从i的位置遍历得到最长子串的长度
let j = i;
while (j < len && !set.has(s[j])) {
set.add(s[j]);
maxLen++;
j++;
}
// 取历史最大值
result = Math.max(result, maxLen);
}
return result;
}
(五) 最小覆盖字串
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
var minWindow = function(s, t) {
let left = 0;
let right = 0;
let res = '';
const map = new Map()
for(let i=0;i<t.length;i++){
map.set(t[i], map.get(t[i]) ? map.get(t[i]) + 1 : 1)
}
let needType = map.size;
while(right<s.length){
const cur = s[right]
if(map.has(cur)){
map.set(cur,map.get(cur)-1)
if(map.get(cur)===0){
needType-=1
}
}
while(needType === 0){
const leftChar = s[left]
let newRes = s.slice(left,right+1)
if(!res||res.length>newRes.length){
res = newRes
}
if(map.has(leftChar)){
map.set(leftChar,map.get(leftChar)+1)
if(map.get(leftChar)===1){
needType += 1
}
}
left++
}
right++
}
return res
};
四、链表
function ListNode(val) {
this.val = val;
this.next = null;
}
(一) 反转链表
var reverseList = function(head) {
let cur = head
let pre = null
while(cur){
let next = cur.next
cur.next = pre
pre = cur
cur = next
}
return pre
};
(二) 合并两个有序链表
var mergeTwoLists = function(l1, l2) {
let head = new ListNode(0)
let now = head
while(l1 !==null && l2 !==null){
if(l1.val <= l2.val){
now.next = l1
l1 = l1.next
} else {
now.next = l2
l2 = l2.next
}
now = now.next
}
now.next = l1===null ? l2 : l1
return head.next
};
var mergeTwoLists = function(l1, l2) {
if(l1 === null) return l2;
if(l2 === null) return l1;
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next, l2)
return l1
} else {
l2.next = mergeTwoLists(l1,l2.next)
return l2
}
};
(三) 合并K个升序链表
其实就是上一题的多个版
var mergeKLists = function(lists) {
function merge2List(l1,l2){
if(l1===null) return l2
if(l2===null) return l1
while(l1!==null && l2!==null){
if(l1.val < l2.val){
l1.next = merge2List(l1.next,l2)
return l1
}else{
l2.next = merge2List(l1,l2.next)
return l2
}
}
}
let res = null
for(let i =0;i<lists.length;i++){
res = merge2List(res,lists[i])
}
return res
};
(四) 判断链表是否有环
var hasCycle = function(head) {
if(head === null){
return false
}
let slow = head
let fast = head.next
while(slow !== null && fast !== null && fast.next !== null){
if(slow === fast){
return true
}
slow = slow.next
fast = fast.next.next
}
return false
};
(五) 判断链表是否有环并返回入口位置
var detectCycle = function(head) {
if(head===null || head.next===null) return null
let slow = head.next
let fast = head.next.next
while(fast&&fast.next){
slow = slow.next
fast = fast.next.next
if(slow===fast){
slow = head
while(fast!==slow){
slow = slow.next
fast = fast.next
}
return slow
}
}
return null
};
(六) 删除链表的倒数第N个节点
var removeNthFromEnd = function (head, n) {
// 创建哨兵节点,简化解题逻辑
let dummyHead = new ListNode(0, head);
let fast = dummyHead;
let slow = dummyHead;
while (n--) fast = fast.next;
while (fast.next !== null) {
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return dummyHead.next;
};
(七) 判断两个链表是否相交
var getIntersectionNode = function(headA, headB) {
let visited = new Set()
let temp = headA
while(temp!=null){
visited.add(temp)
temp = temp.next
}
temp = headB;
while(temp!==null){
if(visited.has(temp)){
return temp
}
temp = temp.next
}
return null
};
(八) K个一组翻转链表
/**
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
var reverseKGroup = function(head, k) {
let arr = [] // 存储处理完之后的数据
let newhead = new ListNode(); // 新链表节点
let now = newhead // 辅助节点
while(head){ // 将链表转化为数组
arr.push(head.val)
head = head.next
}
function splitItem(arr,k){ // 该函数用于分割成 k 份
let res = []
for(let i = 0;i<Math.ceil(arr.length/k);i++){
let start = i * k;
let end = start + k;
res.push(arr.slice(start,end))
}
return res
}
arr = splitItem(arr,k)
for(let i of arr){ // 满足k长度才反转
if(i.length === k){
i = i.reverse()
}
}
arr = arr.flat(2) // 去除括号,扁平化数组
for(let i =0;i<arr.length;i++){ // 重新根据数组构造链表
now.next = new ListNode(arr[i])
now = now.next
}
return newhead.next // 返回
};
五、双指针法
(一) 三数之和
var threeSum = function(nums) {
const res = [], len = nums.length
// 将数组排序
nums.sort((a, b) => a - b)
for (let i = 0; i < len; i++) {
let l = i + 1, r = len - 1, iNum = nums[i]
// 数组排过序,如果第一个数大于0直接返回res
if (iNum > 0) return res
// 去重
if (iNum == nums[i - 1]) continue
while(l < r) {
let lNum = nums[l], rNum = nums[r], threeSum = iNum + lNum + rNum
// 三数之和小于0,则左指针向右移动
if (threeSum < 0) l++
else if (threeSum > 0) r--
else {
res.push([iNum, lNum, rNum])
// 去重
while(l < r && nums[l] == nums[l + 1]){
l++
}
while(l < r && nums[r] == nums[r - 1]) {
r--
}
l++
r--
}
}
}
return res
};
(二) 盛最多水的容器
我们每次以双指针为左右边界(也就是「数组」的左右边界)计算出的容量中的最大值
var maxArea = function(height) {
let maxArea = 0;
let L = 0;
let R = height.length -1;
while(L < R){
if(height[L] < height[R]){
maxArea = Math.max(maxArea, (R - L) * height[L])
L++;
} else {
maxArea = Math.max(maxArea, (R - L) * height[R])
R--;
}
}
return maxArea
};
(三) 回文链表
输入:head = [1,2,2,1]
输出:true
const isPalindrome = (head) => {
const vals = [];
while (head) { // 丢进数组里
vals.push(head.val);
head = head.next;
}
let start = 0, end = vals.length - 1; // 双指针
while (start < end) {
if (vals[start] != vals[end]) { // 理应相同,如果不同,不是回文
return false;
}
start++;
end--; // 双指针移动
}
return true; // 循环结束也没有返回false,说明是回文
};
(四) 和为N的连续正数序列(文件组合)
输入一个正数N,输出所有和为N的连续正数序列
例如:输入15
结果:[[1,2,3,4,5],[4,5,6],[7,8]]
var fileCombination = function(target) {
let list = [];
let left = 1;
let right = 1;
let sum = 0;
while(left < target/2) {
if(sum < target) {
sum += right;
right++;
} else if (sum > target) {
sum -= left;
left++;
} else {
let arr = [];
for (let i = left; i < right; i++) {
arr.push(i);
}
list.push(arr);
sum -= left;
left++;
}
}
return list;
};
六、栈与队列
(一) 用栈实现队列
var MyQueue = function() {
this.stack1 = []
this.stack2 = []
};
MyQueue.prototype.push = function(x) {
this.stack1.push(x)
};
MyQueue.prototype.pop = function() {
const size = this.stack2.length
if(size){
return this.stack2.pop()
}
while(this.stack1.length){
this.stack2.push(this.stack1.pop())
}
return this.stack2.pop()
};
MyQueue.prototype.peek = function() {
const x = this.pop()
this.stack2.push(x)
return x
};
MyQueue.prototype.empty = function() {
return !this.stack1.length && !this.stack2.length
};
/**
* var obj = new MyQueue()
* obj.push(x)
* var param_2 = obj.pop()
* var param_3 = obj.peek()
* var param_4 = obj.empty()
*/
(二) 用队列实现栈
var MyStack = function() {
this.queue = []
};
MyStack.prototype.push = function(x) {
this.queue.push(x)
};
MyStack.prototype.pop = function() {
let size = this.queue.length
while(size-- > 1){
this.queue.push(this.queue.shift())
}
return this.queue.shift()
};
MyStack.prototype.top = function() {
const x = this.pop()
this.queue.push(x)
return x
};
MyStack.prototype.empty = function() {
return !this.queue.length
};
/**
* var obj = new MyStack()
* obj.push(x)
* var param_2 = obj.pop()
* var param_3 = obj.top()
* var param_4 = obj.empty()
*/
(三) 有效的括号
var isValid = function(s) {
let stack = []
let map = {
'{' : '}',
'[' : ']',
'(' : ')',
}
for(let i = 0;i<s.length;i++){
if(map[s[i]]){
stack.push(map[s[i]])
} else {
if(stack.pop()!==s[i]){
return false
}
}
}
if(stack.length === 0){
return true
}
return false
};
七、二叉树
(一) 前序遍历
var preorderTraversal = function(root) {
let res=[];
const dfs=function(root){
if(root===null)return;
//先序遍历所以从父节点开始
res.push(root.val);
//递归左子树
dfs(root.left);
//递归右子树
dfs(root.right);
}
//只使用一个参数 使用闭包进行存储结果
dfs(root);
return res;
};
(二) 中序遍历
var inorderTraversal = function(root) {
let res=[];
const dfs=function(root){
if(root===null){
return ;
}
dfs(root.left);
res.push(root.val);
dfs(root.right);
}
dfs(root);
return res;
};
(三) 后序遍历
var postorderTraversal = function(root) {
let res=[];
const dfs=function(root){
if(root===null){
return ;
}
dfs(root.left);
dfs(root.right);
res.push(root.val);
}
dfs(root);
return res;
};
(四) 层序遍历
var levelOrder = function(root) {
//二叉树的层序遍历
let res = [];
let queue = [];
queue.push(root);
if(root === null) {
return res;
}
while(queue.length !== 0) {
// 记录当前层级节点数
let length = queue.length;
//存放每一层的节点
let curLevel = [];
for(let i = 0;i < length; i++) {
let node = queue.shift();
curLevel.push(node.val);
// 存放当前层下一层的节点
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
//把每一层的结果放到结果数组
res.push(curLevel);
}
return res;
};
(五) 锯齿型层序遍历
var zigzagLevelOrder = function(root) {
let queue = []
let res = []
queue.push(root)
while(queue.length && root !==null){
let curLevel = []
let len = queue.length
while(len--){
let node = queue.shift()
curLevel.push(node.val)
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
res.push(curLevel)
}
res.forEach((item,index)=>{
if(index%2===1){
item.reverse()
}
})
return res
};
(六) 二叉树翻转
var invertTree = (root) => {
if (root === null) {
return null;
}
var left = invertTree(root.left);
var right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
};
(七) 最近公共祖先
var lowestCommonAncestor = function(root, p, q) {
function search(node,p,q){
if(node === null || p === node || q === node){
return node
}
const left = search(node.left, p,q)
const right = search(node.right, p,q)
if(left!==null && right!==null){
return node
}
if(left === null){
return right
}
return left
}
return search(root,p,q)
};
(八) 二叉树的最大深度
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
var maxDepth = function(root) {
let queue = []
queue.push(root)
let dept = 0
while(queue.length && root !==null){
let len = queue.length;
dept++
while(len--){
let node = queue.shift()
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
}
return dept
};
(九) 二叉树是否有总和等于target
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
var hasPathSum = function(root, targetSum) {
if(!root){
return false
}
function search(root, targetSum){
if(targetSum === 0 && !root.left && !root.right){
return true
}
if(!root.left && !root.right){
return false
}
if(root.left && search(root.left, targetSum - root.left.val)){
return true
}
if(root.right && search(root.right, targetSum - root.right.val)){
return true
}
return false
}
return search(root, targetSum - root.val)
};
八、回溯算法
(一) 全排列
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
var permute = function(nums) {
const res = [], path = [];
backtracking(nums, nums.length, []);
return res;
function backtracking(n, k, used) {
if(path.length === k) {
res.push(Array.from(path));
return;
}
for (let i = 0; i < k; i++ ) {
if(used[i]) continue;
path.push(n[i]);
used[i] = true; // 同支
backtracking(n, k, used);
path.pop();
used[i] = false;
}
}
};
(二) 全排列(II)
算法题,fn([['a', 'b'], ['n', 'm'], ['0', '1']]) => ['an0', 'am0', 'an1', 'am1', 'bn0', 'bm0', 'bn1', 'bm0']
function getList(matrix) {
const result = [];
const len = matrix.length;
function dfs(res, curr) {
if (res.length === len) {
result.push(res.join(""));
return;
}
for (let i = 0; i < matrix[curr].length; i++) {
res.push(matrix[curr][i]);
dfs(res, curr + 1);
res.pop();
}
}
dfs([], 0);
return result;
}
(三) 子集问题
var subsets = function(nums) {
let result = []
let path = []
function backtracking(startIndex) {
result.push([...path])
for(let i = startIndex; i < nums.length; i++) {
path.push(nums[i])
backtracking(i + 1)
path.pop()
}
}
backtracking(0)
return result
};
(四) 复原IP地址
var restoreIpAddresses = function(s) {
const res = [], path = [];
backtracking(0, 0)
return res;
function backtracking(i) {
const len = path.length;
if(len > 4) return;
if(len === 4 && i === s.length) {
res.push(path.join("."));
return;
}
for(let j = i; j < s.length; j++) {
const str = s.slice(i, j + 1);
if(str.length > 3 || +str > 255) break;
if(str.length > 1 && str[0] === "0") break;
path.push(str);
backtracking(j + 1);
path.pop()
}
}
};
(五) 电话号码的字母组合
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
var letterCombinations = function(digits) {
const k = digits.length;
const map = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"];
if(!k) return [];
if(k === 1) return map[digits].split("");
const res = [], path = [];
backtracking(digits, k, 0);
return res;
function backtracking(n, k, a) {
if(path.length === k) {
res.push(path.join(""));
return;
}
for(const v of map[n[a]]) {
path.push(v);
backtracking(n, k, a + 1);
path.pop();
}
}
};
九、动态规划
(一) 斐波那契数
var fib = function(n) {
let dp = [0, 1]
for(let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}
console.log(dp)
return dp[n]
};
(二) 爬楼梯
var climbStairs = function(n) {
// dp[i] 为第 i 阶楼梯有多少种方法爬到楼顶
// dp[i] = dp[i - 1] + dp[i - 2]
let dp = [1 , 2]
for(let i = 2; i < n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}
return dp[n - 1]
};
(三) 最长递增子序列
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
var lengthOfLIS = function (nums) {
const dp = new Array(nums.length).fill(1);
for (let i = 0; i < nums.length; i++) {
// i与i前面的元素比较
for (let j = 0; j < i; j++) {
// 找比i小的元素,找到一个,就让当前序列的最长子序列长度加1
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
// 找出最大的子序列
return Math.max(...dp);
};
(四) 打家劫舍
const rob = nums => {
// 数组长度
const len = nums.length;
// dp数组初始化
const dp = [nums[0], Math.max(nums[0], nums[1])];
// 从下标2开始遍历
for (let i = 2; i < len; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[len - 1];
};
(五) 一个数组(不连续)最大子项的和
function maxNonAdjacentSum(nums) {
if (nums.length === 0) return 0;
if (nums.length === 1) return nums[0];
let dp = new Array(nums.length).fill(0);
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (let i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i-1], nums[i] + dp[i-2]);
}
return dp[nums.length - 1];
}
// 示例
const nums = [3, 2, 5, 10, 7];
console.log(maxNonAdjacentSum(nums)); // 输出 15 (选择 3, 10, 2)
(六) 乘积最大子数组
function maxProduct(nums) {
let maxProduct = nums[0]; // 初始化最大乘积
// 遍历每个起始点
for (let i = 0; i < nums.length; i++) {
let currentProduct = 1; // 当前乘积,初始化为1
// 从起始点 i 开始计算到数组末尾的子数组乘积
for (let j = i; j < nums.length; j++) {
currentProduct *= nums[j]; // 更新当前乘积
// 更新最大乘积
maxProduct = Math.max(maxProduct, currentProduct);
}
}
return maxProduct;
}
// 示例
const nums = [2, 3, -2, 4];
console.log(maxProduct(nums)); // 输出: 6
function maxProduct(nums) {
let maxProduct = nums[0];
let currentMax = nums[0];
let currentMin = nums[0];
for (let i = 1; i < nums.length; i++) {
const num = nums[i];
if (num < 0) {
// 负数会交换最大值和最小值
[currentMax, currentMin] = [currentMin, currentMax];
}
currentMax = Math.max(num, currentMax * num);
currentMin = Math.min(num, currentMin * num);
maxProduct = Math.max(maxProduct, currentMax);
}
return maxProduct;
}
// 示例
const nums = [2, 3, -2, 4];
console.log(maxProduct(nums)); // 输出: 6
十、其他
(一) 岛屿问题
输入:grid = [ ["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
DFS为什么要沉岛
- 遍历遇到 1 即遇到土地,土地肯定在一个岛上,计数 +1
- 如果不把与它和同在一个岛的土地变成 0,则DFS遍历到它们时,会对一个岛重复计数
怎么找出同处一岛的所有 1
DFS,以当前 1 为入口
DFS 做的事情:
- 将当前的 1 变 0
- 当前坐标的上下左右依次递归,同处一个岛的 1 都变 0
- dfs 出口:超出矩阵边界,或遇到 0。不用沉岛,直接返回
var numIslands = function(grid) {
let res = 0;
for(let i =0;i<grid.length;i++){
for(let j = 0;j<grid[0].length;j++){
if(grid[i][j]==='1'){
res++;
infect(i,j,grid);
}
}
}
return res
function infect(i,j,grid){
if(i < 0 || j < 0 || i>=grid.length||j>=grid[0].length||grid[i][j]==='0'){
return;
}
grid[i][j] = '0'
infect(i-1,j,grid)
infect(i,j-1,grid)
infect(i+1,j,grid)
infect(i,j+1,grid)
}
};
(二) 买卖股票的最佳时机
因为股票就买卖一次,那么贪心的想法很自然就是取最左最小值,取最右最大值,那么得到的差值就是最大利润。
const maxProfit = prices => {
// 先定义第一天为最低价格
let min = prices[0];
// 利润
let profit = 0;
// 遍历数据
for (let i = 1; i < prices.length; i++) {
// 如果发现比最低价格还低的,更新最低价格
min = Math.min(min, prices[i]);
// 如果发现当前利润比之前高的,更新利润
profit = Math.max(profit, prices[i] - min);
}
return profit;
};
(三) 螺旋矩阵
var spiralOrder = function(matrix) {
if (!matrix.length || !matrix[0].length) {
return [];
}
const rows = matrix.length;
const columns = matrix[0].length;
const order = [];
let left = 0;
let right = columns - 1;
let top = 0;
let bottom = rows - 1;
while (left <= right && top <= bottom) {
for (let column = left; column <= right; column++) {
order.push(matrix[top][column]);
}
for (let row = top + 1; row <= bottom; row++) {
order.push(matrix[row][right]);
}
if (left < right && top < bottom) {
for (let column = right - 1; column > left; column--) {
order.push(matrix[bottom][column]);
}
for (let row = bottom; row > top; row--) {
order.push(matrix[row][left]);
}
}
[left, right, top, bottom] = [left + 1, right - 1, top + 1, bottom - 1];
}
return order;
};