前端算法总结

1. 合并两个有序链表
题⽬描述
将两个升序链表合并为⼀个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输⼊: 1->2->4, 1->3->4
输出: 1->1->2->3->4->4
前置知识
递归
链表
思路
本题可以使⽤递归来解,将两个链表头部较⼩的⼀个与剩下的元素合并,并返回排好序的链表
头,当两条链表中的⼀条为空时终⽌递归。
关键点
掌握链表数据结构
考虑边界情况
代码
JS Code:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
独家版权 抄袭必究 * }
*/
/**
* @param { ListNode } l1
* @param { ListNode } l2
* @return { ListNode }
*/
const 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 ;
}
};
复杂度分析
M N 是两条链表 l1 l2 的⻓度
时间复杂度:
空间复杂度:
扩展
你可以使⽤迭代的⽅式求解么?
迭代的 CPP 代码如下:
class Solution {
public :
ListNode * mergeTwoLists ( ListNode * a , ListNode * b ) {
ListNode head , * tail = & head ;
while ( a && b ) {
if ( a -> val <= b -> val ) {
tail -> next = a ;
a = a -> next ;
} else {
tail -> next = b ;
独家版权 抄袭必究 b
= b -> next ;
}
tail = tail -> next ;
}
tail -> next = a ? a : b ;
return head . next ;
}
};
迭代的 JS 代码如下:
var mergeTwoLists = function ( l1 , l2 ) {
const prehead = new ListNode ( - 1 );
let prev = prehead ;
while ( l1 != null && l2 != null ) {
if ( l1 . val <= l2 . val ) {
prev . next = l1 ;
l1 = l1 . next ;
} else {
prev . next = l2 ;
l2 = l2 . next ;
}
prev = prev . next ;
}
prev . next = l1 === null ? l2 : l1 ;
return prehead . next ;
};
2. 括号⽣成
题⽬描述
数字 n 代表⽣成括号的对数,请你设计⼀个函数,⽤于能够⽣成所有可能的并且 有效的 括号组合。
示例:
输⼊: n = 3
输出: [
"((()))",
"(()())",
"(())()",
独家版权 抄袭必究 "()(())",
"()()()"
]
前置知识
DFS
回溯法
思路
本题是 20. 有效括号 的升级版。
由于我们需要求解所有的可能, 因此回溯就不难想到。回溯的思路和写法相对⽐较固定,并且
回溯的优化⼿段⼤多是剪枝。
不难想到, 如果左括号的数⽬⼩于右括号,我们可以提前退出,这就是这道题的剪枝。 ⽐如
()).... ,后⾯就不⽤看了,直接退出即可。回溯的退出条件也不难想到,那就是:
左括号数⽬等于右括号数⽬
左括号数⽬ + 右括号数⽬ = 2 * n
由于我们需要剪枝, 因此必须从左开始遍历。( WHY ?)
因此这道题我们可以使⽤深度优先搜索 ( 回溯思想 ) ,从空字符串开始构造,做加法, 即 dfs(
括号数 , 右括号数⽬ , 路径 ) , 我们从 dfs(0, 0, '') 开始。
伪代码:
res = []
def dfs ( l , r , s ):
if l > n or r > n : return
if ( l == r == n ): res . append ( s )
# 剪枝,提⾼算法效率
if l < r : return
# 加⼀个左括号
dfs ( l + 1 , r , s + '(' )
# 加⼀个右括号
dfs ( l , r + 1 , s + ')' )
dfs ( 0 , 0 , '' )
return res
独家版权 抄袭必究 由于字符串的不可变性,
因此我们⽆需 撤销
s 的选择 。但是当你使⽤ C++ 等语⾔的时候, 就
需要注意撤销 s 的选择了。类似:
s . push_back ( ')' );
dfs ( l , r + 1 , s );
s . pop_back ();
关键点
l < r 时记得剪枝
代码
JS Code:
/**
* @param { number } n
* @return { string []}
* @param l 左括号已经⽤了⼏个
* @param r 右括号已经⽤了⼏个
* @param str 当前递归得到的拼接字符串结果
* @param res 结果集
*/
const generateParenthesis = function ( n ) {
const res = [];
function dfs ( l , r , str ) {
if ( l == n && r == n ) {
return res . push ( str );
}
// l ⼩于 r 时不满⾜条件 剪枝
if ( l < r ) {
return ;
}
// l ⼩于 n 时可以插⼊左括号,最多可以插⼊ n
if ( l < n ) {
dfs ( l + 1 , r , str + "(" );
}
// r < l 时 可以插⼊右括号
if ( r < l ) {
dfs ( l , r + 1 , str + ")" );
}
}
独家版权 抄袭必究 dfs (
0 , 0 , "" );
return res ;
};
复杂度分析
时间复杂度: O(2^N)
空间复杂度: O(2^N)
3. 合并 K 个排序链表
题⽬描述
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例 :
输⼊ :
[
1->4->5,
1->3->4,
2->6
]
输出 : 1->1->2->3->4->4->5->6
前置知识
链表
归并排序
思路
这道题⽬是合并 k 个已排序的链表,号称 leetcode ⽬前 最难 的链表题。 和之前我们解决的
88.merge-sorted-array 很像。
他们有两点区别:
1. 这道题的数据结构是链表,那道是数组。这个其实不复杂,毕竟都是线性的数据结构。
独家版权 抄袭必究 2. 这道题需要合并
k 个元素,那道则只需要合并两个。这个是两题的关键差别,也是这道题
难度为 hard 的原因。
因此我们可以看出,这道题⽬是 88.merge-sorted-array 的进阶版本。其实思路也有点像,我
们来具体分析下第⼆条。
如果你熟悉合并排序的话,你会发现它就是 合并排序的⼀部分
具体我们可以来看⼀个动画
(动画来⾃ https://zhuanlan.zhihu.com/p/61796021
关键点解析
分治
归并排序 (merge sort)
代码
JavaScript Code
/*
* @lc app=leetcode id=23 lang=javascript
*
* [23] Merge k Sorted Lists
*
* https://leetcode.com/problems/merge-k-sorted-lists/description/
独家版权 抄袭必究 *
*/
function mergeTwoLists ( l1 , l2 ) {
const dummyHead = {};
let current = dummyHead ;
// l1: 1 -> 3 -> 5
// l2: 2 -> 4 -> 6
while ( l1 !== null && l2 !== null ) {
if ( l1 . val < l2 . val ) {
current . next = l1 ; // 把⼩的添加到结果链表
current = current . next ; // 移动结果链表的指针
l1 = l1 . next ; // 移动⼩的那个链表的指针
} else {
current . next = l2 ;
current = current . next ;
l2 = l2 . next ;
}
}
if ( l1 === null ) {
current . next = l2 ;
} else {
current . next = l1 ;
}
return dummyHead . next ;
}
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param { ListNode []} lists
* @return { ListNode }
*/
var mergeKLists = function ( lists ) {
// 图参考: https://zhuanlan.zhihu.com/p/61796021
if ( lists . length === 0 ) return null ;
if ( lists . length === 1 ) return lists [ 0 ];
if ( lists . length === 2 ) {
return mergeTwoLists ( lists [ 0 ], lists [ 1 ]);
}
const mid = lists . length >> 1 ;
const l1 = [];
for ( let i = 0 ; i < mid ; i ++ ) {
独家版权 抄袭必究 l1
[ i ] = lists [ i ];
}
const l2 = [];
for ( let i = mid , j = 0 ; i < lists . length ; i ++ , j ++ ) {
l2 [ j ] = lists [ i ];
}
return mergeTwoLists ( mergeKLists ( l1 ), mergeKLists ( l2 ));
};
复杂度分析
时间复杂度:
空间复杂度:
相关题⽬
88.merge-sorted-array
扩展
这道题其实可以⽤堆来做,感兴趣的同学尝试⼀下吧。
4. 两两交换链表中的节点
题⽬描述
给定⼀个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,⽽是需要实际的进⾏节点交换。
独家版权 抄袭必究 示例 1
输⼊: head = [1,2,3,4]
输出: [2,1,4,3]
示例 2
输⼊: head = []
输出: []
示例 3
输⼊: head = [1]
输出: [1]
提示:
链表中节点的数⽬在范围 [0, 100]
0 <= Node.val <= 100
前置知识
链表
思路
设置⼀个 dummy 节点简化操作, dummy next 指向 head
1. 初始化 first 为第⼀个节点
2. 初始化 second 为第⼆个节点
3. 初始化 current dummy
4. first.next = second.next
5. second.next = first
6. current.next = second
独家版权 抄袭必究 7. current 移动两格
8. 重复
(图⽚来⾃: https://github.com/MisterBooo/LeetCodeAnimation )
关键点解析
1. 链表这种数据结构的特点和使⽤
2. dummyHead 简化操作
代码
JS Code:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param { ListNode } head
独家版权 抄袭必究 * @return
{ ListNode }
*/
var swapPairs = function ( head ) {
const dummy = new ListNode ( 0 );
dummy . next = head ;
let current = dummy ;
while ( current . next != null && current . next . next != null ) {
// 初始化双指针
const first = current . next ;
const second = current . next . next ;
// 更新双指针和 current 指针
first . next = second . next ;
second . next = first ;
current . next = second ;
// 更新指针
current = current . next . next ;
}
return dummy . next ;
};
5. K 个⼀组翻转链表
题⽬描述
给你⼀个链表,每 k 个节点⼀组进⾏翻转,请你返回翻转后的链表。
k 是⼀个正整数,它的值⼩于或等于链表的⻓度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表: 1->2->3->4->5
k = 2 时,应当返回 : 2->1->4->3->5
k = 3 时,应当返回 : 3->2->1->4->5
独家版权 抄袭必究 说明:
你的算法只能使⽤常数的额外空间。
你不能只是单纯的改变节点内部的值,⽽是需要实际进⾏节点交换。
前置知识
链表
思路
题意是以 k nodes 为⼀组进⾏翻转,返回翻转后的 linked list .
从左往右扫描⼀遍 linked list ,扫描过程中,以 k 为单位把数组分成若⼲段,对每⼀段进⾏
翻转。给定⾸尾 nodes ,如何对链表进⾏翻转。
链表的翻转过程,初始化⼀个为 null previous node prev ,然后遍历链表的同时,当
node curr 的下⼀个( next )指向前⼀个 node prev
在改变当前 node 的指向之前,⽤⼀个临时变量记录当前 node 的下⼀个 node curr.next) .
ListNode temp = curr.next;
curr.next = prev;
prev = curr;
curr = temp;
举例如图:翻转整个链表 1->2->3->4->null -> 4->3->2->1->null
独家版权 抄袭必究 这⾥是对每⼀组( k nodes )进⾏翻转,
1. 先分组,⽤⼀个 count 变量记录当前节点的个数
2. ⽤⼀个 start 变量记录当前分组的起始节点位置的前⼀个节点
3. ⽤⼀个 end 变量记录要翻转的最后⼀个节点位置
4. 翻转⼀组( k nodes )即 (start, end) - start and end exclusively
5. 翻转后, start 指向翻转后链表 , 区间 start end 中的最后⼀个节点 , 返回 start
点。
6. 如果不需要翻转, end 就往后移动⼀个( end=end.next ) ,每⼀次移动,都要 count+1 .
如图所示 步骤 4 5 : 翻转区间链表区间 start end
独家版权 抄袭必究 举例如图, head=[1,2,3,4,5,6,7,8], k = 3
独家版权 抄袭必究 NOTE : ⼀般情况下对链表的操作,都有可能会引⼊⼀个新的 dummy node ,因为 head
可能会改变。这⾥ head 1->3 ,
dummy (List(0)) 保持不变。
复杂度分析
时间复杂度 : O(n) - n is number of Linked List
空间复杂度 : O(1)
关键点分析
1. 创建⼀个 dummy node
2. 对链表以 k 为单位进⾏分组,记录每⼀组的起始和最后节点位置
3. 对每⼀组进⾏翻转,更换起始和最后的位置
4. 返回 dummy.next .
独家版权 抄袭必究 代码
javascript code
/**
* @param { ListNode } head
* @param { number } k
* @return { ListNode }
*/
var reverseKGroup = function ( head , k ) {
// 标兵
let dummy = new ListNode ();
dummy . next = head ;
let [ start , end ] = [ dummy , dummy . next ];
let count = 0 ;
while ( end ) {
count ++ ;
if ( count % k === 0 ) {
start = reverseList ( start , end . next );
end = start . next ;
} else {
end = end . next ;
}
}
return dummy . next ;
// 翻转 stat -> end 的链表
function reverseList ( start , end ) {
let [ pre , cur ] = [ start , start . next ];
const first = cur ;
while ( cur !== end ) {
let next = cur . next ;
cur . next = pre ;
pre = cur ;
cur = next ;
}
start . next = pre ;
first . next = cur ;
return first ;
}
};
参考( References)
Leetcode Discussion (yellowstone)
独家版权 抄袭必究 扩展 1
要求从后往前以 k 个为⼀组进⾏翻转。 ( 字节跳动( ByteDance )⾯试题 )
例⼦, 1->2->3->4->5->6->7->8, k = 3 ,
从后往前以 k=3 为⼀组,
6->7->8 为⼀组翻转为 8->7->6
3->4->5 为⼀组翻转为 5->4->3 .
1->2 只有 2 nodes 少于 k=3 个,不翻转。
最后返回: 1->2->5->4->3->8->7->6
这⾥的思路跟从前往后以 k 个为⼀组进⾏翻转类似,可以进⾏预处理:
1. 翻转链表
2. 对翻转后的链表进⾏从前往后以 k 为⼀组翻转。
3. 翻转步骤 2 中得到的链表。
例⼦: 1->2->3->4->5->6->7->8, k = 3
1. 翻转链表得到: 8->7->6->5->4->3->2->1
2. k 为⼀组翻转: 6->7->8->3->4->5->2->1
3. 翻转步骤 #2 链表: 1->2->5->4->3->8->7->6
扩展 2
如果这道题你按照 92.reverse-linked-list-ii 提到的 p1, p2, p3, p4 (四点法) 的思路来思考
的话会很清晰。
代码如下( Python ):
class Solution :
def reverseKGroup ( self , head : ListNode , k : int ) -> ListNode :
if head is None or k < 2 :
return head
dummy = ListNode ( 0 )
dummy . next = head
独家版权 抄袭必究 pre
= dummy
cur = head
count = 0
while cur :
count += 1
if count % k == 0 :
pre = self . reverse ( pre , cur . next )
# end 调到下⼀个位置
cur = pre . next
else :
cur = cur . next
return dummy . next
# (p1, p4 ) 左右都开放
def reverse ( self , p1 , p4 ):
prev , curr = p1 , p1 . next
p2 = curr
# 反转
while curr != p4 :
next = curr . next
curr . next = prev
prev = curr
curr = next
# 将反转后的链表添加到原链表中
# prev 相当于 p3
p1 . next = prev
p2 . next = p4
# 返回反转前的头, 也就是反转后的尾部
return p2
# @lc code=end
复杂度分析
时间复杂度:
空间复杂度:
6. 删除排序数组中的重复项
题⽬描述
给定⼀个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现⼀次,返回移除
后数组的新⻓度。
独家版权 抄袭必究 不要使⽤额外的数组空间,你必须在
原地 修改输⼊数组 并在使⽤ O(1) 额外空间的条件下完
成。
示例 1:
给定数组 nums = [ 1,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
你不需要考虑数组中超出新⻓度后⾯的元素。
说明 :
为什么返回数值是整数,但输出的答案是数组呢 ?
请注意,输⼊数组是以「引⽤」⽅式传递的,这意味着在函数⾥修改输⼊数组对于调⽤者是可
⻅的。
你可以想象内部操作如下 :
// nums 是以 引⽤ ⽅式传递的。也就是说,不对实参做任何拷⻉
int len = removeDuplicates ( nums );
// 在函数⾥修改输⼊数组对于调⽤者是可⻅的。
// 根据你的函数返回的⻓度 , 它会打印出数组中该⻓度范围内的所有元素。
for ( int i = 0 ; i < len ; i ++ ) {
print ( nums [ i ]);
}
前置知识
数组
双指针
公司
独家版权 抄袭必究 阿⾥
腾讯
百度
字节
bloomberg
facebook
microsoft
思路
使⽤快慢指针来记录遍历的坐标。
开始时这两个指针都指向第⼀个数字
如果两个指针指的数字相同,则快指针向前⾛⼀步
如果不同,则两个指针都向前⾛⼀步
当快指针⾛完整个数组后,慢指针当前的坐标加 1 就是数组中不同数字的个数
(图⽚来⾃: https://github.com/MisterBooo/LeetCodeAnimation )
实际上这就是双指针中的快慢指针。在这⾥快指针是读指针, 慢指针是写指针。 从读写指针考
虑, 我觉得更符合本质
独家版权 抄袭必究 关键点解析
双指针
这道题如果不要求, O(n) 的时间复杂度, O(1) 的空间复杂度的话,会很简单。
但是这道题是要求的,这种题的思路⼀般都是采⽤双指针
如果是数据是⽆序的,就不可以⽤这种⽅式了,从这⾥也可以看出排序在算法中的基础性
和重要性。
注意 nums 为空时的边界条件。
代码
Javascript Code:
/**
* @param { number []} nums
* @return { number }
*/
var removeDuplicates = function ( nums ) {
const size = nums . length ;
if ( size == 0 ) return 0 ;
let slowP = 0 ;
for ( let fastP = 0 ; fastP < size ; fastP ++ ) {
if ( nums [ fastP ] !== nums [ slowP ]) {
slowP ++ ;
nums [ slowP ] = nums [ fastP ];
}
}
return slowP + 1 ;
};
复杂度分析
时间复杂度:
空间复杂度:
7. 两数相除
题⽬描述
独家版权 抄袭必究 给定两个整数,被除数
dividend 和除数 divisor 。将两数相除,要求不使⽤乘法、除法和 mod
算符。
返回被除数 dividend 除以除数 divisor 得到的商。
整数除法的结果应当截去( truncate )其⼩数部分,例如: truncate(8.345) = 8 以及
truncate(-2.7335) = -2
示例 1:
输⼊ : dividend = 10, divisor = 3
输出 : 3
解释 : 10/3 = truncate(3.33333..) = truncate(3) = 3
示例 2:
输⼊ : dividend = 7, divisor = -3
输出 : -2
解释 : 7/-3 = truncate(-2.33333..) = -2
提示:
被除数和除数均为 32 位有符号整数。
除数不为 0
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。本题中,如果除法结
果溢出,则返回 231 − 1
前置知识
⼆分法
公司
Facebook
Microsoft
Oracle
思路
独家版权 抄袭必究 符合直觉的做法是,减数⼀次⼀次减去被减数,不断更新差,直到差⼩于
0 ,我们减了多少
次,结果就是多少。
核⼼代码:
let acc = divisor ;
let count = 0 ;
while ( dividend - acc >= 0 ) {
acc += divisor ;
count ++ ;
}
return count ;
这种做法简单直观,但是性能却⽐较差 . 下⾯来介绍⼀种性能更好的⽅法。
独家版权 抄袭必究 通过上⾯这样的分析,我们直到可以使⽤⼆分法来解决,性能有很⼤的提升。
关键点解析
⼆分查找
正负数的判断中,这样判断更简单。
const isNegative = dividend > 0 !== divisor > 0 ;
或者利⽤异或:
const isNegative = dividend ^ ( divisor < 0 );
独家版权 抄袭必究 代码
/*
* @lc app=leetcode id=29 lang=javascript
*
* [29] Divide Two Integers
*/
/**
* @param { number } dividend
* @param { number } divisor
* @return { number }
*/
var divide = function ( dividend , divisor ) {
if ( divisor === 1 ) return dividend ;
// 这种⽅法很巧妙,即符号相同则为正,不同则为负
const isNegative = dividend > 0 !== divisor > 0 ;
const MAX_INTERGER = Math . pow ( 2 , 31 );
const res = helper ( Math . abs ( dividend ), Math . abs ( divisor ));
// overflow
if ( res > MAX_INTERGER - 1 || res < - 1 * MAX_INTERGER ) {
return MAX_INTERGER - 1 ;
}
return isNegative ? - 1 * res : res ;
};
function helper ( dividend , divisor ) {
// ⼆分法
if ( dividend <= 0 ) return 0 ;
if ( dividend < divisor ) return 0 ;
if ( divisor === 1 ) return dividend ;
let acc = 2 * divisor ;
let count = 1 ;
while ( dividend - acc > 0 ) {
acc += acc ;
count += count ;
}
// 直接使⽤位移运算,⽐如 acc >> 1 会有问题
const last = dividend - Math . floor ( acc / 2 );
独家版权 抄袭必究 return count + helper ( last , divisor );
}
复杂度分析
时间复杂度:
空间复杂度:
8. 下⼀个排列
题⽬描述
实现获取下⼀个排列的函数,算法需要将给定数字序列重新排列成字典序中下⼀个更⼤的排列。
如果不存在下⼀个更⼤的排列,则将数字重新排列成最⼩的排列(即升序排列)。
必须原地修改,只允许使⽤额外常数空间。
以下是⼀些例⼦,输⼊位于左侧列,其相应输出位于右侧列。
1,2,3 " 1,3,2
3,2,1 " 1,2,3
1,1,5 " 1,5,1
前置知识
回溯法
公司
阿⾥
腾讯
百度
字节
思路
独家版权 抄袭必究 符合直觉的⽅法是按顺序求出所有的排列,如果当前排列等于
nums ,那么我直接取下⼀个但是
这种做法不符合 constant space 要求(题⽬要求直接修改原数组) , 时间复杂度也太⾼,为
O(n!), 肯定不是合适的解。
我们也可以以回溯的⻆度来思考这个问题,即从后往前思考。
让我们先回溯⼀次,即思考最后⼀个数字是如何被添加的。
由于这个时候可以选择的元素只有 2 ,我们⽆法组成更⼤的排列,我们继续回溯,直到如图:
我们发现我们可以交换 4 2 就会变⼩,因此我们不能进⾏交换。
接下来碰到了 1 。 我们有两个选择:
1 2 进⾏交换
1 4 进⾏交换
两种交换都能使得结果更⼤,但是 和 2 交换能够使得增值最⼩,也就是题⽬中的下⼀个更⼤的
效果。因此我们 1 2 进⾏交换。
独家版权 抄袭必究 还需要继续往⾼位看么?不需要,因为交换⾼位得到的增幅⼀定⽐交换低位⼤,这是⼀个贪⼼
的思想。
那么如何保证增幅最⼩呢 ? 其实只需要将 1 后⾯的数字按照从⼩到⼤进⾏排列即可。
注意到 1 后⾯的数已经是从⼤到⼩排列了(⾮严格递减),我们其实只需要⽤双指针交换即
可,⽽不需要真正地排序。
1 后⾯的数⼀定是从⼤到⼩排好序了吗?当然,否则,我们找到第⼀个可以交换的回溯点
就不是 1 了,和 1 是第⼀个可以交换的回溯点⽭盾。因为第⼀个可以交换的回溯点其实就
是从后往前第⼀个递减的值。
关键点解析
写⼏个例⼦通常会帮助理解问题的规律
在有序数组中⾸尾指针不断交换位置即可实现 reverse
找到从右边起 第⼀个⼤于 nums[i] ,并将其和 nums [ i ]进⾏交换
代码
独家版权 抄袭必究 JavaScript Code:
/*
* @lc app=leetcode id=31 lang=javascript
*
* [31] Next Permutation
*/
function reverseRange ( A , i , j ) {
while ( i < j ) {
const temp = A [ i ];
A [ i ] = A [ j ];
A [ j ] = temp ;
i ++ ;
j -- ;
}
}
/**
* @param { number []} nums
* @return { void } Do not return anything, modify nums in-place instead.
*/
var nextPermutation = function ( nums ) {
// 时间复杂度 O(n) 空间复杂度 O(1)
if ( nums == null || nums . length <= 1 ) return ;
let i = nums . length - 2 ;
// 从后往前找到第⼀个降序的 , 相当于找到了我们的回溯点
while ( i > - 1 && nums [ i + 1 ] <= nums [ i ]) i -- ;
// 如果找了就 swap
if ( i > - 1 ) {
let j = nums . length - 1 ;
// 找到从右边起第⼀个⼤于 nums[i] 的,并将其和 nums[i] 进⾏交换
// 因为如果交换的数字⽐ nums[i] 还要⼩肯定不符合题意
while ( nums [ j ] <= nums [ i ]) j -- ;
const temp = nums [ i ];
nums [ i ] = nums [ j ];
nums [ j ] = temp ;
}
// 最后我们只需要将剩下的元素从左到右,依次填⼊当前最⼩的元素就可以保证是⼤于当前排列的最
⼩值了
// [i + 1, A.length -1] 的元素进⾏反转
reverseRange ( nums , i + 1 , nums . length - 1 );
};
独家版权 抄袭必究 9. 搜索旋转排序数组
题⽬描述
给你⼀个升序排列的整数数组 nums ,和⼀个整数 target
假设按照升序排序的数组在预先未知的某个点上进⾏了旋转。(例如,数组 [0,1,2,4,5,6,7] 可能变
[4,5,6,7,0,1,2] )。
请你在数组中搜索 target ,如果数组中存在这个⽬标值,则返回它的索引,否则返回 -1
示例 1
输⼊: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2
输⼊: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
示例 3
输⼊: nums = [1], target = 0
输出: -1
提示:
1 <= nums.length <= 5000
-10^4 <= nums[i] <= 10^4
nums 中的每个值都 独⼀⽆⼆
nums 肯定会在某个点上旋转
-10^4 <= target <= 10^4
前置知识
数组
⼆分法
公司
独家版权 抄袭必究 阿⾥
腾讯
百度
字节
思路
这是⼀个我在⽹上看到的前端头条技术终⾯的⼀个算法题。
题⽬要求时间复杂度为 logn ,因此基本就是⼆分法了。 这道题⽬不是直接的有序数组,不然就
easy 了。
⾸先要知道,我们随便选择⼀个点,将数组分为前后两部分,其中⼀部分⼀定是有序的。
具体步骤:
我们可以先找出 mid ,然后根据 mid 来判断, mid 是在有序的部分还是⽆序的部分
假如 mid ⼩于 start ,则 mid ⼀定在右边有序部分。
假如 mid ⼤于等于 start , 则 mid ⼀定在左边有序部分。
注意等号的考虑
然后我们继续判断 target 在哪⼀部分, 我们就可以舍弃另⼀部分了
我们只需要⽐较 target 和有序部分的边界关系就⾏了。 ⽐如 mid 在右侧有序部分,即[ mid,
end ]
那么我们只需要判断 target >= mid && target <= end 就能知道 target 在右侧有序部分,我们就
可以舍弃左边部分了 (start = mid + 1) , 反之亦然。
我们以 ( [ 6,7,8,1,2,3,4,5 ] , 4) 为例讲解⼀下:
独家版权 抄袭必究 独家版权 抄袭必究 关键点解析
⼆分法
找出有序区间,然后根据 target 是否在有序区间舍弃⼀半元素
代码
/*
* @lc app=leetcode id=33 lang=javascript
*
* [33] Search in Rotated Sorted Array
独家版权 抄袭必究 */
/**
* @param { number []} nums
* @param { number } target
* @return { number }
*/
var search = function ( nums , target ) {
// 时间复杂度: O(logn)
// 空间复杂度: O(1)
// [6,7,8,1,2,3,4,5]
let start = 0 ;
let end = nums . length - 1 ;
while ( start <= end ) {
const mid = start + (( end - start ) >> 1 );
if ( nums [ mid ] === target ) return mid ;
// [start, mid] 有序
// 注意这⾥的等号
if ( nums
[ mid ] >= nums [ start ]) {
//target [start, mid] 之间
// 其实 target 不可能等于 nums[mid] , 但是为了对称,我还是加上了等号
if ( target >= nums [ start ] && target <= nums [ mid ]) {
end = mid - 1 ;
} else {
//target 不在 [start, mid] 之间
start = mid + 1 ;
}
} else {
// [mid, end] 有序
// target [mid, end] 之间
if ( target >= nums [ mid ] && target <= nums [ end ]) {
start = mid + 1 ;
} else {
// target 不在 [mid, end] 之间
end = mid - 1 ;
}
}
}
return - 1 ;
};
复杂度分析
独家版权 抄袭必究 时间复杂度:
空间复杂度:
10. 组合总和
题⽬描述
给定⼀个⽆重复元素的数组 candidates 和⼀个⽬标数 target ,找出 candidates 中所有可以使
数字和为 target 的组合。
candidates 中的数字可以⽆限制重复被选取。
说明:
所有数字(包括 target )都是正整数。
解集不能包含重复的组合。
示例 1
输⼊: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2
输⼊: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都是独⼀⽆⼆的。
1 <= target <= 500
独家版权 抄袭必究 前置知识
回溯法
公司
阿⾥
腾讯
百度
字节
思路
这道题⽬是求集合,并不是 求极值 ,因此动态规划不是特别切合,因此我们需要考虑别的⽅
法。
这种题⽬其实有⼀个通⽤的解法,就是回溯法。⽹上也有⼤神给出了这种回溯法解题的 通⽤写
,这⾥的所有的解法使⽤通⽤⽅法解答。
除了这道题⽬还有很多其他题⽬可以⽤这种通⽤解法,具体的题⽬⻅后⽅相关题⽬部分。
我们先来看下通⽤解法的解题思路,我画了⼀张图:
独家版权 抄袭必究 每⼀层灰⾊的部分,表示当前有哪些节点是可以选择的, 红⾊部分则是选择路径。 1 2
3 4 5 6 则分别表示我们的 6 个⼦集。
图是 78.subsets ,都差不多,仅做参考。
通⽤写法的具体代码⻅下⽅代码区。
关键点解析
回溯法
backtrack 解题公式
代码
JS Code:
独家版权 抄袭必究 function
backtrack ( list , tempList , nums
, remain , start ) {
if ( remain < 0 ) return ;
else if ( remain === 0 ) return list . push ([ ... tempList ]);
for ( let i = start ; i < nums . length ; i ++ ) {
tempList . push ( nums [ i ]);
backtrack ( list , tempList , nums , remain - nums [ i ], i ); // 数字可以重复使⽤,
i + 1 代表不可以重复利⽤
tempList . pop ();
}
}
/**
* @param { number []} candidates
* @param { number } target
* @return { number [][]}
*/
var combinationSum = function ( candidates , target ) {
const list = [];
backtrack (
list ,
[],
candidates . sort (( a , b ) => a - b ),
target ,
0
);
return list ;
};
11. 接⾬⽔
题⽬描述
给定 n 个⾮负整数表示每个宽度为 1 的柱⼦的⾼度图,计算按此排列的柱⼦,下⾬之后能接多少⾬⽔。
独家版权 抄袭必究 上⾯是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的⾼度图,在这种情况下,可以接 6 个单位的⾬
⽔(蓝⾊部分表示⾬⽔)。 感谢 Marcos 贡献此图。
示例 :
输⼊ : [0,1,0,2,1,0,1,3,2,1,2,1]
输出 : 6
前置知识
空间换时间
双指针
单调栈
公司
阿⾥
腾讯
百度
字节
双数组
思路
这是⼀道⾬⽔收集的问题, 难度为 hard . 如图所示,让我们求下过⾬之后最多可以积攒多少的
⽔。
如果采⽤暴⼒求解的话,思路应该是 height 数组依次求和,然后相加。
伪代码
for ( let i = 0 ; i < height . length ; i ++ ) {
area += ( h [ i ] - height [ i ]) * 1 ; // h 为下⾬之后的⽔位
}
问题转化为求 h ,那么 h [ i ]⼜等于 左右两侧柱⼦的最⼤值中的较⼩值 ,即
h[i] = Math.min( 左边柱⼦最⼤值 , 右边柱⼦最⼤值 )
独家版权 抄袭必究 如上图那么
h 为 [ 0, 1, 1, 2, 2, 2 ,2, 3, 2, 2, 2, 1
]
问题的关键在于求解 左边柱⼦最⼤值 右边柱⼦最⼤值 ,
我们其实可以⽤两个数组来表示 leftMax , rightMax
leftMax 为例, leftMax [ i ]代表 i 的左侧柱⼦的最⼤值,因此我们维护两个数组即可。
关键点解析
建模 h[i] = Math.min( 左边柱⼦最⼤值 , 右边柱⼦最⼤值 ) (h 为下⾬之后的⽔位 )
代码
JS Code:
/*
* @lc app=leetcode id=42 lang=javascript
*
* [42] Trapping Rain Water
*
*/
/**
* @param { number []} height
* @return { number }
*/
var trap = function ( height ) {
let max = 0 ;
let volume = 0 ;
const leftMax = [];
const rightMax = [];
for ( let i = 0 ; i < height . length ; i ++ ) {
leftMax [ i ] = max = Math . max ( height [ i ], max );
}
max = 0 ;
for ( let i = height . length - 1 ; i >= 0 ; i -- ) {
rightMax [ i ] = max = Math . max ( height [ i ], max );
}
for ( let i = 0 ; i < height . length ; i ++ ) {
volume = volume + Math . min ( leftMax [ i ], rightMax [ i ]) - height [ i ];
}
return volume ;
};
独家版权 抄袭必究 复杂度分析
时间复杂度:
空间复杂度:
12. 全排列
题⽬描述
给定⼀个 没有重复 数字的序列,返回其所有可能的全排列。
示例 :
输⼊ : [1,2,3]
输出 :
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
前置知识
回溯
公司
阿⾥
腾讯
百度
字节
思路
独家版权 抄袭必究 回溯的基本思路清参考上⽅的回溯专题。
以 [ 1,2,3 ] 为例,我们的逻辑是:
先从 [ 1,2,3 ] 选取⼀个数。
然后继续从 [ 1,2,3 ] 选取⼀个数,并且这个数不能是已经选取过的数。
如何确保这个数不能是已经选取过的数?我们可以直接在已经选取的数字中线性查找,也
可以将已经选取的数字中放到 hashset 中,这样就可以在
的时间来判断是否已经被
选取了,只不过需要额外的空间。
重复这个过程直到选取的数字个数达到了 3
关键点解析
回溯法
backtrack 解题公式
代码
Javascript Code:
function backtrack ( list , tempList , nums ) {
if ( tempList . length === nums . length ) return list . push ([ ... tempList ]);
for ( let i = 0 ; i < nums . length ; i ++ ) {
if ( tempList . includes ( nums [ i ])) continue ;
tempList . push ( nums [ i ]);
backtrack ( list , tempList , nums );
tempList . pop ();
}
}
/**
* @param { number []} nums
* @return { number [][]}
*/
var permute = function ( nums ) {
const list = [];
backtrack ( list , [], nums );
return list ;
};
复杂度分析
N 为数组⻓度。
独家版权 抄袭必究 时间复杂度:
空间复杂度:
13. 两数之和
题⽬描述
给定⼀个整数数组 nums 和⼀个⽬标值 target ,请你在该数组中找出和为⽬标值的那 两个 整
数,并返回他们的数组下标。
你可以假设每种输⼊只会对应⼀个答案。但是,数组中同⼀个元素不能使⽤两遍。
示例 :
给定 nums = [ 2, 7, 11, 15 ] , target = 9
因为 nums [ 0 ] + nums [ 1 ] = 2 + 7 = 9
所以返回 [ 0, 1 ]
## 解题思路
对于这道题,我们很容易想到使⽤两层循环来解决这个问题,但是两层循环的复杂度为 O
n2 ),我们可以考虑能否换⼀种思路,减⼩复杂度。
这⾥使⽤⼀个 map 对象来储存遍历过的数字以及对应的索引值。我们在这⾥使⽤减法进⾏计算
计算 target 和第⼀个数字的差,并记录进 map 对象中,其中两数差值作为 key ,其索引值作为
value
再计算第⼆个数字与 target 的差,并与 map 对象中的数值进⾏对⽐,若相同,直接返回,如果
没有相同值,就将这个差值也存⼊ map 对象中。
重复第⼆步,直到找到⽬标值。
代码实现
暴⼒循环:
/**
@param {number [] } nums
@param {number} target
@return {number [] }
/
var twoSum = function(nums, target) {
独家版权 抄袭必究 var len=nums.length;
for(var i=0;i<len;i++){
for(var j=0;j<len;j++){
if(nums [ i ] +nums [ j ] == target&&i!=j){
return [ i,j ] ;
}
}
}
};
使⽤ map 对象存储⽅法:
/ *
@param {number [] } nums
@param {number} target
@return {number [] }
*/
var twoSum = function(nums, target) {
const maps = {}
const len = nums.length
for(let i=0;i<len;i++) {
if(maps [ target-nums [ i ]] !==undefined) {
return [ maps [ target - nums [ i ]] , i ]
}
maps [ nums [ i ]] =i
}
};
提交结果
第⼆种⽅法的提交结果:
独家版权 抄袭必究 14. 三数之和
题⽬描述
给你⼀个包含 n 个整数的数组 nums ,判断 nums 中是否存在三个元素 a b c ,使得 a + b +
c = 0 ?请你找出所有满⾜条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
解题思路
这个题和之前的两数之和完全不⼀样,不过这⾥依旧可以使⽤双指针来实现。
我们在使⽤双指针时,往往数组都是有序的,这样才能不断缩⼩范围,所以我们要对已知数组
进⾏排序。
1 )⾸先我们设置⼀个固定的数,然后在设置两个指针,左指针指向固定的数的后⾯那个值,
右指针指向最后⼀个值,两个指针相向⽽⾏。
2 )每移动⼀次指针位置,就计算⼀下这两个指针与固定值的和是否为 0 ,如果是,那么我们
就得到⼀组符合条件的数组,如果不是 0 ,就有⼀下两种情况:
相加之和⼤于 0 ,说明右侧值⼤了,右指针左移
相加之和⼩于 0 ,说明左侧值⼩了,左指针右移
3 )按照上边的步骤,将前 len-2 个值依次作为固定值,最终得到想要的结果。
因为我们需要三个值的和,所以我们⽆需最后两个值作为固定值,他们后⾯已经没有三个值可
以进⾏计算了。
代码实现
JavaScript
复制代码
/**
@param {number [] } nums
@return {number [][] }
*/
var threeSum = function(nums) {
let res = []
let sum = 0
// 将数组元素排序
独家版权 抄袭必究 nums.sort((a,b) => {
return a-b
})
const len =nums.length
for(let i =0; i<len-2; i++){
let j = i+1
let k = len-1
// 如果有重复数字就跳过
if(i>0&& nums [ i ] ===nums [ i-1 ] ){
continue
}
while(j<k){
// 三数之和⼩于 0 ,左指针右移
if(nums [ i ] +nums [ j ] +nums [ k ] <0){
j++
// 处理左指针元素重复的情况
while(j<k&&nums [ j ] ===nums [ j-1 ] ){
j++
}
// 三数之和⼤于 0 ,右指针左移
}else if(nums [ i ] +nums [ j ] +nums [ k ] >0){
k--
// 处理右指针元素重复的情况
while(j<k&&nums [ k ] ===nums [ k+1 ] ){
k--
}
}else{
// 储存符合条件的结果
res.push( [ nums [ i ] ,nums [ j ] ,nums [ k ]] )
j++
k--
while(j<k&&nums[j]===nums[j-1]){
j++
}
while(j<k&&nums[k]===nums[k+1]){
k--
}
}
}
独家版权 抄袭必究 }
return res
};
提交结果
15. 四数之和
题⽬描述
给定⼀个包含 n 个整数的数组 nums 和⼀个⽬标值 target ,判断 nums 中是否存在四个元素 a
b c d ,使得 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) {
const res = []
if(nums.length < 4){
return []
}
nums.sort((a, b) => a - b)
for(let i = 0; i < nums.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
}
for(let j = i + 1; j < nums.length -2; j++){
// 若与已遍历过的数字相同,就跳过,避免结果中出现重复的数组
if(j > i + 1 && nums[j] === nums[j - 1]){
continue
}
let left = j + 1, right = nums.length - 1
while(left < right){
const sum = nums[i] + nums[j] +nums[left] + nums[right]
if(sum === target){
res.push([nums[i], nums[j], nums[left], nums[right]])
}
if(sum <= target){
left ++
while(nums[left] === nums[left - 1]) {
独家版权 抄袭必究 left ++
}
}else{
right --
while(nums[right] === nums[right + 1]){
right --
}
}
}
}
}
return res
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值