LeetCode 算法题全解

001. 两数之和

Description: 求出能组合出目标数的两个元素

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

Example:

Given nums = [2, 7, 11, 15], target = 9,

Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].

解法一: 穷举

时间复杂度: $O(n^2)$
空间复杂度: $O(1)$

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for(int i = 0; i<nums.size(); i++){
for(int j = i+1; j<nums.size(); j++){
if(nums[i] + nums[j] == target){
vector<int> res ={i,j};
return res;
}
}
}
}
};

解法二 : 哈希表, 两次遍历

注意, 题目中说数组的解恰好只有一个, 这是一种很强的假设, 解法二在面对有多个解时, 也只会输出一个
这里要特别注意: 同一个元素不能使用两次, 但是数组中的元素是可以重复的, 重复的元素看作是两个元素. hash表中最终存储的将会是重复元素的最后一个下标, 因此, 在进行比较时, 使用 i!= nums_map[target-nums[i]] 来判断它们是否为同一个元素, 而不能使用nums_map[nums[i]] != nums_map[target-nums[i]]

时间复杂度: $O(n)$ 遍历两次
空间复杂度: $O(n)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> nums_map;
for(int i = 0 ;i<nums.size(); i++){
nums_map.insert({nums[i], i});
}
for(int i = 0 ; i<nums.size(); i++){
if(nums_map.count(target-nums[i]) == 1 && i!= nums_map[target-nums[i]]){
vector<int> res = {i, nums_map[target-nums[i]]}; //这里一定要用i,而不能用nums_map[nums[i]] , 上面也同理
return res;
}
}
}
};

解法三: 哈希表 一次遍历

事实上, 可以将hash表的插入和查找对应元素的操作放在 一个循环里, 这样就只需要进行一次遍历

时间复杂度: $O(n)$ 遍历一次
空间复杂度: $O(n)$

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> nums_map;
for(int i = 0 ;i<nums.size(); i++){
if(nums_map.count(target-nums[i]) == 1 && i!= nums_map[target-nums[i]]){
vector<int> res = {i, nums_map[target-nums[i]]};
return res;
}
nums_map.insert({nums[i], i});
}
}
};

扩展问题

How would you approach the problem if the input array is very large (but limited range) and cannot fit in the memory ? This is a follow-up question for this problem.

002. 两数相加

Description: 链表数之和

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

Example:

Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Explanation: 342 + 465 = 807.

解法一: 顺序相加, 注意进位

从链表的第一个节点开始, 将两个节点的值和进位位想加, 如果大于10, 则当前结果节点的值对10取余, 同时将进位位置1, 如果小于10, 则直接赋值给当前结果节点, 同时将进位位置0.

特别注意 l1l2 的长度问题, 当二者节点遇到 nullptr 时, 将较长的剩余部分重新赋给l1, 并继续判断

最后, 需要注意是否有进位位, 如果有, 则要申请一个新节点, 并将其置为1

时间复杂度: $O(\max(m,n))$
空间复杂度: $O(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
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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {

int carry = 0;
ListNode* head = new ListNode(0); //创建指向最终结果的头指针
if(l1!=nullptr) head->next = l1; // 虽然题目指明为非空链表, 但是最好还是做一下判断
else head->next = l2;
ListNode* pre=head; // pre用于保存l1的上一个指针
while(l1!=nullptr && l2!=nullptr){
l1->val = l1->val + l2->val + carry;
if(l1->val > 9){
l1->val %= 10;
carry = 1;
}else{
carry = 0;
}
pre = l1;
l1 = l1->next; l2 = l2->next;
}

if(l2!=nullptr){ // 此时说明l2比l1长, 用l1的上一个指针指向当前l2剩余的部分,
l1 = pre;
l1->next = l2;
l1 = l1->next;
}
while(l1!=nullptr){ // 此时l1为剩余(l1或l2) 的部分, 只需要考虑是否有进位即可
l1->val = l1->val + carry;
if(l1->val > 9){
l1->val %= 10;
carry = 1;
}else{
carry = 0; // 如果没有进位, 一定要将此处置0, 否则会引起错误
break;
}
pre = l1;
l1 = l1->next;
}
if(carry == 1){ // 对应 999 + 001 的特殊情况, 此时进位会不断传递, 最终数字位数加1, 最高位为1
ListNode* newnode = new ListNode(1);
l1 = pre;
l1->next = newnode;
}
return head->next;

}
};

解法二: 顺序相加, 维持原链表

时间复杂度: $O(\max(m,n))$
空间复杂度: $O(\max(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
25
26
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *dummy = new ListNode(0);
ListNode *pre = dummy;

int carry = 0;
while(l1!=nullptr || l2!=nullptr){
ListNode *cur = new ListNode(0);
int a = l1==nullptr ? 0 : l1->val;
int b = l2==nullptr ? 0 : l2->val;
cur->val = a + b + carry;
carry = cur->val / 10;
cur->val = cur->val % 10;
pre->next = cur;
pre = cur;
if(l1!=nullptr) l1 = l1->next;
if(l2!=nullptr) l2 = l2->next;
}

if(carry > 0){
pre->next = new ListNode(carry);
}
return dummy->next;
}
};

扩展问题

What if the the digits in the linked list are stored in non-reversed order? For example:

$(3 \to 4 \to 2) + (4 \to 6 \to 5) = 8 \to 0 \to 7 (3→4→2)+(4→6→5)=8→0→7$

思路:

先将链表转置 , 再用上面的方法求解

转置时间复杂度: $O(n)$
转置空间复杂度: $O(1)$

003. 无重复字符的最长子串

Description: 寻找无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

Example 1:

1
2
3
Input: "abcabcbb"
Output: 3
Explanation: The answer is "abc", with the length of 3.

Example 2:

1
2
3
Input: "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.

Example 3:

1
2
3
Input: "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3.

Note that the answer must be a substring, “pwke” is a subsequence and not a substring.

解法一:暴力

时间复杂度: $O(n^3)$ 对于每一个字符, 子串的添加以及查重过程时间复杂度为 $O(n^2)$ , 总共n个字符, 所以为 $O(n^3)$
空间复杂度: $O(min(n,m))$ 需要将当前子串存在起来以便查询是否相等, n为字符串length, m为字符集size

解法二: 前后两个指示变量

时间复杂度: $O(2n) = O(n)$
空间复杂度: $O(min(n,m))$

思路: 首先构造一个哈希表, 用来存储当前子串中出现的字符, 这样, 新来的字符可以直接查询哈希表来判断字符是否存在, 构建哈希表空间复杂度为 ( $m$ 为字符集合的大小,一般为26(字母), 128(ASCII), 256(ASCII), $n$ 为字符串的长度)

然后, 使用两个指示变量, 分别指向当前未重复子串的首字符, 和超尾字符, 进行如下几个判断:

  • 如果超尾字符与当前子串中的字符不重复, 那么将超尾字符加入到当前子串中,并将length加1
  • 如果超尾字符与当前子串中的字符重复, 利用哈希表查的重复字符的所在位置, 将当前子串的首字符直接跳向该重复字符的下一个位置( 这样可以保证只遍历一遍 ), 并将包括重复字符在内的之前所有字符都从哈希表中删除(之前的字符不再可能组成更长的子串了), 同时将超尾字符加入, length赋予新值: 超尾位置-重复位置-1;
  • 判断首字符与超尾字符是否相等, 如果相等, 将超尾字符加1, 并将length置为1
  • 看当前length是否比maxlength大, 并重复以上过程,直到超尾字符超出size
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int lengthOfLongestSubstring(string s){
int hash[256]={0};
int max_len = 0;
for(int l=0, r=0; r<s.size(); ){
if(hash[s[r]] == 0){
hash[s[r]] = 1;
max_len = std::max(max_len, r-l+1);
r++;

}else{
hash[s[l]] = 0;
l++;
}
}
return max_len;
}
};

解法三: 只需一次遍历

时间复杂度: $O(n)$, 只需一次遍历
空间复杂度: $O(min(m,n)$, 与解法二相同

当我们在 [i,j) 之间发现了一个重复的下标为 j' 的字符时, 我们不用一点点的增加 i 的值, 而是可以直接将 i 的值跳到 j'+1 处. 故, 我们可以只利用一次遍历就找到最长的不重复子串.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> s_hash;
int max_length = 0;
for(int i = 0 ,j=0 ; j< s.size() ; j++){
if(s_hash.count(s[j])){ // 这里未删除 i 之前的, 所以即使这里的哈希可以查到, 也不一定就是重复.
i = max(i,s_hash[s[j]]+1); //如果遇到重复的, 就将当前的i指向重复的下一个
// (这里用max的原因是, 没有删除当前i到重复字符之间的其他字符, 这些字符
// 后续还可能被检测到, 所以这里只取max的, 也就是i不会倒退)
//s_hash.erase(s[j]); // 该语句是多余的
}
s_hash[s[j]] = j;
max_length = max_length > (j-i+1) ? max_length : (j-i+1);
}
return max_length;
}
};

用数组做哈希表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int lengthOfLongestSubstring(string s){
int hash[256];// 哈希表中存在的值代表下标
for(auto &item : hash) item = -1; // 赋初值
int max_len = 0;
for(int i=0, j=0; j < s.size(); j++){
if(hash[s[j]] != -1){ // 当哈希表中值不为-1时, 说明存在重复
i = std::max(hash[s[j]] + 1, i); // 注意这里必须保证 i 不会倒退, 因此要使用 max
}
max_len = std::max(max_len, j-i+1);
hash[s[j]] = j;
}
return max_len;
}
};

哈希表, 键为字符, 值为字符在字符串中的位置, 如果键不为空, 说明之前出现过重复字符, 此时, 令起始下标i更新, 注意, 如果, i大于之前键的值, 说明已经包在外面了, i 则不变, 核心思路就是 i 不会回退.

时间复杂度 $O(n)$, 空间复杂度 $O(n)$

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
h_dict = {}
max_len = 0
i = 0
for j, c in enumerate(s):
if c in h_dict:
i = max(i, h_dict[c] + 1)
max_len = max(max_len, j - i + 1)
h_dict[c] = j
return max_len

004. 寻找两个有序数组的中位数

Description: 寻找两个有序数组的中位数

There are two sorted arrays nums1 and nums2 of size m and n respectively.

Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).

You may assume nums1 and nums2 cannot be both empty.

Example 1:

1
2
3
4
nums1 = [1, 3]
nums2 = [2]

The median is 2.0

Example 2:

1
2
3
4
nums1 = [1, 2]
nums2 = [3, 4]

The median is (2 + 3)/2 = 2.5

解法一: 根据中位数的特性

题目要求需要时间复杂度为 $O(log (m+n))$.
空间复杂度: $O(1)$, 未使用额外空间

首先我们思考中位数的作用: 中位数可以将一个数组分成两个长度相同的部分, 并且一部分中的数字总比另一部分中的小. 那么对于两个数组的情况, 我们需要做的就是找到一个数字, 可以使这两个数组分别分成两部分, 这两部分长度相等(当个数为奇数时, 前一部分多一个元素), 同时前一部分的元素小于等于后一部分的元素. 首先,我们将数组 A 分成两部分, 由于 A 有 m 个数字, 所以它可以有 m 种不同的分法, 我们以下标 i 对应的数字为界限, 将A分成两部分, 前一部分的长度为 i (从0到 i-1 ), 后一部分的长度为 m-i (从 i 到 m-1): A[1,2,...,i-1] | A[i, i+1, ..., m-1]. 同理,数组 B 也可以做如下分割: B[1,2,...,j-1] | B[j, j+1, ..., n-1].

这里需要注意一个细节, 我们需要确保 A[i] 这个数字可以将两个数组等长的分割, 那么 A 数组的长度 必须小于等于 B 数组的长度. 因为如果 A 数组的长度大于 B 数组的长度, 那么就会出现一种情况: A[i] 前的数字个数已经大于两数组总个数的一半, 此时无论如何也做不到等长分割, 因此, 我们需要先对数组长度判断, 令 A 数组代表的是较短的数组, 利用 swap() 方法可以在 $O(1)$ 时间复杂度内完成.

当两个数组 A 和 B 都被分到了两部分以后, 将它们合起来, 第一部分的数字为 A[1,2,...,i-1]B[1,2,...,j-1], 第二部分的数字为 A[i, i+1, ..., m-1]B[j, j+1, ..., n-1], 我们并不关系两部分内部的顺序, 我们只关心一件事, 那就是: 第一部分和第二部分的长度相等, 并且第一部分的数字都比第二部分小, 于是, i 和 j和取值就必须满足下列关系:

  • i+j = m-i + n-j 或 m-i + n-j + 1 (加1的原因是因为有可能数组总长为奇数, 我们令前一部分比后一部分多1个元素)
  • i=0 或 A[i-1] <= B[j] (前者说明 A 中元素全部被分配到后半段, 即前半段元素均由 B 中元素组成)
  • i=m 或 B[j-1] <= A[i] (前者说明 A 中元素全部在前半段, 即后半段元素均由 B 中元素组成)
  • 由于上式 i+j = m-i + n-j 或 m-i + n-j + 1 , 因此有 j = (m+n+1)/2 - i ; (向下取整). 故而可以只对 i 进行判断 i 是否越界, 只要 i 满足条件, j就不会等于0或n(前提条件是 A 数组长度小于等于 B 数组长度)

根据上面的分析, 解题过程如下:

  • 根据两数组的长度, 将短的一方设为A数组 (j要对应较长的那个数组, 否则的话j有可能小于0 ), 令start=0, end=A.size
  • 令 i=(start+end)/2
  • 计算j = (m+n+1)/2 - i
  • 判断当前 i 和 j 是否满足条件,有三种情况(对这三种情况不断重复, 直到i,j位置刚刚好):
    • i > 0 并且 A[i-1] > B[j], 说明 i 的位置过大, 令 end = i-1.
    • i < m 并且 B[j-1] > A[i], 说明 i 的位置过小, 令 start = i+1;
    • 其他情况(i==0 或 A[i-1] <= B[j] 并且 i==m 或 B[j-1] <= A[i]), 说明 i 和 j的位置刚刚好.
  • 当i,j位置刚好时, 根据数组整体长度的奇偶, 返回正确的中位数:
    • 奇数: 返回前半段的最大元素
    • 偶数: 返回前半段最大元素和后半段最小元素的平均值

非递归写法

Python 实现

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
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
if len(nums2) < len(nums1):
nums1, nums2 = nums2, nums1

m = len(nums1)
n = len(nums2)
start, end = 0, m

while (True):
i = (start + end) // 2
j = (m+n) // 2 - i
if (i > 0 and nums1[i-1] > nums2[j]): # 前半段最后元素 > 后半段最前元素, 说明 i 过大
end = i - 1
elif (i < m and nums2[j-1] > nums1[i]): # 由于 A 长度小于等于 B, 因此, j 不会为 0, 说明 i 过小
start = i + 1
else: # i 刚好使用 A[0~i], B[0~j-1] 与 A[i+1, m-1], B[j, n] 划分开
if i == m: rightmin = nums2[j] # 求右边最小
elif j == n: rightmin = nums1[i]
else: rightmin = min(nums1[i], nums2[j])

if (m+n) % 2 == 1: # 奇数, 直接返回右边最小
return rightmin

if i == 0: leftmax = nums2[j-1] # 求左边最大
elif j == 0: leftmax = nums1[i-1]
else: leftmax = max(nums1[i-1], nums2[j-1])

return (leftmax + rightmin) / 2

C++ 实现

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
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
if(nums1.size() > nums2.size())
nums1.swap(nums2);
int m = nums1.size();
int n = nums2.size();

int start = 0, end=m;
while(start<=end){ //当 start = end 时, 此时 i=start=end, 不能忽略
int i = (start+end) / 2;
int j = (n+m+1)/2 - i;
if(i>0 && nums1[i-1] > nums2[j]) //注意, i=0时说明位置恰好
end = i-1; //i太大
else if(i<end && nums2[j-1] > nums1[i])
start = i+1; // i太小
else{
int leftmax;// 取左边最大的
if(i==0) leftmax=nums2[j-1];
else if(j==0) leftmax=nums1[i-1];
else leftmax = max(nums1[i-1], nums2[j-1]) ;
if( (n+m)%2 == 1) return leftmax;

int rightmin; // 取右边最小的
if(i==m) rightmin = nums2[j];
else if(j==n) rightmin = nums1[i];
else rightmin = min(nums1[i] ,nums2[j]);
return (leftmax+rightmin) / 2.0;
}
}
// return 0.0; //因为, 两数组不会同时为空, 所以这句话主要用于调试
}
};

递归写法

写法一:

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
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
if(nums1.size() <= nums2.size())
return helper(nums1, 0 , nums1.size(),nums2);
else
return helper(nums2, 0 , nums2.size(),nums1);
}

double helper(vector<int>& nums1, int start1, int end1, vector<int>& nums2){
int i = (start1+end1)/2;
int j = (nums1.size()+nums2.size()+1)/2 - i;
// if(start1 > end1) return 0.0; 因为数组一定是有效的, 因此不会出现这种情况
if( (i==0 || nums1[i-1]<=nums2[j]) && (i==nums1.size() || nums2[j-1]<=nums1[i])){ // 如果找到i
int res11, res12;
int res21, res22;
// 首先将左边部分的两个数组分别赋值, 如果i或j为0, 说明对应数组在左边
//只有0个元素 , 将其赋值为INT_MIN(因为要取max(res11, res21))
if(i==0) res11= INT_MIN;
else res11=nums1[i-1];
if(j==0) res21= INT_MIN;
else res21=nums2[j-1];
//同理, 对右边进行处理, 取min(res12, res22)
if(i==nums1.size()) res12= INT_MAX;
else res12=nums1[i];
if(j==nums2.size()) res22= INT_MAX;
else res22=nums2[j];

// 根据数组奇偶个数返回结果
if((nums1.size() + nums2.size())%2 == 1 ){
return max(res11, res21);
}
else{
return ( max(res11,res21)+min(res12,res22) ) / 2.0;
}
}else if(nums1[i-1] > nums2[j]){
return helper(nums1, start1, i-1, nums2);
}else{
return helper(nums1, i+1, end1, nums2);
}
}
};

写法二:

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
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
if(nums1.size() > nums2.size())
return helper(nums2, nums1, 0, nums2.size());
else
return helper(nums1, nums2, 0, nums1.size());
}

double helper(vector<int> &nums1, vector<int> &nums2, int start, int end){
//if (start>end) return 0.0; 因为数组一定是有效的, 因此不会出现这种情况
int m = nums1.size();
int n = nums2.size();
int i = (start+end)/2;
int j = (m+n+1)/2 - i;
if(i>0 && nums1[i-1] > nums2[j])
return helper(nums1, nums2, start, i-1);
else if(i<m && nums2[j-1] > nums1[i])
return helper(nums1, nums2, i+1, end);
else{
int leftmax;
if(i==0) leftmax = nums2[j-1];
else if(j==0) leftmax = nums1[i-1];
else leftmax = max(nums1[i-1], nums2[j-1]);
if((m+n)&1 == 1) return leftmax;

int rightmin;
if(i==m) rightmin = nums2[j];
else if(j==n) rightmin = nums1[i];
else rightmin = min(nums1[i], nums2[j]);
return (leftmax+rightmin)/2.0;
}
}
};

005. 最长回文子串

Description: 最长回文子串

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

Example 1:

Input: “babad”
Output: “bab”
Note: “aba” is also a valid answer.
Example 2:

Input: “cbbd”
Output: “bb”

解法一:最长公共子串

时间复杂度: $O(n^2)$
空间复杂度: $O(n)$

先将字符串 s 利用 reverse 逆置成 s', 然后查找 ss' 的最长公共子串, 即为最长的回文子串.

解法二: 穷举

时间复杂度: $O(n^3)$
空间复杂度: $O(1)$

对于字符串中的每一个字符, 共有 $\frac{n(n-1)}{2}$ 种包含该字符的子串, 所以如果对所有可能的子串判断, 需要 $O(n^3)$ 的时间复杂度

解法三: 动态规划

时间复杂度: $O(n^2)$
空间复杂度: $O(n^2)$

我们令 DP 数组为一个 $n\times n$ 的矩阵, $dp(i,j)$ 代表从 s[i] 开始, 到 s[j] 结束的子串是否为回文串, 如果是, 则为 true. 那么, $dp(i,j)$ 为真的条件就是必须满足 $dp(i+1, j-1)=true$ 并且 $s[i]=s[j]$. dp 数组的初始值为: $dp(i,i)=true$, $dp(i,i+1)= s[i]==s[i+1]$. 由于需要遍历 dp 矩阵中每一个位置的值, 因此时间复杂度为 $O(n^2)$, 空间复杂度很明显为 $O(n^2)$.

解法三: 扩展中心法

时间复杂度: $O(n^2)$
空间复杂度: $O(1)$ 或者 $O(n)$

以每一个字符为中心, 向两边扩展, 将当前能够扩展的长度 len 和最大扩展长度 max_len 作比较, 记录较大者, 同时记录较大者的所对应的中心字符的下标 max_index. 最后, 根据最大扩展的长度max_len 和中心字符的下标 max_index 计算最大回文子串的开始位置和总长度

此处注意, 回文子串有奇偶两种情况, 可采用以下举措之一解决:

  • 分别检查奇数和偶数的情况, 这样多检查一次(虽然多检查一次, 但和下面的检查总次数差不多, 因为下面虽然只检查一次, 但次数较多)
  • 向字符内插入特殊符号 ‘#’, 这样不管偶数奇数, 都可以当做奇数处理, 缺点是占用了额外的 $O(n)$ 空间.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def longestPalindrome(self, s: str) -> str:
if s == "": return ""
s = '$#' + '#'.join(s) + '#&' # 插入字符, 注意 abb, 如果是, $a#b#b%, 则会判断 #b# 和 b#b 的长度一样, 因此需要在外围多加 #
center = 1
max_len = 0
for i in range(1, len(s)-1):
cur_len = 1
while (s[i-cur_len] == s[i+cur_len]):
cur_len += 1
if cur_len-1 > max_len:
max_len = cur_len-1
center = i
res = s[center - max_len : center + max_len + 1]
return res.replace('#', '')

C++ 实现

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
// 空间复杂度 $O(1)$

class Solution {
public:
string longestPalindrome(string s) {
int max_len = 0;
int start = 0;
for(int i=0; i < s.size(); i++){

int len1=0,len2=0;
int left=i, right = i; //通过left和right , 是的对奇偶的分别处理更方便
while( left >=0 && right<s.size() && s[left] == s[right]){
left--; right++;
}
len1 = right-left-1; // 注意, 这里一定是-1, 而不是+1

left=i;
right=i+1;
while( left>=0 && right<s.size() && s[left] == s[right]){
left--; right++;
}
len2 = right-left-1;
int len = max(len1, len2);
if(len>max_len){
max_len = len;
start = i- (len-1)/2;
}
}
return s.substr(start, max_len);
}
};

另一种写法:

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
// 空间复杂度 $O(1)$

class Solution {
public:
string longestPalindrome(string s) {
int max_i = 0;
int max_len = 0;
for(int i = 0; i<s.size(); i++){
int left, right;
left = i, right = i;
while(s[left] == s[right]){ // 奇数情况
left--;
right++;
if(left < 0 || right == s.size()){
break;
}
}
if(max_len < right-left-1){
max_len = right-left-1;
max_i = i;
}

left = i, right = i+1; // 下面要对 right 判断, 防止越界
while(right !=s.size() && s[left] == s[right]){// 偶数
left--;
right++;
if(left < 0 || right == s.size()){
break;
}
}
if(max_len < right-left-1){
max_len = right-left-1;
max_i = i+1;//偶数时令max_i指向偏右的下标
}
}
return s.substr(max_i-max_len/2, max_len);
}
};
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
// 空间复杂度 $O(n)$
class Solution {
public:
string longestPalindrome(string s) {
char* cs = new char[s.size() * 2+1];
cs[0]='#';
for(int i=0; i<s.size() ; i++){ //插入 '#'
cs[i*2+1] = s[i];
cs[i*2+2] = '#';
}
int max_len=0;
int max_index = 0;
for(int i =0; i<s.size()*2+1 ; i++){
int len=0; //记录当前扩展长度len
for(int j=1; i-j>=0 && i+j<s.size()*2+1 ;j++){
if(cs[i-j] == cs[i+j]){ //两边字符若相等, 则len长度增1
len++;
}else
break;
}
if(len > max_len){
max_len = len;
max_index = i;
}
}
int start = (max_index - max_len)/2; //根据maxlen和index 计算回文子串开始坐标
int len = max_len;
delete cs;
return s.substr(start, len);

}
};

解法五: 马拉车(Manacher) 算法

时间复杂度: $O(n)$
空间复杂度: $O(n)$

There is even an O(n), O(n) algorithm called Manacher’s algorithm, explained here in detail. However, it is a non-trivial algorithm, and no one expects you to come up with this algorithm in a 45 minutes coding session. But, please go ahead and understand it, I promise it will be a lot of fun.

马拉车算法的核心思想还是从中心扩展发出发, 不过他必须使用 ‘#’ 字符先对原始字符串插入, 如下所示:

接下来, 在每一次 for 循环当中, 都需要保存这么几个值(命名是个人习惯, 可以用其他名字代替):

  • P: P 为最大右边界下标值, 对应的是所有已检测的回文子串中, 右边界下标最大的那个
  • P_center: 该值是P对应的回文子串的中心下标
  • max_len: 对应当前最大回文子串的半径(aba的半径为1, a的半径为0)
  • max_index: 对应当前最大回文子串的中心下标

然后, 还需要构建一个和插入’#’后的字符串长度相关的数组 p_len, 里面存放着对应位置的回文串半径, 用以后续的计算, 这一步是关键, 有了这个数组 ,才能实现利用之前计算结果

接下来, 遍历 “新字符串”(即插入’#’之后的字符串) 的每一个字符, 设当前下标为 i, 则有如下情况, 分别处理:

  • P>i, 说明 i 在 P 的范围内, 可以利用前面的计算结果
  • P<=i, 说明 i 不在 P 的范围内, 无法利用前面的计算结果, 只能逐个判断

对上面两种情况具体分析如下:

第一种情况: P>i

找到i相对于 P_center 的对称位置, 设为j, 那么如果Len[j]<P-i, 如下图所示:

则以i为中心的回文串的长度至少和以j为中心的回文串一样 , 即Len [i]>=Len[j] , 因此可以直接从Len[j]+1开始判断回文

如果Len[j]>=P-i, 如下图所示:

由对称性, 说明以i为中心的回文串可能会延伸到P之外, 而大于P的部分我们还没有进行匹配, 所以要从P+1位置开始一个一个进行匹配, 直到发生失配

第二种情况: P<=i

如果i比P还要大, 说明对于中点为i的回文串还一点都没有匹配, 这个时候, 就只能老老实实地一个一个匹配了

在这一次循环完成之前, 更新上面提及的四个变量

循环结束后, 根据 max_index 和 max_len 的值返回最长回文子串

时间复杂度分析:

对于每一个字符, 由于如果直接比较过, 那么就可以利用之前比较的结果直接判断, 所以每个字符都只进行了一次比较, 故而时间复杂度为 $O(n)$

C++ 实现

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

class Solution {
public:
string longestPalindrome(string s) {
int cs_size = s.size()*2+1;
char* cs = new char[cs_size];
cs[0] = '#';
for(int i = 0;i<s.size(); i++){
cs[i*2 + 1] = s[i];
cs[i*2 + 2] = '#';
}
int P = 0;
int P_center = 0;
int max_index = 0;
int max_len = 0;
int* p_len = new int[cs_size];
for(int i =0; i<cs_size; i ++){
if( i < P){ // 如果i<P, 说明可以复用前面的计算结果
int j = P_center*2 - i; // j对i关于P_center的对称点
if(P-i > p_len[j]){ // 如果i与P之间的距离比 j 的回文串长度还大,
//说明可以直接从p_len[j] + 1开始比较, 之前的子串一定是回文串
int k = p_len[j] + 1;
while(i-k>=0 && i+k<cs_size && cs[i-k] == cs[i+k]){
k++;
}
p_len[i] = k-1;
}else{ // 如果距离没有p_len[j] + 1大, 则从超出P的部分开始比较
int k = P - i;
while(i-k>=0 && i+k<cs_size && cs[i-k] == cs[i+k]){
k++;
}
p_len[i] = k-1;
}

}else{ //如果i不在P范围内, 则必须从1开始逐个比较, 无法利用之前的计算结果
int k = 1;
while(i-k>=0 && i+k<cs_size && cs[i-k] == cs[i+k]){
k++;
}
p_len[i] = k-1;
}

if(p_len[i] > max_len){
max_len = p_len[i];
max_index = i;
}
if(i+p_len[i] > P){
P = i+p_len[i];
P_center = i;
}

}
delete cs;
delete p_len;
int start = (max_index - max_len)/2;
int len = max_len;
return s.substr(start, len);
}
};

Python 实现

Manacher, 基于扩展中心法, 在符合条件的情况下, 可以利用之前的结果来加速判断流程, 时间复杂度接近于 $O(n)$, 空间复杂度 $O(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
25
26
27
28
class Solution:
def longestPalindrome(self, s: str) -> str:
if s == "": return ""
s = '$#' + '#'.join(s) + '#&' # 插入字符, 注意 abb, 如果是, $a#b#b%, 则会判断 #b# 和 b#b 的长度一样, 因此需要在外围多加 #
center = 1
max_len = 0
len_p = [0] * len(s)
max_p = 0
center_p = 0
for i in range(1, len(s)-1):
if i < max_p: # 在 max_P 范围内, 可以利用之前结果加速
j = center_p-(i-center_p) # 找到 i 关于 center_p 对称的点
cur_len = min(len_p[j], max_p-i)
else: # 否则, 只能老老实实逐个匹配
cur_len = 1

while (s[i-cur_len] == s[i+cur_len]): # 计算长度
cur_len += 1
cur_len -= 1
if cur_len > max_len: # 更新最长回文串信息
max_len = cur_len
center = i
if cur_len + i > max_p: # 更新 p 相关信息
max_p = cur_len + i
len_p[i] = cur_len
center_p = i
res = s[center - max_len : center + max_len + 1]
return res.replace('#', '')

写法二

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
# Manacher 算法实现(python)
class Solution:
def longestPalindrome(self, s: str) -> str:
T = '#'.join('${}^'.format(s))
palin_len = [0] * len(T)
max_P = 0
max_center = 0
max_len = 0
for i in range(1, len(T) - 1):
if i < max_P: # 当 i 处于最长回文的范围内, 则可利用之前的信息
j = max_center - (i-max_center)
p_len = min(palin_len[j], max_P - i)
l = i - p_len - 1
r = i + p_len + 1
else: # 否则, 只能老老实实逐个匹配
l = i - 1
r = i + 1
while (l >= 0 and r < len(T) and T[l] == T[r]):
l -= 1
r += 1
palin_len[i] = r - i - 1 # 记录当前字符的回文串长度(半边的长度)
if (max_P < r): # 更新 max_P 和 max_center
max_P = r - 1
max_center = i
if (max_len < r - i): # 更新最长的回文串信息(超头, 超尾)
max_len = r - i
res_start = l
res_end = r
return T[res_start+1:res_end].replace('#', '') # 返回时注意去除 '#' 符号

006. Z 字形变换-中等

题目链接: https://leetcode-cn.com/problems/zigzag-conversion/

解法

找到字符串下标的对应关系如下:

  • 第一行和最后一行: 字符之间的下标间隔刚好是 2 * numRows - 2
  • 其他行: 字符之间的下标间隔分两种情况交替出现:
    • 1: 字符距离最后一行字符的下标距离的 2 倍
    • 2: 字符距离第一行字符的下标距离的 2 倍

注意一种特殊情况, 就是当numRows为 1 时, 此时2 * numRows - 2的值为0, 会陷入死循环, 实际上, numRows的值为 1 时, 最终的结果就是原字符串, 直接返回即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def convert(self, s: str, numRows: int) -> str:
if numRows == 1: return s
res = ''
for i in range(numRows):
if i == 0 or i == numRows-1:
index = i
while (index < len(s)):
res += s[index]
index += 2 * numRows - 2
else:
index = i
step1 = (numRows - 1 - i ) * 2
step2 = (i) * 2
while (index < len(s)):
res += s[index]
index += step1
step1, step2 = step2, step1
return res

007. 整数翻转

Description: 将数字逆置

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。

Example 1:

1
2
Input: 123
Output: 321

Example 2:

1
2
Input: -123
Output: -321

Example 3:

1
2
Input: 120
Output: 21

解法一: 取余数

这道题本身不难, 只要不断对x的绝对值取余数, 就可以得到反转的整数, 但是, 该题的核心考察点在于边界条件的判断, 稍不注意, 很容易漏解(如果不进行边界判断, 即使写出了解决方法, 面试官也很不满意)

  • x为0
  • x反转后的值,超过了int型数据的表示范围, 检查方法是先用long存储, 然后看情况决定返回值正负.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int reverse(int x) {
if(x==0) return x;
int abs_x = abs(x);
int sign_x = x>0? 1:-1;
long res = 0; // 为了看int是否越界,特意将res声明为long型
while( abs_x!=0 ){
res = res*10 + abs_x%10;
if(res > INT_MAX || res < INT_MIN) return 0; //这一句就是最主要的考察点,看int是否越界
abs_x = abs_x/10 ;
}

if(sign_x ==-1) return 0-res;
return res;
}
};

008. 字符串转换整数 (atoi)

Description: 将字符串转换成整数

解法一: 考虑多种情况

此题时间复杂度为 $O(n)$ , 重点考察是否考虑的全面, 主要有以下几种情况, 缺一不可:

  • +123 dd // 返回123
  • +123d // 返回123
  • d-123 // 返回0
  • -123+ //返回-123
  • -123+4 // 返回-123
  • 323123423423423 // 返回INT_MAX
  • -1231238923894234 // 返回INT_MIN
  • 1234-5 // 返回1234
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
class Solution {
public:
int myAtoi(string str) {

int sign =1;
bool is_first = true; //记录当前非数字字符是否是第一个非空格字符, 如果是, 返回0
bool has_sign = false; // 记录正负号的出现次数, 出现多于1次的, 返回0
long res = 0; //记录当前的int值, 要出现int范围, 返回对应的INT
for(int i =0 ; i<str.size(); i++){
if(str[i] == ' ' && is_first) continue; // 空格, 且没有出现任何非空格字符(如出现了, 则空格也会跟着变成循环停止的标志)
else if( !has_sign && (str[i] == '+' || str[i] == '-') ){ // 判断符号
has_sign = true;
is_first = false;
sign = str[i]=='+' ? 1:-1;
}else if(str[i] <= '9' && str[i] >= '0'){
has_sign = true;
is_first = false;
res = res*10 + int(str[i] - '0') * sign; // 数字累加, 注意这里使用了sign, 因此无需在后面判断正负, 直接加就可以
if (res > INT_MAX) return INT_MAX; // 超限
else if(res < INT_MIN) return INT_MIN;
}else if(is_first){ //首字符为非法字符, 返回0
return 0;
}else{
break;
}

}
return int(res);
}
};

009. 回文数-简单

题目链接: https://leetcode-cn.com/problems/palindrome-number/

解法一: 转换成字符串进行标准的中心扩展法进行判断

需要两次遍历, 一次转换, 一次判断

解法二: 利用数学计算

利用数学计算得到新的回文值, 然后将二者进行比较, 这样进需要一次遍历

1
2
3
4
5
6
7
8
9
class Solution:
def isPalindrome(self, x: int) -> bool:
if (x < 0): return False
new_x = 0
ori_x = x
while (x > 0):
new_x = new_x * 10 + x % 10
x = x // 10
return True if new_x == ori_x else False

010 正则表达式匹配

Description: 正则表达式匹配

Given an input string (s) and a pattern (p), implement regular expression matching with support for ‘.’ and ‘*’.

‘.’ Matches any single character.
‘*’ Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).

Note:
s could be empty and contains only lowercase letters a-z.
p could be empty and contains only lowercase letters a-z, and characters like . or *.

Example 1:

1
2
3
4
5
Input:
s = "aa"
p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".

Example 2:

1
2
3
4
5
Input:
s = "aa"
p = "a*"
Output: true
Explanation: '*' means zero or more of the precedeng element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".

Example 3:

1
2
3
4
5
Input:
s = "ab"
p = ".*"
Output: true
Explanation: ".*" means "zero or more (*) of any character (.)".

Example 4:

1
2
3
4
5
Input:
s = "aab"
p = "c*a*b"
Output: true
Explanation: c can be repeated 0 times, a can be repeated 1 time. Therefore it matches "aab".

Example 5:

1
2
3
4
Input:
s = "mississippi"
p = "mis*is*p*."
Output: false

解法一: 递归实现( 速度很慢, 只超过0.97%的提交)

采用递归法, 首先判断当前字符串 p 是否已经走到尽头, 如果是, 则看 s 是否走到尽头, 返回 true 或者 false.
然后在第一个字符的匹配情况, 并记录之.
然后看是否存在 ‘‘, 并根据情况进行递归调用.
若不存在 ‘
‘, 则按正常匹配处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
bool helper(string &s, int i, string &p, int j) {
int n = s.size(), m = p.size();
if (j == m) return (i == n);

bool firstMatch = (i != n and
(s[i] == p[j] or p[j] == '.'));
if (j < m - 1 and p[j+1] == '*') { //只有长度大于 2 的时候, 才考虑 *
//两种情况
//pattern 直接跳过两个字符. 表示 * 前边的字符出现 0 次
//pattern 不变, 例如 text = aa , pattern = a*
return helper(s, i, p, j+2) or
(firstMatch and helper(s, i+1, p, j));
} else { //
return firstMatch and helper(s, i+1, p, j+1);
}
}
public:
bool isMatch(string s, string p) {
return helper(s, 0, p, 0);
}

};

解法二: 动态规划

This problem has a typical solution using Dynamic Programming. We define the state P[i][j] to be true if s[0..i) matches p[0..j) and false otherwise. Then the state equations are:

  • P[i][j] = P[i - 1][j - 1], if p[j - 1] != ‘*’ && (s[i - 1] == p[j - 1] || p[j - 1] == ‘.’);
  • P[i][j] = P[i][j - 2], if p[j - 1] == ‘*’ and the pattern repeats for 0 times;
  • P[i][j] = P[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == ‘.’), if p[j - 1] == ‘*’ and the pattern repeats for at least 1 times.

Putting these together, we will have the following code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
bool isMatch(string s, string p) {
bool dp[s.size()+1][p.size()+1]{0}; //!! 这里注意一定要初始化, 否则在下面的循环中, dp[2][0] 是循环不到的, 但是dp[2][2]会访问dp[2][0]的值, 如果不进行初始化, 就会发生 RuntimeError !!!
dp[0][0]=true;
for(int i =0; i<s.size()+1; i++){
for(int j = 1;j<p.size()+1;j++){
if(p[j-1] == '*') // 注意这里是j-1
dp[i][j] = ( j > 1 && dp[i][j-2] )|| ( i>0 && (s[i-1] == p[j-2] || p[j-2] == '.') && dp[i-1][j]); // 注意这里是j-2, i-1, 一定要知道这些是为什
else
dp[i][j] = i>0 && dp[i-1][j-1] && (s[i-1] == p[j-1] || p[j-1] == '.');
}
}
return dp[s.size()][p.size()];
}
};

011. 盛最多水的容器

题目链接: https://leetcode-cn.com/problems/container-with-most-water/

Description

Given n non-negative integers a1, a2, …, an , where each represents a point at coordinate (i, ai). n vertical lines are drawn such that the two endpoints of line i is at (i, ai) and (i, 0). Find two lines, which together with x-axis forms a container, such that the container contains the most water.

Note: You may not slant the container and n is at least 2.

The below vertical lines are represented by array [1,8,6,2,5,4,8,3,7]. In this case, the max area of water (blue section) the container can contain is 49.

解法一: 暴力

时间复杂度: $O(n^2)$

用max_area标记当前最大容器的取值, 然后两个for循环遍历所有容器的可能取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int maxArea(vector<int>& height) {
int max_area = 0;
for(int i=0; i<height.size(); i++){
for(int j = i+1; j < height.size(); j++){
if(max_area < min( height[i],height[j] ) * (j-i)){
max_area = min( height[i],height[j] ) * (j-i);
}
}
}
return max_area;
}
};

解法二: 用两个指针

时间复杂度: $O(n)$
空间复杂度: $O(1)$

分别用两个指针指向数组的第一个元素和最后一个元素, 并计算当前的area, 然后移动指针元素值较小的一方, 移动过程中更新max_area的值

原理:

首先假设容器可以具有最大长度的宽, 也就是分别指向首尾元素, 这时候 , 我们想查看是否还有比当前最大容积更大的容器, 那么, 我们必须维持较高的垂直边不动, 而将较低的垂直边移动, 因为只有这样, 我们才 有可能 (注意不是一定)获得比当前容积更大的容器, 这个时候虽然宽变小了, 但是高度却可能增加(因为新增的边有可能大于当前较低边的高). 如果移动较高的边, 那么新增的边由于受到当前较低边的作用, 只有可能减小容器的面积

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0
right = len(height) - 1
max_area = 0
while (left < right):
w = right - left
h = min(height[left], height[right])
max_area = max(max_area, w*h)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int maxArea(vector<int>& height) {
int low = 0, high = height.size()-1;
int max_area = 0;
while(low<high){
int area = min( height[low], height[high] ) * (high-low);
if(max_area < area){
max_area = area;
}
if(height[low] < height[high])
low++;
else
high--;
}
return max_area;
}
};

012. 整数转罗马数字-中等

题目链接: https://leetcode-cn.com/problems/integer-to-roman/

解法: 字典映射(哈希)

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def intToRoman(self, num: int) -> str:
i2r_dict = {1000: 'M', 900: 'CM', 500: 'D', 400: 'CD', 100: 'C', 90:'XC',
50:'L', 40:'XL', 10:'X', 9:'IX', 5:'V', 4:'IV', 1:'I'}
i2r = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
res = ''
for i in range(len(i2r)):
while(i2r[i] <= num):
res += i2r_dict[i2r[i]]
num -= i2r[i]
return res

013. 罗马数字转整数

Description

Roman numerals are represented by seven different symbols: I, V, X, L, C, D and M.

Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
For example, two is written as II in Roman numeral, just two one’s added together. Twelve is written as, XII, which is simply X + II. The number twenty seven is written as XXVII, which is XX + V + II.

Roman numerals are usually written largest to smallest from left to right. However, the numeral for four is not IIII. Instead, the number four is written as IV. Because the one is before the five we subtract it making four. The same principle applies to the number nine, which is written as IX. There are six instances where subtraction is used:

I can be placed before V (5) and X (10) to make 4 and 9.
X can be placed before L (50) and C (100) to make 40 and 90.
C can be placed before D (500) and M (1000) to make 400 and 900.
Given a roman numeral, convert it to an integer. Input is guaranteed to be within the range from 1 to 3999.

Example 1:

Input: “III”
Output: 3
Example 2:

Input: “IV”
Output: 4
Example 3:

Input: “IX”
Output: 9
Example 4:

Input: “LVIII”
Output: 58
Explanation: L = 50, V= 5, III = 3.
Example 5:

Input: “MCMXCIV”
Output: 1994
Explanation: M = 1000, CM = 900, XC = 90 and IV = 4.

解法一: 顺序扫描

时间复杂度: $O(n)$

顺序扫描, 如果当前字符比下一个字符小, 说明是 ‘4’ 或 ‘9’ 的情况, 用下一个字符的值减去当前字符的值

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

class Solution {
public:
int romanToInt(string s) {
unordered_map<char, int> roman_char;
roman_char['I'] = 1;
roman_char['V'] = 5;
roman_char['X'] = 10;
roman_char['L'] = 50;
roman_char['C'] = 100;
roman_char['D'] = 500;
roman_char['M'] = 1000;
int res = 0;
for(int i =0; i<s.size() ; i++){

if( i<s.size()-1 && roman_char[s[i]] < roman_char[s[i+1]]){
res += roman_char[s[i+1]]-roman_char[s[i]];
i++;
}
else
res += roman_char[s[i]];
}
return res;
}
};

扩展问题: 异常检测

上面的解法虽然可以通过OJ, 但是此题还需要进行特别的异常诊断, 即要能够判断出当前输入的罗马输出是否合法! 如 “IVIV” 就是典型的不合法输入, 对于此输入, 上面的程序会输出 , 这显然不正确

014. 最长公共前缀

Description: 最长公共前缀

Write a function to find the longest common prefix string amongst an array of strings.
If there is no common prefix, return an empty string “”.

Example 1:

1
2
Input: ["flower","flow","flight"]
Output: "fl"

Example 2:

1
2
3
Input: ["dog","racecar","car"]
Output: ""
Explanation: There is no common prefix among the input strings.

Note:
All given inputs are in lowercase letters a-z.

解法一: 顺序比较

时间复杂度: $O(S)$, $S$ 为所有字符串中的字符总数
空间复杂度: $O(1)$, 没有使用额外的空间

暴力求解, 先求第一个字符串与第二个字符串最长公共前缀, 然后利用该前缀与第三个字符串比较, 知道公共前缀为空或者比较完所有字符串.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if(strs.size()==0 || strs[0].size()==0) return "";
string prefix = strs[0];
for(int i=0; i<strs.size() && !prefix.empty(); i++){
int j=0;
while(j<prefix.size()&&j<strs[i].size()
&&prefix[j] == strs[i][j]){
j++;
}
prefix = prefix.substr(0, j);
}
return prefix;
}
};

解法二: 垂直比较

时间复杂度: $O(S)$, $S$ 为所有字符串中的字符总数, 最好情况下复杂度为 $O(n\min(s)$, $\min(s)$ 为字符串数组中的最短字符串长度.
空间复杂度: $O(1)$, 没有使用额外的空间

顺序比较所有字符串的值, 直到遇到第一次不相等的位置, 然后输出前面的公共前缀, 需要额外注意处理以下几种特殊情况:
输入

  • 输入为: [] 或 [“”] 应该直接返回””
  • 输入为: [“abc”] 应该直接返回”abc”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if(strs.size() ==0 || strs[0]=="") return "";
if(strs.size() ==1 ) return strs[0];
for(int i =0 ;; i++){
for(int k = 1; k<strs.size(); k++){
if(strs[0][i] != strs[k][i]){
if(i>0) return strs[0].substr(0,i);
else return "";
}
}
}
return "";
}
};

015. 三数之和

Description: 三数和为零

Given an array nums of n integers, are there elements a, b, c in nums such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.

Note:
The solution set must not contain duplicate triplets.

Example:

1
2
3
4
5
6
7
Given array nums = [-1, 0, 1, 2, -1, -4],

A solution set is:
[
[-1, 0, 1],
[-1, -1, 2]
]

解法一: 固定一个数, 剩余两个数用双指针法求

时间复杂度: $O(n^2+nlogn)=O(n^2)$
空间复杂度: $O(1)$, 无需额外空间

解题步骤:

  1. 对整个数组排序, $O(nlogn)$;
  2. 固定下标 i, 令下标j=i+1, 令 k=nums.size()-1. 注意, 固定一个数以后, 另外的两个数必须在这个数 之后 寻找, 否则会出现重复的情况.
  3. 如果 nums[i] 为正数, 说明不可能组成和为零的三元组, 直接返回当前结果;
  4. 为了消除重复, 对于相同的相邻元素, 我们只选其中的一个参与组合. 注意: 这里的重复是指三元组的值的重复, 而不是下标重复, 也就是说, 对于下标不同但值相同的元素, 也算作重复.
  5. 重复(2)(3)(4)过程直到循环终止.

排序的必要性: 这里我们排序的主要目的是为了消除重复, 如果题目允许重复, 那么可以不进行排序, 而采用基于哈希表的 TwoSum 来求解.

消除重复的方法: p1 只能处于重复序列的第一个, p2 只能处于剩余重复序列的第一个.

1
2
0  0  0
p1 p2 p3

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
class Solution {
public:

int partition(vector<int>& nums, int low, int high){
if(nums[low] != nums[(low+high)/2]){ // 注意这里用异或交换的陷阱
nums[low] = nums[low] + nums[(low+high)/2];
nums[(low+high)/2] = nums[low] - nums[(low+high)/2];
nums[low] = nums[low] - nums[(low+high)/2];
} // 主要是将中将的数字和首位交换, 个人觉得可有可无, 因为时间复杂度是一样的
int P = nums[low];
while(low < high){
while(low<high && P<=nums[high]) high--;
nums[low] = nums[high];
while(low<high && P>=nums[low]) low++;
nums[high] = nums[low];
}
nums[low] = P;
return low;
}

void quickSort(vector<int>& nums, int low, int high){
int mid = partition(nums, low, high);
if(low<mid ) quickSort(nums, low, mid-1);
if(mid<high) quickSort(nums, mid+1, high);
}

vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int> > res;
if(nums.size()<3) return res;
quickSort(nums, 0, nums.size()-1);

for(int i =0; i<nums.size(); i++){
if(nums[i]> 0) break; //剪枝, 如果当前数字为正, 那么后面就不可能再有符合条件的三元组, 可以提前退出
if(i>0 && nums[i] == nums[i-1] ) continue; //去除重复, 遇到除第一个外相同的三元组最小的数字, 则跳过
int low = i+1, high = nums.size()-1;

while(low < high){
if(low>i+1 && nums[low] == nums[low-1]){ // 仍然是去除重复,
low++; continue;
}
int sum = nums[low] + nums[i] + nums[high];
if(sum>0) high--;
else if(sum<0) low++;
else{
vector<int> tmp{nums[low], nums[i], nums[high]};
res.push_back(tmp);
low++; // 这一点千万别漏了, 要继续判断, 因为以当前数字开始的三元组可能不止一个
}

}
}
return res;

}
};

更好的写法:

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
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
if(nums.size()<=2)return result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() - 2; i++) {
int a = nums[i];
if(a > 0) break;
if (i > 0 && a == nums[i - 1]) continue;// 这里不能用nums[i]==nums[i+1], 因为会丢掉类似于 -1,-1,2 的解.
for (long j = i + 1, k = nums.size() - 1; j < k;) {
int b = nums[j];
int c = nums[k];
int value = a + b + c;
if (value == 0) {
result.push_back(vector<int>({a, b, c}));
while (j < k && b == nums[++j]); // 主要是这里的写法很优雅, 其他地方和上面差不多
while (j < k &&c == nums[--k]);
} else if (value > 0) {
k--;
} else {
j++;
}
}
}
return result;
}

解法二: python写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
target = 0
res = []
for p1 in range (len(nums) - 2):
if (nums[p1] > 0): return res
if (p1 > 0 and nums[p1] == nums[p1-1]): continue
p2 = p1 + 1
p3 = len(nums) -1
while (p2 < p3):
if (p2-1 != p1 and nums[p2] == nums[p2-1]):
p2 += 1
continue
tmp = nums[p1] + nums[p2] + nums[p3]
if (tmp > 0): p3 -= 1
elif (tmp < 0): p2 += 1
else:
res.append([nums[p1], nums[p2], nums[p3]])
p2 += 1
return res

016. 最接近的三数之和

Description

题目链接

给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

例如,给定数组 nums = [-1,2,1,-4], 和 target = 1.

与 target 最接近的三个数的和为 2. (-1 + 2 + 1 = 2).

解法一: 排序+双指针

时间复杂度: $O(n^2)$

先排序, 然后固定中间位, 移动两边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def threeSumClosest(self, nums: List[int], target: int) -> int:
nums.sort()
sum3 = nums[0] + nums[1] + nums[2]
for center in range(1, len(nums) - 1):
p1 = 0
p2 = len(nums) -1
while (p1 != center and p2 != center):
tmp3sum = nums[p1] + nums[center] + nums[p2]
if (abs(tmp3sum - target) < abs(sum3 - target)):
sum3 = tmp3sum
if (tmp3sum < target):
p1 += 1
elif (tmp3sum > target):
p2 -= 1
else:
break
return sum3

017. 电话号码的字母组合

Description: 九键字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

Example:

1
2
Input: "23"
Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

Note:
Although the above answer is in lexicographical order, your answer could be in any order you want.

C++

解法一: 递归

时间复杂度: $O(4^nn)$, $n$ 为数字的长度
空间复杂度: $O(4^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
25
26
class Solution {
public:
void back_tracking(vector<string>& res, const vector<string>& digit_letters, string& tmp,string digits, int index){
if(index == digits.size()){
res.push_back(tmp);
}
else {
for(int i=0; i<digit_letters[digits[index]-'0'].size(); i++){
tmp.push_back(digit_letters[digits[index]-'0'][i]);
back_tracking(res, digit_letters, tmp, digits, index+1);
tmp.pop_back();// 移除当前末尾元素, 以便可以加下一个
}
}
}

vector<string> letterCombinations(string digits) {
vector<string> res;
if(digits.size() <=0) return res;
//res.push_back(""); //在递归解法中, 不需要改语句.
const vector<string> digit_letters{"","","abc","def","ghi","jkl",
"mno","pqrs","tuv","wxyz"};
string tmp="";
back_tracking(res, digit_letters, tmp, digits, 0);
return res;
}
};

解法二: 非递归

时间复杂度: $O(n4^n)$, $n$ 为数字数组的长度
*空间复杂度:
$O(4^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

class Solution {
public:
vector<string> letterCombinations(string digits) {
vector<string> res;
if(digits.size() <=0) return res;
res.push_back(""); //对res向量初始化,以便开始, 如果不初始化, 则size为0,后面的循环无法进行
const vector<string> digit_letters{"","","abc","def","ghi","jkl",
"mno","pqrs","tuv","wxyz"};
for(int i =0 ;i<digits.size(); i++){
int num = digits[i] - '0';
if(digit_letters[num] == "") continue;

vector<string> tmp; // 申请一个临时vector, 用于存放加上当前数字字符的string集合
for(int k = 0; k < digit_letters[num].size(); k++){
for(int l =0; l < res.size(); l++){
tmp.push_back(res[l]+digit_letters[num][k]);
}
}
res.swap(tmp); // 将res于tmp交换, swap仅仅是改变指针, 比'='更快, 因为'='包含了复制
}
return res;
}
};

Python

解法一: 利用reduce实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:

def letterCombinations(self, digits):
"""
:type digits: str
:rtype: List[str]
"""
if digits=="":
return []
digit_letters = {'0':"", '1':"", '2':"abc",
'3':"def", '4':"ghi", '5':"jkl",
'6':"mno", '7':"pqrs", '8':"tuv", '9':"wxyz"}
from functools import reduce
# 在python3中, reduce()函数已经从全局命名空间移除, 现在存在于functools模块中,使用时需要导入
return reduce(lambda res,digit:[x+y for x in res for y in digit_letters[digit]], digits, [""])

上面的程序在面对 2~9 的输入时没有问题, 但是如果是存在空字符输入, 例如 “123” 这种, 由于 “1” 对应的是空字符串, 所以会导致 lambda 中的 for 循环 不执行, 进而使得最终的列表为空. 解决这个问题的办法是用 [""] 代替 "", 这样, 即使面对空字符串, for 循环也会执行, 只不过不改变元素值. 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:

def letterCombinations(self, digits):
"""
:type digits: str
:rtype: List[str]
"""
if digits=="":
return []
digit_letters = {'0':[""], '1':[""], '2':"abc",
'3':"def", '4':"ghi", '5':"jkl",
'6':"mno", '7':"pqrs", '8':"tuv", '9':"wxyz"}
from functools import reduce
# 在python3中, reduce()函数已经从全局命名空间移除, 现在存在于functools模块中,使用时需要导入
return reduce(lambda res,digit:[x+y for x in res for y in digit_letters[digit]], digits, [""])

018. 四数之和

解法一: 换成两数之和

时间复杂度 $O(n^3)$

先排序
从前往后, 固定一个数字, 这样, 该数字后的剩余数列变成了一个 3sum 问题
然后再固定一个数字, 这样, 剩余数列就变成了一个 2sum 问题

注意1: 要注意重复四元组的判断, 判断方式是当 i > 0 and nums[i] == nums[i-1] 时, 跳过该数字.
注意2: 一定要使用早停, 否则程序的运行时间回大大升高

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
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
def twoSum(nums, target, k, res, tmp_res):
if(len(nums) < k or nums[0] * k > target or nums[-1] * k < target):
return
if (k == 2):
i = 0
j = len(nums) - 1
while (i < j):
if (i > 0 and nums[i-1] == nums[i]):
i += 1
continue
tmp = nums[i] + nums[j]
if (tmp < target): i += 1
elif (tmp > target): j -= 1
else:
res.append(tmp_res + [nums[i], nums[j]])
i += 1
else:
for i in range(len(nums)):
if (i > 0 and nums[i-1] == nums[i]):
#i += 1 这里不论加不加 i 效果都一样, 为什么?
continue
twoSum(nums[i+1:], target-nums[i], k-1, res, tmp_res+[nums[i]])
res = []
twoSum(sorted(nums), target, 4, res, [])
return res

解法二: 换成两数之和:

复杂度 $O(n^2)$

需要注意: set 去重作用于元素类型是 list 时, 需要先将元素类型转换成 tuple 之后, 才能进行去重.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
from itertools import combinations
index_comb = combinations(range(len(nums)), 2) # 获取所有的下标组合, 复杂度 n^2
dict_2sum = dict()
for i, j in index_comb: # 构建 2sum 的字典, 键位 2sum, 值为下标
dict_2sum.setdefault(nums[i] + nums[j], list())
dict_2sum[nums[i] + nums[j]].append((i, j))
res = []
for two_sum in dict_2sum.keys(): # 遍历所有的 two_sum
if target-two_sum in dict_2sum: # 如果存在与之匹配的另一个 two_sum
for i, j in dict_2sum[two_sum]: # 遍历这两个two_sum所有可能的下标组合
for p, q in dict_2sum[target-two_sum]: # 这两个循环最坏情况下使得算法为 n^3, 但是, 通常情况下, 组合某个值的下标可能数不会很多
if (len(set([i, j, p, q])) == 4):
res.append(sorted([nums[i], nums[j], nums[p], nums[q]]))
return set(map(tuple, res))

解法三: 转换成三数之和

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
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
n = len(nums)
nums.sort()
res = []
for i in range(n):
if i > 0 and nums[i] == nums[i-1]:
i += 1
continue
for j in range(i+1, n):
if j-1 != i and nums[j] == nums[j-1]:
j += 1
continue
low = j + 1
high = n - 1
while (low < high):
if low-1 != j and nums[low] == nums[low-1]:
low += 1
continue
four_sum = nums[i] + nums[j] + nums[low] + nums[high]
if four_sum < target:
low += 1
elif four_sum > target:
high -= 1
else:
res.append([nums[i], nums[j], nums[low], nums[high]])
low += 1
high -= 1
return res

019. 删除链表的倒数第N个节点-中等

Description: 移除链表的倒数第 N 个字符

Given a linked list, remove the n-th node from the end of list and return its head.

Example:

1
2
3
Given linked list: 1->2->3->4->5, and n = 2.

After removing the second node from the end, the linked list becomes 1->2->3->5.

Note:
Given n will always be valid.

Follow up:
Could you do this in one pass?

解法一: 遍历两次

第一次遍历求出链表长度, 第二次遍历在对应位置删除节点

解法二: 双指针, 只遍历一次

时间复杂度: $O(n)$ 且只遍历一次

空间复杂度: $O(1)$

维护两个指针, 两指针之间的距离刚好相差n, 当第二个指针到达链表尾部时, 第一个指针刚好指向倒数第n个节点, 直接删除该节点即可.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
dummy = ListNode(0)
dummy.next = head
fast = dummy
slow = dummy
for _ in range(n):
fast = fast.next
while (fast.next != None):
slow = slow.next
fast = fast.next
slow.next = slow.next.next
return dummy.next

C++ 实现:

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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* first = head;
ListNode* second = head;
while (n--) {
first = first->next;
}
if (first == nullptr) return head->next; // 链表长度为n, 删除倒数第n个节点
while (first->next != nullptr) {
second = second->next;
first = first->next;
}
second->next = second->next->next;
return head;
}
};

下面是有一种写法, 新申请了一个节点空间, 用于指向head节点, 可以使代码看起来更容易理解, 对边界条件的判断也更加方便

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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if(head == nullptr || n <= 0) return head; //链表为空, 或者n<=0时, 直接返回head
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* first = dummy;
ListNode* second = dummy;
for(int i = 0; i < n ; i++){
second = second->next;
if(second == nullptr) return dummy->next;// n超出了链表的长度
}

while(second->next!=nullptr){
first = first->next;
second = second->next;
}
first->next = first->next->next;

return dummy->next;
}
};

020. 有效的括号-简单

Description

Given a string containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.
An input string is valid if:
Open brackets must be closed by the same type of brackets.
Open brackets must be closed in the correct order.
Note that an empty string is also considered valid.

Example 1:

1
2
Input: "()"
Output: true

Example 2:

1
2
Input: "()[]{}"
Output: true

Example 3:

1
2
Input: "(]"
Output: false

Example 4:

1
2
Input: "([)]"
Output: false

Example 5:

1
2
Input: "{[]}"
Output: true

解法一: 栈

时间复杂度: $O(n)$
空间复杂度: $O(n)$

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def isValid(self, s: str) -> bool:
stack = []
for c in s:
if c in ['(', '{', '[']:
stack.append(c)
elif ((stack and stack[-1] == '(' and c == ')') or
(stack and stack[-1] == '{' and c == '}') or
(stack and stack[-1] == '[' and c == ']')):
stack.pop()
else:
stack.append(c)
print(stack)
return not len(stack)

C++ 实现:
写法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
bool isValid(string s) {
stack<char> s_brack;

for(int i = 0; i<s.size(); i++){
char c='\0';
if(s[i] == ')') c='(';
else if(s[i] == ']') c='[';
else if(s[i] == '}') c='{';
if(!s_brack.empty() && c == s_brack.top()) s_brack.pop();
else s_brack.push(s[i]);
}
if(!s_brack.empty()) return false;
return true;

}
};

写法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
bool isValid(string s) {
stack<char> parent;
for(auto c : s){
if(c=='(' || c=='{' || c=='[')
parent.push(c);
else if(parent.empty()) return false;
else if((c==')' && parent.top()=='(') ||
(c=='}' && parent.top()=='{') ||
(c==']' && parent.top()=='[')){
parent.pop();
}else
return false;
}
return parent.empty() ? true : false;
}
};

021. 合并两个有序链表

Description

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

Example:

Input: 1->2->4, 1->3->4
Output: 1->1->2->3->4->4

解法一: 遍历融合

时间复杂度: $O(min(m,n))$

空间复杂度: $O(1)$

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode(0)
cur_node = dummy
while (l1 and l2 ):
if l1.val <= l2.val:
cur_node.next = l1
l1 = l1.next
cur_node = cur_node.next
else:
cur_node.next = l2
l2 = l2.next
cur_node = cur_node.next
if l1:
cur_node.next = l1
if l2:
cur_node.next = l2
return dummy.next

C++ 实现:

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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(l1 == nullptr) return l2;
if(l2 == nullptr) return l1;
ListNode* head=nullptr;
if(l1->val < l2->val) {
head = l1;
l1 = l1->next;
}
else{
head = l2;
l2 = l2->next;
}
ListNode* cur = head;
while(l1!=nullptr && l2!=nullptr){
if(l1->val < l2->val){
cur->next = l1;
cur= cur->next;
l1 = l1->next;
}else{
cur->next = l2;
cur = cur->next;
l2 = l2->next;
}
}
if(l2==nullptr) cur->next = l1;
else if(l1 == nullptr) cur->next = l2;
return head;

}
};

上面开关头结点的过程过于复杂, 可以用dummy指针简化这个过程

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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(l1 == nullptr) return l2;
if(l2 == nullptr) return l1;
ListNode* dummy=new ListNode(0);
ListNode* cur = dummy;
while(l1!=nullptr && l2!=nullptr){
if(l1->val < l2->val){
cur->next = l1;
cur = cur->next;
l1 = l1->next;
}else{
cur->next = l2;
cur = cur->next;
l2 = l2->next;
}
}
if(l2==nullptr) cur->next = l1;
else if(l1 == nullptr) cur->next = l2;
return dummy->next;

}
};

022. 括号生成

Description

解法一: 暴力

先求出所有可能性, 然后验证每一种可能性是否正确

解法二: 回溯

有关递归的时间空间复杂度分析起来都不太容易, 这里只上答案(//TODO 具体怎么来没搞懂)

时间复杂度: $O(\frac{4^n}{\sqrt n})$
空间复杂度: $O(\frac{4^n}{\sqrt n})$ 以及 $O(n)$ 的空间来存储组合序列

考虑合法括号组合的规律: 必须首先出现左括号, 然后才能出现右括号, 如果当前的string里面的右括号数量大于左括号数量, 那么就一定会出现)(这种不匹配的情况.

核心思路: 从头开始构建组合, 每次接入一个字符, 接入的字符只有两种可能性, 即左括号或者右括号, 而一旦接入的字符使得当前字符中右括号数量大于左括号, 就会变得不合法组合,其它均为合法. 根据此性质, 进行如下递归:

维护两个变量left_rest, right_rest分别代表 剩余 可以添加的括号的 数量. 采用递归算法, 每次添加一个 ‘(‘ 或者一个 ‘)’, 添加时需要考虑下面几种情况:

  • 为了保证当前string内左括号数量多于右括号数量, left_rest一定要小于right_rest
  • 如果left_rest = right_rest = 0, 则说明此时没有可以添加的括号了.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
helper(res, "", n, n);
return res;
}

void helper(vector<string> &res, string out, int left_rest, int right_rest){ //注意这里的 out 不能生命成引用形式
//if(left_rest > right_rest) return;
if(left_rest == 0 && right_rest ==0) res.push_back(out);
else{
if(left_rest>0) helper(res, out+'(', left_rest-1, right_rest);
if(right_rest>0 && right_rest > left_rest) helper(res, out+')', left_rest, right_rest-1);
}
}
};

解法三: Closure Number

时间复杂度: $O(\frac{4^n}{\sqrt n})$, 同解法4
空间复杂度: $O(\frac{4^n}{\sqrt n})$, 同解法4

该方法可以看做是一种插入法, 选定一组括号 (), 由此便消耗了一组括号的数量, 此时还剩下 n-1 组括号, 我们将这 n-1 组括号插入到选定的括号中, 即 (left)right, 其中, leftright 都是有效的括号组合, 它们的括号组数加起来刚好为 n-1, 因此, left 的括号组数的情况共有 n 种情况: [0, …, n-1], 对应的 right 的组数有 n-1-left 组. 具体代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
if(n==0){
res.push_back("");
}else{
for(int c=0; c<n; c++)
for(string left : generateParenthesis(c))
for(string right : generateParenthesis(n-1-c))
res.push_back("("+left+")"+right);
}
return res;
}
};

解法四: 用栈来模拟递归

首先是最厚的括号包裹状态, 即一开始左边是连续的左括号, 右边是连续的右括号, 然后执行以下逻辑:

  1. 右括号不能比左括号多;
  2. 弹出右括号, 直到遇到第一个左括号, 如果左括号改成右括号仍然合法, 则把它改成右括号; 否则, 左括号继续弹出;
  3. 改完之后一个劲加左括号, 直到所有可以用的左括号都加完为止; 然后再一个劲的加右括号, 直到加完位置;
  4. 循环一直执行到不能弹出括号为止, 即直到栈为空.

这里刚好凸显了一件事情, 那就是要注意尽可能不要将自增或自减操作写在 while() 条件句里面, 否则会造成一些很难发现的错误, 下面代码中的注释会说明

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
stack = ['('] * n + [')'] * n
res = [''.join(stack)]
right = 0
left = 0
while stack:
while stack and stack[-1] == ')':
stack.pop()
right += 1
if stack:
stack.pop()
left += 1
if left < right:
stack.append(')')
right -= 1
while left > 0:
stack.append('(')
left -= 1
while right > 0:
stack.append(')')
right -= 1
res.append(''.join(stack))
return res

C++ 实现

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
class Solution {
public:
vector<string> generateParenthesis(int n) {
int left = n;
int right = n;
vector<string> res;
string s;
// 注意, 将left写在while里面的问题时, 当left为0时才会结束while
// 但是此时会使得 left 变成 -1, 因此, 需要再left++, 或者讲left--写在 while 里面
while (left--) {
s += '(';
}
left++;
while (right--) {
s += ')';
}
right++;
res.push_back(s);
while (!s.empty()) {
if (s.back() == ')') {
s.pop_back();
right++;
} else if (left+1 < right) {
left++; right--;
s.back() = ')';
while (left--) s.push_back('(');
left++;
while (right--) s.push_back(')');
right++;
res.push_back(s);
} else {
s.pop_back();
left++;
}
}
return res;
}
};

023. 合并K个有序链表

合并 k 个排序链表
合并 k 个有序链表

Description: 合并 k 个有序链表

Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.

Example:

1
2
3
4
5
6
7
Input:
[
1->4->5,
1->3->4,
2->6
]
Output: 1->1->2->3->4->4->5->6

解法一: 基于比较的合并

时间复杂度: $O(k \times N)$ k为需要合并和链表个数, 在比较时需要遍历k个链表的头结点, 以便找出最小的. 每插入一个节点, 就要重新遍历一次, 故需要遍历 $N$ 次, $N$ 为所有链表的节点总数.
空间复杂度: $O(1)$

将该问题看做是两个有序链表的合并问题, 只不过每次选择最小的节点时, 需要从vector.size()个节点中选择, 同时还要注意及时移除vector中已经走到头的空链表, 并判断size当前的大小, 当vector中的size大小为1时, 说明其余链表都已经合并完成, 此时退出循环, 直接将该链表接入即可.

另外要注意vector为空, 以及vector中全是nullptr链表的特殊情况.

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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
if(lists.size() == 0) return nullptr; //处理[]的情况
ListNode* dummy = new ListNode(0);
ListNode* cur_node = dummy;
while(lists.size() > 1){
int min_node_index = 0;
for(int i = 0; i<lists.size() ;i++){
if(lists[i] == nullptr) {
lists.erase(lists.begin()+i);
i--; //移除第i个元素后, 下一个元素会自动成为第i个元素,因此, 将当前i--
continue; // continue后, i会++, 最终i指向了下一个元素
}
if(lists[min_node_index]->val > lists[i]->val){
min_node_index = i;
}
}
if(lists.size() == 0) return nullptr;
//主要是应对 [[], []] 的情况, 本身vector的size大于0, 但是经过erase以后size就变成0了, 此时应返回nullptr
cur_node->next = lists[min_node_index];
cur_node = cur_node->next;
lists[min_node_index] = lists[min_node_index]->next;
if(lists[min_node_index] == nullptr) lists.erase(lists.begin()+min_node_index);
}
cur_node->next = lists[0];
return dummy->next;
}
};

解法二: 用小顶堆对解法一的比较操作进行优化

时间复杂度: $O(logk \times N)$, N 代表所有链表的节点总数.
空间复杂度: $O(k)$ 由于要构造堆, 所以需要额外空间

由于我们只需要找到k个节点里面数值最小的那一个, 因此可以利用Priority Queue (实际上就是大顶堆和小顶堆)对上面的比较操作进行优化, 使得比较操作的复杂度从 $k$ 降到 $logk$. 由于每个节点都会进入小顶堆一次, 所有总共需要执行 $N$ 次入堆操作, 故最终的复杂度的 $logk\times N$.

Python 实现:

用 heapq 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

import heapq
class Solution:
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
n = len(lists)
dummy = ListNode(0)
cur_node = dummy
heap = []
for i, l in enumerate(lists):
if l:
heap.append([l.val, i, l]) # heapq 实际上可以优先级相同, 这里仅仅为了与 PriorityQueue 对应, 所以加入了下标
heapq.heapify(heap)
while heap:
top = heapq.heappop(heap)
cur_node.next = top[2]
cur_node = cur_node.next
if cur_node.next:
heapq.heappush(heap, [top[2].next.val, top[1], top[2].next])
return dummy.next

用 PriorityQueue 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
from queue import PriorityQueue
dummy = ListNode(0)
cur_node = dummy
heap = PriorityQueue()
for i, l in enumerate(lists):
if l:
heap.put([l.val, i, l]) # 在 python3 中, PriorityQueue 不允许插入优先级相同的值或无法判断优先级的值
while not heap.empty(): # 注意, 这里不能用 while heap 进行判断, 否则进入死循环
val, index, node = heap.get()
cur_node.next = node
cur_node = cur_node.next
if node.next:
heap.put([node.next.val, index, node.next])
return dummy.next

C++ 实现:

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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
private:
// 用函数对象进行比较
struct cmp{
bool operator()(ListNode *node1, ListNode *node2){
return node1->val > node2->val;
}
};
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
priority_queue<ListNode *, vector<ListNode *>, cmp> min_heap;
// 用 lambda 表达式进行比较
// auto cmp = [](ListNode* node1, ListNode* node2) {
// return node1->val > node2->val; // 小顶堆
// };
//priority_queue<ListNode *, vector<ListNode *>, decltype(cmp)> min_heap(cmp);
for(auto node_head : lists)
if(node_head!=nullptr) min_heap.push(node_head);
if(min_heap.empty()) return nullptr;
ListNode *dummy = new ListNode(0);
ListNode *cur = dummy;
while(!min_heap.empty()){
ListNode *tmp = min_heap.top(); min_heap.pop();
cur->next = tmp;
cur = cur->next;
if(tmp->next != nullptr) min_heap.push(tmp->next);
}
return dummy->next;
}
};

解法三: 转化成双列表合并问题

时间复杂度: $O(k \times N)$
空间复杂度: $O(1)$

双列表合并问题的时间复杂度为 $O(m+n)$ , 可以将多链表合并问题看做是k次双列表合并.

解法四: 对解法三进行优化

时间复杂度: $O(logk \times N)$
空间复杂度: $O(1)$

对列表合并时, 每次都是两两合并(不是解法三中的逐一合并), 这样, 只需要经过 $logk$ 次两两合并就可完成所有合并过程.

迭代实现:

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
class Solution {
private:
ListNode* mergeTwoLists(ListNode *l1, ListNode *l2){
ListNode *dummy = new ListNode(0);
ListNode *cur = dummy;
while(l1!=nullptr && l2!=nullptr){
if(l1->val < l2->val){
cur->next = l1;
cur = cur->next;
l1 = l1->next;
}else{
cur->next = l2;
cur = cur->next;
l2 = l2->next;
}
}
if(l1==nullptr) cur->next = l2;
else cur->next = l1;
return dummy->next;
}
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
if(lists.empty()) return nullptr;
int len = lists.size();
int interval = 1;
while(interval < len){
for(int i=0; i+interval<len; i+= 2*interval){//i应满足: 0,2,4... / 0,4,.. / 0
lists[i] = mergeTwoLists(lists[i], lists[i+interval]);//i+interval必须<len, 否则溢出
}
interval *= 2; //区间大小翻倍
}
return lists[0];
}
};

递归实现: 递归实现需要额外的 $O(logk)$ 的栈空间(调用递归的次数)

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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
private:
ListNode* mergeTwoLists(ListNode *l1, ListNode *l2){
ListNode *dummy = new ListNode(0);
ListNode *cur = dummy;
while(l1!=nullptr && l2!=nullptr){
if(l1->val < l2->val){
cur->next = l1;
cur = cur->next;
l1 = l1->next;
}else{
cur->next = l2;
cur = cur->next;
l2 = l2->next;
}
}
if(l1==nullptr) cur->next = l2;
else cur->next = l1;
return dummy->next;
}

ListNode* partition(vector<ListNode*>& lists, int start, int end){
if(start==end){
return lists[start];
}else if(start < end){
int mid = (start+end)/2;
ListNode* l1 = partition(lists, start, mid);
ListNode* l2 = partition(lists, mid+1, end);
return mergeTwoLists(l1, l2);
}else
return nullptr;
}
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
if(lists.empty()) return nullptr;
return partition(lists, 0, lists.size()-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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
private:
ListNode* mergeTwoLists(ListNode *l1, ListNode *l2){
if(l1==nullptr) return l2;
else if(l2==nullptr) return l1;
else if(l1->val < l2->val){
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}else{
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}

ListNode* partition(vector<ListNode*>& lists, int start, int end){
if(start==end){
return lists[start];
}else if(start < end){
int mid = (start+end)/2;
ListNode* l1 = partition(lists, start, mid);
ListNode* l2 = partition(lists, mid+1, end);
return mergeTwoLists(l1, l2);
}else
return nullptr;
}
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
if(lists.empty()) return nullptr;
return partition(lists, 0, lists.size()-1);
}
};

024. 两两交换链表中的节点-中等

题目链接: https://leetcode-cn.com/problems/swap-nodes-in-pairs/

解法: 按照题目要求逻辑进行交换

递归:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
if (head and head.next):
second_node = head.next
head.next = self.swapPairs(second_node.next)
second_node.next = head
return second_node
return head

非递归:

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
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
if head == None or head.next == None: return head
dummy = ListNode(0)
dummy.next = head
first = dummy.next
second = dummy.next.next
pre = dummy
while (True):
pre.next = second
first.next = second.next
second.next = first
pre = first
if (first.next != None and first.next.next != None):
second = first.next.next
first = first.next
else:
break
return dummy.next

025. K 个一组翻转链表

题目链接: https://leetcode-cn.com/problems/reverse-nodes-in-k-group/

解法: 先断开转换成单个链表的翻转, 再链接起来

A%2FAlgorithms%2Freverse-nodes-in-k-group.png

  • 链表分区为已翻转部分+待翻转部分+未翻转部分
  • 每次翻转前,要确定翻转链表的范围,这个必须通过 k 此循环来确定
  • 需记录翻转链表前驱和后继,方便翻转完成后把已翻转部分和未翻转部分连接起来
  • 初始需要两个变量 pre 和 end,pre 代表待翻转链表的前驱,end 代表待翻转链表的末尾
  • 经过k此循环,end 到达末尾,记录待翻转链表的后继 next = end.next (next在Python中是关键字, 下面的代码中使用rear)
  • 翻转链表,然后将三部分链表连接起来,然后重置 pre 和 end 指针,然后进入下一次循环
  • 特殊情况,当翻转部分长度不足 k 时,在定位 end 完成后,end==null,已经到达末尾,说明题目已完成,直接返回即可
  • 时间复杂度为 $O(nK)$ 最好的情况为 $O(n)$ 最差的情况为 $O(n^2)$
  • 空间复杂度为 $O(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
25
26
27
28
29
30
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
dummy = ListNode(0)
dummy.next = head
end = dummy
while (end):
pre = end
for _ in range(k): # 找到 end 的位置
if end.next:
end = end.next
else: # 如果不足 k, 则不翻转这部分, 直接返回
return dummy.next
rear = end.next # 确定 rear(图中的next) 的位置
end.next = None # 令带翻转部分与后面脱离
end = node = pre.next # 令 end 指向带翻转部分的首节点, 这样翻转后, end 正好指向翻转后部分的末尾节点
last_node = None
while (node): # 对带翻转部分进行翻转
next_node = node.next
node.next = last_node
last_node = node
node = next_node
pre.next = last_node # 翻转完成后, 将前驱和后继节点与该部分相连
end.next = rear
return dummy.next

026. 删除排序数组中的重复项-简单

Description

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

解法一:

双指针法, 一次遍历, 一个指针顺序遍历数组, 另一个指针记录删除后的正确下标. 两种写法, 后者相当精简

Python 实现:

1
2
3
4
5
6
7
8
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
index = 0
for i in range(len(nums)):
if i == 0 or nums[i] != nums[i-1]:
nums[index] = nums[i]
index+=1
return index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if(nums.size()==0) return 0;
int same = nums[0];
int length = 1;
for(int i=1; i<nums.size(); i++){
if(nums[i] == same){
nums.erase(nums.begin()+i);
i--;
continue;
}
else{
same = nums[i];
length++;
}

}
return length;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if(nums.empty()) return 0;
int length=1;
for(auto n:nums){
if(n>nums[length-1])
nums[length++]=n;
}
return length;
}
};

027. 移除元素

题目链接: https://leetcode-cn.com/problems/remove-element/

解法

一次遍历, 双指针法, 当 “快” 指针不等于目标值时, 将其赋值给 “慢” 指针, 最终直接返回 “慢” 指针的位置即可

1
2
3
4
5
6
7
8
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
index = 0
for i in range(len(nums)):
if nums[i] != val:
nums[index] = nums[i]
index += 1
return index

028. 实现 strStr()

字符串匹配算法, 更详细的解析请看字符串匹配算法解析

description: KMP, 判断是否为子串

Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

Example 1:

Input: haystack = “hello”, needle = “ll”
Output: 2
Example 2:

Input: haystack = “aaaaa”, needle = “bba”
Output: -1
Clarification:

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

解法一: 暴力

解法二: KMP

求解next数组: 求解某个位置 $k$ 的 next 数组值是一个循环的过程, 需要不断检查以 位置 $k-1$ 的next值 为下标的元素的 下一位元素当前位置 $k$ 元素 是否相等, 如果相等, 则 next[k] = next[k-1]+1, 如果不相等, 则

029. 两数相除

Description: 实现除法

给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。

返回被除数 dividend 除以除数 divisor 得到的商。

Example 1:

1
2
Input: dividend = 10, divisor = 3
Output: 3

Example 2:

1
2
Input: dividend = 7, divisor = -3
Output: -2

Note:
Both dividend and divisor will be 32-bit signed integers.
The divisor will never be 0.
Assume we are dealing with an environment which could only store integers within the 32-bit signed integer range: $[−2^{31}, 2^{31 − 1}]$. For the purpose of this problem, assume that your function returns 2^{31 − 1} when the division result overflows.

解法一: 循环加法

时间复杂度: $O(dividend)$

这种方法很容易时间超限: 当被除数很大(INT_MAX), 除数很小(1), 则需要循环INT_MAX次才能完成计算.

解法二: 左移法

时间复杂度: $O(log(dividend))$, dividend 为被除数的大小.

对除数进行左移, 相当于每次乘以2, 直到左移后大于被除数, 用被除数减去左移后的数字, 记录左移对应除数的倍数, 然后再次将从除数开始左移, 直到被除数小于除数.

以上是除法的基本实现思路, 但是在具体实现时, 还需要特别考虑下面的情况

  • 当被除数为 INT_MIN, 除数为 -1 时, 此时的返回值为 INT_MAX+1. (根据题目要求, 溢出时刻直接返回 INT_MAX)
  • 当除数为 0 时, 也应该看做是溢出情况.
  • 处理上面情况最方便的方法使用 long 长整型, 而不是 unsigned int 无符号类型. 因为 unsigned int 类型在进行乘以 2 的操作时, 很容易也溢出, 最终造成程序的死循环, 为了防止溢出, 最好使用 long, 具体请看代码.

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int divide(int dividend, int divisor) {
if(divisor==0 || (dividend==INT_MIN&&divisor==-1))
return INT_MAX;
int res=0;
int sign = ((dividend<0) ^ (divisor<0)) ? -1:1;// 用异或来获取符号
long did = labs(dividend); // long与int在有些环境中字节中一样, 此时最好用long long
long dis = labs(divisor);
while(did >= dis){
long temp = dis, multiple = 1;
while( did >= temp<<1 ){
temp = temp<<1;
multiple = multiple<<1;
}
did -= temp;
res+= multiple;
}
return res*sign;
}
};

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def divide(self, dividend: int, divisor: int) -> int:
sign = -1 if (dividend<0) != (divisor<0) else 1
did = abs(dividend)
div = abs(divisor)
res = 0
while did >= div:
temp = 1
while (did >= div << 1):
div = div << 1
temp = temp << 1
res += temp
did -= div
div = abs(divisor)
res = res * sign
if res > 2**31 - 1 or res < -2**31: # 如果溢出, 则返回 2^31 - 1
return 2**31 -1
return res

扩展: 这道题如果不允许使用 long 或者long long 怎么解?

031. 下一个排列-Next Permutation-中等

Description: 实现 next_permutation 函数逻辑

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须原地修改,只允许使用额外常数空间。

Here are some examples. Inputs are in the left-hand column and its corresponding outputs are in the right-hand column.

1
2
3
1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

解法一: next_permutation 实现

时间复杂度: $O(n)$
空间复杂度: $O(1)$

STL中的 next_permutation 函数和 prev_permutation 两个函数提供了对于一个特定排列P, 求出其后一个排列P+1和前一个排列P-1的功能.

next_permutation 的实现方法如下:

  • 从后往前 找第一个小于后一个数的元素 nums[i]: nums[i]<nums[i+1]
  • 从后往前 找第一个大于 nums[i] 的数 nums[j]: nums[j]>nums[i]
  • 交换 nums[i]nums[j]
  • i 之后的元素逆置(reverse)

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
for i in range(n-2, -1, -1):
if nums[i] < nums[i+1]:

for j in range(n-1, i, -1):
if nums[i] < nums[j]:
nums[i], nums[j] = nums[j], nums[i]
nums[i+1:] = nums[i+1:][::-1]
return
nums[:] = nums[::-1]
return

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n=nums.size();
int i = n-2, j = n-1;
while(i>=0 && nums[i]>=nums[i+1]) i--;
if(i>=0){
while(j>=0 && nums[i]>=nums[j]) j--;
swap(nums[i], nums[j]);
}
reverse(nums.begin()+i+1, nums.end());
}
};

033. 搜索旋转排序数组

Description: 在循环有序数组中查找元素

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。

你可以假设数组中不存在重复的元素。

你的算法时间复杂度必须是 O(log n) 级别。

Example 1:

1
2
Input: nums = [4,5,6,7,0,1,2], target = 0
Output: 4

Example 2:

1
2
Input: nums = [4,5,6,7,0,1,2], target = 3
Output: -1

解法一: 二分查找

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

对于数组[4,5,6,7,0,1,2], 可以将其看成是两段: [4,5,6,7] 和 [0,1,2], 可以看出, 前一段中的任意一个数字都大于后一段中的数字, 于是, 令low=0, high=size()-1, 进行二分查找, 其中 mid 对应的数字要么落在前半段(nums[low] <= nums[mid]), 要么落在后半段.

如果 target 和 mid 落在不同段, 则分以下两种情况:

  • mid 在前半段, target 在后半段, 则 low 应该无条件等于 mid+1
  • mid 在后半段, target 在前半段, 则 high 应该无条件等于 mid-1

如果处于同一段, 那么实际上就相当于升序序列的二分查找, 直接根据 mid 和 target 的大小关系更改 low / high 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def search(self, nums: List[int], target: int) -> int:
low = 0
high = len(nums)-1
while (low <= high):
mid = (low + high) // 2
if (nums[mid] == target):
return mid
if nums[mid] >= nums[0] and target < nums[0]: # mid 在前半段, target 在后半段
low += 1
elif nums[mid] < nums[0] and target >= nums[0]: # mid 在后半段, target 在前半段
high -= 1
elif nums[mid] < target: # 否则, mid 与 target 在同一段, 此时使用正常的二分查找
low = mid+1
elif nums[mid] > target:
high = mid-1
return -1

解法二: 二分查找

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

对于数组[4,5,6,7,0,1,2], 可以将其看成是两段: [4,5,6,7] 和 [0,1,2], 可以看出, 前一段中的任意一个数字都大于后一段中的数字, 于是, 令low=0, high=size()-1, 进行二分查找, 其中 mid 对应的数字要么落在前半段(nums[low] <= nums[mid]), 要么落在后半段.

如果落在的前半段, 则看 target 的值是否在 low与mid之间. 是则 high = mid-1, 否则 low = mid+1

反之, 如果落在后半段, 则看 target 的值是否在 midhigh 之间, 是则 low=mid+1 , 否则high = mid-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
25
26
class Solution {
public:
int search(vector<int>& nums, int target) {
int low = 0;
int high = nums.size()-1;
//数组前半段的数字永远大于后半段的数字
while(low<=high){ //当low==high时, mid=low=high, 如果不等于target, 则之后会退出循环
int mid = (low+high)/2;
if(target == nums[mid]) return mid;
if(nums[low] <= nums[mid]){ //说明当前mid落在数组的前半段(), 这里等于号必须带, 否则会漏解
//判断target是否在low与mid之间, 这里low需要带等于号,
//因为target有可能=nums[low], mid无需带等于号
if(target >= nums[low] && target < nums[mid])
high = mid-1;
else
low = mid+1;
}else{ // 只有当nums[low]完全小于nums[mid]时, mid才落在后半段
if(target > nums[mid] && target <= nums[high])
low = mid+1;
else
high = mid-1;
}
}
return -1;
}
};

解法三: 二分查找

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

该方法同样是二分查找, 只不过与上面有一点不同, 对于数组nums=[4,5,6,7,0,1,2]来说, 如果 target < nums[0], 说明 target 位于数组的后半段, 那么可以将数组看做是nums=[INT_MIN,INT_MIN,INT_MIN,INT_MIN,0,1,2] , 这样一来, 就变成了最常规的有序数组, 反之, 如果 target 位于数组的前半段, 那么可以将数组看做是nums=[4,5,6,7,INT_MAX,INT_MAX,INT_MAX].

注意, 这里并不会改变数组内部的值, 我们只是利用一个临时变量num来代替当前的nums[mid]的值, 然后利用 numtarget 比较进行二分查找.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int search(vector<int>& nums, int target) {
int low = 0;
int high = nums.size()-1;

while(low<=high){
int mid = (low+high)/2;
int num;
if(target < nums[0]){ //target在后半段, 所以将前半段都看做INT_MIN
if(nums[mid] < nums[0]) num = nums[mid]; // nums[mid]在后半段
else num = INT_MIN; // nums[mid]在前半段,
}else{ //target在前半段, 所以将后半段都看作是INT_MAX
if(nums[mid] < nums[0]) num = INT_MAX; // nums[mid]在后半段
else num = nums[mid]; // nums[mid]在前半段
}
if(num == target) return mid;
else if(target < num) high = mid-1;
else low = mid+1;
}
return -1;
}
};

更精简的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
int low=0, high=n-1;
while(low<=high){
int mid = (low+high)/2;
int num;
if(target<nums[0])
num = nums[mid]<nums[0] ? nums[mid] : INT_MIN;
else
num = nums[mid]<nums[0] ? INT_MAX : nums[mid];
if(target > num) low = mid+1;
else if(target < num) high = mid-1;
else return mid;
}
return -1;
}
};

034. 在排序数组中查找元素的第一个和最后一个位置

Description: 在有序数组中查找目标的开始位置和结束位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

你的算法时间复杂度必须是 O(log n) 级别。

如果数组中不存在目标值,返回 [-1, -1]。

Example 1:

1
2
Input: nums = [5,7,7,8,8,10], target = 8
Output: [3,4]

Example 2:

1
2
Input: nums = [5,7,7,8,8,10], target = 6
Output: [-1,-1]

解法一: 二分查找

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

先用常规的二分查找找到target, 然后分别用二分查找找到最左边的target和最右边的target下标.

Python 实现:

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
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
low = 0
high = n-1
min_index = -1
while (low <= high): # 找下界
mid = (low + high) // 2
print(nums[mid], mid)
if (nums[mid] == target and (mid==0 or nums[mid] !=nums[mid-1])):
min_index = mid
break
if (nums[mid] < target):
low = mid+1
else:
high = mid-1

low = 0
high = n-1
max_index = -1
while (low <= high): # 找上界
mid = (low + high) // 2
if (nums[mid] == target and (mid == n-1 or nums[mid] != nums[mid+1])):
max_index = mid
break
if (nums[mid] <= target):
low = mid+1
else:
high = mid-1

return [min_index, max_index]

C++ 实现:

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
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int low = 0;
int high = nums.size() - 1;
vector<int> res{-1,-1};
int mid=-1;
while(low <= high){ //正常的二分查找, 先找到target
mid = (low+high)/2;
if(nums[mid] == target) break;
else if(nums[mid] < target) low = mid+1;
else high = mid-1;
}
if(mid==-1 || nums[mid] != target) return res; // 数组为空或者数组内没有target
//以mid为中心, 分别查找下标最小的target和下标最大的target
int llow=low, lhigh=mid; // 左边的二分查找low,high初始化
int rlow=mid, rhigh=high; // 右边的二分查找low,high初始化
while(llow<=lhigh){
int mid = (llow+lhigh)/2;
if(nums[mid] == target){
if(mid==llow || nums[mid-1] != target){ //关键: 只有当等于target并且左边没有元素或者左边元素不等于target时, 当前mid才是最左边的target
res[0] = mid; break;
}else
lhigh = mid-1;
}else if(nums[mid] < target)
llow = mid+1;
else
lhigh = mid-1;
}
while(rlow<=rhigh){
int mid = (rlow+rhigh)/2;
if(nums[mid] == target){
if(mid==rhigh || nums[mid+1] != target){ //同理, 找最右边的target
res[1] = mid; break;
}else
rlow = mid+1;
}else if(nums[mid] < target)
rlow = mid+1;
else
rhigh = mid-1;
}
return res;

}
};

解法二: 二分查找

同样是二分查找, 更加精炼, 先找到最左边的target, 然后以最左边为low, 开始找最右边的target, 需要注意的是不能在nums[mid] == target时就退出循环.

实现一:

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
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
low = 0
high = n-1
min_index = -1
while (low <= high): # 找下界
mid = (low + high) // 2
print(nums[mid], mid)
if (nums[mid] == target and (mid==0 or nums[mid] !=nums[mid-1])):
min_index = mid
break
if (nums[mid] < target):
low = mid+1
else:
high = mid-1
if min_index == -1: # 注意这里加速了寻找过程
return [-1, -1]
low = min_index # 注意这里加速了寻找过程
high = n-1
max_index = -1
while (low <= high): # 找上界
mid = (low + high) // 2
if (nums[mid] == target and (mid == n-1 or nums[mid] != nums[mid+1])):
max_index = mid
break
if (nums[mid] <= target):
low = mid+1
else:
high = mid-1

return [min_index, max_index]

实现二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int low = 0;
int high = nums.size()-1;
vector<int> res{-1, -1};
while(low < high){ //找起始位置, 注意这里不能是 <=, 而必须是=, 否则会死循环
int mid = (low+high)/2; //偏向左边, 很重要, 否则会死循环
if(nums[mid] < target) low = mid+1;
else high = mid; //注意, 这里不是mid-1, 因为现在是在找最左边的target, 故不能在=target时退出, 因此也不能直接令high=mid-1, 否则会丢失mid=target的情况
}
if(nums.size()==0 || nums[low] != target) return res;
res[0]=low;

high = nums.size()-1;// low 已经指向起始位置, 这里只需重置high
while(low < high){ // 找终止位置
int mid = (low+high+1)/2; //使mid偏向右边, 这很重要
if(nums[mid] > target) high = mid-1;
else low = mid;
}
res[1]=high;
return res;
}
};

解法三: lower/upper_bound

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

直接利用 STL 的 lower_bound()upper_bound() 函数分别找到其实位置和终止位置即可, 在使用这两个函数时, 需要注意以下几点:

  • lower_bound() 函数返回首个 不小于 target 的迭代器, 如果数组中所有元素 都小于 target, 则会返回超尾迭代器.
  • upper_bound() 函数返回首个 大于 target 的迭代器, 如果数组中所有元素 都小于等于 target, 则会返回超尾迭代器.
  • 注意 upper_bound() 返回的迭代器是首个 大于 目标值的迭代器, 因此需要将其减一才是我们要找的 target 的终止位置.

Python 实现:

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
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:

def lower_bound(nums, target):
low = 0
high = len(nums)
while (low < high):
mid = (low + high) // 2
if nums[mid] < target:
low = mid +1
else:
high = mid
return high

def upper_bound(nums, target):
low = 0
high = len(nums)
while (low < high):
mid = (low + high) // 2
if nums[mid] <= target:
low = mid + 1
else:
high = mid
return high

lower = lower_bound(nums, target)
upper = upper_bound(nums, target)
return [lower, upper-1] if lower < len(nums) and nums[lower] == target else [-1, -1]

C++ 实现

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()) return vector<int>{-1,-1};
auto lower = std::lower_bound(nums.begin(), nums.end(), target);
if(lower==nums.end() || *lower != target) return vector<int>{-1,-1};
auto upper = std::upper_bound(nums.begin(), nums.end(), target);
return vector<int>{lower-nums.begin(), upper-nums.begin()-1};
}
};

035. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

解法一: 二分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
n = len(nums)
low = 0
high = n-1
while (low <= high):
mid = (low+high) // 2
if (nums[mid] < target):
low = mid+1
elif (nums[mid] > target):
high = mid-1
else:
return mid
return low

036. 有效的数独

Description: 验证一个矩阵是否是数独数据

Determine if a 9x9 Sudoku board is valid. Only the filled cells need to be validated according to the following rules:

Each row must contain the digits 1-9 without repetition.
Each column must contain the digits 1-9 without repetition.
Each of the 9 3x3 sub-boxes of the grid must contain the digits 1-9 without repetition.

解法一: 利用flag数组存储判断矩阵

时间复杂度: $O(9^2)$
空间复杂度: $O(3\times 9^2)$ 虽然要申请三个二维数组, 但都是常数级.

用三个 9×9 大小的矩阵, 分别储存每一行上, 每一列上, 每一个子块上1-9数字是否出现.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from collections import defaultdict
class Solution:
def isValidSudoku(self, board: List[List[str]]) -> bool:
row_dict = defaultdict(set)
col_dict = defaultdict(set)
mat_dict = defaultdict(set)
n = 9
for i in range(9):
for j in range(9):
if board[i][j] == '.':
continue
mat = (i // 3)*3 + j // 3
if board[i][j] in row_dict[i] or board[i][j] in col_dict[j] or board[i][j] in mat_dict[mat]:
return False # 发现重复, 返回 False

row_dict[i].add(board[i][j]) # 更新字典
col_dict[j].add(board[i][j])
mat_dict[mat].add(board[i][j])
return True

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
bool isValidSudoku(vector<vector<char>>& board) {
// 下面三个矩阵分别存储了 行上1-9是否出现, 列上1-9是否出现, sub-box上1-9是否出现的bool值
// 如果row_flag[1][3] 为真, 则说明第1行(从第0行算起)上已经具有数字4(数字比下标大1)了
bool row_flag[9][9] {0}, col_flag[9][9] {0}, sub_flag[9][9] {0};
for(int i = 0 ; i<board.size(); i++){
for(int j = 0; j<board[i].size(); j++){
if(board[i][j] == '.') continue; // 如果为 '.' 则可以直接跳过此次判断
int num = board[i][j] - '0' - 1; //这里-1主要是为了能够直接将num作为下标使用
int k = i/3*3 + j/3;
if(row_flag[i][num] || col_flag[j][num] || sub_flag[k][num])
return false;
row_flag[i][num]=col_flag[j][num]=sub_flag[k][num]=true;
}
}
return true;
}
};

解法二: 位操作

时间复杂度: $O(n^2)=O(9^2)$
空间复杂度: $O(3\times 9)$

这是目前看到的最好的方法, 核心思想就是用一个 short 类型变量的某一位来作为 flag, 这样, 我们可以进一步节省空间的使用, 将空间复杂度从 $O(n^2)$ 降低到 $O(n)$.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
bool isValidSudoku(vector<vector<char>>& board) {
vector<short> row(9,0);
vector<short> col(9,0);
vector<short> block(9,0);
for(int i=0; i<9; i++){
for(int j=0; j<9; j++){
int idx = 1 << (board[i][j]-'0');
if(row[i]&idx || col[j]&idx || block[i/3*3+j/3]&idx)
return false;
row[i] |= idx;//将对应位置为1, 标记已经出现过
col[j] |= idx;
block[i/3*3+j/3] |= idx;
}
}
return true;
}
};

038. 报数

Description

The count-and-say sequence is the sequence of integers with the first five terms as following:

  1. 1
  2. 11
  3. 21
  4. 1211
  5. 111221
    1 is read off as “one 1” or 11.
    11 is read off as “two 1s” or 21.
    21 is read off as “one 2, then one 1” or 1211.

Given an integer n where 1 ≤ n ≤ 30, generate the nth term of the count-and-say sequence.

Note: Each term of the sequence of integers will be represented as a string.

解法一: 依次查看上一次的数字

时间复杂度: $O(nm)$ m为数字字符串的长度
空间复杂度: $O(m)$

每次根据上一次的数字更新当前的数字字符串, 如此迭代直到达到指定次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
string countAndSay(int n) {
string res="1";
int i=1;
while(i<n){
string tmp;
for(int u=0; u<res.size(); u++){
char c=res[u];
int count = 1;
while(u+1<res.size() && res[u+1]==c){
count++;
u++;
}
tmp += to_string(count)+c;
}
res.swap(tmp);
i++;
}
return res;
}
};

039. 组合总和

解法一: 回溯

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
def back_track(res, temp, temp_num, index, target, candidates):
if (temp_num == target):
res.append(temp)
return
for i in range(index, len(candidates)):
if temp_num+candidates[i] <= target:
back_track(res, temp+[candidates[i]], temp_num+candidates[i], i, target, candidates)
res = []
back_track(res, [], 0, 0, target, candidates)
return res

解法二: 先排序再回溯

先排序再回溯, 这样可以在某个元素不满足条件时, 提前终止递归, 加速程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
def back_track(nums, res, index, path, target):
if (target == 0):
res.append(path)
return
for i in range(index, len(nums)):
if nums[i] > target:
break
back_track(nums, res, i, path+[nums[i]], target-nums[i])

res = []
back_track(sorted(candidates), res, 0, [], target)
return res

解法三: 动态规划, 完全背包

这道题用另一种角度来考虑就变成了一个背包类问题:
有一个容量为target大小的背包, 还有一系列物品,物品的大小由candidated数组给出, 每种物品可以使用无限次, 现在问: 要装满这个背包, 总共有多少种装法. (这其实和找零钱问题是一样的)

第一种 dp, 会产生重复解

先遍历所有可能的背包容量, 对于每一种背包容量, 遍历所有的物品, 以更新当前背包容量的值, 这样会产生重复解, 例如, 对于容量等于 5 时的 dp, 物品有 2, 3 两种大小, 那么就会同时将dp[2] + [[3]]以及dp[3] + [[2]]添加到dp[5]中, 形成重复解, 此时, 需要在每一层加入新解时, 进行去重操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
from collections import defaultdict
dp = defaultdict(list)
dp[0] = [[]]
for t in range(1, target+1):
for num in candidates:
if num <= t:
for item in dp[t-num]:
x = item + [num]
x.sort() # 排序去重
if x not in dp[t]:
dp[t].append(x)
return dp[target]

第二种 dp, 不会产生重复解

我们首先遍历所有的物品, 然后对于每一种物品, 我们尝试使用它, 以更新 dp, 这样, 当遍历完一个物品之后, 我们就不会再次使用该物品, 这样一来, 就不会出现重复情况, 也无需进行去重操作.

1
2
3
4
5
6
7
8
9
10
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
from collections import defaultdict
dp = defaultdict(list)
dp[0] = [[]]
for num in candidates:
for t in range(num, target+1):
for item in dp[t-num]:
dp[t].append(item+[num])
return dp[target]

040. 组合总和 II

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
]

1
2
3
4
5
6
7
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
]

示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
]

1
2
3
4
[
  [1,2,2],
  [5]
]

解法一: 回溯

注意要进行去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
def back_track(res, temp, temp_num, index, nums, target):
if (temp_num == target):
temp.sort() # 去重
if temp not in res:
res.append(temp)
return
for i in range(index, len(nums)):
if temp_num + nums[i] <= target:
back_track(res, temp + [nums[i]], temp_num+nums[i], i+1, nums, target)
res = []
back_track(res, [], 0, 0, candidates, target)
return res

041. 缺失的第一个正数

寻找数组中缺失的最小的正数

Description

Given an unsorted integer array, find the smallest missing positive integer.

Example 1:

Input: [1,2,0]
Output: 3
Example 2:

Input: [3,4,-1,1]
Output: 2
Example 3:

Input: [7,8,9,11,12]
Output: 1
Note:

Your algorithm should run in O(n) time and uses constant extra space.

解法一: 下标与正数对应

时间复杂度: $O(n)$
空间复杂度: $O(1)$ (但是对改变了原始的数组, 这是一个小缺陷)

将下标与正数相应对, 例如对于正数5, 我们就将放置在nums[4]上, 这样一来, 再次遍历数组的时候, 当遇到第一个与下标不对应的数字时, 该下标对应的正数(i+1)就是缺少的正数.

放置正数到正确位置上时, 需要注意几点:

  • swap之后需要继续将原来位置上(nums[4])的数放置到正确的位置上, 这里需要一个while循环
  • 在检查数组时, 如果所有数组内所有数字都处在正确位置上, 那么就应该返回nums.size+1 (包括了数组为空的情况: 返回0+1=1)
  • 对于不能用作下标的元素, 如超过数组长度或者为负数的, 直接令i ++.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
i = 0
while i < n:
num = nums[i]
#if num-1 < n and num-1 >= 0 and i != num-1 and nums[num-1] != num:
if num-1 < n and num-1 >= 0 and nums[i] != nums[nums[i]-1]: # 这两条 if 语句等价, 上面的更好理解, 下面的更简洁
nums[i], nums[num-1] = nums[num-1], nums[i] # 令 nums[num-1] = num
else:
i += 1
for i in range(n):
if i != nums[i]-1:
return i+1
return n+1

C++ 实现:
写法一: for+while

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++){
// 注意这些条件: 前两个是为了令交换下标合法, 后一个是防止相同的数交换, 造成死循环
while(nums[i] > 0 && nums[i]<nums.size() && nums[i] != nums[nums[i]-1])
std::swap(nums[i], nums[nums[i]-1]);
}
for(int i =0; i<nums.size(); i++){
if(nums[i] != i + 1)
return i+1;
}
return nums.size()+1;
}
};

写法二: while

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int i = 0;
while(i<nums.size()){
if(nums[i] > 0 &&nums[i]<nums.size() && nums[i] != nums[nums[i]-1])
std::swap(nums[i], nums[nums[i]-1]); // 如果进行了swap, 就不要i++
else
i++;
}
for(int i =0; i<nums.size(); i++){
if(nums[i] != i + 1)
return i+1;
}
return nums.size()+1;
}
};

解法二: 哈希

时间复杂度: $O(n)$ (3次for循环, 毫无争议的 $O(n)$ )
空间复杂度: $O(1)$ (但是对改变了原始的数组, 这是一个小缺陷)

注意: 虽然这里的时间复杂度是毫无争议的 $O(n)$ , 但是不一定会上面的速度快, 因为上面只有两次循环, 内第一次内部的循环次数一般情况下都不会很大.

从哈希的角度理解: 可以将数组下标看成是hash的key

  1. for any array whose length is l, the first missing positive must be in range [1,…,l+1],
    so we only have to care about those elements in this range and remove the rest.
  2. we can use the array index as the hash to restore the frequency of each number within
    the range [1,…,l+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
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
// 丢失的最小正数只可能在 [1,2,...,nums.size()+1] 之间
// 这里的pushback是必须的, 因为下面会将不符合要求的元素都置为0,
//因此nums[0]需要与0对应, 以代表所有的非法元素,
//这点与上面基于swap的方法不同, 上面的swap是让nums[0] 与 1 对应.
nums.push_back(0);
int length = nums.size();
for(int i =0 ; i<length; i++){
if(nums[i] < 0 || nums[i] >= length)
nums[i] = 0; // 将所有不符合丢失正数的数移除, 这一步必须单独用一个for循环做
}

for(int i = 0; i<length; i++){
nums[nums[i]%length] += length;
}
for(int i = 1 ; i<length; i++){
if(nums[i]/length == 0)
return i;
}
return length;
}
};

042 Trapping Rain Water-接雨水

数组中每个值代表柱状体的高度, 每个柱状体的宽度都为1, 根据数组内的值组成的高低不同的块, 能够存储多少个bin (1×1)的水

Description

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it is able to trap after raining.

The above elevation map is represented by array [0,1,0,2,1,0,1,3,2,1,2,1]. In this case, 6 units of rain water (blue section) are being trapped. Thanks Marcos for contributing this image!

Example:

Input: [0,1,0,2,1,0,1,3,2,1,2,1]
Output: 6

解法一: 左右指针

时间复杂度: $O(n)$
空间复杂度: $O(1)$

分别用两个变量left和right指向左边和右边的柱子, 并再用两个变量maxleft和maxright维护左边最高的柱子和右边最高的柱子, 统计的时候, 先固定left和right当中柱子高度较高的那一个, 然后统计较低柱子上存储的水量. 利用, 如果当前left的高度小于right的高度, 则我们计算left上面能够存储的水量, 有两种情况, 当left柱子的高度大于等于maxleft时, 则left柱子上没法存储水, 因为谁会从左边全部流失(右边比左边高, 所以不会从右边流失). 如果left的高度小于maxleft时, 由于水无法从左边流失, 也不能从右边流失, 因此当前柱子上就会存储水, 存储的水量为maxleft-height[left] (不考虑maxright, 因为maxright大于maxleft, 因为当开始的时候, right = maxright, 此时只有 left > right 时, right 才会左移, 也就是肯定有 max_left > max_right, 反之同理).

注意: 此题中的柱子是有 宽度 的, 这一点很重要, 如果柱子的宽度为0 , 那么就是另一种情况了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int trap(vector<int>& height) {
int len = height.size();
int left = 0, right = len-1;
int res = 0, maxleft = 0, maxright = 0;
while(left <= right){
if(height[left] <= height[right]){ //固定较大的一个柱子
if(height[left] > maxleft) maxleft = height[left];// 如果当前柱子的高度大于左边max柱子的高度, 那么该柱子所处位置一定存不下水
else res = res + maxleft - height[left]; // 反之, 该柱子位置上可以存储的水的量为 坐标max高度减去当前的高度
left++;
}else{
if(height[right] > maxright) maxright = height[right];
else res = res + maxright - height[right];
right--;
}
}
return res;
}
};

更简洁的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int trap(vector<int>& height) {
if (height.empty()) return 0;
int left = 0, right = height.size() -1;
int maxLH = height[left], maxRH = height[right];
int res = 0;
while (left < right) {
maxLH = std::max(maxLH, height[left]);
maxRH = std::max(maxRH, height[right]);
if (height[left] < height[right]) {
res += maxLH - height[left];
left++;
} else {
res += maxRH - height[right];
right--;
}
}
return res;
}
};

044. Wildcard Matching

Description: 通配符匹配

Given an input string (s) and a pattern (p), implement wildcard pattern matching with support for ‘?’ and ‘*’.

‘?’ Matches any single character.
‘*’ Matches any sequence of characters (including the empty sequence).
The matching should cover the entire input string (not partial).

Note:

s could be empty and contains only lowercase letters a-z.
p could be empty and contains only lowercase letters a-z, and characters like ? or *.
Example 1:

Input:
s = “aa”
p = “a”
Output: false
Explanation: “a” does not match the entire string “aa”.
Example 2:

Input:
s = “aa”
p = “
Output: true
Explanation: ‘
‘ matches any sequence.
Example 3:

Input:
s = “cb”
p = “?a”
Output: false
Explanation: ‘?’ matches ‘c’, but the second letter is ‘a’, which does not match ‘b’.
Example 4:

Input:
s = “adceb”
p = “ab”
Output: true
Explanation: The first ‘‘ matches the empty sequence, while the second ‘‘ matches the substring “dce”.
Example 5:

Input:
s = “acdcb”
p = “a*c?b”
Output: false

解法一: 迭代

时间复杂度: $O(m+n)$
空间复杂度: $O(1)$

对于每次循环迭代, ij其中至少有一个前进一步, 所以时间复杂度为 $O(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
25
26
27
28
29
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size();
int m = p.size();
int i = 0, j = 0;
int star_index = -1, match = -1;
while (i < n) {
if (j < m and (s[i] == p[j] or p[j] == '?')) { // 单个字符匹配, i, j继续匹配下一个
i++;
j++;
} else if (j < m and p[j] == '*') {
star_index = j; // 如果当前字符为 *, 则有可能如0或若干个字符匹配, 首先假设至于0个字符匹配
match = i; // 只与0个字符匹配时, 记录当前i的值, 然后将j++, i不变
j++;
} else if (star_index != -1) { // 如果前面两个条件都不满足, 说明之间的匹配方法不正确, 此时重新从前一个 * 开始匹配
match++; // 令 * 与之前标记的未匹配的i进行匹配, 然后将标记往后移一位
i = match; // 令 i 和 j 都等于下一个字符, 继续匹配过程
j = star_index+1;
} else {
return false;
}
}
for (int jj = j ; jj < m; jj++) { // 当 i==n 退出循环时, j 有可能还未达到m, 因为有可能是 ***** 的形式
if (p[jj] != '*') return false;
}
return true;
}
};

解法二: DP

时间复杂度: $O(nm)$, $n$ 为s的长度, $m$ 为p的长度
空间复杂度: $O(n)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
bool isMatch(string s, string p) {
int pLen = p.size(), sLen = s.size(), i, j, k, cur, prev;
if(!pLen) return sLen == 0;
bool matched[2][sLen+1];
fill_n(&matched[0][0], 2*(sLen+1), false);

matched[0][0] = true;
for(i=1; i<=pLen; ++i)
{
cur = i%2, prev= 1-cur;
matched[cur][0]= matched[prev][0] && p[i-1]=='*';
if(p[i-1]=='*') for(j=1; j<=sLen; ++j) matched[cur][j] = matched[cur][j-1] || matched[prev][j];
else for(j=1; j<=sLen; ++j) matched[cur][j] = matched[prev][j-1] && (p[i-1]=='?' || p[i-1]==s[j-1]) ;
}
return matched[cur][sLen];
}
};

解法三: DP

时间复杂度: $O(nm)$, $n$ 为s的长度, $m$ 为p的长度
空间复杂度: $O(nm)$

采用和第 10 题相同的思路, 令dp[i][j]代表s[0, i)p[0,j)是否匹配, 该解法的空间复杂度比解法二高.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size();
int m = p.size();
bool dp[n+1][m+1];
std::fill_n(&dp[0][0], (n+1)*(m+1), false); // 用 fill_n 初始化
dp[0][0] = true;
for (int i = 0; i < n+1; i++) {
for (int j = 1; j < m+1; j++) {
if (p[j-1] == '*') {
dp[i][j] = (i > 0 and dp[i-1][j]) or (dp[i][j-1]);
} else {
dp[i][j] = i > 0 and dp[i-1][j-1] and (s[i-1] == p[j-1] or p[j-1] == '?');
}
}
}
return dp[n][m];

}
};

045. 跳跃游戏 II

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

这道题用 DP 解会超时

解法一: 贪心(最优)

时间复杂度: $O(n)$
空间复杂度: $O(1)$

对于每一点i来说, 它可以只用一步达到i+nums[i]中的任意一点, 此时我们设置一步边界为cur_right, 如果某一次走到位置j, 并且该位置正好是边界cur_right, 则说明, 下一次, 无论我们怎么走, steps都必须要增加, 所以此时, 我们更新steps++, 并且更新cur_right为当前只利用steps步能达到的最远距离. 代码如下

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def jump(self, nums: List[int]) -> int:
right_most = 0
cur_right = 0
steps = 0
for i in range(len(nums)-1):
right_most = max(right_most, i + nums[i])
if i == cur_right:
cur_right = right_most
steps += 1
return steps

046. 全排列

全排列, 注意是distict的数字, 故而不需要进行重复检查

Description: 不含重复数字的全排列

Given a collection of distinct integers, return all possible permutations.

Example:

Input: [1,2,3]
Output:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

解法一: 递归

时间复杂度: $O(A^n_n)$ , 每一种情况都是 $O(1)$ , 共有 $O(A^n_n)$ 种情况. (对吗?)

用一个变量pos指向nums的第一个位置, 然后将pos与后面所有位置上的数字交换(包括自己), 最终会得到n种可能性, 这n种可能性就是出现在第一位置上的所有可能字符的情况集合, 然后将第一位固定, 并将pos指向下一位, 此时问题转换成了n-1个字符的全排列, 按照这种想法一致递归下去, 就可以找到所有位置上的所有组合情况(用pos==nums.size()判断)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
if(nums.size()==0) return res;
permute_helper(res, 0, nums);
return res;
}
void permute_helper(vector<vector<int> > &res, int pos, vector<int> &nums){
if(pos == nums.size())
res.push_back(nums); // 当pos走到最后时, 说明一种情况诞生, 将其添加到res中
else{
for(int i = pos; i<nums.size(); i++){
std::swap(nums[pos], nums[i]);
permute_helper(res, pos+1, nums);
std::swap(nums[pos], nums[i]); // 能够去掉这句话的前提是对res内的字符串进行重复检查, 具体可看牛客分析
//在面对含有重复字符的情况时, 最好加上这句话
}
}
}
};

解法二: 迭代

时间复杂度: $O(n^3)$
空间复杂度: $O(A_n^n)$ 全排列的size

对于n个数的全排列问题, 可以想象成已经获得了n-1个数的全排列, 然后将第n个数插入到n-1个数的n个空位上( 如将3插入到12的空位上分别为: 312,132,123).

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
res = [[]]
for i in range(n):
sub_res = [] # 存在中间结果
for item in res: # 已经得到前 i-1 个元素排列的结果
temp = []
for k in range(len(item)+1): # 将第 i 个元素插入到前 i-1 个元素形成的排列中的各个位置
temp.append(item[:k] + [nums[i]] + item[k:])
sub_res.extend(temp)
res = sub_res # 更新成前 i 个元素的排列结果
return res

C++实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
vector<vector<int> > permute(vector<int> &num) {
vector<vector<int>> res(1,vector<int>());

for(int i=0; i<num.size(); i++){
vector<vector<int>> tmp_res(std::move(res)); // move之后, res内部会自动被清空, 而且move的效率较高
for(int j=0; j<tmp_res.size(); j++){
for(int k=0; k<=tmp_res[0].size(); k++){ // 注意这里是<=, 因为还要往尾部插
vector<int> tmp(tmp_res[j]);
tmp.insert(tmp.begin()+k, num[i]);
res.push_back(tmp);
}
}
}
return res;
}
};

解法三: 利用C++的内置函数 next_permutation

关于 next_permutation() 的详细解析请看这里

STL中的 next_permutation 函数和 prev_permutation 两个函数提供了对于一个特定排列P, 求出其后一个排列P+1和前一个排列P-1的功能.

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
do{
res.push_back(nums);
}while(next_permutation(nums.begin(), nums.end()));
return res;
}
};

这道题利用 prev_permutation 也可以解决, 但是这里就多了一步 reverse 的操作, 这里贴出来只是帮助理解 STL 函数的内部实现, 对于 Permutation2 题也是同理:

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end(), greater<int>()); // 倒序排序
do{
res.push_back(nums);
}while(prev_permutation(nums.begin(), nums.end()));//使用 prev
return res;
}
};

解法四: 自己实现 next_permutation

用迭代器作为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
template <typename T>
bool nextPermutation(T first, T last) {
auto i = last - 2;
auto j = last - 1;
while (i >= first && *i >= *(i+1)) i--;
if (i >= first) {
while (j >= first && *i >= *j) j--;
std::iter_swap(i, j);
std::reverse(i+1, last);
}
return i>=first ? true : false;
}
public:
vector<vector<int>> permute(vector<int>& nums) {
std::sort(nums.begin(), nums.end());
std::vector<std::vector<int>> res;
do {
res.push_back(nums);
} while (nextPermutation(nums.begin(), nums.end()));
return res;
}
};

用数组作为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
private:
bool nextPermutation(vector<int>& nums) {
int n=nums.size();
int i = n-2, j = n-1;
while(i>=0 && nums[i]>=nums[i+1]) i--;
if(i>=0){
while(j>=0 && nums[i]>=nums[j]) j--;
swap(nums[i], nums[j]);
}
reverse(nums.begin()+i+1, nums.end());
return i>=0 ? true : false;
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
do{
res.push_back(nums);
}while(nextPermutation(nums));
return res;
}
};

prev_permutation 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
private:
bool prevPermutation(vector<int>& nums) {
int n=nums.size();
int i = n-2, j = n-1;
while(i>=0 && nums[i]<=nums[i+1]) i--;
if(i>=0){
while(j>=0 && nums[i]<=nums[j]) j--;
swap(nums[i], nums[j]);
}
reverse(nums.begin()+i+1, nums.end());
return i>=0 ? true : false;
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end(), greater<int>());
do{
res.push_back(nums);
}while(prevPermutation(nums));
return res;
}
};

next_permutation python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
n = len(nums)
i = n - 2
j = n - 1
while (i >= 0 and nums[i] >= nums[i+1]): i -= 1 # 找到i
if (i >= 0):
while (j > i and nums[i] >= nums[j]): j -= 1 # 找到 j
nums[i], nums[j] = nums[j], nums[i] # 交换, 并将 i 之后的进行逆置
nums[i+1:] = nums[i+1:][::-1]
return True if i != -1 else False

def permute(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
res.append(nums.copy()) # 注意这里一定要用copy, 否则后续的更改会影响前面的nums的值
while(self.nextPermutation(nums)):
res.append(nums.copy())
return res

prev_permutation python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def prevPermutation(self, nums: List[int]) -> None:
n = len(nums)
i = n - 2
j = n - 1
while (i >= 0 and nums[i] <= nums[i+1]): i -= 1 # 找到i
if (i >= 0):
while (j > i and nums[i] <= nums[j]): j -= 1 # 找到 j
nums[i], nums[j] = nums[j], nums[i] # 交换, 并将 i 之后的进行逆置
nums[i+1:] = nums[i+1:][::-1]
return True if i != -1 else False

def permute(self, nums: List[int]) -> List[List[int]]:
nums.sort(reverse=True)
res = []
res.append(nums.copy()) # 注意这里一定要用copy, 否则后续的更改会影响前面的nums的值
while(self.prevPermutation(nums)):
res.append(nums.copy())
return res

解法五: 回溯

在进行回溯时, 每次将当前选择的字符串剔除, 当递归到当前可选字符数为 0 时, 代表找到了其中一种排列. 代码如下

1
2
3
4
5
6
7
8
9
10
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def back_track(res, temp, nums):
if len(nums) == 0: # 代表找到了其中一种排列, 将其加入到结果中
res.append(temp)
for i in range(len(nums)):
back_track(res, temp+[nums[i]], nums[:i]+nums[i+1:]) # 将第 i 个字符从 nums 中剔除
res = []
back_track(res, [], nums)
return res

047. 全排列 II

Description: 带有重复元素的全排列

解法一: 递归+set

时间复杂度:
空间复杂度:

set 插入元素的时间复杂度为 $O(logn)$, $n$ 为当前 set 的大小.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
private:
void helper(set<vector<int>> &res, int pos, vector<int> &nums){
int len = nums.size();
if(pos==len)
res.insert(nums);
for(int i=pos; i<len; i++){
if(i!=pos && nums[i]==nums[pos]) continue;
swap(nums[pos], nums[i]);
helper(res, pos+1, nums);
swap(nums[pos], nums[i]);
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
set< vector<int>> res;
helper(res, 0, nums);
return vector<vector<int>>(res.begin(), res.end());
}
};

解法二: STL 的 next_permutation 函数

关于 next_permutation() 的详细解析请看这里

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
do{
res.push_back(nums);
}while(next_permutation(nums.begin(), nums.end()));
return res;
}
};

使用 prev_permutation() 也可解决, 不过需要记得要倒序排序.

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end(), greater<int>()); // 倒序排序
do{
res.push_back(nums);
}while(prev_permutation(nums.begin(), nums.end())); // prev
return res;
}
};

解法三: 自己实现 next_permutation()

python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
def nextPermutation(nums):
n = len(nums)
i = n - 2
j = n - 1
while (i >= 0 and nums[i] >= nums[i+1]): i -=1
if (i>=0):
while (j > i and nums[i] >= nums[j]): j -=1
nums[i], nums[j] = nums[j], nums[i]
nums[i+1:] = nums[i+1:][::-1]
return True if i != -1 else False

nums.sort()
res = []
res.append(nums.copy())
while (nextPermutation(nums)):
res.append(nums.copy())
return res

用迭代器做参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {

template <typename T>
bool nextPermutation(T first, T last) {
auto i = last - 2;
auto j = last - 1;
while (i >= first && *i >= *(i+1)) i--;
if (i >= first) {
while (j >= first && *i >= *j) j--;
std::iter_swap(i, j);
std::reverse(i+1, last);
}
return i>=first ? true : false;
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
std::sort(nums.begin(), nums.end());
std::vector<std::vector<int>> res;
do {
res.push_back(nums);
} while(nextPermutation(nums.begin(), nums.end()));
return res;
}
};

用数组做参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
private:
bool nextPermutation(vector<int>& nums) {
int n=nums.size();
int i = n-2, j = n-1;
while(i>=0 && nums[i]>=nums[i+1]) i--;
if(i>=0){
while(j>=0 && nums[i]>=nums[j]) j--;
swap(nums[i], nums[j]);
}
reverse(nums.begin()+i+1, nums.end());
return i>=0 ? true : false;
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
do{
res.push_back(nums);
}while(nextPermutation(nums));
return res;
}
};

解法四: 回溯+排序去重

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
nums.sort() # 先排序, 便于去重
def back_track(res, temp, nums):
if (len(nums) == 0):
res.append(temp)
for i in range(len(nums)):
if i == 0 or nums[i] != nums[i-1]:
back_track(res, temp+[nums[i]], nums[:i]+nums[i+1:])
res = []
back_track(res, [], nums)
return res

048. 旋转图像

旋转图像, 矩阵旋转

Description: 图片旋转 90 度

给定一个 n × n 的二维矩阵表示一个图像。

将图像顺时针旋转 90 度。

说明:

你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

Example 1:

Given input matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],

rotate the input matrix in-place such that it becomes:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
Example 2:

Given input matrix =
[
[ 5, 1, 9,11],
[ 2, 4, 8,10],
[13, 3, 6, 7],
[15,14,12,16]
],

rotate the input matrix in-place such that it becomes:
[
[15,13, 2, 5],
[14, 3, 4, 1],
[12, 6, 8, 9],
[16, 7,10,11]
]

解法一: 逆置+转置

时间复杂度: $O(n^2)$, 因为转置的复杂度为 $O(n^2)$

将图像矩阵看做是线性代数中的行列式, 首先将所有的行逆置(行与行交换), 然后对整个矩阵转置.

原理: 利用线性代数行列式的运算法则可证明(数学归纳法)

clockwise rotate
first reverse up to down, then swap the symmetry

1
2
3
1 2 3     7 8 9     7 4 1
4 5 6 => 4 5 6 => 8 5 2
7 8 9 1 2 3 9 6 3

Python 实现

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
matrix[:] = matrix[::-1]
n = len(matrix)
for i in range(n):
for j in range(i+1, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
return

C++ 实现

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
std::reverse(matrix.begin(), matrix.end()); //逆置
for(int i = 0; i<matrix.size(); i++){
for(int j=i+1; j<matrix[i].size();j++) // 转置, 注意j=i+1
std::swap(matrix[i][j], matrix[j][i]);
}
}
};

解法二: 转置+列逆置

先求转置, 再对列逆置(列与列交换):

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {

for(int i = 0; i<matrix.size(); i++){
for(int j=i+1; j<matrix[i].size();j++)
std::swap(matrix[i][j], matrix[j][i]);
}
for(auto &vec_i : matrix) std::reverse(vec_i.begin(), vec_i.end());
}
};

补充: 逆时针旋转90度

先使用列逆置(列与列交换), 然后对矩阵使用转置

1
2
3
1 2 3     3 2 1     3 6 9
4 5 6 => 6 5 4 => 2 5 8
7 8 9 9 8 7 1 4 7
1
2
3
4
5
6
7
void anti_rotate(vector<vector<int> > &matrix) {
for (auto vi : matrix) reverse(vi.begin(), vi.end());
for (int i = 0; i < matrix.size(); ++i) {
for (int j = i + 1; j < matrix[i].size(); ++j)
swap(matrix[i][j], matrix[j][i]);
}
}

补充: 图片旋转 180 度(上下翻转)

将所有的行逆置

1
2
3
1 2 3     7 8 9
4 5 6 => 4 5 6
7 8 9 1 2 3

1
reverse(matrix.begin(), matrix.end())

补充: 图片左右翻转

将所有的列逆置

1
2
3
1 2 3     3 2 1
4 5 6 => 6 5 4
7 8 9 9 8 7

1
for (auto vi : matrix) reverse(vi.begin(), vi.end());

049. 字母异位词分组

Description: 找出同字母的异序词, 并按字母分组输出

给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。

Example:

Input: [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”],
Output:
[
[“ate”,”eat”,”tea”],
[“nat”,”tan”],
[“bat”]
]
Note:

All inputs will be in lowercase.
The order of your output does not matter.

解法一: 哈希表+sort

用哈希表来存, 键为有序的字符序列, 值为string数组, 里面存着各个与有序字符序列包含字符相同的其他序列

时间复杂度: $O(nmlogm)$ , 其中, n为输入字符串数组的长度, m为每个字符串的长度, 对于n个字符串, 要进行n次哈希表的插入, 同时每次插入时, 需要对字符串进行排序, 排序复杂度为 $O(mlogm)$.

空间复杂度: $O(mn)$

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
from collections import defaultdict
d = defaultdict(list)
for s in strs:
k = ''.join(sorted(s))
d[k].append(s)

res = []
for v in d.values():
res.append(v)
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
std::unordered_map<string,vector<string>> res_map;
for(auto str: strs){
string str_value = str;
std::sort(str.begin(), str.end());
res_map[str].push_back(str_value); //key 为字母有序string, value为含有这些字母的序列
}
vector<vector<string>> res_vec;
for(auto str : res_map)
res_vec.push_back(str.second); //将map中的所有的string转移到vec返回结果中
return res_vec;
}
};

解法二: 哈希表(不使用sort)

时间复杂度: $O(nm)$ , 其中, n为string个数, m为每个string的字母数.
空间复杂度: $O(nm)$

由于上面的解法二需要使用排序, 故而时间上不够优化, 因此, 这里我们可以设计新的键来代替sort, 基本思想是对26个字母, 分别赋予一个素数值, 然后, 计算键的时候, 将对应字母的素数 相乘 即可, 这样一来, 每一种字符串的key都是唯一的( 因为最终的乘积可以唯一的表示成素数相乘的序列 ).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 该解法是错误的
class Solution {
public:
int primer[26] = {2, 3, 5, 7, 11 ,13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101};
int get_sum_id(string str){
int sum = 1;
for(auto c : str){
sum * = primer[(int)(c-'a')];
}
return sum;
}
vector<vector<string>> groupAnagrams(vector<string>& strs) {
std::unordered_map<int,vector<string>> res_map;
for(auto str: strs){
res_map[get_sum_id(str)].push_back(str); //key 为字母有序string, value为含有这些字母的序列
}
vector<vector<string>> res_vec;
for(auto str : res_map)
res_vec.push_back(str.second); //将map中的所有的string转移到vec返回结果中
return res_vec;
}
};

解法三: 另一种生成 key 的解法(不用sort)

应该将字符count作为键, 所谓字符count就是统计每个字符出现的次数, 然后根据该信息就可以生成唯一的一个字符串, 例如, 对于 “abbb”, 来说, ‘a’ 出现了一次, ‘b’ 出现了三次, 因此, 其字符count就应该为: (1,3,0,…0), 总共有 26 个元素, 为了将其转换成字符串, 需要用一个特殊符号来做分隔符, 因此可以生成如下的字符串: "#1#3#0#0...#0"(这也是通常的内置 hash 的键的实现方法之一).
该解法的时间复杂度为 $O(mn)$, 其中, $n$ 为遍历字符串数组的时间, $m$ 为获取 key 的时间, 无需进行排序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
private:
string get_key(string str){
int str_count[26]{0};
for(auto c : str)
str_count[c-'a']++;
string str_key;
for(auto i : str_count)
str_key += "#" + to_string(i);
return str_key;
}
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> res_hash;
for(auto str : strs){
string s = get_key(str);
res_hash[s].push_back(str);
}
vector<vector<string>> res;
for(auto s : res_hash)
res.push_back(s.second);
return res;
}
};

050. Pow(x, n)

实现幂乘操作

Descriptin

Implement pow(x, n), which calculates x raised to the power n (x^n).

Example 1:

Input: 2.00000, 10
Output: 1024.00000
Example 2:

Input: 2.10000, 3
Output: 9.26100
Example 3:

Input: 2.00000, -2
Output: 0.25000
Explanation: 2-2 = 1/22 = 1/4 = 0.25
Note:

-100.0 < x < 100.0
n is a 32-bit signed integer, within the range $[−2^{31}, 2^{31} − 1]$

解法一: 递归

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

当n为偶数时: $x^n = x^{n/2} \times x^{n/2}$
当n为奇数时: $x^n = x\times x^{n/2} \times x^{n/2}$

Python 实现

1
2
3
4
5
6
7
8
9
10
class Solution:
def myPow(self, x: float, n: int) -> float:
if n < 0:
x = 1/x
n = abs(n)
if n == 1:
return x
if n == 0:
return 1
return self.myPow(x*x, n//2) * x if n % 2 == 1 else self.myPow(x*x, n//2)

C++ 实现
这里需要注意一点: abs(INT_MIN) 的值仍然是负值, 因为 int 只有 32 位, abs(INT_MIN) 时, 仍然是 32 位, 因此不会变成正值, 解决方法是先把该值赋给 long 型变量, 然后对 long 型变量调用 abs() 函数, 另一种解决方法是利用 unsigned int:

1
2
3
4
5
6
7
8
int min = INT_MIN; // -2147483648
long min_abs1 = abs(min); // 2147483648, 这里 min_abs1 的值仍然是 INT_MIN, 因为调用 abs 的时候, 仍然是32位

long min_abs2 = min;
min_abs2 = abs(min_abs2); // 2147483648, 这里是对64位调用 abs, 所以成功转化成正数

// 解决方法二是利用 unsigned int
unsigned int abs_min = abs(min) //2147483648

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
double myPow(double x, int n) {
if(n==0) return 1.0;
unsigned int un = abs(n); //注意这里必须是 unsigned类型, 就算是long , 也必须带unsigned, 主要是因为abs(INT_MIN)仍为负数INT_MIN!
if(n < 0)
x = 1/x;
return (un%2==0) ? myPow(x*x, un/2) : x*myPow(x*x, un/2);
}
};

解法二: 非递归

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

用循环来模拟递归的过程, 并用一个变量res来维护当前的值, 同样根据公式:
当n为偶数时: $x^n = x^{n/2} \times x^{n/2}$
当n为奇数时: $x^n = x\times x^{n/2} \times x^{n/2}$
所以, 在循环时, 如果 n 为偶数, 那么就令 x 翻倍, 令 n 减半, 如果 n 为奇数, 那么就令res与当前x相乘, 这样 相当于 减少了一次乘法, 使得 n 从奇数变成了偶数, 最终, res 的值就是我们要求的幂乘.
举例来说,
对于 x=2, n=10 , 每次将x和自身相乘, 同时将 n 减半, n 和 x 的值分别为:

1
2
n: 10, 5,  2,   1, 0
x: 2, 4, 16, 256, 65536

可以看到, 我们将 n 为奇数时的 x 相乘, 就是最终的幂乘: $4\times 256 = 2^{10} = 1024$. 当 n 为奇数时也是同理, 如下所示:

1
2
n: 11, 5,  2,   1, 0
x: 2, 4, 16, 256, 65536

最终幂乘: $2\times 4\times \times 256 = 2^{11} = 2048$

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def myPow(self, x: float, n: int) -> float:
if n < 0: # 无需在意符号, x 之间的相乘会自动根据乘法的奇偶次数决定最终的符号
x = 1/x
n = abs(n)
res = 1
while n > 0:
if n % 2 == 1:
res *= x
n = n//2
x = x*x
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
double myPow(double x, int n) {
if(n<0) x = 1/x;
long ln = n;
ln = abs(ln);
double res=1;
while(ln>0){
if(ln%2==1) res = res * x;
x = x * x;
ln = ln/2;
}
return res;
}
};

053. 最大子序和 / 最大子区间和

连续子数组的最大和

Description

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

Example:

Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.
Follow up:

If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.

解法一: 记录当前最大值

时间复杂度: $O(n)$
根据数组性质, 设置两个变量, 一个记录当前的最大值, 一个记录当前的子序列之和. 首先, 如果当前子序列之和为负, 那么就是说, 从当前位置开始的子序列, 比从之前位置开始的子序列大, 那么就可以不考虑从之前位置开始的子序列, 之前累计的和也被抛弃

Python 实现

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
cur_sum = 0
max_sum = -float('inf')
for num in nums:
if cur_sum < 0:
cur_sum = num
else:
cur_sum += num
max_sum = max(max_sum, cur_sum)
return max_sum

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int sum = 0;
int max_sum = INT_MIN; //数组有可能全负, 所以不能赋值为0
for(auto num : nums){
if(num > max_sum) max_sum = num; //主要是为了预防数组中全是负数的情况
sum += num;
if(sum!=0 && sum>max_sum) max_sum = sum; // sum!=0 , 为了预防数组全负时, 0一定大于sum, 造成的错解
if(sum <0) sum =0;
}
return max_sum;
}
};

更简洁的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.empty()) return 0;
int tmpRes = nums[0];
int res = nums[0];
for (int i = 1; i < nums.size(); i++) {
if (tmpRes < 0) {
tmpRes = nums[i];
} else {
tmpRes += nums[i];
}
res = std::max(res, tmpRes);
}
return res;
}
};

解法二: 动态规划

递推公式: $d[i] = max(d[i-1]+nums[i], nums[i])$

Python 实现:

1
2
3
4
5
6
7
8
9
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
if len(nums) == 0: return 0
dp = -float('inf')
res = -float('inf')
for num in nums:
dp = max(dp+num, num)
res = max(res, dp)
return res

扩展一: 数组首尾相连怎么办

解法一: 利用最小子序和

最大子序和存在两种情况:

  1. 不包括首尾相连的数: 则直接用 dp 即可解
  2. 包括首尾相连的数: 则此时不包括在最大子序和中的序列必定是原数组中的最小子序, 因此, 用 dp 找到最小子序和, 然后用sum(nums)减去即可

时间复杂度: $O(n)$
空间复杂度: $O(1)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
if len(nums) == 0: return 0
dp = -float('inf')
res = -float('inf')
for num in nums:
dp = max(dp+num, num)
res = max(res, dp)

dp = float('inf')
min_sum = float('inf')
for num in nums:
dp = min(dp+num, num)
min_sum = min(min_sum, dp)
return max(res, sum(nums) - min_sum)

解法二: 控制结束下标

对于超出的部分, 自动下标取余, 同时利用一个 flag, 记录当前下标是取余后的, 还是未取余的

1
# 貌似不可行

扩展二: 数组是二维的怎么办

求二维数组的最大子矩阵: https://www.nowcoder.com/questionTerminal/a5a0b05f0505406ca837a3a76a5419b3

考虑将二维数组按照行维度进行压缩, 这样, 可以将二维数组转换成一维数组进行计算, 为了实现快速压缩, 我们需要引入辅助数组presum[m][n], 其中, presum[i][j] = num[0][j]+num[1][j]+...+num[i][j], 即第j列从1行到i行的和,这样compress[j]=presum[c][j]-presum[a][j]。穷举所有可以压缩的二维数组的复杂度为 $O(m^2)$,调用一维算法的复杂度为 $O(n)$,所以算法的整体复杂度为 $O(m^2n)$

Python 解法:

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
def max_subarr(nums): # 一维数组最大子序和
dp = -float('inf')
res = -float('inf')
for num in nums:
dp = max(dp+num, num)
res = max(dp, res)
return res

def max_submatrix(matrix):
if (len(matrix) == 0): return 0
presum = matrix.copy()
m = len(matrix)
n = len(matrix[0])
presum.append([0]*n) # 在 presum 后面多加一维全0数组, 方便后面的运算
for i in range(m): # 构建 presum 辅助矩阵
for j in range(n):
presum[i][j] = presum[i-1][j] + matrix[i][j]
res = -float('inf')
for k in range(m):
for i in range(k, m): # 枚举所有的可压缩矩阵
compress = [0] * n
for j in range(n): # 求压缩后的一维数组
compress[j] = presum[i][j] - presum[i-k-1][j]
res = max(res, max_subarr(compress)) # 利用一维数组最大子序和求res
return res

N = int(input()) # get input
matrix = [None] * N
for i in range(N):
matrix[i] = [int(item) for item in input().split()]
print(max_submatrix(matrix))

054. 螺旋矩阵

以顺时针螺旋顺序返回矩阵元素, 顺时针打印矩阵

题目链接: https://leetcode-cn.com/problems/spiral-matrix/submissions/

Description

Given a matrix of m x n elements (m rows, n columns), return all elements of the matrix in spiral order.

Example 1:

Input:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
Output: [1,2,3,6,9,8,7,4,5]
Example 2:

Input:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
Output: [1,2,3,4,8,12,11,10,9,5,6,7]

解法: 按层次输出(由外而内)

时间复杂度: $O(n)$
空间复杂度: $O(n)$

输出形式如下(按层次编码, 以4×6的矩阵为例), 需要注意边界控制条件:

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

class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> res;
if(matrix.size()==0 || matrix[0].size() ==0) return res;
int row_layer = (matrix.size()+1)/2;
int col_layer = (matrix[0].size()+1)/2;
int layer = min( row_layer, col_layer); // 计算总共的层数
int cur_layer =0; // 用于记录当前所处哪一层
int len_row = matrix.size();
int len_col = matrix[0].size(); //分别为行和列的size
while(cur_layer < layer){
//top 输出上边
for(int j =cur_layer; j<len_col-cur_layer; j++)
res.push_back(matrix[cur_layer][j]);
//right 输出右边
for(int i = cur_layer+1; i<len_row-1-cur_layer; i++)
res.push_back(matrix[i][len_col - 1 - cur_layer]);
//bottom 输出下边, 这里注意为了防止重复输出, 需要确保上边和下边的行数不同,即:
// cur_layer!=len_row-1-cur_layer
for(int j= len_col - 1 -cur_layer; cur_layer!=len_row-1-cur_layer && j >=cur_layer ;j--)
res.push_back(matrix[len_row - 1 -cur_layer][j]);
//left 输出左边, 同样, 要确保左边和右边的列数不同, 即: cur_layer!=len_col-1-cur_layer
for(int i = len_row-2-cur_layer; cur_layer!=len_col-1-cur_layer && i>cur_layer; i--)
res.push_back(matrix[i][cur_layer]);
cur_layer++;
}
return res;
}
};

另一种写法:

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
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
std::vector<int> res;
if (matrix.empty() or matrix[0].empty()) return res;
int n = matrix.size();
int m = matrix[0].size();
int rowUp = -1; // 记录上边界
int rowDown = n; // 下边界
int colLeft = -1; // 左边界
int colRight = m; // 右边界
while (rowUp < rowDown and colLeft < colRight) {
rowUp++;
rowDown--;
if (rowUp > rowDown) break; // 如果越界, 则直接退出
colLeft++;
colRight--;
if (colLeft > colRight) break; // 越界则退出
for (int j = colLeft; j <= colRight; j++) {
res.emplace_back(matrix[rowUp][j]);
}
for (int i = rowUp+1; i <= rowDown-1; i++) {
res.emplace_back(matrix[i][colRight]);
}
for (int j = colRight; rowUp != rowDown and j >= colLeft; j--) {
res.emplace_back(matrix[rowDown][j]);
}
for (int i = rowDown-1; colLeft != colRight and i >= rowUp+1; i--) {
res.emplace_back(matrix[i][colLeft]);
}
}
return res;
}
};

解法二: 遇到越界时, 转变方向(推荐)

模拟顺时针
绘制螺旋轨迹路径,我们发现当路径超出界限或者进入之前访问过的单元格时,会顺时针旋转方向。

当我们遍历整个矩阵,下一步候选移动位置是 $\text{(cr, cc)}(cr, cc)$ 。如果这个候选位置在矩阵范围内并且没有被访问过,那么它将会变成下一步移动的位置;否则,我们将前进方向顺时针旋转之后再计算下一步的移动位置。

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
ans = []
i = 0
j = 0
n = len(matrix) # n rows
if n == 0: return ans
m = len(matrix[0]) # m cols
direct = [(0, 1), (1, 0), (0, -1), (-1, 0)] # 控制当前的坐标的移动方向
flag = 0
visited = [[False]*m for _ in range(n)]
while i < n and j < m and not visited[i][j]: # 如果 i, j 超界或者当前位置已访问, 则说明遍历完成
ans.append(matrix[i][j])
visited[i][j] = True
new_i = i + direct[flag%4][0] # 获取新的坐标点
new_j = j + direct[flag%4][1] # 获取新的坐标点
if new_i == n or new_i < 0 or new_j == m or new_j < 0 or visited[new_i][new_j]: # 如果新的坐标点不合规, 则应该方向应该顺时针旋转
flag += 1 # 方向更新
new_i = i + direct[flag%4][0]
new_j = j + direct[flag%4][1]
i = new_i
j = new_j # 更新 i, j
return ans

旷视矩阵

输入矩阵的尺寸: m 行, n lp
从左上角开始沿着对角型进行 “S 型” 输出

例:

1
2
3
4
5
m = 3, n = 4

1 2 6 7
3 5 8 11
4 9 10 12

解法一:

按照题意输出, 注意边界条件的控制.

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
def megvii_matrix(n, m):
flag = 1
matrix = [[0]*m for _ in range(n)]
i = 0
j = 0
count = 1
while (count <= m*n):
if flag == 1:
if i < 0 and j < m:
flag = 0
i += 1
elif j == m:
flag = 0
i += 2
j -= 1
else:
if j < 0 and i < n:
flag = 1
j += 1
elif i == n:
flag = 1
i -= 1
j += 2
print(i, j)
matrix[i][j] = count
count += 1
if flag == 1:
i -= 1
j += 1
else:
i += 1
j -= 1

print(matrix)

n = 3
m = 4
megvii_matrix(n, m)

055. 跳跃游戏-中等

数组的数字为最大的跳跃步数, 根据数组判断是否能跳到最后一位上

Description

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

Example 1:

Input: [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.
Example 2:

Input: [3,2,1,0,4]
Output: false
Explanation: You will always arrive at index 3 no matter what. Its maximum
jump length is 0, which makes it impossible to reach the last index.

解法一: 回溯

时间复杂度: $O(2^n)$ 总共有 $2^n$ 种跳法来跳到最后一个位置上(对于任意一个位置, 有经过和不经过两个种可能性)
空间复杂度: $O(n)$

试遍所有的可能性, 正常来说会超时, 并且也肯定不是最佳答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
bool canJump(vector<int>& nums) {

return helper(nums, 0);
}
bool helper(vector<int> &nums, int position){
int final_position = nums.size()-1;
if(position == final_position) return true;
int furthest = std::min(position+nums[position], final_position);
for(int i = position+1; i<=furthest; i++){
//这里有个小小的优化, 就是令i从最大步长开始, i--, 这种优化虽然最坏情况时一样的
//但在实际使用中, 会比从position+1开始要快一点(但是依然超时)
if(helper(nums, i)) return true;
}
return false;
}
};

解法二: top-down 动态规划(递归)

时间复杂度: $O(n^2)$ , 对于每个点来说, 都是要找到下一个good_position, 则需要进行 $(O)$ 的查找, 又因为总共有 $O(n)$个元素, 所以复杂度为 $O(n^2)$.
空间复杂度: $O(2n)$, 递归需要 $O(n)$ , memo需要 $O(n)$.

设计一个数组, 用来记录当前下标对应位置是否可又达到终点, 如果能, 则该位置为good position, 如果不能, 则为bad position, 刚开始的时候都是unknown position(除了最后一个位置为good).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
enum class Status{GOOD, BAD, UNKNOWN};
bool canJump(vector<int>& nums) {
vector<Status> memo;
for(int i=0; i<nums.size()-1; i++)
memo.push_back(Status::UNKNOWN);
memo.push_back(Status::GOOD);
return helper(nums, memo, 0);
}
bool helper(vector<int> &nums, vector<Status> &memo, int position){
int final_position = nums.size()-1;
if(memo[position] != Status::UNKNOWN) return memo[position]==Status::GOOD ? true : false;
int furthest = std::min(position+nums[position], final_position);
for(int i = furthest; i>position; i--){
if(helper(nums, memo, i)){
memo[position] = Status::GOOD; //注意是position, 不是i
return true;
}
}
memo[position] = Status::BAD;
return false;
}
};

解法三: down-top 动态规划(非递归)

时间复杂度: $O(n^2)$ , 对于每个点来说, 都是要找到下一个good_position, 则需要进行 $(O)$ 的查找, 又因为总共有 $O(n)$个元素, 所以复杂度为 $O(n^2)$.
空间复杂度: $O(n)$, 无需递归 , 只需要memo, $O(n)$.

动态规划的非递归版本.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
enum class Status{GOOD, BAD, UNKNOWN};
bool canJump(vector<int>& nums) {
//if(nums.size() ==0) return false;
vector<Status> memo;
for(int i=0; i<nums.size()-1; i++)
memo.push_back(Status::UNKNOWN);
memo.push_back(Status::GOOD);
int final_position = nums.size()-1;
for(int i=nums.size()-2; i>=0; i--){
int furthest = std::min(i+nums[i], final_position);
//for(int j = i+1; j<=furthest; j++){
for(int j = furthest; j>i;j--){
if(memo[j] == Status::GOOD){ // 只要有一个GOOD, 当前i位置就为GOOD, 而无需考虑BAD的情况
memo[i] = memo[j];
break;
}
}
}
return memo[0] == Status::GOOD ? true : false;
}
};

解法四: 贪心-O(n) 复杂度, 最优

时间复杂度: $O(n)$
空间复杂度: $O(1)$

由上面的down-top递归可以看出, 当前下标位置的点是否为good点, 实际上只取决于当前点是否能够达到右边坐标中(从右往左走)最左边的good(可以看上面的break语句), 如果能够达到, 则当前点一定为good点, 因此, 我们只需要用一个变量left_most_good来维护当前点右边的最左good点下标即可, 无需任何其他空间和操作.(速度极快)

从后往前: Python 实现

1
2
3
4
5
6
7
8
9
class Solution:
def canJump(self, nums: List[int]) -> bool:
n = len(nums)
left_most = n-1
for i in range(n-2, -1, -1):
max_step = min(i+nums[i], n-1)
if max_step >= left_most:
left_most = i
return left_most == 0

从后往前: C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
bool canJump(vector<int>& nums) {
int left_most_good = nums.size()-1;
for(int i = nums.size()-2; i>=0; i--){
if(i+nums[i] >= left_most_good){
left_most_good = i;
}
}
return left_most_good==0;
}
};

另一种贪心的形式: 记录当前能够达到的最大位置

从前往后: Python 实现

1
2
3
4
5
6
7
8
9
class Solution:
def canJump(self, nums: List[int]) -> bool:
right_most = 0 # 当前能达到的最大的位置
for i in range(len(nums)):
if right_most < i: # 如果不可达当前点, 则后面的点均不可达
break
if i+nums[i] > right_most: # 更新能到达的最大位置
right_most = i+nums[i]
return right_most >= len(nums)-1 # 返回最大的位置是否包含最后的节点

从前往后: C++ 实现

1
2
3
4
5
6
7
8
9
class Solution {
public:
bool canJump(vector<int>& nums) {
int i =0;
for(int reach=0; i<nums.size() && i<=reach; i++ )
reach = max(i+nums[i], reach);
return i==nums.size(); // 或者用 reach >= nums.size()-1 判断
}
};

056. 合并区间

融合区间

Description

给出一个区间的集合,请合并所有重叠的区间。

Example 1:

Input: [[1,3],[2,6],[8,10],[15,18]]
Output: [[1,6],[8,10],[15,18]]
Explanation: Since intervals [1,3] and [2,6] overlaps, merge them into [1,6].
Example 2:

Input: [[1,4],[4,5]]
Output: [[1,5]]
Explanation: Intervals [1,4] and [4,5] are considerred overlapping.

解法一: sort+O(n)

时间复杂度: $O(nlogn)$, 主要是排序
空间复杂度: $O(n)$

最简单的实现方法, 先按照interval.start用sort排序, 排好序以后, 能够融合的interval都会聚到一起, 这个时候, 因为start是呈递增的, 只需要看end的大小关系就可以.

最简单的实现方法就是sort之后, 通过额外申请空间来存储融合后的interval, 最后返回

Python 实现

1
2
3
4
5
6
7
8
9
10
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort()
res = []
for inter in intervals:
if res and inter[0] <= res[-1][1]:
res[-1][1] = max(res[-1][1], inter[1])
else:
res.append(inter)
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
vector<Interval> merge(vector<Interval>& intervals) {
if(intervals.size()==0) return vector<Interval>{};
vector<Interval> res;
std::sort(intervals.begin(), intervals.end(), [](Interval a, Interval b){return a.start < b.start;});
res.push_back(intervals[0]);
for(auto iv : intervals){
if(res.back().end < iv.start) res.push_back(iv);
else res.back().end = std::max(res.back().end, iv.end);
}
return res;
}
};

解法二: sort+O(1)

时间复杂度: $O(nlogn)$ , 主要是排序
空间复杂度: $O(1)$

上面的方法在逻辑上不够好, 因为既然已经申请了额外的内存来存储放回结果, 说明我们不希望改变原vector内部的数据, 但是sort之后, 数据顺序已经被破坏了, 既然已经破坏了, 那最好就是直接使用原地融合的办法, 来减少内存的开销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<Interval> merge(vector<Interval>& intervals) {
if(intervals.size()==0) return vector<Interval>{};
//vector<Interval> res; 既然决定使用sort, 就说明已经改变了intervals, 此时不应该在额外申请空间, 而应该进行原地融合.
std::sort(intervals.begin(), intervals.end(), [](Interval a, Interval b){return a.start < b.start;});
auto cur_iv = intervals.begin();
auto next_iv = intervals.begin()+1;
for(; next_iv!=intervals.end(); next_iv++){
if( (*cur_iv).end < (*next_iv).start ){
cur_iv++;
(*cur_iv) = (*next_iv);
}else{
(*cur_iv).end = std::max( (*cur_iv).end, (*next_iv).end );
}
}
intervals.erase(cur_iv+1, intervals.end());
return intervals;
}
};

解法三: 不使用sort

有时, 我们要求不能改变原向量intervals的内容, 此时, 就不能使用sort (除非牺牲大量空间留副本,但单肯定不推荐).

//TODO, 未细看, 但时间复杂度应该会高于 O(nlogn)
https://leetcode.com/problems/merge-intervals/discuss/153979/Elegant-c++-solutions.-One-without-modifying-intervals-and-one-inplace

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

Without modifying intervals
Since we can't sort interval, we want to instead ensure our destination vector is sorted. A insertion sort is required then. Insertion should be done as follows;

Find first destination interval that ends after the incoming interval starts. Called it
If no such interval is found or the incoming interval end is less than found intervals start then we can just insert and be done.
Otherwise there must be an overlap, but it could be more than one. Do another search, this time for the first interval whose start is greater than incoming interval end. Called last
Everything from [it, last) can be merged together with incoming interval into a single interval
vector<Interval> merge(vector<Interval>& intervals) {
std::vector<Interval> ret;

for (auto& interval : intervals) {
auto it = std::lower_bound(ret.begin(), ret.end(), interval.start, [](const Interval& l, int r) { return l.end < r; });

if (it == ret.end() || interval.end < it->start)
// No overlap, insert as is
ret.insert(it, interval);
else {
// There is an overlap, there might be more, so find the upper bound too
it->start = std::min(it->start, interval.start);
auto last = std::upper_bound(it, ret.end(), interval.end, [](int l, const Interval& r) { return l < r.start; });
it->end = std::max((last - 1)->end, interval.end);
ret.erase(it + 1, last);
}
}
return ret;
}

059. 螺旋矩阵 II

题目链接: https://leetcode-cn.com/problems/spiral-matrix-ii/

给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。

解法一: 遇到越界时, 转变方向

和 54 的解法二类似, 都是模拟顺时针的方向, 如果遇到了非法的坐标或者已访问的位置, 则按照顺时针的方向更改当前的移动方向.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def generateMatrix(self, n: int) -> List[List[int]]:
matrix = [[0]*n for _ in range(n)]
i = 0
j = 0
direct = [(0, 1), (1, 0), (0, -1), (-1, 0)]
cur_val = 1
flag = 0
while i < n and j < n and matrix[i][j] == 0: # 用 0 做visited标记
matrix[i][j] = cur_val
cur_val += 1
new_i = i + direct[flag%4][0]
new_j = j + direct[flag%4][1] # 获取新坐标
if new_i<0 or new_j<0 or new_i == n or new_j == n or matrix[new_i][new_j] != 0:
flag +=1 # 若新坐标不合规, 则顺时针转向
new_i = i + direct[flag%4][0]
new_j = j + direct[flag%4][1]
i = new_i
j = new_j
return matrix

060. 第 k 个排列

题目链接: https://leetcode-cn.com/problems/permutation-sequence/

给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。

按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

“123”
“132”
“213”
“231”
“312”
“321”
给定 n 和 k,返回第 k 个排列。

说明:
给定 n 的范围是 [1, 9]。
给定 k 的范围是[1, n!]。

示例 1:
输入: n = 3, k = 3
输出: “213”

示例 2:
输入: n = 4, k = 9
输出: “2314”

解法一: 从高位到低位逐个确定

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
以 n=4 为例 k=7为例子 由于k从1开始变为从0开始 k = k - 1 = 6 li = [1,2,3,4]:
1: 1234
2: 1243
3: 1324
4: 1342
5: 1423
6: 1432

7: 2134
8: 2143
9: 2314
10:2341
....
计算结果第一位li = [1,2,3,4] n = 4 k = 6
li = [1,2,3,4]
k/(n-1)! = 6/(3*2) = 1
结果第一位为li[k/(n-1)!] = li[1] = 2 弹出li[1]

计算结果第二位 li = [1,3,4] k = k%(n-1)! = 0 n = 3
li = [1,3,4]
k/(n-1)! = 0/(2*1) = 0
结果第二位为li[k/(n-1)!] = li[0] = 1 弹出li[0]

计算结果第三位li = [3,4] k = k%(n-1)! = 0 n = 2
li = [3,4]
k/(n-1)! = 0/1! = 0
结果第二位为li[k/(n-1)!] = li[0] = 3 弹出li[0]

计算结果第四位li = [4] k = k%(n-1)! = 0 n = 1
li = [4]
k/(n-1)! = 0/0! = 0
结果第二位为li[k/(n-1)!] = li[0] = 4 弹出li[0]

n = 0 递归终止返回self.ans

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def getPermutation(self, n: int, k: int) -> str:
res = ""
from functools import reduce
factorial = reduce(lambda x, y: x*y, range(1, n+1)) # 求阶乘
nums = [str(i) for i in range(1, 10)] # ['1', '2', .., 'n']
k = k - 1 # 令 k - 1, 这样方便确定每一位的值
for i in range(n):
factorial /= n-i # (n-1) !
x = int(k // (factorial)) # 确定当前位应该是哪一个数字
k = int(k % (factorial)) # 更新 k
res += nums.pop(x) # 将对应数字弹出并添加到结果中
return res

网易笔试题

已知某个排列, 如 123, 则推出它是第 Q 个排列, 请返回倒数第 Q 个排列.

方法: 最小的与最大的交换, 次小的与次大的交换

061. 旋转链表

给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

示例 1:
输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL

示例 2:
输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL

解法一: 双指针

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
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def rotateRight(self, head: ListNode, k: int) -> ListNode:
if head == None: return head
dummy = ListNode(0)
dummy.next = head
fast = dummy
slow = dummy
list_len = 0
for i in range(k):
fast = fast.next
list_len += 1
if fast.next == None: # i == len+list_len
k = k % list_len
fast = dummy
for j in range(k):
fast = fast.next
break
while fast.next != None:
slow = slow.next
fast = fast.next
fast.next = dummy.next
dummy.next = slow.next
slow.next = None
return dummy.next

解法二: 破环(推荐)

先将链表连成一个环, 同时记录其链表长度 n, 然后 k 取余, 最后在链表的倒数第 k(0, 1, 2…, n-1) 个节点将环破坏即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def rotateRight(self, head: ListNode, k: int) -> ListNode:
if head == None: return
node = head
list_len = 1
while node.next != None: # 找到最后一个节点, 同时统计长度
node = node.next
list_len += 1
node.next = head # 构成环

k = k % list_len
for _ in range(list_len - k): # 找到原链表中的倒数第 k 个节点, k = 0, 1, 2, ..., n-1
node = node.next
new_head = node.next # 记录新的头结点
node.next = None # 破开环

return new_head

062. 不同路径

题目链接: https://leetcode-cn.com/problems/unique-paths/

Description

A robot is located at the top-left corner of a m x n grid (marked ‘Start’ in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked ‘Finish’ in the diagram below).

How many possible unique paths are there?

解法一: DP

时间复杂度: $O(mn)$
空间复杂度: $O(mn)$

这是一道经典的DP问题, 当机器人处于某一点时, 它只能从上面或者左边到达该点, 因此很容易得出path[i][j] = path[i-1][j] + path[i][j-1];, 其中 path[i][j]指到达 $(i,j)$ 点的可能路径数量.

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> path(m, vector<int>(n,1));
for(int i = 1 ;i<m; i++){
for(int j=1 ; j<n; j++){
path[i][j] = path[i-1][j] + path[i][j-1];
}
}
return path[m-1][n-1];
}
};

解法二: 优化的DP

时间复杂度: $O(mn)$
空间复杂度: $O(n)$

通过分析知道, 当前点的可能路径数量只与上面点和左边点的值有关, 在上面的方法中, 我们用一个 $m\times n$ 的数组来存储当前点上面和左边的值, 实际上, 我们 只需要用一行数组 就可以完成这个功能, 首先, 求出第一行的所有点的值, 这里只会用每个点左边的值, 然后, 对于第二行的第一个点来说, 它只会用到上面的值, 也就是第一行的第一个值, 因此可以通过行数组直接得到, 然后, 对于第二行的第二个值, 它可以从第二行的第一个值, 以及第二行的第二个值得到, 这些值都是已知的, 所以可以直接求的, 由于在求得以后, 我们就再也不需要第一行的第二个值了, 所以我们可以用这个存储空间来存储第二行的第二个值, 如此递归执行, 我们只需要 $O(n)$ 的空间即可.

Python 实现:

1
2
3
4
5
6
7
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [1] * n
for i in range(1, m):
for j in range(1, n):
dp[j] = dp[j] + dp[j-1]
return dp[-1]

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int uniquePaths(int m, int n) {
vector<int> path(n,1);
for(int i = 1; i<m; i++){
for(int j = 1; j<n; j++){
path[j] = path[j] + path[j-1];
}
}
return path[n-1];
}
};

解法三: 排列组合(最优)

时间复杂度: $O(n)$
空间复杂度: $O(1)$

实际上, 仔细分析该问题, 可以把该问题看成是一个典型的排列组合问题. 首先, 将机器人向右走记为 1, 将机器人向下走记为 0. 题目问有多少种不同的走法, 实际上就是在问1/0序列的不同排列有多少种, 并且, 1/0 的长度必须为 $(m -1 + n - 1)$. 因此, 这个问题可以看做是从 $(m-1+n-1)$ 个空槽位上选择 $(m-1)$ 个槽位, 将其置为1, 并将剩余的 $n-1$ 个槽位置为0, 故而就是组合问题: $C_{m-1+n-1}^{m-1}$ . 又因为 $C_{m-1+n-1}^{m-1} = C_{m-1+n-1}^{n-1}$ , 所以为了防止溢出, 我们可以选择小的进行计算

注意, 在排列如何时, 因为涉及到除法, 所以一定要注意计算法则的先后顺序, 具体请看代码

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int uniquePaths(int m, int n) {
long res = 1; //需要注意的是, 由于下面的计算操作是会有先乘一个数, 再初以一个数的操作, 因此很有可能乘完后超过int上限, 所以需要声明为long整型
for(int i = 1; i< std::min(m,n); i++){
res = res * (m-1+n-1 - i+1) / i;
// 这里如果写成 res *= (m-1+n-1+i+1) / i, 则会报错, 因为这样会先计算除法, 这样有可能会出现浮点数, 但是排列组合是不会出现浮点数的, 切记!
}
return res;
}
};

Python 实现

1
2
3
4
5
6
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
res = 1
for i in range(1, min(m, n)):
res = res * (m-1+n-1 - i+1) / (i)
return int(res)

直接调用 scipy 计算包:

1
2
from scipy.special import comb, perm # comb 组合, perm 排序
return comb(m-1+n-1, min(m, n) - 1)

迭代器(超时):

1
2
3
4
5
6
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
from itertools import combinations
data = range(m-1+n-1)
combs = (combinations(data, min(m, n)-1)) # 返回一个迭代器
return sum(1 for _ in combs) # 注意, 迭代器迭代一轮就停止了, 不会重新回到头部, 即不能重复使用

063. 不同路径 II

一个机器人位于一个 m x n 网格的左上角(起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

解法: 动态规划

设置 dp 数组, 由于只需要上方和左方的元素, 因此 dp 数组可以只设为 2 x n 的大小, dp 数组中的值代表走到当前点总共的路径数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
if len(obstacleGrid) == 0:
return 0
length = len(obstacleGrid[0])
dp = [[0] * (length+1), [0] * (length+1)]
if obstacleGrid[0][0] != 1:
dp[0][0] = 1
for i in range(len(obstacleGrid)):
for j in range(len(obstacleGrid[0])):
if obstacleGrid[i][j] == 1:
dp[1][j] = 0
continue
dp[1][j] = dp[1][j-1] + dp[0][j]
dp = dp[::-1]
return dp[0][-2] # 注意, 这里由于上面替换了 dp 的 0 和 1, 所以结果存在 dp[0] 中, 而不是 dp[1] 中

解法二: 空间优化的动态规划

实际上, 并不需要 2xn 大小的数组, 只需要一个 n 长度的一维数组就够了, 思路和 62 题一直, 具体实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
if len(obstacleGrid) == 0: return 0
m = len(obstacleGrid)
n = len(obstacleGrid[0])
dp = [0] * n
for i in range(m):
for j in range(n):
if obstacleGrid[i][j] == 1: dp[j] = 0
elif i == 0 and j == 0: dp[j] = 1
elif j == 0: dp[j] = dp[j]
elif i == 0: dp[j] = dp[j-1]
else: dp[j] = dp[j] + dp[j-1]
return dp[-1]

064. 最小路径和

题目链接: https://leetcode-cn.com/problems/minimum-path-sum/

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例:

输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。

解法: 动态规划

申请dp[2][n]数组, dp[1][j]代表走到grid[i][j]所需的最小步数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#sys.maxint
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
if (len(grid) == 0): return 0
m = len(grid)
n = len(grid[0])
dp = [[0] * n, [0] * n]
for i in range(m):
for j in range(n):
if j == 0:
dp[1][j] = dp[0][j] + grid[i][j]
elif i == 0:
dp[1][j] = dp[1][j-1] + grid[i][j]
else:
dp[1][j] = min(dp[0][j], dp[1][j-1]) + grid[i][j]
dp = dp[::-1]
return dp[0][n-1] # 由于上面dp[::-1]交换了顺序, 所以最终的结果存在dp[0]当中

解法: 动态规划空间复杂度优化

实际上, 当前点只依赖与前一个点和上一个点的值, 因此, 我们可以通过一个以为数组就能够存储足够的信息, 故而可以将空间占用量减半

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
if (len(grid) == 0): return 0
m = len(grid)
n = len(grid[0])
dp = [0] * n
for i in range(m):
for j in range(n):
if j == 0:
dp[j] = dp[j] + grid[i][j]
elif i == 0:
dp[j] = dp[j-1] + grid[i][j]
else:
dp[j] = min(dp[j], dp[j-1]) + grid[i][j]
return dp[n-1]

06x. 迷宫的最小路径(旷视)

给定一个大小为N*M的迷宫,由通道(‘.’)和墙壁(‘#’)组成,其中通道S表示起点,通道G表示终点,每一步移动可以达到上下左右中不是墙壁的位置。试求出起点到终点的最小步数。(本题假定迷宫是有解的)(N,M<=100)

解法: BFS+队列

在这个问题中,找到从起点到终点的最短路径其实就是一个建立队列的过程:

1.从起点开始,先将其加入队列,设置距离为0;

2.从队列首端取出位置,将从这个位置能够到达的位置加入队列,并且让这些位置的距离为上一个位置的距离加上1;

3.循环2直到将终点添加到队列中,这说明我们已经找到了路径;

注意到在这个过程中,每次处理的位置所对应的距离是严格递增的,因此一旦找到终点,当时的距离就是最短距离;

同样基于这个原因,搜索可移动到的位置所使用的判断条件中不仅仅是不碰墙壁、不超过边界,还有一个就是没有到达过,因为如果已经到达了这个位置,这说明已经有更短的路径到达这个位置,这次到达这个位置的路径是更差的,不可能得到更好的最终解。

066. 加一

数组代表一个整数, 模拟整数的加法

Description

给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

Example 1:

Input: [1,2,3]
Output: [1,2,4]
Explanation: The array represents the integer 123.
Example 2:

Input: [4,3,2,1]
Output: [4,3,2,2]
Explanation: The array represents the integer 4321.

解法一: 直接模拟

时间复杂度: $O(n)$
空间复杂度: $O(1)$

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def plusOne(self, digits: List[int]) -> List[int]:
carry = 1
n = len(digits)
for i in range(n-1, -1, -1):
if digits[i] + carry > 9:
digits[i] = (digits[i] + carry) % 10
carry = 1
else:
digits[i] = digits[i] + carry
carry = 0
return digits if not carry else [1] + digits

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<int> plusOne(vector<int>& digits) {
int carry = 0, last_i = digits.size()-1;
digits[last_i] += 1;
if(digits[last_i] > 9) {
digits[last_i] = 0;
carry=1;
}
for(int i = last_i-1; i>=0 && carry ; i--){
digits[i] += carry;
if(digits[i] > 9)
digits[i] = 0;
else
carry = 0;
}
if(carry == 1) digits.insert(digits.begin(), 1);
return digits;
}
};

另一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
vector<int> plusOne(vector<int>& digits) {
int carry = 0;
int one = 1;
int cur = digits.size()-1;
for(int cur=digits.size()-1; cur>=0; cur--){
digits[cur] += one + carry;
one = 0;
carry = digits[cur] / 10;
digits[cur] = digits[cur] % 10;
}
if(carry) digits.insert(digits.begin(), 1);
return digits;
}
};

解法二: 不使用加法(更快更简单, 击败100%)

Python 实现

1
2
3
4
5
6
7
8
9
10
class Solution:
def plusOne(self, digits: List[int]) -> List[int]:
carry = 1
n = len(digits)
for i in range(n-1, -1, -1):
if digits[i]!=9:
digits[i] += 1
break
digits[i] = 0
return digits if digits[0] != 0 else [1] + digits

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
vector<int> plusOne(vector<int> &digits) { //未考虑前缀0的情况
for(int i = digits.size() - 1; i >= 0; i--)
{
if(digits[i] != 9)
{
digits[i] ++;
break;
}
digits[i] = 0;
}
if(digits[0] == 0)
digits.insert(digits.begin(), 1);
return digits;

}
};

069. x 的平方根

实现开方算法, 开根, sqrt

Description

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

Example 1:

1
2
Input: 4
Output: 2

Example 2:

1
2
3
Input: 8
Output: 2
Explanation: The square root of 8 is 2.82842..., and since the decimal part is truncated, 2 is returned.

解法一: 二分法

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def mySqrt(self, x: int) -> int:
low = 0
high = x
a = (low+high) / 2
while (abs(a*a - x) > 0.0001):
if (a*a - x > 0):
high = a
elif (a*a - x < 0):
low = a
a = (low+high)/2
#from math import ceil
#if ceil(a)*ceil(a) == x: a = ceil(a)
if round(a)*round(a) == x: a = round(a) # 该语句在这里与上面等价, 因为当 ceil(a) 是 x 的开根时, a 一定是里 ceil 非常近的, 所以可以四舍五入
return int(a)

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int mySqrt(int x) {
double low=0, high=x;
double res = high;
while( std::abs(res*res - x) > 0.00001 ){
if(res*res > x){
high = res;
res = (low+high)/2;
}else{
low = res;
res = (low+high)/2;
}
}
if(ceil(res)*ceil(res)==x) return ceil(res); // 为了能够正确截断, 必须加上此句
return int(res);
}
};

解法二: 牛顿迭代法

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

相当于求解 $f(res)=res^2 - x = 0$ 中 $res$ 的解. 则对于任意一点 $(res, f(res))$, 都有切线方程:

其中, $res’$ 是该直线与 $x$ 轴的交点. 令新的 $res$ 为该值, 就可以不断逼近 $f(res)$ 的零点, $res’$ 的值为:

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int mySqrt(int x) {
double res = x;
while( std::abs(res*res - x) > 0.00001 ){
res = (res*res+x) / (2*res);
}
if(ceil(res)*ceil(res)==x) return ceil(res); // 为了能够正确截断, 必须加上此句
return int(res);
}
};

解法三: 按位检索

时间复杂度: $O(logn)$
空间复杂度: $O(1)$

由于本题要返回的是整数, 而上面的两种方法都是针对double类型的精确开根方法, 时间复杂度为 $O(logn)$, 实际上, 当只需要返回整数时, 我们可以按整数的位进行检索, 而整数总共只有32位(传入的x位int型, 所以开根后不可能超过int), 因此时间复杂度只有 $O(32)$ , 也就是 $O(1)$.

注意: 由于该方法是首先找到比 x 大的那一位, 因此有可能超过int上限, 所以要换成long整型

找到后依然需要进行二分查找来找到最终的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int mySqrt(int x) {
long res=0;
int h=0;
while( long(1<<h) * long(1<<h) <= x) h++;
long b = 1<<(h-1);
while( b > 0){
if( (res+b) * (res+b) <= x)
res += b;
b = b/2;
}
return res;

}
};

070. 爬楼梯

实际上就是斐波那契数列, 更具体分析可看牛客的跳台阶

Description

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

Example 1:

Input: 2
Output: 2
Explanation: There are two ways to climb to the top.

  1. 1 step + 1 step
  2. 2 steps
    Example 2:

Input: 3
Output: 3
Explanation: There are three ways to climb to the top.

  1. 1 step + 1 step + 1 step
  2. 1 step + 2 steps
  3. 2 steps + 1 step

解法一: 递归

超时

1
2
3
4
5
6
class Solution:
def climbStairs(self, n: int) -> int:
if n == 2: return 2
if n == 1: return 1
if n == 0: return 0
return self.climbStairs(n-1) + self.climbStairs(n-2)

解法二: 迭代

Python 实现

1
2
3
4
5
6
7
8
9
class Solution:
def climbStairs(self, n: int) -> int:
dp1 = 1
dp2 = 2
for i in range(3, n+1):
cur = dp1+dp2
dp1 = dp2
dp2 = cur
return dp2 if n > 1 else 1

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int climbStairs(int n) {
if(n==0) return 0;
if(n==1) return 1;
int n1 = 1;
int n2 = 2;
for(int i=3; i<=n; i++){
int temp = n2;
n2 = n1+n2;
n1 = temp;
}
return n2;
}
};

071. 简化路径

以 Unix 风格给出一个文件的绝对路径,你需要简化它。或者换句话说,将其转换为规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。更多信息请参阅:Linux / Unix中的绝对路径 vs 相对路径

请注意,返回的规范路径必须始终以斜杠 / 开头,并且两个目录名之间必须只有一个斜杠 /。最后一个目录名(如果存在)不能以 / 结尾。此外,规范路径必须是表示绝对路径的最短字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def simplifyPath(self, path: str) -> str:
if len(path) == 0: return ""
path = path.split('/') # 将目录按照 '/' 进行拆分
stack = ['/']
for item in path:
if item == '..' and stack[-1] != '/': # 遇到 .. 且没有达到根时, 返回上次目录
stack.pop()
elif item == '' or item == '.' or item == '..': # 保持在当前目录
continue
else: # 进入新的目录
stack.append(item)
return '/' + '/'.join(stack[1:]) # 将目录拼接起来, 同时加上根目录符号

072. 编辑距离

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

解法一: DP

时间复杂度: $O(mn)$
空间复杂度: $O(mn)$

我们的目的是让问题简单化,比如说两个单词 horse 和 ros 计算他们之间的编辑距离 D,容易发现,如果把单词变短会让这个问题变得简单,很自然的想到用 D[n][m] 表示输入单词长度为 n 和 m 的编辑距离。

具体来说,D[i][j] 表示 word1 的前 i 个字母和 word2 的前 j 个字母之间的编辑距离。

状态转移方程如代码中所示.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n = len(word1)
m = len(word2)
dp = [[0] * (m+1) for _ in range(n+1)] # dp[i][j] 代表 0~i-1 的字符串和 0~j-1 字符串之间的编辑距离
for i in range(1, n+1): # 对 "" 与 word1 的编辑距离进行初始化
dp[i][0] = i
for j in range(1, m+1): # 对 "" 与 word2 的编辑距离初始化
dp[0][j] = j
for i in range(1, n+1):
for j in range(1, m+1):
if word1[i-1] == word2[j-1]: # 如果相等, 说明有三种情况: 删除某一个(操作加1), 或者直接用前面的最短编辑距离
dp[i][j] = min(1+dp[i-1][j], 1+dp[i][j-1], dp[i-1][j-1])
else: # 不相等的情况
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
return dp[n][m];

073. 矩阵置零

Description

给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。

解法一: 穷举

时间复杂度: $O(nm)$
空间复杂度: $O(nm)$

记录所有出现0的位置, 然后根据这些位置坐标将对应的行和列上的值置为0.

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
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
vector<int> rows;
vector<int> cols;
for(int i=0; i<matrix.size(); i++){
for(int j=0; j<matrix[0].size(); j++){
if(matrix[i][j] == 0){
rows.push_back(i);
cols.push_back(j);
}
}
}
for(auto i:rows){
for(int j=0; j<matrix[0].size(); j++){
matrix[i][j] = 0;
}
}
for(auto j:cols){
for(int i=0; i<matrix.size(); i++){
matrix[i][j] = 0;
}
}
}
};

解法二: 穷举(减少空间复杂度)

时间复杂度: $O(nm)$
空间复杂度: $O(n+m)$

上面在记录位置坐标时没有进行重复检查, 实际上, 对于已经记录过的行或列, 可以不用再记录, 此时, 空间复杂度可以降为 $O(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
25
26
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
vector<int> rows;
vector<int> cols;
for(int i=0; i<matrix.size(); i++){
for(int j=0; j<matrix[0].size(); j++){
if(matrix[i][j] == 0){
// 记录行或列坐标之前先进行重复检查
if(std::count(rows.begin(), rows.end(), i)==0) rows.push_back(i);
if(std::count(cols.begin(), cols.end(), j)==0) cols.push_back(j);
}
}
}
for(auto i:rows){
for(int j=0; j<matrix[0].size(); j++){
matrix[i][j] = 0;
}
}
for(auto j:cols){
for(int i=0; i<matrix.size(); i++){
matrix[i][j] = 0;
}
}
}
};

解法三: 穷举(无空间复杂度)

时间复杂度: $O(nm\times (m+n))$
空间复杂度: $O(1)$

遍历矩阵时, 如果遇到 $(i,j)$ 上的值为0, 那么就将对应的行和列上的所有非0值全部置为一个矩阵范围外的值NAN(解答里面用的是-100000, 实际上这种解法存在问题, 因为理论上矩阵中的元素可以是表示范围内的任何值 ).

之后将所有的NAN值置为0, 就可以完成置0任务, 并且没有使用额外的空间. 由于每次找到一个0时, 都要遍历这个位置上的行和列, 因此时间复杂度较高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
if len(matrix) ==0 or len(matrix[0]) == 0: return
m = len(matrix)
n = len(matrix[0])
for i in range(m):
for j in range(n):
if matrix[i][j] == 0:
for ii in range(m):
if matrix[ii][j] != 0:
matrix[ii][j] = 1.5
for jj in range(n):
if matrix[i][jj] != 0:
matrix[i][jj] = 1.5
for i in range(m):
for j in range(n):
if matrix[i][j] == 1.5:
matrix[i][j] = 0
return

解法四: 用第一行和第一列记录

时间复杂度: $O(nm)$
空间复杂度: $O(1)$

用第一行和第一列的值记录是否应该将对应的行和列置为0, 此时由于第一行和第一列被用作了标记数组, 因此第一行和第一列的0不能用来判断是否应该置为全0, 所以需要额外设置两个变量记录.

Python 实现

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
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
if len(matrix) == 0 or len(matrix[0]) == 0: return
m = len(matrix)
n = len(matrix[0])
is_row = False
is_col = False

for i in range(m):
for j in range(n):
if matrix[i][j] == 0:
if i == 0: is_row = True
if j == 0: is_col = True
matrix[i][0] = 0
matrix[0][j] = 0

for i in range(1, m):
if matrix[i][0] == 0:
for j in range(1, n):
matrix[i][j] = 0

for j in range(1, n):
if matrix[0][j] == 0:
for i in range(1, m):
matrix[i][j] = 0

if is_row:
for j in range(n):
matrix[0][j] = 0
if is_col:
for i in range(m):
matrix[i][0] = 0
return

C++ 实现

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
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
bool is_row=false, is_col = false; // 用第一行和第一列的值来做标记, 因此需要额外的记录第一行和第一列本身是有应该全0
for(int i=0; i<matrix.size(); i++){
for(int j=0; j<matrix[0].size(); j++){

if(matrix[i][j] == 0){
if(i==0) is_row=true;
if(j==0) is_col=true;
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
for(int i=1; i<matrix.size(); i++){
if(matrix[i][0]!=0) continue;
for(int j=0; j<matrix[0].size(); j++){
matrix[i][j] = 0;
}
}
for(int j=1; j<matrix[0].size(); j++){
if(matrix[0][j]!=0) continue;
for(int i=0; i<matrix.size(); i++){
matrix[i][j] = 0;
}
}
if(is_row){ //需要特别判断第一行和第一列是否应该置为0
for(int j=0; j <matrix[0].size();j++) matrix[0][j]=0;
}
if(is_col){
for(int i=0; i< matrix.size(); i++) matrix[i][0]=0;
}
}
};

074. 搜索二维矩阵

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值.
该矩阵具有如下特性:
每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。

解法一: 从左下角开始(最优)

时间复杂度: $O(m+n)$
空间复杂度: $O(1)$

左下角 开始找, 如果该值大于 target, 则向上找, 否则, 向右找.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
if len(matrix) == 0 or len(matrix[0]) == 0: return False
m = len(matrix)
n = len(matrix[0])
i = m - 1
j = 0
while i >= 0 and j < n:
if matrix[i][j] > target:
i -= 1
elif matrix[i][j] < target:
j += 1
else:
return True
return False

075. 颜色分类

对0,1,2 (颜色: RGB) 进行排序

Description

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

Example:

Input: [2,0,2,1,1,0]
Output: [0,0,1,1,2,2]
Follow up:

A rather straight forward solution is a two-pass algorithm using counting sort.
First, iterate the array counting number of 0’s, 1’s, and 2’s, then overwrite array with total number of 0’s, then 1’s and followed by 2’s.
Could you come up with a one-pass algorithm using only constant space?

解法一: 两次遍历

时间复杂度: $O(n)$
空间复杂度: $O(1)$

第一次遍历统计0,1,2的个数, 第二次遍历根据0,1,2的个数覆盖数组原有值

解法二: 一次遍历

时间复杂度: $O(n)$
空间复杂度: $O(1)$

设置mid, low, high三个指示变量, 如果mid==0, 则将其与low交换, 如果mid==2, 则将其与high交换, 直到mid>high为止.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def sortColors(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
low = 0
high = len(nums) - 1
mid = low
while (mid <= high):
if nums[mid] == 0:
nums[mid], nums[low] = nums[low], nums[mid]
low += 1
mid += 1
elif nums[mid] == 2:
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1
else:
mid += 1
return

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
void sortColors(vector<int>& nums) {
int low=0, mid=0, high=nums.size()-1;
while(mid<=high){
if(nums[mid]==2)
std::swap(nums[mid], nums[high--]);
else if(nums[mid]==0)
std::swap(nums[mid++], nums[low++]);
//这里 mid 可以直接++ 的原因是因为mid已经将0和2的情况进行处理,
// 所以现在 low 指向的值只可能是 1, 因此交换后无需再对nums[mid]判断, 直接++即可
else
mid++;
}
}
};

076. 最小覆盖子串

求包含子串字符的最小窗口

Description

给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。

Example:

Input: S = “ADOBECODEBANC”, T = “ABC”
Output: “BANC”
Note:

If there is no such window in S that covers all characters in T, return the empty string “”.
If there is such window, you are guaranteed that there will always be only one unique minimum window in S.

解法: 两个变量记录当前窗口大小

时间复杂度: $O(n)$
空间复杂度: $O(1)$

Python 实现

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
class Solution:
def minWindow(self, s: str, t: str) -> str:
from collections import defaultdict
count = len(t)
d = defaultdict(int)
for c in t:
d[c] += 1
j = 0
head = 0
flag = False
min_window = float('inf')
for i, c in enumerate(s):
if d[c] > 0:
count -= 1
d[c] -= 1
while (count == 0):
if i-j < min_window:
min_window = i-j
head = j
flag = True
d[s[j]] += 1
if d[s[j]] > 0:
count += 1
j += 1
return s[head:head+min_window+1] if flag else ""

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
string minWindow(string s, string t) {
vector<int> hmap(256,0);
for(auto c:t) hmap[int(c)]++;
int count = t.size(), begin=0, end=0, head=0, cur_window=INT_MAX;
while(end<s.size()){
// 这里可以直接写成 if(hmap[int(s[end++])]-- > 0) count--; 但是可读性很差, 不建议这样写.
if(hmap[int(s[end])] > 0) count--;
hmap[int(s[end])]--; end++;
while(count==0){ //end 超尾
if( (end-begin) < cur_window) cur_window = end - (head=begin);
// 同样, 可以直接写成 if(hmap[int(s[begin++])]++ > 0) count++; 但是可读性很差
if(hmap[int(s[begin])] == 0) count++;
hmap[int(s[begin])]++; begin++;
}
}
return cur_window==INT_MAX ? "" : s.substr(head, cur_window);
}
};

子串相关题目的模板解法

https://leetcode.com/problems/minimum-window-substring/discuss/26808/Here-is-a-10-line-template-that-can-solve-most-'substring'-problems

对于大多数的子串相关的问题, 通常可以描述为给定一个字符串, 要求找到满足某些限制条件的子串, 这类都可以用下面的基于哈希表和两个辅助指示变量的模板来求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int findSubstring(string s){
vector<int> hmap(128,0);
int count; // 用于检查子串是否合法
int begin=0, end=0; // 两个指示变量, 分别指向子串的头和尾(end会在++后退出循环, 因此最后end会变成超尾)
int len_sub; // 子串的长度
for(){ }//对hasp map进行初始化
while(end<s.size()){
//if(hmap[s[end++]]-- ? ) { } //修改count
//上面的语句可读性很差, 最后拆开来写, 后面也同理, 拆开写
if(hmap[int(s[end])] ? ) { } //修改count
hmap[int(s[end])]--; //注意顺序
end++;
while( count? ){ // 检查count是否满足条件
// update len_sub

if(hmap[int(s[begin])] ?) { } //修改count
hmap[int(s[begin])]++;
begin++;
}
}
}

例如, 对于问题 Longest Substring At Two Distinct Characters 的模板解法如下:

对于问题 Longest Substring Without Repeating Characters 的模板解法如下:

1
2
3
4
5
6
7
8
9
10
int lengthOfLongestSubstring(string s){
vector<int> map(256,0);
int begin=0,end=0,len_sub=0,count=0;
while(end<s.size()){
if(map[int(s[end])] > 0) count++;
map[int(s[end])]++;
end++;
while(count>0) if(map[int(s[begin])] > 1) count;
}
}

077. 组合

Description: 输出所有的组合

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。即求 $C_n^k$.

Example:

1
2
3
4
5
6
7
8
9
10
Input: n = 4, k = 2
Output:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

解法一: 回溯, 递归

时间复杂度: $O(kC_n^k)$, 每得到一种组合, 都需要 k 次递归
空间复杂度: $O(C_n^k)$

标准的回溯(深度游戏遍历)解法

Python 实现

1
2
3
4
5
6
7
8
9
10
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
def back_track(res, temp, index, n, k):
if len(temp) == k:
res.append(temp)
for i in range(index, n+1):
back_track(res, temp+[i], i+1, n, k)
res = []
back_track(res, [], 1, n, k)
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
private:
void dfs_helper(vector<vector<int>> &res, vector<int> &out, int n, int k, int level){
int count = out.size();
if(count==k){
res.push_back(out);
}
for(int i=level; i<n; i++){
out.push_back(i+1);
dfs_helper(res, out, n, k, i+1);
out.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> res;
vector<int> out;
dfs_helper(res, out, n, k, 0);
return res;
}
};

解法三: 迭代

  • 先申请一个 k 长度的元素为 0 的数组temp, 然后, 用一个变量i指示当前有效数组的长度, 初始时指向下标 0(长度为1). 当有效数组长度达到 k 时, 说明已经找到了一种组合.
  • 在 i 位置不断递增 1, 递增后, 如果超过上限, 则回溯.
  • 当所有的位置都会超限时, 回溯结束, i 指向 -1.

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res = []
out = [0] * k
i = 0
while i >= 0:
out[i] += 1 # 逐个递增
if out[i] > n: i -= 1 # out[i] 超过上限, 回溯
elif i == k-1: res.append(out.copy()) # 此时, out[i] 没有超过上限, 且 i 已经处于 k-1 位置, 新的组合产生
# 需要注意的是, python 的列表是可变对象, 所以这里应该使用其副本, 负责后面的操作会影响这里的值
else: # 此时的 i+1 已经超过上限, 我们令其等于 i 上的值, 然后 i 上的值会在下一次循环时递增
i += 1
out[i] = out[i-1]
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> res;
vector<int> out(k, 0);
int i = 0;
while (i >= 0) {
out[i]++;
if (out[i] > n) i--;
else if (i == k - 1) res.push_back(out);
else {
i++;
out[i] = out[i - 1];
}
}
return res;
}
};

078. 子集

返回给定数字序列的子集, 序列中每个元素都不同(这是一个很重要的条件!!)

Description

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集

Example:

Input: nums = [1,2,3]
Output:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]

解法一: 迭代直接求出子集

时间复杂度: $O(2^n)$ , 对于任意一个元素, 有包含和不包含两种情况
空间复杂度: $O(2^n)$

由于序列中的每个元素都不同, 因此, 对于任意一个元素, 只需要将其添加都前面序列所组成的子集的每一个子序列的末尾即可, 无需考虑是否包含重复元素的情况.

C++ 实现

1
2
3
4
5
6
7
8
9
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = [[]]
for num in nums:
temp_res = []
for item in res:
temp_res.append(item + [num])
res.extend(temp_res)
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res {vector<int>{}};

for(auto n : nums){
int len = res.size();
for(int i=0; i<len; i++){
vector<int> sub_item = res[i]; // c++中, =为复制赋值, move函数为移动赋值
sub_item.push_back(n);
res.push_back(sub_item);

}
}
return res;

}
};

解法二: 回溯

https://leetcode.com/problems/subsets/discuss/27281/A-general-approach-to-backtracking-questions-in-Java-(Subsets-Permutations-Combination-Sum-Palindrome-Partitioning)
回溯法可以解决一系列相关问题, 先看Subsets的求解

Python 实现

1
2
3
4
5
6
7
8
9
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def back_track(res, temp, index, nums):
res.append(temp)
for i in range(index, len(nums)):
back_track(res, temp+[nums[i]], i+1, nums)
res = []
back_track(res, [], 0, nums)
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
vector<int> sub_item;
back_track(res, sub_item, 0, nums);
return res;
}

void back_track(vector<vector<int>> &res, vector<int> sub_item, int start, vector<int> &nums){
res.push_back(sub_item);
for(int i=start; i<nums.size(); i++){
sub_item.push_back(nums[i]);
back_track(res, sub_item, i+1, nums);
sub_item.pop_back();
}
}
};

其他问题:

Subsets II (contains duplicates) : https://leetcode.com/problems/subsets-ii/
悠悠 11:05:53
Permutations : https://leetcode.com/problems/permutations/
悠悠 11:06:01
Permutations II (contains duplicates) : https://leetcode.com/problems/permutations-ii/
悠悠 11:06:09
Combination Sum : https://leetcode.com/problems/combination-sum/
悠悠 11:06:16
Combination Sum II (can’t reuse same element) : https://leetcode.com/problems/combination-sum-ii/
悠悠 11:06:23
Palindrome Partitioning : https://leetcode.com/problems/palindrome-partitioning/

解法三: bit控制

时间复杂度: $O(n\times 2^n)$ , 最慢的方法.
空间复杂度: $O(2^n)$
因为对于任意一个数只有两种可能性, 出现在子序列中, 或者不出现在子序列中, 因此对于长度为 n 的(无相同元素的)序列来说, 共有 $2^n$ 个子序列, 我们先为这些子序列申请空间, 然后根据位操作(刚好有0,1两种情况)来决定对应位置上的字符出现还是不出现.

在实现时, 观察到, 第一个元素每隔两个子序列出现一次, 第二个元素每隔四个子序列出现两次, 第三个元素每隔八个子序列出现四次…

依次类推, 我们可以根据当前元素的位置来决定当前元素是否出现(间隔的前一半出现, 后一半不出现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
int len_subsets = std::pow(2,nums.size());
vector<vector<int>> res(len_subsets, vector<int>{});
for(int i =0; i<nums.size(); i++){
for(int j=0; j<len_subsets; j++){
if(j>>i & 1 == 1){
res[j].push_back(nums[i]);
}
}
}
return res;
}
};

079. 单词搜索

判断指定单词是否存在于字符矩阵中(可以通过上下左右邻接字符相连的才算是一个单词)

Description: 判断指定单词是否存在于字符矩阵中

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

Example:

1
2
3
4
5
6
7
8
9
board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
Given word = "ABCCED", return true.
Given word = "SEE", return true.
Given word = "ABCB", return false.

解法一: dfs+回溯

时间复杂度: $O(mn 4^k)$, 暴力求解, $mn$ 为字符矩阵的宽和高, 也即 cell 数量, 对于 dfs 中的每个 cell, 有4个扩展方向, 一共需要扩展 $k$ 次($k$ 为单词的长度).
空间复杂度: $O(mn)$ , 回溯时, 用#来记录已经遍历过的点, 无需申请额外空间来记录. 但是递归程序需要占用 $O(mn)$ 的空间复杂度.

Python 实现

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
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
def dfs(board, x, y, index, word):
n = len(board)
m = len(board[0])
if index == len(word):
return True
direct = [[-1, 0], [1, 0], [0, -1], [0, 1]]

for d in direct:
new_x = x + d[0]
new_y = y + d[1]
if new_x >= 0 and new_x < n and new_y >= 0 and new_y < m and board[new_x][new_y] == word[index]:
temp_c = board[new_x][new_y]
board[new_x][new_y] = '#' # 标记已访问
if dfs(board, new_x, new_y, index+1, word):
return True
board[new_x][new_y] = temp_c # 访问状态重置
return False

n = len(board)
m = len(board[0])
for i in range(n):
for j in range(m):
if board[i][j] == word[0]:
temp_c = board[i][j]
board[i][j] = '#' # 标记已访问
if dfs(board, i, j, 1, word):
return True
board[i][j] = temp_c
return False

C++ 实现

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
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
if(board.size()==0 || board[0].size()==0) return false;

for(int i=0; i<board.size(); i++){
for(int j=0; j<board[0].size(); j++){
if(dfs(board, word, 0, i, j)) return true;
}
}
return false;
}

bool dfs(vector<vector<char>> &board, string word, int start, int x, int y){
char cur_c = board[x][y];
if(cur_c != word[start]) return false;
if(start == word.size()-1) return true;
board[x][y]='#';
bool res=false, b_down=false, b_left=false, b_right=false;
if(x>0) res = dfs(board, word, start+1, x-1, y);
if(!res && x<board.size()-1) res = dfs(board, word, start+1, x+1, y);
if(!res && y>0) res = dfs(board, word, start+1, x, y-1);
if(!res && y<board[0].size()-1) res = dfs(board, word, start+1, x, y+1);
board[x][y]=cur_c;
return res;
}
};

另一种写法:

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
class Solution {
private:
bool dfs(vector<vector<char>> &board, string &word, int i, int j, int pos){
if(board[i][j] != word[pos]) return false;
if(pos == word.size()-1) return true; // 注意是size-1
int direct[4][2] = {{0,-1},{0,1},{-1,0},{1,0}};
int m = board.size();
int n = board[0].size();

char c = board[i][j];
board[i][j] = '#'; // 标记成已访问
for(auto d : direct){
int x=i+d[0];
int y=j+d[1];
if(x>=0 && x<m && y>=0 && y<n && board[x][y]!='#'){
if(dfs(board, word, x, y, pos+1)) return true;
}
}
board[i][j] = c; // 退出前重置访问状态
return false;
}
public:
bool exist(vector<vector<char>>& board, string word) {
if(board.empty() || board[0].empty()) return false;
if(word.empty()) return true;
int m = board.size();
int n = board[0].size();
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
if(dfs(board, word, i, j, 0))
return true;
}
}
return false;
}
};

080 删除排序数组中的重复项 II

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

解法一: 双指针法

时间复杂度: $O(n)$
空间复杂度: $O(1)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if not nums: return 0
index = 1 # 去重后的数组超尾下标
count = 1 # 首个字符不用判断, 一定符合要求, 所以初始计数 = 1
for i in range(1, len(nums)): # 从 i = 1 开始遍历
if nums[i] == nums[i-1]: # 如果重复, 则计数 +1
count += 1
else: # 如果不与前面的相等, 则计数重置, =1
count = 1
if count <= 2: # 如果重复次数 <= 2 次, 则仍然保留
nums[index] = nums[i]
index += 1
return index

081. 搜索旋转排序数组 II

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。

编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。

题目链接: https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/

解法一:

时间复杂度: 评价 $O(logn)$, 最坏 $O(n)$

该问题和 33 题类似, 只不过该问题中的元素可以 重复, 那么对于1, 1, 1, 3, 1, 1, 我们无法判断1到底是位于前半段还是后半段, 因此, 我们通过比较nums[low]nums[high]的值, 令high-1, 直到nums[low]nums[high]不相等. 当不相等后, 我们就可以通过nums[low]来判断nums[mid]到底处于前半段还是后半段, 剩下的就与 33 题完全相同了.

但是这里有一个问题, 当 low 和 high 更新后, 他们对应的值可能又会重新相等, 此时按道理应该再次消去一部分, 但是 leetcode 上即使不消去, 也不会报错, 应该是样例数不足的原因.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution:
def search(self, nums: List[int], target: int) -> bool:
if not nums: return False
low = 0
high = len(nums) - 1

if target == nums[low]: return True # 将首尾中的重复元素去掉一部分, 这样做有利于判定 前半段 和 后半段
while nums[low] == nums[high] and low <= high:
high -= 1

while low <= high:
mid = (low + high) // 2
if nums[mid] >= nums[low] and target < nums[low]:
low = mid + 1
elif nums[mid] < nums[low] and target >= nums[low]:
high = mid - 1
elif nums[mid] < target:
low = mid + 1
elif nums[mid] > target:
high = mid - 1
else:
return True
return False

083. 删除排序链表中的重复元素

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

解法一: 顺序方法, 逐个删除

时间复杂度: $O(n)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def deleteDuplicates(self, head: ListNode) -> ListNode:
if not head: return head
node = head
while node.next != None:
if node.val == node.next.val: # 当前节点直接指向下下个节点, 此时不能 node++, 因为下下个节点依然有可能重复
node.next = node.next.next
else: # 当前节点与下个节点不相等时, 可以指向下个节点
node = node.next
if node == None: # 执行 node.next = node.next.next 后, node 有可能指向空
break
return head

084. 柱状图中最大的矩形-困难

求最大面积的矩形

Description

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

Example:
Input: [2,1,5,6,2,3]
Output: 10

解法一: 穷举

时间复杂度: $O(n^2)$, 超时
空间复杂度: $O(1)$

列出以每一个i上的值为矩形高度的矩形面积, 然后取得最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int max_area = 0;

for(int i =0; i<heights.size(); i++){
int low = i;
while(low>=0 && heights[low] >=heights[i]) low--;
low++;
int high = i;
while(high<heights.size() && heights[high] >= heights[i]) high++;
high--;

int cur_area = heights[i]* (high-low+1);
if(max_area<cur_area) max_area = cur_area;
}
return max_area;
}
};

解法二: 解法一的改进-空间换时间

时间复杂度: $O(n)$, 前面省略常数项(因为不好确定常数项的值)
空间复杂度: $O(2n)$

从解法一中我们可以看出, 核心的要点就在于求取每一个i对应的矩形的左端和右端, 如下图所示:

那么, 如果我们可以在 $O(1)$ 的时间内获取到左端和右端的值, 则时间复杂度就可以降低到 $O(n)$, 因此, 首先想到的是用数组将每个i对应的左端和右端的值保存起来. 于是, 我们需要先求取这两个数组(左端,右端)的值, 在对左端和右端求值时, 我们要确保时间复杂度不能超过 $O(n)$, 因此, 我们不能每次都重新从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
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {


int *left = new int[heights.size()];
int *right = new int[heights.size()];

left[0]=-1;
for(int i=1; i<heights.size(); i++){
int p = i-1;
while(p>=0 && heights[p] >= heights[i])
p = left[p];
left[i] = p;
}

right[heights.size()-1] = heights.size();
for(int i=heights.size()-2; i>=0; i--){
int p = i+1;
while(p<heights.size() && heights[p] >= heights[i])
p = right[p];
right[i] = p;
}
int max_area = 0;

for(int i =0; i<heights.size(); i++){
int low = left[i];
int high = right[i];

int cur_area = heights[i]*(high-low-1);
if(max_area<cur_area) max_area = cur_area;
}
return max_area;
}
};

解法三: 最优-栈

时间复杂度: $O(n)$, 无常数项
空间复杂度: $O(n)$, 无常数项

上面的解法二, 虽然时间复杂度为 $O(n)$, 但实际上其时间复杂度是略微高于 $O(n)$, 因为在求取左端右端时, 每次跳跃的次数是大于等于1, 而不是仅为1次的.(只不过大O记法不考虑常数项). 而对于空间复杂度来说, 实际上是 $O(2n)$. 下面我们从另外一个角度出发: 不再以当前i对应的高度为最低, 向左右两边探索, 改为以当前i对应的高度为最低, 仅仅向左边探索, 实现算法如下:

  • 首先, 构造一个空栈
  • 从heights数组的第一个bar开始, 遍历所有的bar值(0~n-1), 并执行以下逻辑:
    • 如果当前栈为空, 或者当前数组bar值大于等于栈顶bar值, 则将bar值下标入栈
    • 否则, 将栈顶出栈, 并以栈顶下标对应的bar值作为最低的高度, 求该高度对应的面积, 因为 当前数组bar值小于栈顶下标对应的bar值, 因此可以将当前bar值下标作为right_index, 又因为 栈顶bar值下标的前一个元素, 要么小于栈顶, 要么等于栈顶, 不论哪种情况, 都可以将其下标作为left_index(因为栈顶退出对, 次栈顶就会成为新的栈顶, 所以可以包括bar值相等的情况), 得到了高度, right_index, left_index, 即可计算当前栈顶对应的面积, 并与max_area判断, 更新max_area的值
  • 最后, 如果遍历完以后栈顶不为空(说明后面有几个连续的bar值相等, 或者bar只呈递增排序), 则依次强制弹出栈顶计算面积, 并更新max_area. 注意此时的 right_index 恒为 len(heights).

复杂度分析: 由于入栈出栈的元素仅为heights数组元素, 可以栈的size就是heights数组的大小, 即空间复杂度为 $O(n)$, 时间复杂度从代码中可看出约为 $O(n)$.

python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
max_area = 0
stack = []
#i = 0
for i in range(len(heights)):
while stack and heights[stack[-1]] > heights[i]:
h_index = stack.pop()
w = i - stack[-1] - 1 if stack else i
max_area = max(max_area, w * heights[h_index])
stack.append(i)

n = len(heights)
print(stack)
while stack:
h_index = stack.pop()
w = n - stack[-1] - 1 if stack else n
max_area = max(max_area, w * heights[h_index])
return max_area

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
stack = []
area = 0
i = 0
while i < len(heights):
if len(stack) == 0 or heights[stack[-1]] <= heights[i]:
stack.append(i)
i += 1
else:
height_index = stack.pop()
w = (i - stack[-1] - 1) if stack else i
area = max(area, w * heights[height_index])

while stack:
height_index = stack.pop()
w = (i - stack[-1] - 1) if stack else i
area = max(area, w * heights[height_index])

return area

C++ 实现

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
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {

std::stack<int> s;
int max_area = 0;
int cur_area = 0;
int height_index=0;
int i=0;
while(i<heights.size()){
if(s.empty() || heights[i] >= heights[s.top()])
s.push(i++);
else{
height_index = s.top(); s.pop();
cur_area = heights[height_index] * ( s.empty()? i : i-s.top()-1 );
// 注意, 如果栈为空, 则说明当前i对应的bar值是前i个bar值中最小的, 所以宽为i, 否则宽为i-s.top()-1
if(cur_area > max_area) max_area = cur_area;
}
}

while(!s.empty()){
height_index = s.top(); s.pop();
cur_area = heights[height_index] * ( s.empty()? i : i-s.top()-1 );
// 注意, 如果栈为空, 则说明当前i对应的bar值是前i个bar值中最小的, 所以宽为i, 否则宽为i-s.top()-1
if(cur_area > max_area) max_area = cur_area;
}
return max_area;
}
};

085. 最大矩形-困难-待完善

题目链接: https://leetcode-cn.com/problems/maximal-rectangle/

解法一: 动态规划

时间复杂度: $O(mn)$
空间复杂度: $O(1)$

该思路来自于题目 “最大正方形”, 不同的地方在于, 横向和纵向分别可以形成高为 1 和宽为 1 的长条矩形, 这种情况也要考虑在内. 其余的和最大正方形相同, 使用右上角作为dp, 同时沿着对角线进行更新, 可以只使用常数量的空间复杂度.

更新: 该解法存在问题, 因为 dp 原本是有三种可能的, 但是每次只保留了当前最大面积的可能, 故而会漏解, 通过 OJ 是因为 OJ 的样例不全, 对于下面的例子就无法输出正确答案:

1
[["0","0","0","0","1","1","1","0","1"],["0","1","1","1","1","1","1","0","1"],["0","0","0","1","1","1","1","1","0"]]

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
class Solution:
def maximalRectangle(self, matrix: List[List[str]]) -> int:
if len(matrix) == 0: return 0
m = len(matrix)
n = len(matrix[0])

res = 0
i = 0
j = 0
while i < m or j < n:
if i < m:
ii = i
jj = 0
i += 1
elif j < n:
ii = 0
jj = j
j += 1

dp = [0, 0]
while ii < m and jj < n:
if matrix[ii][jj] == '0':
dp = [0, 0]
else:
w, h = dp
kw, kh = 1, 1
while (kw <= w and matrix[ii][jj-kw] == '1'): # 求宽
kw += 1
while (kh <= h and matrix[ii-kh][jj] == '1'): # 求高
kh += 1

row = 1
while (row <= ii and matrix[ii-row][jj] == '1'): # 纵向的矩形
row += 1
col = 1
while (col <= jj and matrix[ii][jj-col] == '1'): # 横向的矩形
col += 1
index = max(zip([col, row, kw*kh], range(3)))[1]

dp = [[col, 1], [1, row], [kw, kh]][index]
#print(ii, jj, dp, kw, kh)
ii += 1
jj += 1
res = max(res, dp[0]*dp[1])
return res

解法二: 借助柱状图的最大矩形进行计算

时间复杂度: $O(mn)$
空间复杂度: $O(m)$, 对组柱状图的临时存储大小

可以将每一行都看作一组柱状图, 由此计算相似的最大矩形面积即可.

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
class Solution:
def maximalRectangle(self, matrix: List[List[str]]) -> int:

def largestRectangle(heights): # 84题, 柱状图的最大矩形, 复杂度 O(m)
m = len(heights)
max_area = 0
stack = []
for i in range(m):
while stack and heights[stack[-1]] > heights[i]:
h_index = stack.pop()
w = i - stack[-1] -1 if stack else i
max_area = max(max_area, w * heights[h_index])
stack.append(i)
while stack:
h_index = stack.pop()
w = m - stack[-1] - 1 if stack else m
max_area = max(max_area, w * heights[h_index])
return max_area

rectangle = matrix.copy()
if len(matrix) == 0 or len(matrix[0]) == 0: return 0
n = len(matrix)
m = len(matrix[0])
rectangle[0] = list(map(int, rectangle[0])) # 注意, matrix 里面的元素是 字符串, 这里要转换一下
for i in range(1, n):
for j in range(m):
rectangle[i][j] = rectangle[i-1][j] + 1 if rectangle[i][j] == '1' else 0 # 注意这里是 == '1' 而不是 == 1
max_rect = 0
for i in range(n): # 对于每一行, 都可看做是一组柱状图, 分别调用代码即可
max_rect = max(max_rect, largestRectangle(rectangle[i]))
return max_rect

086. 分隔链表

给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。

你应当保留两个分区中每个节点的初始相对位置. (也就是说不能排序)

解法一: 先断链再重组

时间复杂度: $O(n)$
空间复杂度: $O(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
25
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def partition(self, head: ListNode, x: int) -> ListNode:
dummy1 = ListNode(0)
dummy2 = ListNode(0)
node1 = dummy1
node2 = dummy2
node = head
while node != None:
#print(node.val)
if node.val < x:
node1.next = node
node1 = node1.next
else:
node2.next = node
node2 = node2.next
node = node.next
node1.next = dummy2.next
node2.next = None # 去环, 注意, 上面的循环走完后, node2 有可能会指向 node1 中的点, 直接返回会陷入死循环
return dummy1.next

088. 合并两个有序数组

Merge Sorted Array

融合两个有序数组, 其中第一个数组的元素长度为n, 第二个为m, 题目假设第一个数组的空间为n+m.

Description

解法一: 后移+插入融合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
for(int i =n+m-1; i>=n; i--)
nums1[i]=nums1[i-n];
//for(int i =n; i<n+m; i++) 注意, 这样写是有问题的, 例如对于 [1,2,3,4,0], 这种情况, 从前往后的复制方法会造成元素覆盖
// nums1[i]=nums1[i-n];
int i =n, j=0, k=0;
while(i<n+m && j<n){
if(nums1[i] < nums2[j]){
nums1[k] = nums1[i];
k++; i++;
}else{
nums1[k] = nums2[j];
k++; j++;
}
}
while(i<n+m)
nums1[k++] = nums1[i++];
while(j<n)
nums1[k++] = nums2[j++];
}
};

从后往前插入融合(最优)

时间复杂度: $O(m+n)$
空间复杂度: $O(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
25
class Solution:
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
"""
Do not return anything, modify nums1 in-place instead.
"""
index = m + n - 1
i = m - 1
j = n - 1
while i >= 0 and j >= 0:
if nums1[i] < nums2[j]:
nums1[index] = nums2[j]
j -= 1
else:
nums1[index] = nums1[i]
i -= 1
index -= 1
while i >= 0:
nums1[index] = nums1[i]
i -= 1
index -= 1
while j >= 0:
nums1[index] = nums2[j]
j -= 1
index -= 1
return

090. 子集 II

Description: 含重复元素的数组的子集

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

Example:

1
2
3
4
5
6
7
8
9
10
Input: [1,2,2]
Output:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]

解法一: 迭代

时间复杂度: $O(2^n)$, 时间复杂度为子集的个数
时间复杂度: $O(n)$, 空间复杂度为最长子集的长度

先排序, 然后对于一个元素, 如果这个元素与前一个元素相等, 那么在插入的时候, 就不能从第一个子集插入, 因为这样会重复, 因此要从不会造成重复的元素开始插入, 具体可看代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
std::sort(nums.begin(), nums.end());
vector<vector<int>> res {vector<int> {}};
int pre_start = 0;
for (int i = 0; i < nums.size(); i++) {
int j = (i>0 and nums[i]==nums[i-1]) ? pre_start : 0;
// 从不会重复的元素开始 或者 从头开始
int len = res.size();
for ( ; j < len; j++) {
auto sub_item = res[j];
sub_item.emplace_back(nums[i]);
res.emplace_back(sub_item);
}
pre_start = len; // 更新该值
}
return res;
}
};

解法二: 数组排序+判重+回溯

时间复杂度: $O(2^n)$, 时间复杂度为子集的个数
时间复杂度: $O(n)$, 空间复杂度为递归的深度

先排序, 然后同样, 如果遇到相等元素, 则跳过, 以避免重复

Python 实现

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
def back_track(res, temp, index, nums):
res.append(temp)
for i in range(index, len(nums)):
if i == index or nums[i] != nums[i-1]: # 判断当前的元素不与前一个元素重复
back_track(res, temp+[nums[i]], i+1, nums)
res = []
nums.sort() # 在回溯前判重, 需要对 nums 排序
back_track(res, [], 0, nums)
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
std::sort(nums.begin(), nums.end());
vector<vector<int>> res;
vector<int> sub_item;
back_trace(res, sub_item, 0, nums);
return res;
}

void back_trace(vector<vector<int>>& res, vector<int>& sub_item,
int start, vector<int>& nums) {
res.push_back(sub_item);
for (int i = start; i < nums.size(); i++) {
if (i > start and nums[i] == nums[i-1]) continue;
sub_item.emplace_back(nums[i]);
back_trace(res, sub_item, i+1, nums);
sub_item.pop_back();
}
}
};

解法三: 回溯+子集排序判重

对于每一个子集, 将其排序, 然后看该子集是否已经加入到了结果中, 以此去重.

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
def back_track(res, temp, index, nums):
temp.sort()
if temp not in res:
res.append(temp)
for i in range(index, len(nums)):
back_track(res, temp+[nums[i]], i+1, nums)
res = []
back_track(res, [], 0, nums)
return res

091. 解码方法

Decode Ways

Description

一条包含字母 A-Z 的消息通过以下方式进行了编码:

1
2
3
4
'A' -> 1
'B' -> 2
...
'Z' -> 26

给定一个只包含数字的非空字符串,请计算解码方法的总数。

1
2
3
4
5
6
7
8
9
10
Example 1:

Input: "12"
Output: 2
Explanation: It could be decoded as "AB" (1 2) or "L" (12).
Example 2:

Input: "226"
Output: 3
Explanation: It could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).

解法一(最优): DP constant space

时间复杂度: $O(n)$
空间复杂度: $O(1)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s)
if s == "" or s[0] == '0': return 0
f2 = 1 # 当前位向前2步的编码数量
f1 = 1 # 当前位向前1步的编码数量
for i in range(1, n):
if s[i] == '0':
if s[i-1] == '1' or s[i-1] == '2':
#dp[i] = dp[i-2]
f1, f2 = f2, f1
else:
return 0
elif (s[i-1] == '1') or (s[i-1] == '2' and s[i] < '7'):
f1 = f1 + f2
f2 = f1 - f2
else:
f2 = f1
return f1

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int numDecodings(string s) {
if(s.size()==0 || s.front()=='0') return 0; // 注意, 不能用s.front() == "0"
int f1=1, f2=1;
for(int i=1; i<s.size(); i++){
if(s[i]=='0') f1=0; //注意, 不能用s[i] == "0"
if(s[i-1]=='1' || (s[i-1]=='2' && s[i]<='6')){
f1 = f1+f2; // 令f1为前i-1字符的可能组合+前i-2字符的可能组合
f2 = f1-f2; // 令f2为前i-1字符的可能组合, 也就是对于下一个i来说的前i-2的可能组合
}
else
f2 = f1; // 如果当前字符不能与前一个字符组合, 则当前字符f1不变, 而f2有变为下一个i的前i-2的可能组合, 即让新f2等于旧的f1
}
return f1;
}
};

修复了上述的问题, 现在遇到 0 时会进行额外的判断, 0 不能单独编码, 必须与前面的字符组合, 如果无法组合, 则应该返回0, 如 230001, 就应该返回 0, 代码如下:

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
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
if(n==0 || s[0] == '0') return 0;
//if(n==1) return 1;
vector<int> dp(n, 0);
dp[0] = 1;
int i = 1;
while(i<n){
if(s[i]=='0'){
if(s[i-1] =='2' || s[i-1] == '1') // 0 不能单独编码, 必须与前面的数字组合, 因此这里是 dp[i-2]
dp[i] = i>1 ? dp[i-2] : 1;
else // 如果 0 前面的值大于 2, 则无法组成编码, 应返回 0
return 0;
}
else if(s[i-1]=='1' ||(s[i-1]=='2' && s[i] <= '6')){
int prev_two = i>1 ? dp[i-2] : 1;
dp[i] = dp[i-1] + prev_two;
}else{
dp[i] = dp[i-1];
}
i++;
}
return dp[n-1];
}
};

上面的代码使用了 DP 数组, 空间复杂度为 $O(n)$, 实际上我们并不需要这么多空间, 只需要常数空间就可以完成数组, 即只需要当前 dp 值的前两个 dp 值即可. 代码如下:

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
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
if(n==0 || s[0] == '0') return 0;
//if(n==1) return 1;
vector<int> dp(n, 0);
int f1 = 1; // 代表当前dp值之前一位的dp值
int f2 = 1; // 代表当前dp值之前两位的dp值
dp[0] = 1;
int i = 1;
while(i<n){
if(s[i]=='0'){
if(s[i-1] =='2' || s[i-1] == '1'){ // 0 不能单独编码, 必须与前面的数字组合, 因此这里是 dp[i-2]
int tmp = f1;
f1 = f2; // 令当前dp值为f2 (当前的dp值会成为下一个f1值)
f2 = tmp;
}
else // 如果 0 前面的值大于 2, 则无法组成编码, 应返回 0
return 0;
}
else if(s[i-1]=='1' ||(s[i-1]=='2' && s[i] <= '6')){
f1 = f1 + f2;
f2 = f1 - f2;
// 上面两个式子相当于:
// int tmp = f1; f1 = f1+f2; f2 = tmp;
//int prev_two = i>1 ? dp[i-2] : 1;
//dp[i] = dp[i-1] + prev_two;
}else{
f2 = f1; // 当前dp值不变, 所以只需要更新 f2 即可
}
i++;
}
return f1;
}
};

另一种写法, 更好理解:

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
class Solution {
public:
int numDecodings(string s) {
if (s.empty() || s[0] == '0') return 0;
int dp1 = 1; // 记录当前字符前一位的可能组合数
int dp2 = 1; // 记录当前字符前两位的可能组合数
long res = 1; // 记录当前字符的可能组合数
for (int i = 1; i < s.size(); i++) {
if (s[i] == '0') {
if (s[i-1] == '1' or s[i-1] == '2') { // d
res = dp2;
} else {
return 0;
}d
}
else if (s[i-1] == '1'
or (s[i-1] == '2' and s[i] < '7' and s[i] > '0')) {
res = dp1 + dp2;
} else {
res = dp1;
}
dp2 = dp1;
dp1 = res;
}
return res;
}
};

解法二: 递归

时间复杂度: $O(n^2)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int numDecodings(string s) {
if(s.size()==0) return 0;
return recurve(0,s);

}

int recurve(int pos, string &s){
if(pos==s.size()) return 1;
if(s[pos]=='0') return 0;

int tmp_res = recurve(pos+1, s);
if(pos<s.size()-1 && (s[pos]=='1' || (s[pos]=='2'&&s[pos+1]<='6'))) tmp_res += recurve(pos+2, s);
return tmp_res;
}
};

092. 反转链表 II

解法一: 记录翻转的 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
37
38
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
i = 0
dummy = ListNode(0)
dummy.next = head
node = head
pre = dummy # 记录当前节点的上一个节点
rear = None # 记录当前节点的下一个节点
while node:
i += 1
if i > n: # 如果超过 n, 则将 start 指向翻转后的开始节点, 完成链表的对接
start.next = pre
end.next = node
return dummy.next
elif i == m: # m 时, 要记录 start 和 end
start = pre
end = node
rear = node.next
node.next = pre
pre = node
node = rear
elif i > m:
rear = node.next
node.next = pre
pre = node
node = rear
else:
pre = node
node = node.next
start.next = pre
end.next = node
return dummy.next

094. 二叉树中序遍历

中序遍历二叉树

Description

Given a binary tree, return the inorder traversal of its nodes’ values.

Example:

1
2
3
4
5
6
7
8
Input: [1,null,2,3]
1
\
2
/
3

Output: [1,3,2]

Follow up: Recursive solution is trivial, could you do it iteratively?

解法一: 递归

Python

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res = []
def inorder(node):
if node.left:
inorder(node.left)
res.append(node.val)
if node.right:
inorder(node.right)
if root:
inorder(root)
return res

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
if(root==nullptr) return res;
inorder(root, res);
return res;
}
void inorder(TreeNode* root, vector<int> &res){
if(root->left!=nullptr) inorder(root->left, res);
res.push_back(root->val);
if(root->right!=nullptr) inorder(root->right, res);
}
};

解法二: 非递归

标准的中序非递归遍历算法
Python

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res = []
stack = []
while stack or root:
while root:
stack.append(root)
root = root.left
root = stack.pop()
res.append(root.val)
root = root.right
return res

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
if(root==nullptr) return res;
std::stack<TreeNode*> s_tree;
while(!s_tree.empty() || root!=nullptr){
while(root!=nullptr){
s_tree.push(root);
root= root->left;
}
if(!s_tree.empty()){
root = s_tree.top(); s_tree.pop();
res.push_back(root->val);
root = root->right;
}
}
return res;
}
};

095. 不同的二叉搜索树 II

题目链接: https://leetcode-cn.com/problems/unique-binary-search-trees-ii/

解法一: 递归

思想与95题类似, 只不过此时我们需要将左右子树的可能情况都列举出来

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
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def generateTrees(self, n: int) -> List[TreeNode]:

def generate_trees(start, end): # 递归函数
if start > end:
return [None]
all_trees = []
for i in range(start, end+1): # 以每个节点作为根节点
left = generate_trees(start, i-1) # 获取所有可能的左子树
right = generate_trees(i+1, end) # 获取所有可能的右子树
for l in left: # 将左右子树逐个连接起来
for r in right:
root = TreeNode(i)
root.left = l
root.right = r
all_trees.append(root)
return all_trees

return generate_trees(1, n) if n else []

解法二: 动态规划

首先我们每次新增加的数字大于之前的所有数字, 所以新增加的数字出现的位置只可能是根节点或者是根节点的右孩子, 右孩子的右孩子, 右孩子的右孩子的右孩子等等, 总之一定是右边. 其次, 新数字所在位置原来的子树, 改为当前插入数字的左孩子即可, 因为插入数字是最大的.

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
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def generateTrees(self, n: int) -> List[TreeNode]:
def tree_copy(root):
if root is None: return None
new_root = TreeNode(root.val)
if root.left: new_root.left = tree_copy(root.left)
if root.right: new_root.right = tree_copy(root.right)
return new_root

all_trees = [None]
for i in range(1, n+1):
tmp_trees = []
for root in all_trees:
# 新插入的节点作为新的根
r1 = tree_copy(root)
new_root = TreeNode(i)
new_root.left = r1
tmp_trees.append(new_root)

for j in range(1, n): # 逐个找到可以插入新节点的节点
j_root = tree_copy(root)
jr_root = j_root
k = 0
if jr_root is None:
break
for k in range(j): #
pre = jr_root
jr_root = jr_root.right


new_node = TreeNode(i)
pre.right = new_node
new_node.left = jr_root
tmp_trees.append(j_root)
if jr_root is None:
break
all_trees = tmp_trees
return all_trees if n > 0 else []

096. 不同的二叉搜索树

题目链接: https://leetcode-cn.com/problems/unique-binary-search-trees/

解法一: 递归

模拟每个节点作为根节点时的状态, 将左子树的可能性与右子树的可能性相乘, 最后将所有节点的可能性相加

解法二: 动态规划

思路和解法一一致, 将所有节点为根的情况一一计算并相加

时间复杂度 : 上述算法的主要计算开销在于包含 dp[i] 的语句. 因此, 时间复杂度为这些语句的执行次数, 也就是 $\sum_{i=2}^{n} i = \frac{(2+n)(n-1)}{2}$. 因此, 时间复杂度为 $O(N^2)$
空间复杂度 : 上述算法的空间复杂度主要是存储所有的中间结果, 因此为 $O(N)$

1
2
3
4
5
6
7
8
9
class Solution:
def numTrees(self, n: int) -> int:
dp = [0] * (n+1)
dp[0] = 1
dp[1] = 1
for i in range(2, n+1):
for j in range(1, i+1):
dp[i] += dp[j-1] * dp[i-j]
return dp[-1]

解法三: 卡特兰数

根据解法二分析的递推公式, 完全符合卡特兰数的定义, 关于卡特兰数的介绍请看算法经典题型整理

时间复杂度: $O(n)$
空间复杂度: $O(1)$

1
2
3
4
5
6
class Solution:
def numTrees(self, n: int) -> int:
res = 1
for i in range(1, n):
res *= (4*i + 2) / (i+2)
return int(res)

098. Validate Binary Search Tree

Description

Given a binary tree, determine if it is a valid binary search tree (BST).

Assume a BST is defined as follows:

The left subtree of a node contains only nodes with keys less than the node’s key.
The right subtree of a node contains only nodes with keys greater than the node’s key.
Both the left and right subtrees must also be binary search trees.
Example 1:

Input:
2
/ \
1 3
Output: true
Example 2:

5

/ \
1 4
/ \
3 6
Output: false
Explanation: The input is: [5,1,4,null,null,3,6]. The root node’s value
is 5 but its right child’s value is 4.

解法一: 递归

用一个指针来指向当前节点在顺序上的前一个节点, 判断是否为BST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
bool isValidBST(TreeNode* root) {
TreeNode* pre_node = nullptr;
return isBST(root, pre_node);
}
bool isBST(TreeNode* root, TreeNode * &pre_node){ // 注意!!! 要维持递归时的pred_node, 因此必须使用 * &, 否则每次的pre_node = root;实际上只是改变了pred_node的副本
if(root==nullptr) return true;
if(isBST(root->left, pre_node) == false) return false;
if(pre_node!=nullptr && pre_node->val >= root->val) return false;
pre_node = root;
if(isBST(root->right, pre_node)==false) return false;
return true;
}
};

下面的代码是典型错误解法: 因为, 我们不知只要考虑左子树节点值要小于当前节点值, 还要满足的另外一个条件是左子树本身也是一个二叉搜索树, 下面的代码没有进行该判断.

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
/*
Input
[10,5,15,null,null,6,20]
Output
true
Expected
false
*/

class Solution {
public:
bool isValidBST(TreeNode* root) {
if(root==nullptr) return true;
bool b=true;
if(root->left!=nullptr){
if(root->left->val >= root->val) return false;
b = isValidBST(root->left);
}
if(b==false) return b;
if(root->right!=nullptr){
if(root->right->val <= root->val) return false;
b = isValidBST(root->right);
}
return b;
}
};

解法二: 迭代(中序)

中序遍历二叉搜索树时, 返回的是一个有序的数组, 因此, 我们可以在遍历时, 一旦发现不有序, 就返回 false, 需要注意一点的是, 本题中二叉搜索树中的节点值是唯一的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
bool isValidBST(TreeNode* root) {
TreeNode* prev = nullptr;
stack<TreeNode*> s;
while(root!=nullptr || !s.empty()){
while(root!=nullptr){
s.push(root);
root = root->left;
}

if(!s.empty()){
root = s.top(); s.pop();
if(prev!=nullptr && prev->val >= root->val)
return false;
prev = root;
root = root->right;
}
}
return true;
}
};

101. Symmetric Tree

判断一个二叉树是否为对称的.(与自身镜像相等)

Description

Given a binary tree, check whether it is a mirror of itself (ie, symmetric around its center).

For example, this binary tree [1,2,2,3,4,4,3] is symmetric:

1
2
3
4
5
    1
/ \
2 2
/ \ / \
3 4 4 3

But the following [1,2,2,null,3,null,3] is not:

1
2
3
4
5
  1
/ \
2 2
\ \
3 3

Note:
Bonus points if you could solve it both recursively and iteratively.

解法一: 递归

时间复杂度: $O(n)$ , 遍历了整个树中的每个节点一次
空间复杂度: $O(n)$ , 调用递归的次数与树的高度有关, 在最差的情况下, 树的高度为n.

Python

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
def helper(node1, node2):
if node1 == None and node2 == None: return True
if node1 == None or node2 == None: return False
if node1.val != node2.val:
return False
return helper(node1.left, node2.right) and helper(node1.right, node2.left)

if root == None: return True
return helper(root.left, root.right)

C++

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
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if(root==nullptr) return true;
return isSymHelper(root->left, root->right);
}

bool isSymHelper(TreeNode* subRoot1, TreeNode* subRoot2){
if(subRoot1 == nullptr && subRoot2 == nullptr) return true;
if(subRoot1 == nullptr || subRoot2 == nullptr) return false;
if(subRoot1->val != subRoot2->val) return false;

bool b1 = isSymHelper(subRoot1->left, subRoot2->right);
bool b2 = isSymHelper(subRoot1->right, subRoot2->left);
return b1&&b2;
}
};

更整洁的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
bool is_sym(TreeNode* t1, TreeNode* t2){
if(t1==nullptr && t2==nullptr) return true;
if(t1==nullptr || t2==nullptr) return false;
if(t1->val == t2->val)
return is_sym(t1->left, t2->right) && is_sym(t2->left, t1->right);
else return false;
}
public:
bool isSymmetric(TreeNode* root) {
if(root==nullptr) return true;
return is_sym(root->left, root->right);
}
};

解法二: 迭代

时间复杂度: $O(n)$ , 遍历了整个树中的每个节点一次
空间复杂度: $O(n)$ , 层次遍历创建了两个队列, 其大小总和刚好为n. (有一种说法是: 层次遍历我们最多只会同时保存两层的节点数, 而最后一层的节点数最多为 $logn$, 所以空间复杂度实际上是 $O(logn)$ (常数项被约掉), 这种说法对吗??)

层次遍历, 注意不应该左子树和右子树做非空检查, 因此判断是否对称时, 需要包含节点为空的情况.(因为不需要知道当前的深度, 所以也不用维护深度信息)

Python

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

class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
from queue import Queue

if root == None: return True
q1 = Queue()
q2 = Queue()
q1.put(root.left)
q2.put(root.right)

while not q1.empty() and not q2.empty():
node1 = q1.get()
node2 = q2.get()
if node1 == None and node2 == None: continue
if node1 == None or node2 == None:
return False
if node1.val != node2.val:
return False
q1.put(node1.left)
q1.put(node1.right)
q2.put(node2.right)
q2.put(node2.left)

return True

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if(root==nullptr) return true;
queue<TreeNode*> q1;
queue<TreeNode*> q2;
q1.push(root->left);
q2.push(root->right);
TreeNode * cur1, * cur2;
while(!q1.empty() && !q2.empty()){
cur1 = q1.front(); q1.pop();
cur2 = q2.front(); q2.pop();
if(cur1==nullptr && cur2 ==nullptr) continue;
if(cur1==nullptr || cur2 == nullptr) return false;
if(cur1->val != cur2->val) return false;
q1.push(cur1->left); q1.push(cur1->right);
q2.push(cur2->right); q2.push(cur2->left);
}
return true;
}
};

解法三: 迭代

时间复杂度: $O(n)$
空间复杂度: $O(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
25
26
27
28
29
30
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (root == nullptr) return true;
std::queue<TreeNode*> queueTree;
queueTree.push(root);
while (!queueTree.empty()) {
int len = queueTree.size();
std::vector<double> vec;
for (int i = 0; i < len; i++) {
auto node = queueTree.front();
queueTree.pop();
if (node == nullptr) {
vec.push_back(0.5);
} else {
vec.push_back(node->val);
queueTree.push(node->left);
queueTree.push(node->right);
}
}
int n = vec.size();
for (int i = 0; i < n/2; i++) {
if (vec[i] != vec[n - i - 1]) {
return false;
}
}
}
return true;
}
};

102. Binary Tree Level Order Traversal

按层次输出二叉树节点的值(每层的值要分开)

Description

解法一: 层次遍历

时间复杂度: $O(n)$ , 每个节点遍历一次
空间复杂度: $O(n)$ , 存储了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
25
26
27
28
29
30
31
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if(root == nullptr) return res;
std::queue<TreeNode*> q;
TreeNode * cur_node;
q.push(root);
while(!q.empty()){
int len = q.size();
vector<int> layer;
for(int i=0; i<len; i++){
cur_node = q.front(); q.pop();
layer.push_back(cur_node->val);
if(cur_node->left != nullptr) q.push(cur_node->left);
if(cur_node->right != nullptr) q.push(cur_node->right);
}
res.push_back(layer);
}
return res;
}
};

103. Binary Tree Zigzag Level Order Traversal

按之字形打印二叉树

Description

Given a binary tree, return the zigzag level order traversal of its nodes’ values. (ie, from left to right, then right to left for the next level and alternate between).

For example:
Given binary tree [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
return its zigzag level order traversal as:
[
[3],
[20,9],
[15,7]
]

解法一:利用reverse

时间复杂度为 $O(n^2)$ 空间复杂度为 $O(n)$

然后每次访问节点时, 都判断当前节点的层数, 如果为奇数层, 则将该层直接push back到结果向量中, 如果为偶数, 则将该层数据进行reverse后再push back到结果向量中. 通过while里面内置for循环, 来保证每次for循环都会将一整层的节点放进队列中, 无需额外的数组来存储depth信息

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
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int>> res;
if(pRoot == NULL)
return res;
queue<TreeNode*> que;
que.push(pRoot);
bool even = false;
while(!que.empty()){
vector<int> vec; //将vec声明在内部, 省去每次的clear操作, clear操作需要对vector进行遍历, 并将每个元素置为null?
const int size = que.size(); //当前存的节点数目就是这一层所有的节点, 之前层的到已经被取出, 并且这一层的子节点还没有开始入队列
for(int i=0; i<size; ++i){ //将该层所有节点的子节点入队列, 同时当到达该层最后一个节点时终止
TreeNode* tmp = que.front();
que.pop();
vec.push_back(tmp->val);
if(tmp->left != NULL)
que.push(tmp->left);
if(tmp->right != NULL)
que.push(tmp->right);
}
if(even) //根据奇偶标识判断是否需要reverse
std::reverse(vec.begin(), vec.end());
res.push_back(vec);
even = !even;
}
return res;
}
};

解法二: 最优(不用reverse)

时间复杂度: $O(n)$
空间复杂度: $O(n)$

在解法二中, 复杂度高的原因是因每次遇到偶数层的时候都要进行 reverse, 实际上, 当我们知道了该层的节点个数以后, 我们可以直接开辟一个指定大小的 vector, 然后根据下标随机访问来填入该层的节点值, 这样一来就不用进行 reverse, 并且空间复杂度与解法二相同

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
class Solution {
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
vector<vector<int>> res;
if(root == nullptr) return res;
std::queue<TreeNode*> q;
q.push(root);
bool is_odd = true;
TreeNode* cur_node;
while(!q.empty()){
int layer_len = q.size();
vector<int> cur_layer(layer_len);
for(int i=0; i<layer_len; i++){
cur_node = q.front(); q.pop();
if(is_odd==true)
cur_layer[i] = cur_node->val;
else
cur_layer[layer_len-1-i ] = cur_node->val;

if(cur_node->left!=nullptr) q.push(cur_node->left);
if(cur_node->right!=nullptr) q.push(cur_node->right);

}
res.push_back(cur_layer);
is_odd = !is_odd;
}
return res;
}
};

解法三: 利用双端队列

时间复杂度: $O(n)$
空间复杂度: $O(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
25
26
27
28
29
30
31
32
class Solution {
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
vector<vector<int>> res;
if (root == nullptr) return res;
std::deque<TreeNode*> dqTree;
dqTree.push_back(root);
int depth = 0;
while (!dqTree.empty()) {
depth++;
int len = dqTree.size();
vector<int> tmpRes;
for (int i = 0; i < len; i++) {
if (depth & 1) {
auto node = dqTree.front();
dqTree.pop_front();
tmpRes.push_back(node->val);
if (node->left != nullptr) dqTree.push_back(node->left);
if (node->right != nullptr) dqTree.push_back(node->right);
} else {
auto node = dqTree.back();
dqTree.pop_back();
tmpRes.push_back(node->val);
if (node->right != nullptr) dqTree.push_front(node->right);
if (node->left != nullptr) dqTree.push_front(node->left);
}
}
res.push_back(tmpRes);
}
return res;
}
};

104. Maximum Depth of Binary Tree

求二叉树的最大深度(树的深度)

Description

Given a binary tree, find its maximum depth.

The maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.

Note: A leaf is a node with no children.

Example:

Given binary tree [3,9,20,null,null,15,7],

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

return its depth = 3.

解法一: 层次遍历

时间复杂度: $O(n)$
空间复杂度: $O(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
25
26
27
28
29
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root == nullptr) return 0;
std::queue<TreeNode*> q;
q.push(root);
int depth = 0;
TreeNode* cur_node;
while(!q.empty()){
int layer_len = q.size();
depth++;
for(int i=0; i<layer_len; i++){
cur_node = q.front(); q.pop();
if(cur_node->left!=nullptr) q.push(cur_node->left);
if(cur_node->right!=nullptr) q.push(cur_node->right);
}
}
return depth;
}
};

解法二: 递归

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
private:
int height(TreeNode *root){
if(root == nullptr) return 0;
int left_height = height(root->left);
int right_height = height(root->right);
return 1+max(left_height, right_height);
}
public:
int maxDepth(TreeNode* root) {
return height(root);
}
};

105. Construct Binary Tree from Preorder and Inorder Traversal

Description: 根据先序和中序遍历构造二叉树

Given preorder and inorder traversal of a tree, construct the binary tree.

Note:
You may assume that duplicates do not exist in the tree.(如果没有该条件则通常无法还原唯一的二叉树)

For example, given

preorder = [3,9,20,15,7]
inorder = [9,3,15,20,7]
Return the following binary tree:

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

解法一: 递归

时间复杂度: $O(n^2)$, 在中序遍历中查找根节点的复杂度为 $O(n)$, 先序序列中总共有 $n$ 个根节点, 所以需要查找 $n$ 次
空间复杂度: 根据树的结构, 最坏情况下的递归深度为 $O(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
25
26
27
28
29
30
31
32
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
private:
TreeNode* helper(vector<int> &preorder, int i, int j, vector<int> &inorder, int k, int l){
// tree 8 4 5 3 7 3
// preorder 8 [4 3 3 7] [5]
// inorder [3 3 4 7] 8 [5]
if(i >= j || k >= l){// 注意, 这里的 j 和 l 均为超尾下标
return nullptr;
}
int root_val = preorder[i];
auto in_index = find(inorder.begin()+k, inorder.begin()+l, root_val);
int dis = in_index - inorder.begin() - k;

TreeNode *root = new TreeNode(root_val);
root->left = helper(preorder, i+1, i+1+dis, inorder, k, k+dis);
root->right = helper(preorder, i+1+dis, j, inorder, k+dis+1, l);
return root;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
return helper(preorder, 0, preorder.size(), inorder, 0, inorder.size());
}
};

解法二: 迭代

时间复杂度: $O(n)$
空间复杂度: $O(n)$

  1. 先将 preorder[i] 压入栈中, 如果当前 preorder 的元素与 inorder 中的元素不匹配, 则将 preorder 中的值构造成节点压入栈中, 并且新构造的节点一定是栈顶的左孩子. 重复该过程直到元素值匹配为止: preorder[i] = inorder[index]
  2. 当先序和中序的值匹配时, 则将节点出栈, 直到不再匹配为止.
  3. TODO: 该解法还没彻底搞清, 暂时搁置
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
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
stack<TreeNode*> s;
if(preorder.empty()) return nullptr;
TreeNode *root = new TreeNode(preorder[0]);
s.push(root);
int index = 0;
for(int i=1; i < preorder.size(); i++){
TreeNode* cur = s.top();
if(cur->val != inorder[index]){
cur->left = new TreeNode(preorder[i]);
s.push(cur->left);
}else{
while(!s.empty() && s.top()->val == inorder[index]){
cur = s.top(); s.pop(); index++;
}
if(index < inorder.size()){
cur->right = new TreeNode(preorder[i]);
s.push(cur->right);
}
}
}
return root;
}
};

108. Convert Sorted Array to Binary Search Tree

根据 有序数组 构造平衡二叉搜索树(不唯一, 只要符合规则即可)

Description

Given an array where elements are sorted in ascending order, convert it to a height balanced BST.

For this problem, a height-balanced binary tree is defined as a binary tree in which the depth of the two subtrees of every node never differ by more than 1.

Example:

1
2
3
4
5
6
7
8
9
Given the sorted array: [-10,-3,0,5,9],

One possible answer is: [0,-3,9,-10,null,5], which represents the following height balanced BST:

0
/ \
-3 9
/ /
-10 5

解法一: 递归构造

时间复杂度: $O(n)$
空间复杂度: $O(n)$, 递归了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
25
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {

return construct_BST(nums, 0, nums.size()-1);
}

TreeNode* construct_BST(vector<int>& nums, int low, int high){
if(low>high) return nullptr;
int mid = (low+high)/2;
TreeNode* root = new TreeNode(nums[mid]);
root->left = construct_BST(nums, low, mid-1);
root->right = construct_BST(nums, mid+1, high);
return root;
}
};

解法二: 迭代

时间复杂度: $O(n)$, 只不过需要遍历两次树的size
空间复杂度: $O(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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
int tree_len = nums.size();
if(tree_len == 0) return nullptr;
std::queue<TreeNode*> q;
TreeNode* root = new TreeNode(0);
q.push(root); tree_len--;
TreeNode* cur_node;
int layer_len = 1;
while(tree_len>0){
layer_len *= 2;
for(int i=0; i<layer_len && tree_len>0; i++){
cur_node = q.front(); q.pop();
TreeNode* left = new TreeNode(0);
cur_node->left = left;
q.push(cur_node->left); tree_len--;
if(tree_len>0){
TreeNode *right = new TreeNode(0);
cur_node->right = right;
q.push(cur_node->right); tree_len--;
}
}
}

std::stack<TreeNode*> s;
cur_node = root;
int i = 0;
while(!s.empty() || cur_node!=nullptr){
while(cur_node!=nullptr){
s.push(cur_node);
cur_node = cur_node->left;
}
if(!s.empty()){
cur_node = s.top(); s.pop();
cur_node->val =nums[i++];
cur_node = cur_node->right;
}
}
return root;
}
};

解法三: 迭代(只中根遍历一次)

【链接】Loading…
https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/discuss/35218/Java-Iterative-Solution

111. minimum depth of binary tree

题目描述

Given a binary tree, find its minimum depth.

The minimum depth is the number of nodes along the shortest path from the root node down to the nearest leaf node.

Note: A leaf is a node with no children.

Example:

1
2
3
4
5
6
7
Given binary tree [3,9,20,null,null,15,7],

3
/ \
9 20
/ \
15 7

解法一:

层次优先遍历,遇到的首个叶子结点(左右子树为空)即为最短的深度

注意:

利用while内嵌for循环的方式, 可以省去对每个结点depth的维护, 只需要每次进入for循环之前, depth++即可(因为一个for循环会将当前层所有的结点都入队列, for循环结束后, 意味着进入了下一层, 所以depth++即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int run(TreeNode *root) {
queue<TreeNode*> q_node;
if(root==nullptr) return 0;
q_node.push(root);
int depth = 0;
while(!q_node.empty()){
const int size = q_node.size();
depth++;
for(int i = 0; i< size; i++){
TreeNode* node = q_node.front(); q_node.pop();
if(node->left!=nullptr) q_node.push(node->left);
if(node->right!=nullptr) q_node.push(node->right);
if(node->left==nullptr && node->right == nullptr) return depth;
}
}

return -1;
}
};

解法二(递归):

让当前结点为空, 则当前结点深度为0, 若当前结点左子树为空, 则当前结点深度等于左子树深度, 反之 ,等于右子树深度. 若当前结点左右子树均不为空, 则当前结点的 最小深度 等于左右子树深度 较小者 加1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int run(TreeNode* root) {
if(root== nullptr) return 0;
if(root->left==nullptr)
return run(root->right) + 1;
else if(root->right ==nullptr)
return run(root->left) + 1;
else{
int depth1=run(root->left);
int depth2=run(root->right);
return depth1<depth2 ? depth1+1 : depth2+1;
}
}
};

114. 二叉树展开为链表-中等

给定一个二叉树,原地 将它展开为链表。
题目链接: https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/

解法一: 先序遍历

先序遍历需要申请一个全局变量来维护最后访问的节点, 同时注意将右子树做一个备份, 因为在遍历左子树的时候, 有可能会改变根节点的右节点, 这样会导致访问错误的地址.

该解法貌似存在一些问题, 下面的两种实现均不能通过 OJ

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def flatten(self, root: TreeNode) -> None:
"""
Do not return anything, modify root in-place instead.
"""
def helper(root, last):
if root == None: return
if last != None:
last.right = root
last.left = None
last = root
copy_right = root.right
helper(root.left, last)
helper(copy_right, last)
last = None
helper(root, last)

C++ 实现:

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
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/

TreeNode* last = nullptr;
class Solution {
public:
void flatten(TreeNode* root) {
if (root == nullptr) return;
if (last != nullptr) {
last->right = root;
last->left = nullptr;
}
last = root;
auto right_copy = root->right;
flatten(root->left);
flatten(right_copy);
return;
}
};

解法二: 后序遍历, 递归

依据二叉树展开为链表的特点, 使用后序遍历完成展开.
Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def flatten(self, root: TreeNode) -> None:
"""
Do not return anything, modify root in-place instead.
"""
def helper(root):
if root == None: return
helper(root.left)
helper(root.right)
if root.left != None: # 后序遍历
pre = root.left # 令 pre 指向左子树
while pre.right: pre = pre.right # 找到左子树中的最右节点
pre.right = root.right # 令左子树中的最右节点的右子树 指向 根节点的右子树
root.right = root.left # 令根节点的右子树指向根节点的左子树
root.left = None # 置空根节点的左子树
root = root.right # 令当前节点指向下一个节点
helper(root)

C++ 实现:

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
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/

TreeNode* last = nullptr;
class Solution {
public:
void flatten(TreeNode* root) {
if (root == nullptr) return;
flatten(root->left);
flatten(root->right);
if (root->left != nullptr) {
auto pre = root->left;
while (pre->right != nullptr) pre = pre->right;
pre->right = root->right;
root->right = root->left;
root->left = nullptr;
}
root = root->right;
return;
}
};

解法三: 非递归, 不使用辅助空间及全局变量

前面的递归解法实际上也使用了额外的空间, 因为递归需要占用额外空间. 下面的解法无需申请栈, 也不用全局变量, 是真正的 In-Place 解法.

C++ 解法:

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
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
void flatten(TreeNode* root) {
while (root != nullptr) {<