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
};