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) {
if (root->left != nullptr) {
auto most_right = root->left; // 如果左子树不为空, 那么就先找到左子树的最右节点
while (most_right->right != nullptr) most_right = most_right->right; // 找最右节点
most_right->right = root->right; // 然后将跟的右孩子放到最右节点的右子树上
root->right = root->left; // 这时候跟的右孩子可以释放, 因此我令左孩子放到右孩子上
root->left = nullptr; // 将左孩子置为空
}
root = root->right; // 继续下一个节点
}
return;
}
};

Python 解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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.
"""
while (root != None):
if root.left != None:
most_right = root.left
while most_right.right != None: most_right = most_right.right
most_right.right = root.right
root.right = root.left
root.left = None
root = root.right
return

116. Populating Next Right Pointers in Each Node

令每个节点中的 next 指针指向它的右兄弟, 如果没有右兄弟, 那么就置为 nullptr, 注意, 题目给定的树是满二叉树

Description

Given a binary tree

struct TreeLinkNode {
TreeLinkNode left;
TreeLinkNode
right;
TreeLinkNode * next;
}
Populate each next pointer to point to its next right node. If there is no next right node, the next pointer should be set to NULL.

Initially, all next pointers are set to NULL.

Note:

You may only use constant extra space.
Recursive approach is fine, implicit stack space does not count as extra space for this problem.
You may assume that it is a perfect binary tree (ie, all leaves are at the same level, and every parent has two children).
Example:

Given the following perfect binary tree,

 1

/ \
2 3
/ \ / \
4 5 6 7
After calling your function, the tree should look like:

 1 -> NULL

/ \
2 -> 3 -> NULL
/ \ / \
4->5->6->7 -> NULL

解法一: 层次遍历

时间复杂度: $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
33
34
35
36
37
/**
* Definition for binary tree with next pointer.
* struct TreeLinkNode {
* int val;
* TreeLinkNode *left, *right, *next;
* TreeLinkNode(int x) : val(x), left(NULL), right(NULL), next(NULL) {}
* };
*/
class Solution {
public:
Node* connect(Node* root) {
if (root==nullptr) return nullptr;
std::queue<Node*> treeQ;
treeQ.push(root);
while (!treeQ.empty()) {
int len = treeQ.size();
for (int i = 0; i < len; i++) {
auto node = treeQ.front();
treeQ.pop();
Node* nextNode;
if ( i < len -1) {
nextNode = treeQ.front();
} else {
nextNode = nullptr;
}
node->next = nextNode;
if (node->left != nullptr) {
treeQ.push(node->left);
}
if (node->right != nullptr) {
treeQ.push(node->right);
}
}
}
return root;
}
};

解法二: 利用 next 指针的特性

时间复杂度: $O(n)$, 每个节点都要访问一次(仅访问一次)
空间复杂度: $O(1)$

由于是满二叉树, 因此我们可以轻易的利用next指针自身的特性来实现层次遍历.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
Node* connect(Node* root) {
Node* curFirst = root;
while (curFirst != nullptr) {
Node* curNode = curFirst;
while (curNode != nullptr) {
if (curNode->left != nullptr) {
curNode->left->next = curNode->right;
}
if (curNode->next != nullptr && curNode->right != nullptr) {
curNode->right->next = curNode->next->left;
}
curNode = curNode->next;
}
curFirst = curFirst->left;
}
return root;
}
};

118. 杨辉三角形

Pascal’s Triangle

Pascal 三角形

Description

给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。

Example:

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

解法一: 按照三角形的性质进行赋值

赋值时, 每一行的两端都是1, 无需重复赋值, 注意控制好边界条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> res;
for(int i =0; i<numRows; i++){
vector<int> temp(i+1, 1);
for(int j=1; j<i; j++){ // 两边默认为1, 无需重复赋值
temp[j] = res[i-1][j-1]+res[i-1][j];// i和j的值只有在大于1时才会进入循环, 所以无需担心i-1或j-1<0
}
res.push_back(temp);
}
return res;
}
};

121. 买卖股票的最佳时机-简单

获取最大的股票利润
题目链接: https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/

Description

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

注意你不能在买入股票前卖出股票。

Example 1:

1
2
3
Input: [7,1,5,3,6,4]
Output: 5
Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5. Not 7-1 = 6, as selling price needs to be larger than buying price.

Example 2:

1
2
3
Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.

解法一: 穷举

计算所有可能性, $O(n^2)$

解法二: 一次遍历

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

维护两个变量 min_pricemax_profit, 每次检查元素, 一方面如果当前价格更低, 则更改 min_price 变量, 另一方面如果当前利润超过 max_profit, 则更新之.

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

同样也是一次遍历, 下面的写法更加简洁, 我们这里记录一个变量 buy, 用来指示可能的买入下标, 之后, 如果下一个价格比 buy 对应的价格高, 我们就尝试更新最大利润, 否则, 就改变 buy 到当前的价格下标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int maxProfit(vector<int>& prices) {
int maxfit = 0;
int buy = 0;
for(int i=1; i<prices.size(); i++){
if(prices[buy] < prices[i]){
maxfit = max(maxfit, prices[i] - prices[buy]);
}else
buy = i;
}
return maxfit;
}
};

实际上, 我们只需要用一个变量记录迄今为止遇到的最小的股票值即可, 然后对于每一个新值, 我们都更新最高利润和最小值即可, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.empty()) return 0;
int low = prices[0];
int res = 0;
for (auto const p : prices) {
res = std::max(res, p - low);
low = std::min(low, p);
}
return res;
}
};

解法三: 通用 DP 解法

详细分析见后面的 “股票问题通用解法”

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() <= 1) return 0;
int dp[2] = {-prices[0], 0}; // 持有, 不持有 base case
for (int i = 1; i < prices.size(); i++) {
int hold = std::max(dp[0], -prices[i]);
int not_hold = std::max(dp[1], dp[0] + prices[i]);
dp[0] = hold; dp[1] = not_hold;
}
return std::max(dp[0], dp[1]);
}
};

Python 实现:

1
2
3
4
5
6
7
8
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices) <= 1: return 0
dp = [-prices[0], 0] # 持有, 不持有
for price in prices[1:]:
# 持有: 要么之前买过, 要么第一次买入; 不持有: 要么维持之前不持有的状态, 要么今天买了
dp = [max(dp[0], -price), max(dp[1], dp[0] + price)]
return max(dp)

122. 买卖股票的最佳时机 II-简单

可以多次交易, 统计最大利润和
题目链接: https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/

Description

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

Example 1:

1
2
3
Input: [7,1,5,3,6,4]
Output: 7
Explanation: Buy on day 2 (price = 1) and sell on day 3 (price = 5), profit = 5-1 = 4.Then buy on day 4 (price = 3) and sell on day 5 (price = 6), profit = 6-3 = 3.

Example 2:

1
2
3
Input: [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4. Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are engaging multiple transactions at the same time. You must sell before buying again.

Example 3:

1
2
3
Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.

解法一: 用变量维护最低价格

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

寻找递增序列, 一旦出现递减的情况, 则说明应该及时卖出, 并将 min_price 重新赋值. 因为最后一个元素后面没有值来判断是否递减, 因此需要对最后一个元素进行单独判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size() ==0) return 0 ;

int min_price = prices[0];
int sum_profit = 0, pre_price=prices[0];
for(int i=1; i<prices.size(); i++){
if(prices[i] < pre_price){ //如果小于之前的price, 则说明此时应该卖出
sum_profit += pre_price-min_price; //计算卖出利润
min_price = prices[i];
}
pre_price = prices[i];
if(i==prices.size()-1 && prices[i] > min_price) //到了最后一个元素, 查看是否应该卖出
sum_profit += prices[i] - min_price;
}
return sum_profit;
}
};

同样和上一道题一样, 利用 buy 可以更加整洁的实现:

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

解法二: 每两个相邻数字当做一次交易

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

实际上和解法一本质相同, 只不过在累加利润上有一点小区别.
该解法是将每两个相邻数字看做是一次交易, 如果后者大于前者, 说明应该执行交易, 并累加交易所的利润.

1
2
3
4
5
6
7
8
9
10
Cclass Solution {
public:
int maxProfit(vector<int>& prices) {
int sum_profit = 0;
for(int i =1 ; i<prices.size(); i++){
if(prices[i] > prices[i-1]) sum_profit += prices[i] - prices[i-1];
}
return sum_profit;
}
};

解法三: 通用 DP 解法

详细分析见后面的 “股票问题通用解法”

Python 实现:

1
2
3
4
5
6
7
8
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices) <= 1: return 0
dp = [-prices[0], 0] # 持有, 不持有, base case
for price in prices[1:]:
# 更新状态[max(维持持有; 之前不持有, 今天买入), max(维持不持有; 之前持有, 今天卖出)]
dp = [max(dp[0], dp[1]-price), max(dp[1], dp[0]+price)]
return max(dp)

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() <=1) return 0;
int dp[2] = {-prices[0], 0}; // base case
for (int i = 1; i < prices.size(); i++) {
int hold = std::max(dp[0], dp[1]-prices[i]); // update
int not_hold = std::max(dp[1], dp[0]+prices[i]);
dp[0] = hold; dp[1] = not_hold;
}
return std::max(dp[0], dp[1]);
}
};

123. 买卖股票的最佳时机 III-困难

题目链接: https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

解法三: 通用 DP 解法

时间复杂度: $O(2n)$
空间复杂度: $O(2k)$, dp 数组的空间是否可以进一步压缩? 答案是不行的, 表面上看起来, dp[k] 只会用到 dp[k-1] 即相邻的状态, 但是实际上, 这里用到的是上一轮循环中的结果, 我们必须把这一轮训练的结果都存储下来, 才能进行到下一轮, 因此这里的空间复杂度不能优化了.

详细分析见后面的 “股票问题通用解法”

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices) <= 1: return 0
k = 2 # k 代表最大的可交易次数, 该解法可以轻松扩展至 k 次的情况
dp = [None] * (k+1)
dp[0] = [0, 0] # 至多 0 次交易, 则均为0(不能持有)
for i in range(1, k+1): # base case, 至多 1 次交易时, dp 的状态
dp[i] = [-prices[0], 0]
for price in prices[1:]:
for i in range(1, k+1):
# 至多进行 i 次交易, 以买入计算交易次数
# 持有: 本次不买入(维持持有), 本次买入, 交易次数增加(之前不持有, 本次消费利润)
# 不持有: 维持不持有, 或者之前持有, 本次卖出(只计入买入次数即可)
dp[i] = [max(dp[i][0], dp[i-1][1]-price), max(dp[i][1], dp[i][0]+price)]
return max(dp[k])

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() <= 1) return 0;
int k = 2;
std::vector<std::pair<int, int>> dp(k+1, {-prices[0], 0}); // base case: [hold, not_hold]
for (int i = 1; i < prices.size(); i++) {
for (int j = 1; j < k+1; j++) {
int hold = std::max(dp[j].first, dp[j-1].second-prices[i]); // 买入计入交易次数
int not_hold = std::max(dp[j].second, dp[j].first+prices[i]); // 卖出不计入次数
dp[j].first = hold; dp[j].second = not_hold;
}
}
return std::max(dp[k].first, dp[k].second);
}
};

124. Binary Tree Maximum Path Sum

求二叉树中, 以任意节点为起始的路径和(这里是将二叉树看成无向图来计算路径的)的最大值, 例如对于下面的二叉树, 具有最大值的为:2->1->3 = 6

1
2
3
  1
/ \
2 3

Description: 求最长路径加权和

Given a non-empty binary tree, find the maximum path sum.

For this problem, a path is defined as any sequence of nodes from some starting node to any node in the tree along the parent-child connections. The path must contain at least one node and does not need to go through the root.

Example 1:

1
2
3
4
5
Input: [1,2,3]
1
/ \
2 3
Output: 6

Example 2:

1
2
3
4
5
6
7
Input: [-10,9,20,null,null,15,7]
-10
/ \
9 20
/ \
15 7
Output: 42

解法一: 递归

这道题的难点在于能否解读出题目的求值过程实际上是一个后序遍历的过程.

对于本题来说, 我们需要求得每个节点所在的路径的最大值, 以下面的例子来说:

1
2
3
4
5
    4
/ \
11 13
/ \
7 2

我们需要求的最大和的路径为: 7->11->4->13. 而根据二叉树的遍历性质, 我们假设现在已经遍历到节点7, 此时, 左右子树均为空, 所以左右子树的最大和为0, 那么此时节点7所在的路径的最大和为: 左子树+右子树+当前节点值 = 7. 然后, 回溯到了节点11, 此时同理, 节点11所在的路径的最大和为: 左子树+右子树+当前节点值 = 11.(忽略节点2的遍历过程). 接下来对于节点4, 同理也应为: 左子树+右子树+当前节点值. 右子树返回的值很容易看出是13, 但是左子树应该返回多少呢? 由于我们希望求得当前的最大和, 因此, 左子树就应该返回它的最大和, 但是, 不能统计两条路径, 而应该选择以左节点为根节点的左右子树的较大者, 因此, 应该返回的是: max(左节点左子树, 左节点右子树)+左节点的值, 因此, 返回的是: 7+11 = 18. 于是, 节点4对应的最大和就为: 18+13+4. 可以看到, 这实际上就是一个后序遍历的过程.

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:
int maxPathSum(TreeNode* root) {
int res=INT_MIN;
helper(root, res);
return res;
}

int helper(TreeNode* cur_node, int &res){
if(cur_node==nullptr) return 0;
int left = std::max(helper(cur_node->left, res), 0);
int right = std::max(helper(cur_node->right, res), 0);
res = std::max(res, left+right+cur_node->val);
return std::max(left, right)+cur_node->val;
}
};

解法二: 迭代

后序遍历的迭代实现

125 Valid Palindrome

判断是否为回文子串

Description

Given a string, determine if it is a palindrome, considering only alphanumeric characters and ignoring cases.

Note: For the purpose of this problem, we define empty string as valid palindrome.

Example 1:

Input: “A man, a plan, a canal: Panama”
Output: true
Example 2:

Input: “race a car”
Output: false

解法一: 前后两个指示变量, 向中间遍历判断

时间复杂度: $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
class Solution {
public:
bool isPalindrome(string s) {
for(int i=0, j=s.size()-1; i<j; ){
if(is_alphanumeric(s[i]) == false){
i++; continue;
}
if(is_alphanumeric(s[j]) == false){
j--; continue;
}
if(std::tolower(s[i]) != std::tolower(s[j])) return false;
i++; j--;

}
return true;
}

bool is_alphanumeric(char c){
if(c>='0' && c<='9') return true;
else if(c>='a' && c<='z') return true;
else if(c>='A' && c<='Z') return true;
else return false;
}
};

写法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
bool isPalindrome(string s) {
for(int i=0, j=s.size()-1; i<=j;i++,j-- ){
while(i<s.size() && is_alphanumeric(s[i]) == false) i++;
while(j>=0 && is_alphanumeric(s[j]) == false) j--;
if(std::tolower(s[i]) != std::tolower(s[j])) return false;

}
return true;
}

bool is_alphanumeric(char c){
if(c>='0' && c<='9') return true;
else if(c>='a' && c<='z') return true;
else if(c>='A' && c<='Z') return true;
else return false;
}
};

127. 单词接龙

实际上是图的BFS(广度优先搜索)

Description

Given two words (beginWord and endWord), and a dictionary’s word list, find the length of shortest transformation sequence from beginWord to endWord, such that:

Only one letter can be changed at a time.
Each transformed word must exist in the word list. Note that beginWord is not a transformed word.
Note:

Return 0 if there is no such transformation sequence.
All words have the same length.
All words contain only lowercase alphabetic characters.
You may assume no duplicates in the word list.
You may assume beginWord and endWord are non-empty and are not the same.
Example 1:

1
2
3
4
5
6
7
8
9
Input:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

Output: 5

Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
return its length 5.

Example 2:

1
2
3
4
5
6
7
8
Input:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]

Output: 0

Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.

解法一: BFS

时间复杂度: $O(nl)$, 其中, $l$ 为单词的长度, $n$ 是单词的数量, 因为广度优先遍历会对每个节点遍历一次, 而每个节点计算邻居时, 需要对 $l$ 个字母进行替换(替换26种, 常数级别), 另外, unordered_set 的 find 复杂度也为常数.
空间复杂度: $O(n)$ 需要额外借助队列进行广度优先遍历, 另外还使用了 unordered_set 来存储单词表

我们可以将此题看做是图的广度优先搜索, 首先, 以 beginWord 为图的起始节点, 然后, 那些所有与 beginWord 只有一个字母不相同的单词都可以看做是 beginWord 的邻居节点, 依次类推, 直到找到一个单词, 与 endWord 相同为止, 此时, 返回当前 endWord 与 beginWord 的距离. (距离的记录方式和二叉树层次遍历时的方式差不多, 都是利用当前队列中的元素大小来控制深度的).

需要注意的地方有以下几点:

  • 这里的图和树不太一样, 这里图没有链表指针来指示, 因此, 在每次将某一个单词入队列以后, 都需要在单词列表中删除掉这个单词(或者额外设置标记也行), 以防止重复搜索
  • 题目给的是没有重复单词的单词表, 因此推荐使用 set 结构来进行删除 (erase) 操作, vector 结构的删除 (erase) 操作的时间复杂度较高.
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:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
std::unordered_set<string> word_dict;
for(auto word : wordList)
word_dict.insert(word);
std::queue<string> to_visit;
//word_dict.erase(beginWord); //beginWord本来就不在字典中
to_visit.push(beginWord);
int dist = 1;
while(!to_visit.empty()){
int len = to_visit.size();
for(int i =0; i<len; i++){
string word = to_visit.front(); to_visit.pop();
if(word == endWord) return dist;
add_next_word(word, word_dict, to_visit);

}
dist++;
}
return 0;
}

void add_next_word(string &word, std::unordered_set<string> &word_dict, std::queue<string> &to_visit){
// word_dict.erase(word);
for(int i=0; i<word.size(); i++){
char letter = word[i];
for(int k=0; k<26; k++){
word[i] = 'a'+k;
if(word_dict.find(word) != word_dict.end()){
to_visit.push(word);
word_dict.erase(word);
}
}
word[i] = letter;
}
}
};

解法二: BFS+快速找到连接单词

算法中最重要的步骤是找出相邻的节点,也就是只差一个字母的两个单词。为了快速的找到这些相邻节点,我们对给定的 wordList 做一个预处理,将单词中的某个字母用*代替。

这个预处理帮我们构造了一个单词变换的通用状态。例如:Dog ----> D*g <---- DigDogDig 都指向了一个通用状态 D*g

用通用状态作为 key, 用对应的单词作为 val, 可以快速的帮助我们找到相邻节点.

例如,在广搜时我们需要访问 Dug 的所有邻接点,我们可以先生成 Dug 的所有通用状态:

1
2
3
Dug => *ug
Dug => D*g
Dug => Du*

第二个变换 D*g 可以同时映射到 Dog 或者 Dig,因为他们都有相同的通用状态。拥有相同的通用状态意味着两个单词只相差一个字母,他们的节点是相连的。

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
from collections import defaultdict
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
if (not wordList or endWord not in wordList):
return 0
n = len(beginWord) # 所有单词长度一样
pattern = defaultdict(list) # key: *og, val: dog, log, cog
for word in wordList: # 完成 dict 的构建
for i in range(n):
pattern[word[:i]+'*'+word[i+1:]].append(word)

q = [beginWord] # 初始化 BFS 队列
visited = dict()
level = 0
while q: # 循环直至队列为空
level += 1 # 层深
level_len = len(q)
for _ in range(level_len):
cur = q.pop(0)
for i in range(n): # 查找所有的相邻单词, 将其加入队列
intermediate = cur[:i] + '*' + cur[i+1:]
for w in pattern[intermediate]:
if w not in visited: # 确保当前单词没有被访问过
if w == endWord:
return level+1
visited[w] = 1 # 标记访问符号
q.append(w)
return 0

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
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
if (wordList.empty()) return 0;
int n = beginWord.size();
std::unordered_map<std::string, std::vector<std::string>> hash;
for (auto &word : wordList) {
for (int i = 0; i < n; i++) {
std::string intermediate = word.substr(0, i) + "*" + word.substr(i+1);
hash[intermediate].emplace_back(word);
}
}

int level = 0;
std::queue<string> q; q.push(beginWord);
std::unordered_set<string> visited;
while (!q.empty()) {
level++;
int level_len = q.size();
for (int j = 0; j < level_len; j++) {
auto cur = q.front(); q.pop();
for (int i = 0; i < n; i++) {
std::string intermediate = cur.substr(0, i) + "*" + cur.substr(i+1);
for (auto & w : hash[intermediate]) {
if (!visited.count(w)) {
if (w == endWord) return level+1;
visited.insert(w);
q.push(w);
}
}
}
}
}
return 0;
}
};

解法三: 双向广度优先搜索

根据给定字典构造的图可能会很大,而广度优先搜索的搜索空间大小依赖于每层节点的分支数量。假如每个节点的分支数量相同,搜索空间会随着层数的增长指数级的增加。考虑一个简单的二叉树,每一层都是满二叉树的扩展,节点的数量会以 2 为底数呈指数增长。

如果使用两个同时进行的广搜可以有效地减少搜索空间。一边从 beginWord 开始,另一边从 endWord 开始。我们每次从两边各扩展一个节点,当发现某一时刻两边都访问了某一顶点时就停止搜索。这就是双向广度优先搜索,它可以可观地减少搜索空间大小,从而降低时间和空间复杂度。

128. Longest Consecutive Sequence

返回无序数组中, 可以组成的最长的连续子串的长度

Description

Given an unsorted array of integers, find the length of the longest consecutive elements sequence.

Your algorithm should run in O(n) complexity.

Example:

Input: [100, 4, 200, 1, 3, 2]
Output: 4
Explanation: The longest consecutive elements sequence is [1, 2, 3, 4]. Therefore its length is 4.

解法一: 排序

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

先排序, 然后在从头往后遍历, 并用一个变量维护当前的最长连续序列的长度.

解法二: 利用哈希表

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

利用 unordered_set 将所有的数字存储起来, 然后遍历每一个数字 num, 查看这个数字是否为所在连续序列的开头(即查看 num-1 是否存在). 若 num 就是所在连续序列的开头, 则查看当前序列的长度, 并更新最大长度. 故而时间复杂度为 $O(n+n) = O(n)$. 同时, 因为使用了 unordered_set, 所以空间复杂度为 $O(n)$.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> sets(nums.begin(), nums.end());
int longest = 0;
for(auto num : sets){
if(sets.find(num-1) == sets.end()){
int cur_len = 1;
while(sets.find(num+1) !=sets.end()){
num++;
cur_len++;
}
if(longest < cur_len) longest = cur_len;
}
}
return longest;
}
};

解法三: 另一种哈希表用法

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

主题思想与解法二相同, 不过是从另一角度来使用 unordered_map, 首先, 依然利用 unordered_mapnums 存储起来, 然后遍历 nums, 对于 nums 中的每一个 num, 查看其是否存在于 unordered_map 中, 如果存在, 则分别向左向右查找当前数字 num 所在序列的最左端和最右端的数字, 同时, 将在 unordered_map 中遍历过的数字都移除(因为每个数字只可能唯一的属于一个连续序列). 之后, 利用最左端和最右端来更新最长连续序列的长度. 这样, 遍历的时间复杂度也为 $O(n+n) = O(n)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> sets(nums.begin(), nums.end());
int longest = 0;
for(auto num : nums){
if(sets.find(num)!=sets.end()){
sets.erase(num);
int pre = num-1, next = num+1;
while(sets.find(pre)!=sets.end())
sets.erase(pre--);
while(sets.find(next)!=sets.end())
sets.erase(next++);
if(longest < next-pre) longest = next-pre-1;
}
}
return longest;
}
};

130. Surrounded Regions

类似于围棋, 将被包裹住(4连通)的字符 O 全部转换成字符 X.

Descriptioin

Given a 2D board containing ‘X’ and ‘O’ (the letter O), capture all regions surrounded by ‘X’.

A region is captured by flipping all ‘O’s into ‘X’s in that surrounded region.

Example:

X X X X
X O O X
X X O X
X O X X
After running your function, the board should be:

X X X X
X X X X
X X X X
X O X X
Explanation:

Surrounded regions shouldn’t be on the border, which means that any ‘O’ on the border of the board are not flipped to ‘X’. Any ‘O’ that is not on the border and it is not connected to an ‘O’ on the border will be flipped to ‘X’. Two cells are connected if they are adjacent cells connected horizontally or vertically.

解法一: 递归

时间复杂度: $O(n)$, n 为 board 中的元素个数
空间复杂度: $O(n)$, 递归深度优先遍历的递归次数最坏情况下为 n 次.

根据题目的要求, 我们可以从 board 的四个边界开始, 每遇到一次 O 就执行深度优先遍历, 将其相邻的所有 O 都变成另一个字符(如 #). 然后, 在顺序遍历整个 board, 将 board 中所有的 O 变成 X, 将所有的 # 变成 O, 即得解.

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:
void solve(vector<vector<char>>& board) {
if(board.size()==0 || board[0].size()==0) return;
for(int i=0, j=0; j<board[i].size(); j++) //上边界
if(board[i][j]=='O') dfs_helper(i,j,board);
for(int i=1, j=board[i].size()-1; i<board.size()-1; i++) //右边界
if(board[i][j]=='O') dfs_helper(i,j,board);
for(int i=board.size()-1, j=0; j<board[i].size(); j++) //下边界
if(board[i][j]=='O') dfs_helper(i,j,board);
for(int i=1, j=0; i<board.size()-1; i++) //左边界
if(board[i][j]=='O') dfs_helper(i,j,board);

for(int i=0; i<board.size(); i++){
for(int j=0; j<board[i].size(); j++){
if(board[i][j]=='O') board[i][j]='X';
else if(board[i][j]=='#') board[i][j]='O';
}
}

}

void dfs_helper(int i, int j, vector<vector<char>> &board){
board[i][j]='#';
if(i>0 && board[i-1][j]=='O') dfs_helper(i-1, j, board);
if(j>0 && board[i][j-1]=='O') dfs_helper(i, j-1, board);
if(i<board.size()-1 && board[i+1][j]=='O') dfs_helper(i+1, j, board);
if(j<board[i].size()-1 && board[i][j+1]=='O') dfs_helper(i, j+1, board); //注意是 j<board[i].size()-1, 不是 board.size()-1
}
};

解法二: 迭代

时间复杂度: $O(n)$, n 为 board 中的元素个数
空间复杂度: $O(n)$, 额外申请队列的大小为 n

思想和解法一相同, 不过采用 BFS 迭代实现, 利用一个队列来实现

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:
void solve(vector<vector<char>>& board) {
if(board.size()==0 || board[0].size()==0) return;
for(int i=0, j=0; j<board[i].size(); j++) //上边界
if(board[i][j]=='O') bfs_helper(i,j,board);
for(int i=1, j=board[i].size()-1; i<board.size()-1; i++) //右边界
if(board[i][j]=='O') bfs_helper(i,j,board);
for(int i=board.size()-1, j=0; j<board[i].size(); j++) //下边界
if(board[i][j]=='O') bfs_helper(i,j,board);
for(int i=1, j=0; i<board.size()-1; i++) //左边界
if(board[i][j]=='O') bfs_helper(i,j,board);

for(int i=0; i<board.size(); i++){
for(int j=0; j<board[i].size(); j++){
if(board[i][j]=='O') board[i][j]='X';
else if(board[i][j]=='#') board[i][j]='O';
}
}

}

void bfs_helper(int i, int j, vector<vector<char>> &board){
std::queue<int> bfs_q;
int len = board[i].size();
bfs_q.push(i*len +j);
board[i][j]='#';
while(!bfs_q.empty()){
i = bfs_q.front()/len; j = bfs_q.front()%len; bfs_q.pop();
if(i>0 && board[i-1][j]=='O'){ board[i-1][j]='#';bfs_q.push( (i-1)*len+j); } //注意这里一定要更改了字符以后再存入队列, 负责可能引起字符重复入队列, 最终内存超限
if(j>0 && board[i][j-1]=='O') { board[i][j-1]='#'; bfs_q.push( i*len+j-1); }
if(i<board.size()-1 && board[i+1][j]=='O') { board[i+1][j]='#'; bfs_q.push( (i+1)*len + j );}
if(j<board[i].size()-1 && board[i][j+1]=='O') { board[i][j+1]='#'; bfs_q.push( i*len + j+1); }
}
}
};

131. Palindrome Partitioning

划分回文子串

Description

解法一: 回溯+验证回文子串

时间复杂度: $O(n\times 2^n)$, 其中, 可能的 partition 情况最多有 $2^n$ 种, 而对于每一种都要进行复杂度为 $O(n)$ 的回文子串检查
空间复杂度: $O(n\times 2^n)$ ? 数组 res 的大小最坏情况下可达 $(n\times 2^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
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> res;
vector<string> part_res;
dfs(s, 0, part_res, res);
return res;
}

void dfs(string s, int start, vector<string> &part_res, vector<vector<string>> &res){
if(start == s.size()){
res.push_back(part_res);
}
for(int i=start; i<s.size(); i++){
if(is_palin(start, i, s)){
part_res.push_back(s.substr(start, i-start+1));
dfs(s, i+1, part_res, res);
part_res.pop_back();

}
}
}

bool is_palin(int start, int end, string s){
while(start < end){
if(s[start]!=s[end]) return false;
start++;end--;
}
return true;
}
};

解法二: 回溯+DP

时间复杂度: $O(2^n)$, 利用 DP 建立一个 $n\times n$ 的 bool 数组, 其中 dp[i][j] 代表字符串从第 i 个字符开始, 到第 j 个字符组成的子串是否为回文串. 因此, 检查回文串时无需执行 $O(n)$ 的检查.
空间复杂度: $O(n\times 2^n + n^2)$, 需要额外的数组空间来实现 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
37
38
39
40
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> res;
vector<string> part_res;

vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
for(int j=0; j<s.size(); j++){
for(int i=0; i<=j; i++){ // 注意这两个for循环的顺序和控制条件, dp算法一定要保证在计算当前元素时, 之前的元素已经计算完成并且存入到了数组当中, 否则建立出的dp数组会出现漏解
if(s[i]==s[j] && (j-i<=2 || dp[i+1][j-1]==true))
dp[i][j]=true;
}
}

dfs(s, 0, part_res, res, dp);
return res;
}

void dfs(string s, int start, vector<string> &part_res, vector<vector<string>> &res, vector<vector<bool>> &dp ){
if(start == s.size()){
res.push_back(part_res);
}
for(int i=start; i<s.size(); i++){
if(dp[start][i]==true){
part_res.push_back(s.substr(start, i-start+1));
dfs(s, i+1, part_res, res, dp);
part_res.pop_back();

}
}
}

bool is_palin(int start, int end, string s){
while(start < end){
if(s[start]!=s[end]) return false;
start++;end--;
}
return true;
}
};

133. 克隆图

题目链接: https://leetcode-cn.com/problems/clone-graph/

解法一: DFS + 哈希

该题是典型的图遍历, 主要难点在于如何在遍历的时候将该节点的邻居副本加入到列表中, 由于要保证快速的找到相应的副本, 因此我们可以利用哈希表来实现, 其中, key为原始的node, 而val为对应的副本.

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
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> neighbors;

Node() {}

Node(int _val, vector<Node*> _neighbors) {
val = _val;
neighbors = _neighbors;
}
};
*/
class Solution {
Node* dfs(Node* node, unordered_map<Node*, Node*> & hash) { // hash: node, copy
if (node == nullptr) return node;
if (hash.count(node)) return hash[node]; // 如果副本已经存在, 则直接返回
Node* copy = new Node(node->val); // 若不存在, 则应该新建一个副本
hash[node] = copy;
for (int i = 0; i < node->neighbors.size(); i++) { // DFS, 递归遍历, 并将node中的邻居副本加入到copy中
copy->neighbors.emplace_back(dfs(node->neighbors[i], hash));
}
return copy;
}
public:

Node* cloneGraph(Node* node) {
unordered_map<Node*, Node*> hash; // key: node, val: copy
return dfs(node, hash);

}
};

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""
# Definition for a Node.
class Node:
def __init__(self, val, neighbors):
self.val = val
self.neighbors = neighbors
"""
class Solution:
def cloneGraph(self, node: 'Node') -> 'Node':
node_copy = dict()
def dfs(node):
if node is None: return node
if node not in node_copy:
node_copy[node] = Node(node.val, [])
node_copy[node].neighbors = list(map(dfs, node.neighbors))
return node_copy[node]
return dfs(node)

Python 实现2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"""
# Definition for a Node.
class Node:
def __init__(self, val, neighbors):
self.val = val
self.neighbors = neighbors
"""
class Solution:
def cloneGraph(self, node: 'Node') -> 'Node':
visited = dict()
def dfs(node):
for adj_node in node.neighbors:
if adj_node not in visited:
visited[adj_node] = Node(adj_node.val, [])
dfs(adj_node)
visited[node].neighbors.append(visited[adj_node])
visited[node] = Node(node.val, [])
dfs(node)
return visited[node]

134. Gas Station

加油站问题, 根据油量和消耗量判断是否能走完一圈

Description

There are N gas stations along a circular route, where the amount of gas at station i is gas[i].

You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from station i to its next station (i+1). You begin the journey with an empty tank at one of the gas stations.

Return the starting gas station’s index if you can travel around the circuit once in the clockwise direction, otherwise return -1.

Note:

If there exists a solution, it is guaranteed to be unique.
Both input arrays are non-empty and have the same length.
Each element in the input arrays is a non-negative integer.
Example 1:

Input:
gas = [1,2,3,4,5]
cost = [3,4,5,1,2]

Output: 3

Explanation:
Start at station 3 (index 3) and fill up with 4 unit of gas. Your tank = 0 + 4 = 4
Travel to station 4. Your tank = 4 - 1 + 5 = 8
Travel to station 0. Your tank = 8 - 2 + 1 = 7
Travel to station 1. Your tank = 7 - 3 + 2 = 6
Travel to station 2. Your tank = 6 - 4 + 3 = 5
Travel to station 3. The cost is 5. Your gas is just enough to travel back to station 3.
Therefore, return 3 as the starting index.
Example 2:

Input:
gas = [2,3,4]
cost = [3,4,3]

Output: -1

Explanation:
You can’t start at station 0 or 1, as there is not enough gas to travel to the next station.
Let’s start at station 2 and fill up with 4 unit of gas. Your tank = 0 + 4 = 4
Travel to station 0. Your tank = 4 - 3 + 2 = 3
Travel to station 1. Your tank = 3 - 3 + 3 = 3
You cannot travel back to station 2, as it requires 4 unit of gas but you only have 3.
Therefore, you can’t travel around the circuit once no matter where you start.

解法: 最优

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

首先要知道, 如果总油量大于总消耗量, 那么就一定存在一个起始点, 使得可以走完全程. 因此, 设置两个变量 total_leftcur_left, 前者存储从0点开始的总的剩余量, 后者存储从起点 start 开始的剩余量. 当 cur_left<=0 时, 说明从 start 开始一直到当前位置之间的任何一个加油站都不能够成为起点, 因此将 start 置为下一个位置, 重新开始, 并令 cur_left=0. 在遍历完所有加油站以后, 如果总的剩余量不小于0, 则此时 start 所指的位置就一定是解.(由题意知, 该解是唯一解).

只遍历一遍的原因: 虽然cur_left不是转一圈, 但是, 只要cur_left在遍历到最后时仍大于 0, 且total_left也大于 0, 说明start 一定 是解, 同时, 另外, 可以用反证法证明若 start 之后的某一位置构成解, 则start也一定可以构成解(后面开始的大于可以转一圈, start开始的肯定也可以转一圈)

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int total_left = 0;
int cur_left =0;
int start=0;
for(int i=0; i<gas.size(); i++){
total_left += gas[i]-cost[i];
cur_left += gas[i]-cost[i];
if(cur_left<0){
start = i+1;
cur_left=0;
}
}
return total_left < 0 ? -1:start;
}
};

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
start = 0
total_left = 0
cur_left = 0
for i, (g, c) in enumerate(zip(gas, cost)):
total_left += g - c
cur_left += g - c
if (cur_left < 0):
start = i+1
cur_left = 0
return -1 if total_left < 0 else start

136. Single Number

数组中有一个数字出现了1次(奇数次), 其他均出现了2次(偶数次), 找到出现1次(奇数次)的数字.

Description

Given a non-empty array of integers, every element appears twice except for one. Find that single one.

Note:

Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

Example 1:

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

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

解法一: 哈希

时间复杂度: $O(n)$, 一次遍历
空间复杂度: $O(n)$, 哈希表额外空间

遍历数组, 对于每一个数, 如果当前的数存在于hash表中, 则将表中哈希删除, 如果不存在, 则添加到哈希表中, 最终, 哈希表中存在的值就是只出现一次的值

解法二: 数学公式

将数组中的元素转换为 set(无重复元素), 然后利用上面的公式纠结
时间复杂度: $O(n + n)=O(n)$, 转换为 set 需要 $O(n), 公式求解遍历也需要 $O(n)$
空间复杂度: $O(n)$. set 所占额外空间

解法三: 异或

任何数和0异或不变, 和自身异或变为0

1
2
3
4
5
6
7
8
9
class Solution {
public:
int singleNumber(vector<int>& nums) {
int res=0;
for(auto num : nums)
res ^= num;
return res;
}
};

其他更多扩展问题可看剑指Offer第40题.

138. Copy List with Random Pointer

复杂链表的复制, 复制带有随机指针的链表

Description

A linked list is given such that each node contains an additional random pointer which could point to any node in the list or null.

Return a deep copy of the list.

解法一: 复制+拆分

时间复杂度: $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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;

Node() {}

Node(int _val, Node* _next, Node* _random) {
val = _val;
next = _next;
random = _random;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
if (head == nullptr) return nullptr;
Node* node = head;
Node* copyNode = nullptr;
while (node != nullptr) { // 复制节点
copyNode = new Node(node->val, node->next, node->random);
node->next = copyNode;
node = node->next->next;
}

node = head;
while (node != nullptr) { // 设值 random 的值
copyNode = node->next;
if (node->random != nullptr) {
copyNode->random = node->random->next;
}
node = node->next->next;
}

node = head;
Node* copyHead = head->next;
while (node != nullptr) { // 拆分两个链表
copyNode = node->next;
node->next = node->next->next;
if (copyNode->next != nullptr) {
copyNode->next = copyNode->next->next;
}
node = node->next; // 不要忘了让 node 指向下一个节点
}
return copyHead;
}
};

解法二: 一次遍历

时间复杂度: $O(n)$, 一次遍历
空间复杂度: $O(n)$, 需要申请链表长度的哈希表

利用一个哈希表来存储已经访问过的节点, 哈希表的键值为: {cur_node, copy_node}, 其中, cur_node 代表旧链表中的节点, copy_node 代表新链表中的节点. 顺序遍历旧链表, 对于旧链表中的每一个节点, 查看其 next 节点是否存在于哈希表 visit 中, 如果存在, 则将 copy_nodenext 指针指向该节点(键)对应的复制节点(值). 对于 random 指针也是同理

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:
RandomListNode *copyRandomList(RandomListNode *head) {
if(head==nullptr) return nullptr;
RandomListNode *cur_node = head;
RandomListNode *copy_node = new RandomListNode(head->label);
unordered_map<RandomListNode *, RandomListNode *> visit; // key: old_node, value: copy_node
visit.insert({cur_node, copy_node}); //注意不要少了花括号
while(cur_node!=nullptr){

RandomListNode *next_node=nullptr;
if(cur_node->next==nullptr) copy_node->next = nullptr;
else if(visit.find(cur_node->next)==visit.end()){
next_node = new RandomListNode(cur_node->next->label);
copy_node->next = next_node;
visit.insert({cur_node->next, next_node});
}else
copy_node->next = visit[cur_node->next];

RandomListNode *random_node=nullptr;
if(cur_node->random==nullptr) copy_node->random = nullptr;
else if(visit.find(cur_node->random) == visit.end()){
random_node = new RandomListNode(cur_node->random->label);
copy_node->random = random_node;
visit.insert({cur_node->random, random_node});
}else
copy_node->random = visit[cur_node->random];

cur_node = cur_node->next;
copy_node = copy_node->next;
}
return visit[head];
}
};

解法三: 递归

时间复杂度: $O(n)$
空间复杂度: $O(n)$, 除了哈希表所占空间外, 递归还需额外空间, 但是可以近似看做是 $O(n)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
unordered_map<RandomListNode *, RandomListNode *> visit;
public:
RandomListNode *copyRandomList(RandomListNode *head) {
if(head==nullptr) return nullptr;
if(visit.find(head)!=visit.end())
return visit[head];
RandomListNode *node = new RandomListNode(head->label);

visit.insert({head, node});
node->next = copyRandomList(head->next);
node->random = copyRandomList(head->random);
return node;
}
};

139. Word Break

判断字符串是否可以划分成字典里面的单词

Description

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, determine if s can be segmented into a space-separated sequence of one or more dictionary words.

Note:

The same word in the dictionary may be reused multiple times in the segmentation.
You may assume the dictionary does not contain duplicate words.
Example 1:

Input: s = “leetcode”, wordDict = [“leet”, “code”]
Output: true
Explanation: Return true because “leetcode” can be segmented as “leet code”.
Example 2:

Input: s = “applepenapple”, wordDict = [“apple”, “pen”]
Output: true
Explanation: Return true because “applepenapple” can be segmented as “apple pen apple”.
Note that you are allowed to reuse a dictionary word.
Example 3:

Input: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
Output: false

解法一: 回溯

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

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 wordBreak(string s, vector<string>& wordDict) {
// 纯回溯实现, 复杂度很高, 很容易超时
unordered_set<string> word_dict(wordDict.begin(), wordDict.end());
return helper(s,-1,word_dict);
}

bool helper(string &s, int seg, unordered_set<string> &word_dict){
if(seg==s.size()-1)
return true;
string temp="";
for(int i=seg+1; i<s.size(); i++){
temp+=s[i];
if(word_dict.find(temp) != word_dict.end() && helper(s, i, word_dict)==true){
return true;
}
}
return false;
}
};

解法二: DP

时间复杂度: $O(n^2)$, $n$ 为字符串的长度
空间复杂度: $O(n)$, dp 数组额外空间, unordered_set 额外空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> word_dict(wordDict.begin(), wordDict.end());
if(wordDict.size()==0) return false;
vector<bool> dp(s.size(), false);
for(int i=0; i<s.size(); i++){
for(int j=i; j>=0; j--){
if(j-1<0 || dp[j-1]==true){
string temp = s.substr(j, i-j+1);
if(word_dict.find(temp) != word_dict.end()){
dp[i]=true;
break; // break to next i
}
}
}
}
return dp.back();
}
};

解法三: DP

时间复杂度: $O(nm)$, $n$ 为字符串的长度, $m$ 为字典的 size
空间复杂度: $O(n)$, dp 数组额外空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> word_dict(wordDict.begin(), wordDict.end());
if(wordDict.size()==0) return false;
vector<bool> dp(s.size(), false);
for(int i=0; i<s.size(); i++){
for(int j=0; j<wordDict.size(); j++){
if(i>=wordDict[j].size()-1){
int len = wordDict[j].size();
string temp= s.substr(i-len+1, len);
if(temp == wordDict[j] && ((i-len)<0 || dp[i-len]==true))// 这里注意, .size() 返回的类型并不是int, 如果使用i-wordDict[j].size() <0, 就会造成runtime error, 正确做法是进行强制的类型转换, 或者用一个int变量代表之.
dp[i]=true;
}
}
}
return dp.back();
}
};

更简洁的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> dp(s.size(), false);
for (int i = 0; i < s.size(); i++) {
for (auto const& word : wordDict) {
int lenW = word.size();
if (!dp[i] and i+1 >= lenW and word == s.substr(i-lenW+1, lenW)) {
dp[i] = (i-lenW+1 == 0) ? true : dp[i-lenW];
}
}
}
return dp[s.size() - 1];
}
};

140. Word Break II

Description

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, add spaces in s to construct a sentence where each word is a valid dictionary word. Return all such possible sentences.

Note:

The same word in the dictionary may be reused multiple times in the segmentation.
You may assume the dictionary does not contain duplicate words.
Example 1:

Input:
s = “catsanddog”
wordDict = [“cat”, “cats”, “and”, “sand”, “dog”]
Output:
[
“cats and dog”,
“cat sand dog”
]
Example 2:

Input:
s = “pineapplepenapple”
wordDict = [“apple”, “pen”, “applepen”, “pine”, “pineapple”]
Output:
[
“pine apple pen apple”,
“pineapple pen apple”,
“pine applepen apple”
]
Explanation: Note that you are allowed to reuse a dictionary word.
Example 3:

Input:
s = “catsandog”
wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
Output:
[]

解法一: DP

直接使用回溯法, 有大量重复计算, 导致时间超时, 无法通过 OJ, 因此考虑 DP 思想. 将中间的计算结果缓存起来, 再次遇到的时候无需重复计算, 只需直接使用即可.
利用一个哈希表将每个字符串与该字符串能拆分出的句子联系起来, 其中, key 为字符串, value 为字符串拆分后的句子. 假设我们已经求出一个字符串的解为 res, 并将其存入到哈希表中, 此时, 如果在该字符串的前面再加上一个单词(单词表的中任意一个), 那么新的解就应该为: word+" "+res[i]. 代码实现如下.

注意, 这里我们要对 wordDict 进行遍历来查找可以拆分的情况, 如果是对字符串 s 查找可拆分情况, 那么哈希表中的键将会大幅增加, 例如对于"aaaaaaaaaaa"这种情况.

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:
vector<string> wordBreak(string s, vector<string>& wordDict) {
unordered_map<string, vector<string>> hash_dict;
return DP_helper(s, wordDict, hash_dict);
}

vector<string> DP_helper(string s, vector<string> &wordDict, unordered_map<string, vector<string>> &hash_dict){
if(hash_dict.find(s)!=hash_dict.end()) return hash_dict[s];
if(s=="") return {""}; //这里必须返回具有一个元素("")的vector, 否则下面的push_back语句不会执行
vector<string> res;
for(auto word : wordDict){
if(s.substr(0, word.size()) != word) continue;
vector<string> res_word = DP_helper(s.substr(word.size()), wordDict, hash_dict); //s.substr(word.size()) 代表截取剩余的字符, 所以有可能出现空字符的情况
for(auto str : res_word){ // 如果返回的是空的vector, 则不会执行该语句, 因此, 不能返回空vector, 当遇到空字符串时, 因该返回 {""}, 即只有一个元素的vector, 该元素为"".
res.push_back(word + (str==""? "":" ") + str); //这里根据 str的值来决定是否加空格, 如果str为空, 说明是word是最后一个字符, 则其后不应该添加空格
}
}
hash_dict[s] = res;
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
class Solution {
public:
vector<string> wordBreak(string s, vector<string>& wordDict) {
unordered_map<string, vector<string>> hash;

for (int i = 0; i < s.size(); i++) {
vector<string> res;
for (auto const& word : wordDict) {
int lenW = word.size();
if (i+1 >= lenW and s.substr(i-lenW+1, lenW) == word) {
if (i+1 == lenW) {
res.push_back(word);
} else {
string tmp_s = s.substr(0, i-lenW+1);
if (hash.find(tmp_s) != hash.end()) {
auto tmp_words = hash[tmp_s];
for (auto str : tmp_words) {
res.push_back(str + " " + word);
}
}
}
}
}
hash[s.substr(0, i+1)] = res;
}
if (hash.find(s) != hash.end()) return hash[s];
return {""};
}
};

141. Linked List Cycle

Description

Given a linked list, determine if it has a cycle in it.

Follow up:
Can you solve it without using extra space?

解法一: Floyd Cycle(Floyd 判圈算法)

时间复杂度: $O(n+k)$, 可以认为是$O(n)$, $n$ 为链表长度, $k$ 为环长
空间复杂度: $O(1)$

从头结点开始, slow每次走一步, fast每次走两步, 那么只要有环, slow和fast就一定会在环中的某个节点处相遇, 如果无环, 则fast一定先到达空指针

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
bool hasCycle(ListNode *head) {
if (head == nullptr or head->next == nullptr) return false;
ListNode* fast = head;
ListNode* slow = head;
do {
slow = slow->next;
fast = fast->next->next;
} while (slow != fast and fast!=nullptr and fast->next != nullptr);
return slow == fast ? 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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head==nullptr) return false;
ListNode* slow=head, *fast=head->next;
while(fast!=nullptr && slow != fast){
slow= slow->next;
if(fast->next == nullptr) return false;
fast = fast->next->next;
}
if(fast==nullptr) return false;
return true;

}
};

更多扩展见牛客第55题, 链表中环的入口节点

142. Linked List Cycle II

Description: 求链表中环的开始节点

Given a linked list, return the node where the cycle begins. If there is no cycle, return null.

Note: Do not modify the linked list.

Follow up:
Can you solve it without using extra space?

解法一: Floyd 的乌龟和兔子(Floyd 判环算法)

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

此题更多解析可以看剑指offer第55题

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 singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(head==nullptr) return nullptr;
ListNode *slow = head;
ListNode *fast = head;
do{
slow = slow->next;
fast = fast->next;
if(fast==nullptr) return fast;// 不存在环
fast = fast->next;
if(fast==nullptr) return fast;// 不存在环
}while(slow!=fast);

fast = slow;
slow = head;
while(fast!=slow){
slow = slow->next;
fast = fast->next;
}
return slow;
}
};

144. 二叉树先序遍历

Description: 先根遍历

Given a binary tree, return the preorder traversal of its nodes’ values.

Example:

1
2
3
4
5
6
Input: [1,null,2,3]
1
\
2
/
3

Output: [1,2,3]
Follow up: Recursive solution is trivial, could you do it iteratively?

解法一: 递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
std::vector<int> res;
preorder(root, res);
return res;
}

void preorder(TreeNode* root, vector<int>& res) {
if (root == nullptr) return;
res.push_back(root->val);
preorder(root->left, res);
preorder(root->right, res);
return;
}
};

解法二: 迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
std::vector<int> res;
std::stack<TreeNode*> s;
while (!s.empty() or root != nullptr) {
if (root != nullptr) {
res.push_back(root->val);
s.push(root);
root = root->left;
} else {
root = s.top();
s.pop();
root = root->right;
}
}
return res;
}
};

145. 二叉树的后序遍历

Description

题目链接: https://leetcode-cn.com/problems/binary-tree-postorder-traversal/

Given a binary tree, return the postorder traversal of its nodes’ values.

Example:

1
2
3
4
5
6
Input: [1,null,2,3]
1
\
2
/
3

Output: [3,2,1]
Follow up: Recursive solution is trivial, could you do it iteratively?

解法一: 递归

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:
vector<int> postorderTraversal(TreeNode* root) {
std::vector<int> res;
postorder(root, res);
return res;
}

void postorder(TreeNode* root, std::vector<int>& res) {
if (root == nullptr) return;
postorder(root->left, res);
postorder(root->right, res);
res.push_back(root->val);
return;
}
};

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
def post_order(root, res):
if (root.left is not None): post_order(root.left, res)
if (root.right is not None): post_order(root.right, res)
res.append(root.val)
return res
res = []
if root is None: return res
post_order(root, res)
return res

解法二: 迭代

用一个变量 pre 来维护上一个输出的节点, 当上一个输出的节点是当前节点的右孩子的时候, 说明左右都遍历完了.

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
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
std::vector<int> res;
std::stack<TreeNode*> s;
TreeNode* pre = nullptr;
while (!s.empty() or root != nullptr) {
while (root != nullptr) {
s.push(root);
root = root->left;
}
if (!s.empty()) {
auto node = s.top(); // 注意这里要用 node, 因为要将有可能进入 else, 此时没有对 root 赋新值, 所以使用 root 的话会陷入死循环
if (node->right != nullptr and pre != node->right) {
root = node->right;
} else {
res.emplace_back(node->val);
pre = node;
s.pop();
}
}
}
return res;
}
};

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 postorderTraversal(self, root: TreeNode) -> List[int]:
res = []
stack = []
pre = None
while(root or stack): # root 非空, 或者, 栈非空
while(root): # 左儿子一直入栈
stack.append(root)
root = root.left
node = stack[-1] # 取栈尾, 注意此时不一定访问栈尾
if node.right is None or node.right == pre: # 只有当右儿子为空或者右儿子已经被访问过时, 才能访问当前节点
res.append(node.val)
pre = stack.pop() # 标记访问的节点, 以便进行右儿子的判断
else:
root = node.right # 继续循环入栈
return res

146. LRU Cache

实现一个 LRU 缓存器, 即 Least Recently Used (最近最少使用).

Description

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。 如果密钥已经存在, 则应该 更新 其数值

进阶:

你是否可以在 O(1) 时间复杂度内完成这两种操作?

Follow up:
Could you do both operations in O(1) time complexity?

Example:

LRUCache cache = new LRUCache( 2 ); // 2 is capacity

cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // returns 1
cache.put(3, 3); // evicts key 2
cache.get(2); // returns -1 (not found)
cache.put(4, 4); // evicts key 1
cache.get(1); // returns -1 (not found)
cache.get(3); // returns 3
cache.get(4); // returns 4

解法一: 利用哈希表和双端链表

时间复杂度: $O(n)$, getput均为 $O(n)$

空间复杂度: $O(n)$, 哈希表和双端链表

利用哈希表(unordered_map)来存储键值对, 用于实现 $O(1)$ 复杂度的查找和返回特定键对应的值.
利用双端链表(list)来维护LRU逻辑, 即每次访问(get)时, 如果键存在, 那么在返回之前, 还应当将list中的键移到最顶端(最后), 首先, 顺序遍历找到该键($O(n)$复杂度), 然后将其删除($O(1)$复杂度), 接着, 将其插入到最后一位上($O(1)$复杂度). 对于插入(put)的情况, 首先判断是否已经存在($O(1)$复杂度), 如果已经存在, 那么将其value值更新并将其移动至最顶端($O(n)$复杂度). 否则, 判断当前是否溢出, 如果溢出, 则将list中的首部key值删除, 并将对应的hash键值对也删除($O(1)$复杂度), 然后执行插入逻辑($O(1)$复杂度). 如果没有溢出, 则直接插入.

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
class LRUCache {
private:
int L_capacity;
std::unordered_map<int, int> kv_map;
std::list<int> key_l;
public:
LRUCache(int capacity) {
L_capacity = capacity;
}

int get(int key) {
if(kv_map.find(key) != kv_map.end()){// 访问了key, 将其移到最顶端
for(auto it=key_l.begin(); it!=key_l.end(); it++){
if(*it == key){
key_l.erase(it);
break;
}
}
key_l.push_back(key); // 访问了key, 将其移到最顶端
return kv_map[key];
}
return -1;

}

void put(int key, int value) {
if(kv_map.find(key) != kv_map.end()){// 访问了key, 将其移到最顶端
for(auto it=key_l.begin(); it!=key_l.end(); it++){
if(*it == key){
key_l.erase(it);
break;
}
}
key_l.push_back(key);// 访问了key, 将其移到最顶端
kv_map[key]=value; //更新value值, 因为有可能同样的key对应的value不同
}else if(key_l.size() == L_capacity){
int evict_key = key_l.front(); key_l.pop_front(); // 删除最少访问的key
kv_map.erase(evict_key); // 删除最少访问的key

key_l.push_back(key);
kv_map.insert({key, value});
}else{
key_l.push_back(key);
kv_map.insert({key, value});
}
}
};

/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/

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
class LRUCache:

def __init__(self, capacity: int):
self.capacity = capacity
self.cache = []
self.kv_dict = {}

def get(self, key: int) -> int:
if key in self.kv_dict: # get 时, 如果存在, 则先将 key 更新成最近访问, 然后返回对应value
self.cache.remove(key)
self.cache.insert(0, key)
return self.kv_dict[key]
else:
return -1

def put(self, key: int, value: int) -> None:
if key in self.kv_dict: # key 已存在, 此时容量不会爆, 只需更新最近访问值即可
self.kv_dict[key] = value
self.cache.remove(key)
self.cache.insert(0, key)
elif len(self.cache) >= int(self.capacity): # 容量要爆, 需要先删除最后的不常访问元素, 然后添加新元素
old_key = self.cache.pop()
del self.kv_dict[old_key]
self.cache.insert(0, key)
self.kv_dict[key] = value
else: # 容量不爆且原来没有, 则直接加入即可
self.cache.insert(0, key)
self.kv_dict[key] = value

# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

Follow Up

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

上面的解法一的 $O(n)$ 复杂度主要是在查找满足键的迭代器上面, 而对于list来说, 有一个非常重要的性质, 那就是list的元素迭代器在list被修改后 仍然保持不变, 永远不会失效(永远删除该节点), 因此, 我们可以做一个小小的改动, 就是让哈希表中存储的不再是value, 而是直接对应list中的迭代器, 这样, 就可以直接访问迭代器进行元素的移除操作.

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
class LRUCache {
int capacity;
std::list<std::pair<int, int>> linkList;
std::unordered_map<int, std::list<std::pair<int, int>>::iterator> hash;
public:
LRUCache(int capacity) {
this->capacity = capacity;
}

int get(int key) {
if (hash.find(key) != hash.end()) {
auto it = hash[key];
auto keyValue = *it;
linkList.erase(it);
linkList.push_front(keyValue);
hash[key] = linkList.begin();
return (*hash[key]).second;
} else {
return -1;
}
}

void put(int key, int value) {
if (linkList.size() < capacity) {
if (hash.find(key) != hash.end()) {
linkList.erase(hash[key]);
}
linkList.push_front(std::make_pair(key, value));
hash[key] = linkList.begin();
} else {
if (hash.find(key) != hash.end()) { // 如果已经存在, 则将其移动到list最前
linkList.erase(hash[key]);
} else {
auto keyValue = linkList.back();
hash.erase(keyValue.first);
linkList.pop_back();
}
linkList.push_front(std::make_pair(key, value));
hash[key] = linkList.begin();
}
}
};

/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/

Python 实现

有两种方法, 一种是使用普通的字典和双端链表实现, 另一种是使用OrderedDict

使用OrderedDict

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

def __init__(self, capacity: int):
self.capacity = capacity
self.ordered_dict = collections.OrderedDict()

def get(self, key: int) -> int:
if key in self.ordered_dict:
self.ordered_dict.move_to_end(key)
return self.ordered_dict[key]
else:
return -1

def put(self, key: int, value: int) -> None:
self.ordered_dict[key] = value
self.ordered_dict.move_to_end(key)
if len(self.ordered_dict) > self.capacity:
self.ordered_dict.popitem(last=False) # False 弹出最不常用的

# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

使用普通字典 + 双端链表:

  • get(key): 判断 key 是否存在于字典中, 若不存在, 返回-1, 若存在, 返回 value, 同时将 key 对应的节点移动到双端链表的尾部, 取出再放入, 复杂度 $O(1)$
  • put(key, value): 判断 key 是否已经存在, 若存在, 则更新其 value, 同时将 key 对应节点移动至双端链表尾部. 若不存在, 判断是否超过容量上限, 若超过, 则将双端链表头部弹出, 然后在尾部新添加元素.

作者:liye-3
链接:https://leetcode-cn.com/problems/two-sum/solution/shu-ju-jie-gou-fen-xi-python-ha-xi-shuang-xiang-li/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ListNode:
def __init__(self, key=None, value=None):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.hashmap = {}
# 新建两个节点 head 和 tail
self.head = ListNode()
self.tail = ListNode()
# 初始化链表为 head <-> tail
self.head.next = self.tail
self.tail.prev = self.head

# 因为get与put操作都可能需要将双向链表中的某个节点移到末尾, 所以定义一个方法
def move_node_to_tail(self, key):
# 先将哈希表key指向的节点拎出来, 为了简洁起名node
# hashmap[key] hashmap[key]
# | |
# V --> V
# prev <-> node <-> next pre <-> next ... node
node = self.hashmap[key]
node.prev.next = node.next
node.next.prev = node.prev
# 之后将node插入到尾节点前
# hashmap[key] hashmap[key]
# | |
# V --> V
# prev <-> tail ... node prev <-> node <-> tail
node.prev = self.tail.prev
node.next = self.tail
self.tail.prev.next = node
self.tail.prev = node

def get(self, key: int) -> int:
if key in self.hashmap:
# 如果已经在链表中了久把它移到末尾(变成最新访问的)
self.move_node_to_tail(key)
res = self.hashmap.get(key, -1)
if res == -1:
return res
else:
return res.value

def put(self, key: int, value: int) -> None:
if key in self.hashmap:
# 如果key本身已经在哈希表中了就不需要在链表中加入新的节点
# 但是需要更新字典该值对应节点的value
self.hashmap[key].value = value
# 之后将该节点移到末尾
self.move_node_to_tail(key)
else:
if len(self.hashmap) == self.capacity:
# 去掉哈希表对应项
self.hashmap.pop(self.head.next.key)
# 去掉最久没有被访问过的节点, 即头节点之后的节点
self.head.next = self.head.next.next
self.head.next.prev = self.head
# 如果不在的话就插入到尾节点前
new = ListNode(key, value)
self.hashmap[key] = new
new.prev = self.tail.prev
new.next = self.tail
self.tail.prev.next = new
self.tail.prev = new

148. Sort List

对链表进行排序, 要求时间复杂度为 $O(nlogn)$, 空间复杂度为常数

Description

Sort a linked list in O(n log n) time using constant space complexity.

Example 1:

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

Example 2:

1
2
Input: -1->5->3->4->0
Output: -1->0->3->4->5

解法一: 递归 自顶向下

时间复杂度: $O(nlogn)$
空间复杂度: $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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def sortList(self, head: ListNode) -> ListNode:

def split_list(l1, blocksize):
while l1 != None and blocksize > 1:
l1 = l1.next
blocksize -= 1
if l1 == None:
return l1
l2 = l1.next
l1.next = None
return l2

def merge_list(l1, l2, dummy):
cur = dummy
while l1 and l2:
if l1.val < l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next

while l1:
cur.next = l1
l1 = l1.next
cur = cur.next
while l2:
cur.next = l2
l2 = l2.next
cur = cur.next
return cur

cur = ListNode(0)
cur.next = head
length = 0
while cur.next:
length += 1
cur = cur.next

blocksize = 1
dummy = ListNode(0)
dummy.next = head
while blocksize < length:
cur_dummy = dummy
curHead = dummy.next
while curHead != None:
l1 = curHead
l2 = split_list(curHead, blocksize)
curHead = split_list(l2, blocksize)
cur_dummy = merge_list(l1, l2, cur_dummy)
blocksize *= 2
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
41
42
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(head==nullptr || head->next==nullptr) return head; //链表中至少应有两个元素, 否则不能进行融合, 会产生运行时错误
ListNode *slow=head, *fast=head, *pre=head; // 两指针, 找到最中间的元素, 用slow指向
while(fast!=nullptr && fast->next!=nullptr){
pre = slow;
slow = slow->next;
fast = fast->next->next;
}
pre->next = nullptr; // 将前后两个链断开
ListNode* sort1 = sortList(head); // 将前一半排序
ListNode* sort2 = sortList(slow); // 将后一半排序
return merge_sort(sort1, sort2); // 融合两个有序链表
}

ListNode* merge_sort(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;
l1 = l1->next;
}else{
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
if(l1!=nullptr) cur->next = l1;
if(l2!=nullptr) cur->next = l2; // 将最后的一个非空元素加入排序链表
return dummy->next;
}
};

解法二: 迭代 自底向上

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

先两两合并, 再四四合并, 逐渐向上, 直到完全合并. 注意这里之所以可以在 $O(1)$ 的空间复杂度内进行归并排序, 是因为采用了链表的底层结构, 使得 merge 操作可以在 $O(1)$ 的空间复杂度下进行. 但是对于一般的归并排序, 采用的是数组结构, 数组结构在进行 merge 时, 要么在 $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
43
44
45
46
47
48
49
50
51
52
53
54
class Solution {
private:
ListNode* splitList(ListNode* l1, int blockSize) {
while (blockSize > 1 and l1 != nullptr) {
l1 = l1->next;
blockSize--;
} // 找到 l1 的尾部
if (l1 == nullptr) return l1;
ListNode* l2 = l1->next; // l1 尾部的下一个就是 l2 的头部
l1->next = nullptr; // split l1 and l2
return l2;
}

ListNode* mergeList(ListNode* l1, ListNode* l2, ListNode* dummy) {
ListNode* cur = dummy;
while (l1 != nullptr and l2 != nullptr) {
if (l1->val <= l2->val) {
cur->next = l1;
l1 = l1->next;
} else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
cur->next = (l1 != nullptr) ? l1 : l2;
while (cur->next != nullptr) cur = cur->next;
return cur; // 该节点是下一段链表的 dummy 节点
}

public:
ListNode* sortList(ListNode* head) {
ListNode* node = head;
int length = 0;
while (node != nullptr) {
length++;
node = node->next;
}
ListNode* dummy = new ListNode(0);
dummy->next = head;
for (int blockSize = 1; blockSize < length ; blockSize <<= 1) {
ListNode* curDummy = dummy;
ListNode* curHead = dummy->next;
while (curHead != nullptr) {
ListNode* l1 = curHead;
ListNode* l2 = splitList(l1, blockSize);
curHead = splitList(l2, blockSize); // 获取下一段链表的头节点, 并将l2的尾部置为nullptr
curDummy = mergeList(l1, l2, curDummy); // 合并, 并获取当前段的最后一个非空节点
}
}
return dummy->next;

}
};

149. 直线上最多的点数

Max Points on a Line

Description 最大的共线点个数

给定一个二维平面,平面上有 n 个点,求最多有多少个点在同一条直线上。

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

1
2
3
4
5
6
7
^
|
| o
| o
| o
+------------->
0 1 2 3 4

Example 2:
Input: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
Output: 4
Explanation:

1
2
3
4
5
6
7
8
^
|
| o
| o o
| o
| o o
+------------------->
0 1 2 3 4 5 6

解法一: 哈希表

时间复杂度: $O(n^2)$, 求取任意两点间的斜率
空间复杂度: $O(n)$, 哈希表, 存储斜率

由于要求共线点个数, 就必须获取任意两点间的斜率, 因此, 时间复杂度最少为 $O(n^2)$. 算法流程如下:

  • 对于每一个点来说, 构造一个哈希表, 表中的键为斜率, 表中的值为对应斜率的点的个数, 这里注意, 当我们求完第i个点与第j个点之间的斜率之后, 就不用再求第j个点与第i个点之间的斜率情况了(即令int j = i+1, 而不是int j = 0)
  • 对于重点的情况, 需要单独设置一个变量来记录, 之后将该重复次数加入到该点所在的每条直线上(因为重点也算是共线)
  • 对于斜率不存在的情况, 可以考虑利用INT_MAX来作为键值
  • 精度: 在求取斜率时, 会进行除法, 而在计算机内部, 除法在精度上始终会有一定误差, 会造成斜率相同的两对点在计算成浮点数以后斜率不同, 因此, 要 避免使用除法, 解决办法是利用 最大公约数, 求取y2-y1x2-x1之间的最大公约数, 然后对进行约分, 用约分后的值作为键来存储, 就不会造成精度上的损失, 但是, 此时需要用pair作为键, 故不能用unordered_map(C++没有为pair类型提供对应的哈希函数), 而只能用map(键只有重载了<>就可以使用map, 搜索的时间复杂度为 $O(logn)$), 另一种可选做法是利用string类型, 将两个int数值转换成string后再拼接, 此时就可以使用unordered_map了(搜索的时间复杂度为 $O(1)$, 但是intstring的类型转换也需要消耗时间).
  • 当采用公约数以后, 因为没有了除法, 因此可以不用特殊处理斜率不存在的情况, 代码更加简洁.

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 maxPoints(self, points: List[List[int]]) -> int:
def gcd(a, b): # 求最大公约数
return a if b == 0 else gcd(b, a%b)

if len(points) == 0: return 0
n = len(points)
max_num = 0
for i in range(n):
d = dict()
duplicate = 1
for j in range(i+1, n): # 求任意节点之间的斜率, 根据斜率统计共线点的个数
if points[i][0] == points[j][0] and points[i][1] == points[j][1]:
duplicate += 1
else:
a = points[i][0] - points[j][0]
b = points[i][1] - points[j][1]
gcd_num = gcd(a, b)
d.setdefault(str(a//gcd_num)+str(b//gcd_num), 0)
d[str(a//gcd_num)+str(b//gcd_num)] += 1
max_num = max(max_num, duplicate)
for k, v in d.items():
max_num = max(max_num, v+duplicate)
return max_num

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
C/**
* Definition for a point.
* struct Point {
* int x;
* int y;
* Point() : x(0), y(0) {}
* Point(int a, int b) : x(a), y(b) {}
* };
*/
class Solution {
public:
int maxPoints(vector<Point>& points) {

int res=0;

for(int i=0; i<points.size(); i++){

int duplicate = 1;
map<pair<int,int>, int> lines_hash; //这里用map的原因是因为unordered_map的键的类型只能是基本类型, 不能是pair
for(int j=i+1; j<points.size(); j++){
if(points[i].x==points[j].x && points[i].y==points[j].y){
duplicate++;
}else{
int a = points[j].y-points[i].y;
int b = points[j].x-points[i].x;
int d = gcd(a, b);
lines_hash[{a/d, b/d}]++;
}
}
res = max(res, duplicate); // 如果points里面只有一个点, 则哈希表中不会有键值, 因此需要先处理只有一个点的情况
for(auto line : lines_hash){
res = max(res, duplicate+line.second);
}
}
return res;
}
int gcd(int a, int b){ // 求a与b的最大公约数
return (b==0) ? a : gcd(b, a%b);
}
};

用 string 做键, 使用哈希表而不是map:

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:
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a%b);
}
int maxPoints(vector<Point>& points) {
int n = points.size();
int res = 0;
for (int i = 0; i < n; i++) {
std::unordered_map<std::string, int> line_hash;

int duplicate = 1;
for (int j = i + 1; j < n; j++) {
if (points[i].x == points[j].x and points[i].y == points[j].y) {
duplicate++;
continue;
}
int a = points[j].y - points[i].y;
int b = points[j].x - points[i].x;
int d = gcd(a, b);
std::string slope = std::to_string(a/d) + std::to_string(b/d);
line_hash[slope]++;
}
res = std::max(res, duplicate);
for (auto it : line_hash) {
res = std::max(res, duplicate + it.second);
}
}
return res;
}
};

150. 逆波兰表达式求值

计算逆波兰表达式

Description

根据逆波兰表示法,求表达式的值。

有效的运算符包括+, -, *, /。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

说明:

整数除法只保留整数部分。
给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Example 1:
Input: ["2", "1", "+", "3", "*"]
Output: 9
Explanation: ((2 + 1) * 3) = 9
Example 2:

Input: ["4", "13", "5", "/", "+"]
Output: 6
Explanation: (4 + (13 / 5)) = 6
Example 3:

Input: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]
Output: 22
Explanation:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

解法一: 栈

时间复杂度: $O(n)$, 一次遍历
空间复杂度: $O(n)$, 需要一个额外的栈来存储中间结果

用栈来实现, 从到开始扫描字符串vector, 如果当前字符串不为运算符, 则直接入栈, 如果为运算符 , 则取栈顶两个元素进行运算然后将计算结果入栈. 最终, 栈中只剩一个结果值

需要注意的是: 首先要确保输入的逆波兰表达式是没有问题的, 其次还有要进行零除判断, 这几点本题没有考查, 但仍需注意

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 evalRPN(vector<string>& tokens) {
stack<int> polish;
for(auto token : tokens){
int a,b,c;
if(token.back()=='+' || token.back()=='-' || token.back()=='*' || token.back()=='/'){ // 用back的原因是数字有可能是 -13 这种形式
b = polish.top(); polish.pop();
a = polish.top(); polish.pop();
}
switch(token.back()){
case '+': c=a+b; break;
case '-': c=a-b; break;
case '*': c=a*b; break;
case '/': c= (b==0) ? 0 : a/b; break;
default: c = c=std::stoi(token);
}
polish.push(c);
}
return polish.top();

}
};

解法二: 栈+异常

解法与上面相同, 不同借助了异常, 显得更加简洁

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 {
public:
int evalRPN(vector<string> &tokens) {
stack<int> rpn;

for(int i =0; i<tokens.size(); i++){
try{
rpn.push(stoi(tokens[i]));
}
catch (exception e){
int num1 = rpn.top(); rpn.pop();
int num2 = rpn.top(); rpn.pop();
switch(tokens[i][0]){
case '+': rpn.push(num2+num1);break;
case '-': rpn.push(num2-num1);break;
case '*': rpn.push(num2*num1);break;
case '/': rpn.push(num2/num1);break;
}
}
}

if(rpn.size()==1)
return rpn.top();
else
return 0;

}
};

解法三: 栈+lambda

思路与解法一一直, 另一种写法: 借助哈希表和lambda表达式, 使程序更加整洁

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 evalRPN(vector<string>& tokens) {
unordered_map<string, function<int(int, int)>> op_map={
{"+", [](int a, int b){return a+b;}}, //注意要用双引号, 因为token是stirng类型, 而不是char类型
{"-", [](int a, int b){return a-b;}},
{"*", [](int a, int b){return a*b;}},
{"/", [](int a, int b){return (b==0) ? 0 : a/b;}}
};
stack<int> polish;
for(auto token : tokens){
if(!op_map.count(token))
polish.push(std::stoi(token));
else{
int b = polish.top(); polish.pop();
int a = polish.top(); polish.pop();
polish.push(op_map[token](a, b));
}
}
return polish.top();
}
};

解法四: 栈+lambda+异常

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 evalRPN(vector<string>& tokens) {

std::unordered_map <std::string, std::function<int(int, int)>> op = {
{"+", [](int a, int b){return a+b;}},
{"-", [](int a, int b){return a-b;}},
{"*", [](int a, int b){return a*b;}},
{"/", [](int a, int b){return b == 0 ? 0 : a/b;}}
};

std::stack<int> polish;
for (auto const& token : tokens) {
try {
polish.push(std::stoi(token));
} catch (exception e) {
int b = polish.top(); polish.pop();
int a = polish.top(); polish.pop();
polish.push(op[token](a, b));
}
}
return polish.top();
}
};

151. 翻转字符串里的单词

给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:

输入: “the sky is blue”
输出: “blue is sky the”
示例 2:

输入: “ hello world! “
输出: “world! hello”

注意: 要去除开头和结尾的空格, 同时将中间的多个空格合并成一个.

152. Maximum Product Subarray

求连续子序列的最大乘积

Description

Given an integer array nums, find the contiguous subarray within an array (containing at least one number) which has the largest product.

Example 1:

Input: [2,3,-2,4]
Output: 6
Explanation: [2,3] has the largest product 6.
Example 2:

Input: [-2,0,-1]
Output: 0
Explanation: The result cannot be 2, because [-2,-1] is not a subarray.

解法一: 递归

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

这道题和连续子序列的最大和比较相似, 但是更难一些, 我们需要考虑负负得正这种情况, 因此, 我们不仅仅要维护最大值, 还要维护最小值. 考虑利用递归的方法来实现, 假设我们现在已经知道了以第 i-1 个数为结尾的连续子序列的最大乘积值max和最小乘积值min, 那么如果数组中新来一个数 nums[i], 则以第 i 个数为结尾的连续子序列的最大乘积就一定是max * nums[i], min*nums[i], nums[i]之中的最大者, 最小值为这三者的最小者. 由于我们还不知道最终的连续子序列是以第几个字符为结尾的, 因此我们利用一个变量res来维护当前找到的最大的子序列乘积, 并且随着循环的进行不断更新这个值, 最终, 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 maxProduct(vector<int>& nums) {
if(nums.size() == 0 ) return 0;
int res=nums[0];
helper(nums, nums.size()-1, res);
return res;
}
pair<int, int> helper(vector<int> &nums, int index, int &res){ //注意这里要设置一个引用res来不断更新最大值
if(index == 0) return make_pair(nums[0], nums[0]);
pair<int, int> max_min = helper(nums, index-1, res);
int a = max_min.first * nums[index];
int b = max_min.second * nums[index];
int c = nums[index];
max_min.first = max(a, max(b,c));
max_min.second = min(a, min(b,c));
res = max(res, max_min.first);
return max_min;

}
};

解法二 迭代实现

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

思路和解法一相同, 只不过换成了迭代实现

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

解法三: DP 迭代

时间复杂度: $O(n)$
空间复杂度: $O(n)$, 该解法需要额外数组, 实际上这是不必要的, 详细可看解法二

上面的递归写法, 可以转换成DP迭代, 为此需要两个dp数组, 一个用来保存以第i个元素为结尾的连续子序列的最大值, 另一个保存最小值. 代码如下:

写法一: new数组

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 maxProduct(vector<int>& nums) {
if(nums.size() == 0 ) return 0;
int res=nums[0];
int *dp_max = new int[nums.size()]();
int *dp_min = new int[nums.size()]();
dp_max[0] = nums[0];
dp_min[0] = nums[0];
for(int i = 1; i<nums.size(); i++){
int a = dp_max[i-1]*nums[i];
int b = dp_min[i-1]*nums[i];
int c = nums[i];
dp_max[i] = max(a, max(b,c));
dp_min[i] = min(a, min(b,c));
res = max(res, dp_max[i]);
}
delete[] dp_max;
delete[] dp_min;
return res;
}
};

写法二: vector数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CCclass Solution {
public:
int maxProduct(vector<int>& nums) {
if(nums.size() == 0 ) return 0;
int res=nums[0];
vector<int> dp_max(nums.size(), 0);
vector<int> dp_min(nums.size(), 0);
dp_max[0] = nums[0];
dp_min[0] = nums[0];
for(int i = 1; i<nums.size(); i++){
int a = dp_max[i-1]*nums[i];
int b = dp_min[i-1]*nums[i];
int c = nums[i];
dp_max[i] = max(a, max(b,c));
dp_min[i] = min(a, min(b,c));
res = max(res, dp_max[i]);
}
return res;
}
};

155. Min Stack

获取栈中最小的元素

Description

Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.

push(x) — Push element x onto stack.
pop() — Removes the element on top of the stack.
top() — Get the top element.
getMin() — Retrieve the minimum element in the stack.
Example:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); —> Returns -3.
minStack.pop();
minStack.top(); —> Returns 0.
minStack.getMin(); —> Returns -2.

解法一: 两个栈

时间复杂度: $O(1)$
空间复杂度: $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
class MinStack {
private:
stack<int> s1;
stack<int> s2;
public:
/** initialize your data structure here. */

MinStack(){

}

void push(int x) {
s1.push(x);
if(s2.empty() || x <= s2.top()) s2.push(x);
}

void pop() {
if(s1.top() == s2.top()) s2.pop();
s1.pop();
}

int top() {
return s1.top();
}

int getMin() {
return s2.top();
}
};

/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(x);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/

160. Intersection of Two Linked Lists

两个链表的第一个公共节点

Description

Write a program to find the node at which the intersection of two singly linked lists begins.

For example, the following two linked lists:

1
2
3
4
5
A:          a1 → a2

c1 → c2 → c3

B: b1 → b2 → b3

begin to intersect at node c1.

Notes:

If the two linked lists have no intersection at all, return null.
The linked lists must retain their original structure after the function returns.
You may assume there are no cycles anywhere in the entire linked structure.
Your code should preferably run in O(n) time and use only O(1) memory.

解法一:栈

时间复杂度: $O(m+n)$, 遍历两个链表
空间复杂度: $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
30
31
32
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
stack<ListNode*> s1;
stack<ListNode*> s2;
for(ListNode* cur = pHead1; cur!=nullptr; cur = cur->next){
s1.push(cur);
}
for(ListNode* cur = pHead2; cur!=nullptr; cur = cur->next){
s2.push(cur);
}
ListNode* firstCN = nullptr;
while(!s1.empty() && !s2.empty()){
if(s1.top() == s2.top()){
firstCN = s1.top();
s1.pop();
s2.pop();
}else
break;
}

return firstCN;
}
};

解法二: 常数空间复杂度

时间复杂度: $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
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 {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lengthA = 0;
ListNode* nodeA = headA;
while (nodeA != nullptr) {
nodeA = nodeA->next;
lengthA++;
}
int lengthB = 0;
ListNode* nodeB = headB;
while (nodeB != nullptr) {
nodeB = nodeB->next;
lengthB++;
}

ListNode* longNode = lengthA > lengthB ? headA : headB;
ListNode* shortNode = lengthA > lengthB ? headB : headA;
int l = std::abs(lengthA - lengthB);
while (l--) {
longNode = longNode->next;
}

while (shortNode != longNode) {
shortNode = shortNode->next;
longNode = longNode->next;
}
return shortNode;
}
};

162. Find Peak Element

Description: 局部最大值

A peak element is an element that is greater than its neighbors.

Given an input array nums, where nums[i] ≠ nums[i+1], find a peak element and return its index.

The array may contain multiple peaks, in that case return the index to any one of the peaks is fine.

You may imagine that nums[-1] = nums[n] = -∞.

Example 1:

Input: nums = [1,2,3,1]
Output: 2
Explanation: 3 is a peak element and your function should return the index number 2.
Example 2:

Input: nums = [1,2,1,3,5,6,4]
Output: 1 or 5
Explanation: Your function can return either index number 1 where the peak element is 2,
or index number 5 where the peak element is 6.

解法一: $O(n)$ 复杂度

$O(n)$ 的时间复杂度, 不合符题目要求, 仅仅记录一下.

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

解法二: $O(logn)$ 复杂度

二分查找, 分为以下几种情况:

  • If num[i-1] < num[i] > num[i+1], then num[i] is peak
  • If num[i-1] < num[i] < num[i+1], then num[i+1…n-1] must contains a peak
  • If num[i-1] > num[i] > num[i+1], then num[0…i-1] must contains a peak
  • If num[i-1] > num[i] < num[i+1], then both sides have peak
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int findPeakElement(vector<int>& nums) {
if(nums.size() ==0) return -1;
int low = 0;
int high = nums.size()-1;
int mid;
while(low < high-1){ //避免low和high相邻, 使得mid-1或mid+1可能非法
mid = (low+high)/2;
if(nums[mid-1] < nums[mid] && nums[mid] > nums[mid+1]) return mid;
else if(nums[mid] < nums[mid+1]) low = mid+1;
else high = mid-1;
}
return nums[low]>nums[high] ? low : high; // 当low或high相邻时, 即为两端时的情况
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int findPeakElement(vector<int>& nums) {
if (nums.empty()) return -1;
int low = 0;
int high = nums.size()-1;
while (low < high) {
int mid = (low + high) / 2; // 向下取整
if (nums[mid] > nums[mid+1]) high = mid;
else low = mid + 1;
}
return low;
}
};

递归实现:

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

163. 缺失的区间-中等

题目链接: https://leetcode-cn.com/problems/missing-ranges/

解法一: 排序

仔细审题发现, 题目给定的已经是排序数组

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def findMissingRanges(self, nums: List[int], lower: int, upper: int) -> List[str]:
nums.append(upper+1);
res = []
bound = lower-1
for num in nums:
interval = num - bound
if interval == 2:
res.append(str(num-1))
elif interval > 2:
res.append(str(bound+1) + "->" + str(num-1))
bound = num
return res

C++ 实现:

注意, 如果 upper 等于 INT_MAX, 或者 lower 等于 INT_MIN, 那么 +1, -1 的操作就会使得元素超出范围, 因此这里需要用 long 类型来解决.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<string> findMissingRanges(vector<int>& nums, int lower, int upper) {
vector<string> res;
vector<long> long_nums;// 注意类型为 long
for (auto num : nums) long_nums.emplace_back(num);
long_nums.emplace_back(long(upper)+1); // 注意类型为 long
long bound = long(lower) - 1; // 注意类型为 long
for (auto num : long_nums) {
long interval = num - bound; // 注意类型为 long
if (interval == 2) {
res.emplace_back(std::to_string(num-1));
} else if (interval > 2) {
res.emplace_back(std::to_string(bound+1) + "->" + std::to_string(num-1));
}
bound = num;
}
return res;
}
};

166. Fraction to Recurring Decimal

Description: 无限循环小数

Given two integers representing the numerator and denominator of a fraction, return the fraction in string format.

If the fractional part is repeating, enclose the repeating part in parentheses.

Example 1:

Input: numerator = 1, denominator = 2
Output: “0.5”
Example 2:

Input: numerator = 2, denominator = 1
Output: “2”
Example 3:

Input: numerator = 2, denominator = 3
Output: “0.(6)”

解法一: 用余数作为哈希表的key

时间复杂度: $O(logn)$, 每次都会乘以10再取余数
空间复杂度: $O(logn)$, 余数的哈希表

首先, 获取最终浮点数的符号和整数部分, 此处由于可能出现分子为-2147483648, 而分母为-1的情况, 为此, 建议使用long长整型来避免溢出.
在计算小数部分时, 将余数作为key, 小数当前位置作为value存入哈希表中, 然后将余数乘以10, 再计算当前小数位的值, 并将取余得到新的余数.
题目指明浮点数是无限循环小数, 则如果小数部分没有循环, 那么一定会出现余数为0的情况, 此时, 返回当前的res即可. 如果小数存在循环, 那么循环一定出现在余数相同的时刻, 此时, 将添加后扩号, 并根据哈希表中的value添加前括号.

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:
string fractionToDecimal(int numerator, int denominator) {
if(numerator == 0 || denominator == 0) return "0";
string res;
if(numerator<0 ^ denominator<0) res+="-";

long numer = (numerator < 0) ? (long)(numerator)*-1 : (long)numerator; // 注意, 不能写成 (long)(numerator*-1)
long denom = (denominator < 0) ? (long)(denominator)*-1 : (long)denominator;
long integral = numer/denom;
res += std::to_string(integral); // 添加整数部分
long rmd = numer % denom;
if(rmd!=0)
res += "."; // 存在小数
unordered_map <long, long> hash;
while(rmd!=0){
if(hash.find(rmd) != hash.end()){ // 判断余数
res.insert(hash[rmd], "(");
res += ")";
break;
}
hash[rmd] = res.size();
rmd = rmd*10;
long quotient = rmd/denom;
res += std::to_string(quotient);
rmd = rmd%denom;
}
return res;
}
};

169 Majority Element

Description: 找出数组中超过一半的数字

Given an array of size n, find the majority element. The majority element is the element that appears more than ⌊ n/2 ⌋ times.

You may assume that the array is non-empty and the majority element always exist in the array.

Example 1:

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

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

题目中指明了该数字一定存在, 所以无需进行count检查, 如果该数字有可能不存在, 则根据情况需要进行 $O(n)$ 复杂度的count检查(即检查当前的数字是否出现了大于 n/2 次).

解法一: 排序

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

先排序, 然后取中间元素, 即为 majority element.
(如有需要可进行count检查, $O(n)$)

解法二: 哈希

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

每个元素的值为哈希的 key, 每个元素出现的次数为哈希的 value, 如果某个 key 的 value 大于 n/2, 则该元素即为 majority element.
哈希法记录的元素的出现次数, 所以无需进行 count 检查.

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map< int, int> hash;
for(auto num: nums){
hash[num]++;
if(hash[num] > nums.size()/2) return num;
}
}
};

解法三: 同增异减

如果数组中存在这样一个数, 那么这个数的出现次数一定大于其他所有数的出现次数总和, 因此, 设置两个变量, 一个 cur_num 用来存储当前数组中的可能解, 另一个 count 为统计差值. 即每遇到一个和可能解相同的元素, 就 count++, 否则, count—. 如果 count=0, 则说明当前的可能解已经注定不是最终的解, 则令新的元素为可能解.
最终, 对可能解进行 $O(n)$ 的 count 检查, 判断是否存在 majority element (题目假设一定存在, 所以可以不做此检查).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int majorityElement(vector<int>& nums) {
int major = 0;
int count = 0;
for (auto const num : nums) {
if (num == major) {
count++;
} else {
count--;
if (count < 0) {
major = num;
count = 1;
}
}
}
return major; // 因为题目保证major一定存在, 所以可以直接返回, 否则的话还需要再判断major的个数是否大于 n/2
}
};

解法四: 随机

如果确定数组中存在 majority element 的话, 则我们可以从数组中随机选取一个元素, 并判断这个元素是否为 majority element. 这种解法依赖于统计学的概率知识, 实际的时间复杂度与数组的组成规律有关.

171. Excel Sheet Column Number

Description: Excel列表数字

Given a column title as appear in an Excel sheet, return its corresponding column number.

For example:

A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...

Example 1:

Input: “A”
Output: 1
Example 2:

Input: “AB”
Output: 28
Example 3:

Input: “ZY”
Output: 701

解法一: 遍历字符串

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

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
int titleToNumber(string s) {
int res=0;
for(auto c : s){
res += res*25 + int(c-'A') + 1;
}
return res;
}
};

172. Factorial Trailing Zeroes

Description: 阶乘的尾部含有0的个数

解法一: 统计5的个数

首先, 求出阶乘值在取余求0个数的方法肯定不可以, 阶乘会轻松溢出(n=13时就已经 int 溢出了)

时间复杂度: $O(logn)$, 以5位基数
空间复杂度: $O(1)$

因为尾部的0只可能来自于 $2\times 5$ 这样的数, 对于 $n$ 的阶乘 $1\times 2\times 3\times, …, n$ 来说, $2$ 一定是充足的, 所以我们只需要统计 $5$ 的个数就可以.
统计时, 每个5个数字会出现一次5, 每隔25个数字会额外出现一次5, 每个125个数字又会额外出现一次5…, 如此循环下去, 最终5的个数就是尾部0的个数.

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int trailingZeroes(int n) {
int res = 0;
for(long i =5; n/i >0; i*=5){
//注意这里的i的字节数一定要大于n, 因为n有可能为INT_MAX, 而 n/i >0 时, i必须>n
res += n/i;
}
return res;
}
};

解法二: 另一个角度

时间复杂度: $O(logn)$, 以5位基数
空间复杂度: $O(1)$ (迭代), $O(logn)$ (递归需额外空间)

核心思想是相同的, 同样是统计5的出现个数, 只不过这里我们是先求出 n 中 5 的倍数, 然后再求 n/5 中 5 的倍数, 实际上这里就是相当于求 n 中 25 的倍数. 因此, 和解法一是相同的, 只不过解法二因为是通过减小 n, 而不是增大 i (5,25,125,..)的方式来统计 5 个数, 因此解法二有个好处就是可以不使用 long 类型的变量, 下面分别是该方法的递归实现和迭代实现.

递归:

1
2
3
4
5
6
class Solution {
public:
int trailingZeroes(int n) {
return n < 5 ? 0 : n / 5 + trailingZeroes(n / 5);
}
};

迭代:

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int trailingZeroes(int n) {
int res=0;
while(n>=5){
res += n/5;
n /= 5;
}
return res;
}
};

179. Largest Number

Description: 排列数字使其字符串形式的数字为最大

Given a list of non negative integers, arrange them such that they form the largest number.

Example 1:

Input: [10,2]
Output: “210”
Example 2:

Input: [3,30,34,5,9]
Output: “9534330”

解法一: 构造比较函数, 快排排序

时间复杂度: $O(nlogn)$, 快排时间复杂度
空间复杂度: $O(logn)$, 快排空间复杂度, 如果使用其他排序算法, 可将空间复杂度降为 $O(1)$

我们可以构造一个新的比较函数来决定两个元素的先后关系, 对于任意两个元素 ab, 首先将其转换成字符串形式 s_as_b, 我们知道, 若整形 a>b, 则一定有 s_a > s_b, 因此我们可以比较 s_a+s_bs_b+s_a 的大小关系, 根据题目要求, 我们要进行递减排序. 得到比较函数以后, 利用快排排序即可.

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
class Solution {
public:
string largestNumber(vector<int>& nums) {
q_sort(nums, 0, nums.size()-1);
if(nums.size()!=0 && nums[0] == 0) return "0"; // 对于输入[0, 0, 0] 应该返回 "0", 而不是"000", 必须要放在排序后, nums[0] == 0 说明所有元素均为0
string res;
for(auto num: nums){
res += std::to_string(num);

}
return res;
}


bool str_geq(int a, int b){
string s_a = std::to_string(a);
string s_b = std::to_string(b);
if(s_a+s_b >= s_b+s_a) return true; //注意是递减排序, 所以为 >=
else return false;
}

int partition(vector<int> &nums, int low, int high){
int P = nums[low];
while(low < high){
while(low<high && str_geq(P, nums[high])) high--;
nums[low] = nums[high];
while(low<high && str_geq(nums[low], P)) low++;
nums[high] = nums[low];
}
nums[low] = P;
return low;
}

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

解法二: 利用 STL sort() 函数

时间复杂度: $O(nlogn)$, 快排时间复杂度
空间复杂度: $O(logn)$, 快排空间复杂度, 如果使用其他排序算法, 可将空间复杂度降为 $O(1)$

思路与解法一一致, 只不过省略了排序算法的实现, 使用了 STL 的 sort 函数.

需要注意, 在 C++ STL 的 sort 函数中, bool 返回真的时候, 必须是绝对大于或者绝对小于, 对于等于的情况, 只能返回 false(因为当返回 true 时, 元素会继续下一个, 这样对于极端情况, 如所有元素都一样时, 会出现越界, 从而导致段错误)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool str_geq(int a, int b){    
string s_a = std::to_string(a);
string s_b = std::to_string(b);
if(s_a+s_b > s_b+s_a) return true; // 这里用 >= 会产生运行时错误, 用 > 则可以通过, 为什么?
else return false;
}
class Solution {
public:
string largestNumber(vector<int>& nums) {
std::sort(nums.begin(), nums.end(), str_geq);
if(nums.size()!=0 && nums[0] == 0) return "0"; // 对于输入[0, 0, 0] 应该返回 "0", 而不是"000", 必须要放在排序后, nums[0] == 0 说明所有元素均为0
string res;
for(auto num: nums){
res += std::to_string(num);
}
return res;
}
};

188. 买卖股票的最佳时机 IV

题目链接: https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

解法: 通用 DP 解法

注意, 本题由于 k 的大小可以非常大, 所以在声明 dp 数组前, 一定要先判断 k 的大小, 如果超过范围, 则要转换为无限次的股票买卖, 否则会导致爆栈.

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
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if (prices.size() <= 1) return 0;
//std::vector<std::pair<int, int>> dp(k+1, {-prices[0], 0});
// 这里有一个隐藏很深的 bug, 就如果 k 的值很大, 就会直接把栈爆掉!!
// 所以应该按照 k 值做优化, 将 vector 声明在 if 语句内部
if (k < prices.size() / 2) {
std::vector<std::pair<int, int>> dp(k+1, {-prices[0], 0});
for(int i=1;i<prices.size();i++)
{
for(int j=1; j < k+1; j++)
{
int hold = std::max(dp[j].first, dp[j-1].second-prices[i]);
int not_hold = std::max(dp[j].second, dp[j].first+prices[i]);
dp[j].first = hold; dp[j].second = not_hold;
}
}
return dp[k].second; //max(dp[k].first, dp[k].second);
} else {
std::pair<int, int> dp = {-prices[0], 0};
for (int i=1; i<prices.size(); i++) {
int hold = std::max(dp.first, dp.second-prices[i]);
int not_hold = std::max(dp.second, dp.first+prices[i]);
dp.first = hold; dp.second = not_hold;
}
return dp.second;
}
}
};

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
if len(prices) <= 1: return 0
if (k < len(prices) // 2) :
dp = [[-prices[0], 0] for i in range(k+1)]
for price in prices[1:]:
for i in range(1, k+1):
dp[i] = [max(dp[i][0], dp[i-1][1]-price), max(dp[i][1], dp[i][0]+price)]
return dp[k][1]
else:
dp = [-prices[0], 0]
for price in prices[1:]:
dp = [max(dp[0], dp[1]-price), max(dp[1], dp[0]+price)]
return dp[1]

189. 旋转数组

Description: 循环右移数组

Given an array, rotate the array to the right by k steps, where k is non-negative.

Example 1:

Input: [1,2,3,4,5,6,7] and k = 3
Output: [5,6,7,1,2,3,4]
Explanation:
rotate 1 steps to the right: [7,1,2,3,4,5,6]
rotate 2 steps to the right: [6,7,1,2,3,4,5]
rotate 3 steps to the right: [5,6,7,1,2,3,4]

Example 2:

Input: [-1,-100,3,99] and k = 2
Output: [3,99,-1,-100]
Explanation:
rotate 1 steps to the right: [99,-1,-100,3]
rotate 2 steps to the right: [3,99,-1,-100]

Note:
Try to come up as many solutions as you can, there are at least 3 different ways to solve this problem.
Could you do it in-place with O(1) extra space?

解法一: 暴力

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

所有的数字每次移动一步, 攻移动 k 次. 超时

解法二: 使用额外数组

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

申请一个长度相等的数组, 复制原数组中的 $i$ 号元素到新数组中的 $i+k$ 号位置.

解法三: 循环置换

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

每次直接将元素放置在正确的位置, 放置前, 需要用一个临时变量将被放置的元素保存起来以防止覆盖, 然后将临时变量的元素再直接放到正确的位置, 循环进行, 知道临时变量指向了最开始的变量, 然后再继续从下一个元素开始这个过程. 在代码中设置一个 count 变量, 用来统计放置的次数, 当次数等于数组长度时, 说明已经完成移动.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int count=0;
for(int start=0; count<nums.size(); start++){
int cur_pos = start;
int cur_val = nums[start];
do{
int next_pos = (cur_pos + k) % nums.size();
int temp = nums[next_pos];
nums[next_pos] = cur_val;
cur_pos = next_pos;
cur_val = temp;
count++;
}while(start!=cur_pos);
}
}
};

解法四: reverse

时间复杂度: $O(n)$, 调用三次 reverse 函数
空间复杂度: $O(1)$

1
2
3
4
5
6
7
8
class Solution {
public:
void rotate(vector<int>& nums, int k) {
std::reverse(nums.begin(), nums.end()-k);
std::reverse(nums.end()-k, nums.end());
std::reverse(nums.begin(), nums.end());
}
};

190. Reverse Bits

Description: 按位逆置

Reverse bits of a given 32 bits unsigned integer.

Example:

Input: 43261596
Output: 964176192
Explanation: 43261596 represented in binary as 00000010100101000001111010011100,
return 964176192 represented in binary as 00111001011110000010100101000000.
Follow up:
If this function is called many times, how would you optimize it?

解法一: 按位进行32次操作

每次取 n 的最后一位, 如果为 1, 则令res左移一位并加一, 如果为0, 则只左移一位. 进行32次(n的32位).

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
uint32_t res= 0;
for(int i=0; i<32; i++){
res = (res<<1) | ((n>>i)&1); //res = (res<<1) | (n&1); n = (n>>1);
}
return res;
}
};

解法二: 按位二分进行5次操作

先将前16位和后16位交换(利用位移和位操作实现)
然后再将16位中的前8位和后8位交换
然后再将8位中的前4位和后4位交换
然后再将4位中的前2位和后2位交换
最后将2位中的前1位和后1位交换.

上述交换全部采用位操作实现, 因此, 速度上有所优化.

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
n = (n>>16) | (n<<16);
n = ( ((n & 0xff00ff00)>>8) | ((n & 0x00ff00ff)<<8) );
n = ( ((n & 0xf0f0f0f0)>>4) | ((n & 0x0f0f0f0f)<<4) );
n = ( ((n & 0xcccccccc)>>2) | ((n & 0x33333333)<<2) );
n = ( ((n & 0xaaaaaaaa)>>1) | ((n & 0x55555555)<<1) );
return n;
}
};

191. Number of 1 Bits

Description: 统计二进制中1的个数

Write a function that takes an unsigned integer and returns the number of ‘1’ bits it has (also known as the Hamming weight).

Example 1:

Input: 11
Output: 3
Explanation: Integer 11 has binary representation 00000000000000000000000000001011
Example 2:

Input: 128
Output: 1
Explanation: Integer 128 has binary representation 00000000000000000000000010000000

解法一: 逐位统计

时间复杂度: $O(1)$, 循环32次
空间复杂度: $O(1)$

查看每一位上的二进制是否为1, 若为1, 则count++

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
int hammingWeight(uint32_t n) {
int count=0;
for(int i=0; i<32; i++){
if( (n & (1<<i)) != 0) count++;
}
return count;
}
};

解法二: 和 $n-1$ 按位与

时间复杂度: $O(1)$, 循环次数为二进制中1的个数.
空间复杂度: $O(1)$

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int hammingWeight(uint32_t n) {
int count=0;
while(n!=0){
count++;
n = n&(n-1);
}
return count;
}
};

198. 打家劫舍-简单

Description: 房屋小偷获取最大收益

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.

Example 1:

Input: [1,2,3,1]
Output: 4
Explanation:
Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.
Example 2:

Input: [2,7,9,3,1]
Output: 12
Explanation:
Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1).
Total amount you can rob = 2 + 9 + 1 = 12.

解法一: DP

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

依据 DP 的思想, 对于一个任意价格的房子, 我们有两种选择: 偷或不偷. 如果选择不偷, 那么前 $(i+1)$ 个房子的最大收益, 就应该是前 $i$ 个房子的最大收益(偷或者不偷第 $i$ 个房子收益中的较大者), 如果选择偷, 那么就不能偷第 $i$ 个房子.
根据上面的描述, 我们可以维护两个变量 cur_robcur_nrob, 前者代表偷第 $i$ 个房子的收益, 后者代表不偷第 $i$ 个房子的收益, 则最大收益就应该为二者中的较大者. 详细代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int rob(vector<int>& nums) {
int cur_rob=0;
int cur_nrob=0;
for(int i =0; i<nums.size(); i++){
int temp = cur_nrob;
cur_nrob = std::max(cur_rob, cur_nrob);
cur_rob = temp+nums[i];
}
return std::max(cur_rob, cur_nrob);
}
};

解法二: 根据房屋的编号奇偶性

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

因为偷取的房屋不能相邻, 因此我们可以维护两个变量, even 是前偶数个房屋的最大收益, odd 是前奇数个房屋的最大收益, 对于任意的一个新来的房屋, 如果该新房屋的编号为奇数, 那么它的最大收益就是 odd+neweven 当中的较大者(因为不能相邻, 所以只能令 odd+new). 对于偶数的情况同理. 最终返回 oddeven 的较大者.(因为有可能包含最后一个元素, 也有可能不包含) 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int rob(vector<int>& nums) {
int odd=0;
int even=0;
for(int i=0; i<nums.size(); i++){
if(i%2==0) even = std::max(odd, even+nums[i]);
else odd = std::max(odd+nums[i], even);
}
return std::max(odd, even);
}
};

200. Number of Islands

Description: 区块的个数

Given a 2d grid map of ‘1’s (land) and ‘0’s (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

Example 1:

Input:
11110
11010
11000
00000

Output: 1
Example 2:

Input:
11000
11000
00100
00011

Output: 3

解法一: DFS 遍历

时间复杂度: $O(n)$, 至多遍历两次 grid
空间复杂度: $O(1)$

遍历 grid 中的每一个元素, 如果为1, 则将与之相连的所有的1都置为0, 并且区块个数加1, 这样, 最坏的情况就是 grid 中的所有数字均为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
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int res = 0;
for(int i =0 ;i<grid.size(); i++){
for(int j=0; j<grid[i].size(); j++){
if(grid[i][j] == '1'){
fill(grid, i, j);
res++;
}
}
}
return res;
}

void fill(vector<vector<char>>& grid, int i, int j) {
grid[i][j] = '2';
int n = grid.size();
int m = grid[0].size();
int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (auto dir : dirs) {
int x = i + dir[0];
int y = j + dir[1];
if (x >=0 && x < n && y >=0 && y < m && grid[x][y] == '1') {
fill(grid, x, y);
}
}
}
};

202. Happy Number

Description: 判断一个数字是否是 Happer Number

Write an algorithm to determine if a number is “happy”.

A happy number is a number defined by the following process: Starting with any positive integer, replace the number by the sum of the squares of its digits, and repeat the process until the number equals 1 (where it will stay), or it loops endlessly in a cycle which does not include 1. Those numbers for which this process ends in 1 are happy numbers.

Example:

Input: 19
Output: true
Explanation:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

解法一: 模拟计算过程

时间复杂度: $O(logn)$, 基数为10
空间复杂度: 未知, 取决于无序集合的size.

按照题目中的逻辑, 模拟整个计算过程, 如果出现1, 则返回 true, 如果出现循环(即在集合中发现已存在元素), 则返回 false.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
bool isHappy(int n) {
unordered_set<int> num_set;
while(n!=1 && num_set.find(n)==num_set.end()){
num_set.insert(n);
int temp = 0;
while(n!=0){
temp += (n%10) * (n%10);
n = n/10;
}
n = temp;
}
if(n==1) return true;
return false;
}
};

解法二: Floyd 判圈算法

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

利用 Floyd 判圈算法维护两个变量 slowfast, fast 每次都比 flow 多走一步, 那么, 当 fast==1 时, 说明应该返回 true, 当 slow==fast 时, 说明存在循环, 应该返回 false.

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 isHappy(int n) {
int slow=n, fast=n;
do{
slow = digitSquareSum(slow);
fast = digitSquareSum(fast);
fast = digitSquareSum(fast);
}while(fast!=1 && slow!=fast);
if(fast == 1) return true;
return false;
}
int digitSquareSum(int n){
int temp = 0;
while(n!=0){
temp += (n%10) * (n%10);
n = n/10;
}
return temp;
}
};

204. Count Primes

Description: 素数的个数

Count the number of prime numbers less than a non-negative number, n.

Example:

Input: 10
Output: 4
Explanation: There are 4 prime numbers less than 10, they are 2, 3, 5, 7.

解法一: 填充非素数

时间复杂度: $O(n)$, 至多遍历两次 $n$ 大小的数组, 可优化为只遍历一次.
空间复杂度: $O(n)$, 申请了 $n$ 大小的一维布尔数组来标识是否为负数

如上图, 我们从 $2\times 2$ 开始填充, 将所有能与2相乘切乘积小于 $n$ 的数对应下标置为 false, 然后从 $3\times 3$ 开始填充(注意不是从 $3\times 2$, 因为这样会与前面的 $2\times 3$ 重复), 接着从 $4\times 4$ 开始填充, 因此, 填充的开始位置最大为 $\sqrt{n}$. 另外需要注意的是, 0 和 1 均不是素数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int countPrimes(int n) {
if(n==0 || n==1) return 0;
int div_n = sqrt(n)+1; // 注意这里是开根号
vector<bool> is_primes(n, true);
for(int i=2; i<div_n; i++){
for(int j=i*i; j<n; j+=i){
is_primes[j]=false;
}
}
int res_count=0;
for(auto primes : is_primes){
if(primes==true) res_count++;
}
return res_count-2; //去掉0和1的情况
}
};

优化1: 因为任何一个合数都可以拆分成素数的乘积, 因此我们只在当前元素为素数的时候才开始填充, 例如, 对于4, 我们不填充16, 20, ..等数字, 因为这些数字在开始元素为2的时候已经填充过了. 因此, 可以避免这些重复填充, 减少迭代次数, 代码如下(多加了一条if语句).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int countPrimes(int n) {
if(n==0 || n==1) return 0;
int div_n = sqrt(n)+1; // 注意这里是开根号
vector<bool> is_primes(n, true);
for(int i=2; i<div_n; i++){
if(is_primes[i]){
for(int j=i*i; j<n; j+=i){
is_primes[j]=false;
}
}
}
return std::count(is_primes.begin(), is_primes.end(), true)-2; //去掉0和1的情况
}
};

优化2: 只遍历一次. 首先我们将判断数组isPrime的初始状态设为true, 这样, 每次只在遇到奇数时才检查其是否为素数, 如果该奇数是素数, 那么就将该奇数的倍数全部置为非素数, 同时, 将速度的count加1. 这样, 不仅可以减少判断次数(不再判断偶数), 同时可以在一次遍历的时间内完成素数统计.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int countPrimes(int n) {
std::vector<bool> isPrime(n, true); // 默认全是素数
int upper = std::sqrt(n); // 控制 i*i, 防止越界
if (n <= 2) return 0; // 判断 0 ~ n-1 是否为素数, 当 n = 2 时, 返回0
int count = 1; // 2 也为素数
for (int i = 3; i < n; i+=2) { // 只有奇数才有可能是速度, 并且 1 不是素数
if (isPrime[i]) {
count++;
if (i > upper) continue; // 这里必须进行判断, 否则 i*i 有可能越界
for (int j = i*i; j < n; j+=i) { // 将 i 的倍数全部置为非素数
isPrime[j] = false;
}
}
}
return count;
}
};

206. 反转链表-简单

Description: 逆置链表

Reverse a singly linked list.

Example:

Input: 1->2->3->4->5->NULL
Output: 5->4->3->2->1->NULL

解法一: 迭代

时间复杂度: $O(n)$, 遍历一次链表
空间复杂度: $O(1)$, 借助3个复制指针完成逆置

Python 实现

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

class Solution:
def reverseList(self, head: ListNode) -> ListNode:
prev = None
cur = head
rear = None
while cur != None:
rear = cur.next
cur.next = prev
prev = cur
cur = rear
return prev

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

解法二: 递归

时间复杂度: $O(n)$, 一次遍历
空间复杂度: $O(n)$, 迭代需要占用 $O(n)$ 大小的栈空间

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==nullptr || head->next==nullptr) return head;
ListNode *P = reverseList(head->next); //令下一个开始的节点逆置, 返回新链表的头结点
head->next->next = head; // 将当前节点逆置
head->next=nullptr; // 将当前节点的下一个置空, 主要是处理新的尾节点, 其他节点的next会在递归中正确赋值
return P; //返回新的头结点
}
};

207. Course Schedule

Description: 课程表 / 判断有向图是否存在环

There are a total of n courses you have to take, labeled from 0 to n-1.

Some courses may have prerequisites, for example to take course 0 you have to first take course 1, which is expressed as a pair: [0,1]

Given the total number of courses and a list of prerequisite pairs, is it possible for you to finish all courses?

Example 1:
Input: 2, [[1,0]]
Output: true
Explanation:
There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible.

Example 2:
Input: 2, [[1,0],[0,1]]
Output: false
Explanation:
There are a total of 2 courses to take. To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.

解法一: BFS / 拓扑排序

时间复杂度: $O(V+E)$, 统计入度时需要 $O(V)$, 处理队列需要 $O(E)$, 其中 $V$ 为节点个数, $E$ 为边的个数
空间复杂度: $O(V+E)$, 入度数组和队列分别需要 $(V)$, 邻接表需要 $O(V+E)$.

首先将图的边表示结构转换成邻接表形式(用vector来实现邻接表, 使其支持随机访问). 然后再申请一个 $O(V)$ 大小的数组来存储每个节点的入度. 在拓扑排序时, 先将所有入度为0的节点添加都一个队列当中, 然后从队列顶端拿出一个节点, 将该节点的所有直接后序节点的入度都减1, 然后再将所有入度为0的节点入队列. 如此迭代下去, 直至所有队列为空. 此时, 如果还有某个节点的入度不为0, 则说明存在环, 应该返回 false, 否则, 返回 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
class Solution {
public:
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<vector<int>> graph_c(numCourses, vector<int>(0));
vector<int> in_degree(numCourses, 0);
for(auto p : prerequisites){
graph_c[p.second].push_back(p.first);
in_degree[p.first]++;
}
queue<int> q; // 入度为0的节点队列
for(int i=0; i<numCourses; i++){
if(in_degree[i]==0) q.push(i); //将所有入度为0的节点入队列
}
while(!q.empty()){
int cur_c = q.front(); q.pop();
for(auto next_c : graph_c[cur_c]){ // next_c为cur_c的直接后序课程
in_degree[next_c]--; // 后序节点的入度减1
if(in_degree[next_c]==0) q.push(next_c);//如果减为0, 则入队列
}
}
for(auto in : in_degree){
if(in!=0) return false;
}
return true;
}
};

解法二: DFS

时间复杂度: $O(V+E)$, 复杂度和 BFS 算法近似, 其中 $V$ 为节点个数, $E$ 为边的个数
空间复杂度: $O(V+E)$, visit数组和递归栈分别需要 $(V)$, 邻接表需要 $O(V+E)$.

首先, 和 BFS 一样, 建立关于图的邻接表结构, 然后, 申请 $O(V)$ 大小的访问数组visit, 初始值全部为0, 表示所有节点均为访问. 然后, 根据 DFS 算法的执行过程. 将当前正在访问的节点置为-1, 将已经访问过且确认无环的节点置为1. 则则DFS过程中, 如果访问到了一个已经被置为-1的节点, 则说明该节点是当前循环内的正在访问的节点, 因此, 构成了一个环, 返回 false. 如果遇到了一个被置为1的节点, 因为已经确认该节点无环, 因此可以直接返回 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
class Solution {
public:
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<vector<int>> graph_c(numCourses, vector<int>(0));
vector<int> visit(numCourses, 0);
for(auto p : prerequisites)
graph_c[p.second].push_back(p.first);
for(int i=0; i<numCourses; i++){ // 因为当前的图并不是一个连通图, 所以必须遍历所有的节点
if(canFinishDFS(graph_c, visit, i) == false) return false;
}

return true;
}

bool canFinishDFS(vector<vector<int>> &graph_c, vector<int> &visit, int i){
if(visit[i] == -1) return false;
if(visit[i] == 1) return true;
visit[i] = -1; // 将当前节点置为正在访问状态
for(auto node : graph_c[i]){
if(canFinishDFS(graph_c, visit, node) == false) return false; // 当前节点上存在环
}
visit[i] = 1; // 将当前节点置为已经访问过且确认无环状态
return true; // 确认节点i无环, 返回true
}
};

208. Implement Trie (Prefix Tree)

Description: 实现字典树(前缀树)

Implement a trie with insert, search, and startsWith methods.

Example:

1
2
3
4
5
6
7
8
Trie trie = new Trie();

trie.insert("apple");
trie.search("apple"); // returns true
trie.search("app"); // returns false
trie.startsWith("app"); // returns true
trie.insert("app");
trie.search("app"); // returns true

解法一

https://www.cnblogs.com/grandyang/p/4491665.html

时间复杂度: $O(k)$, 插入, 查找, 找前缀均只需要 $O(k)$复杂度, $k$ 为字符串长度
空间复杂度: 与字符串的公共部分的多少有关, 公共部分越多, 越节省空间, 反之, 空间复杂度较高. 最差情况下为 $O(wk)$, 其中, $w$ 为单词的个数, $k$ 为单词的最长长度.

字母字典树是一个26叉树, 树的根节点没有字符, 其他节点有且仅有一个字符, 我们模仿二叉树的定义, 构建一个26叉树的数据结构, 用子节点的编号代表字母(即0号节点代表字母a, 1号代表b,…,25号代表z), 另外需要定义一个布尔值来标识当前节点是否构成一个单词. 插入时, 根据字符串遍历树, 如果当前字符不存在, 则新建一个. 查找和找前缀时, 如果不存在则直接返回false.

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Trie:

def __init__(self):
"""
Initialize your data structure here.
"""
self.root = dict()
self.end = '#'

def insert(self, word: str) -> None:
"""
Inserts a word into the trie.
"""
curNode = self.root
for c in word:
if c not in curNode:
curNode[c] = dict()
curNode = curNode[c]
curNode[self.end] = True # 结束标志


def search(self, word: str) -> bool:
"""
Returns if the word is in the trie.
"""
curNode = self.root
for c in word:
if c not in curNode: # 如果字符没有在字典树中, 则返回 False
return False
curNode = curNode[c]
if self.end not in curNode: # 虽然存在于树中, 但是只是前缀, 依然返回 False
return False
return True

def startsWith(self, prefix: str) -> bool:
"""
Returns if there is any word in the trie that starts with the given prefix.
"""
curNode = self.root
for c in prefix:
if c not in curNode:
return False
curNode = curNode[c]
return True

# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)

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
60
61
62
class TrieNode{
public:
TrieNode *child[26];
bool is_word;
TrieNode():is_word(false){
for(auto &c : child){ // 对c进行改动, 需要用引用&
c = nullptr;
}
}
};

class Trie {
private:
TrieNode *root;
public:
/** Initialize your data structure here. */
Trie() {
root = new TrieNode();
}

/** Inserts a word into the trie. */
void insert(string word) {
TrieNode *p = root;
for(auto letter : word){
int i = letter - 'a';
if(p->child[i] == nullptr) p->child[i]=new TrieNode();
p = p->child[i];
}
p->is_word = true;
}

/** Returns if the word is in the trie. */
bool search(string word) {
TrieNode *p = root;
for(auto letter : word){
int i = letter - 'a';
if(p->child[i]==nullptr) return false;
p = p->child[i];
}
return (p->is_word == true) ? true : false;

}

/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
TrieNode *p = root;
for(auto letter : prefix){
int i = letter - 'a';
if(p->child[i]==nullptr) return false;
p = p->child[i];
}
return true;
}
};

/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* bool param_2 = obj.search(word);
* bool param_3 = obj.startsWith(prefix);
* /

210. Course Schedule II

Description: 判断有向图是否有环, 若无环, 则返回拓扑序列

There are a total of n courses you have to take, labeled from 0 to n-1.

Some courses may have prerequisites, for example to take course 0 you have to first take course 1, which is expressed as a pair: [0,1]

Given the total number of courses and a list of prerequisite pairs, return the ordering of courses you should take to finish all courses.

There may be multiple correct orders, you just need to return one of them. If it is impossible to finish all courses, return an empty array.

Example 1:
Input: 2, [[1,0]]
Output: [0,1]
Explanation:
There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct course order is [0,1] .

Example 2:
Input: 4, [[1,0],[2,0],[3,1],[3,2]]
Output: [0,1,2,3] or [0,2,1,3]
Explanation:
There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is [0,2,1,3] .

解法一: BFS, 拓扑排序

时间复杂度: $O(V+E)$, 统计入度时需要 $O(V)$, 处理队列需要 $O(E)$, 其中 $V$ 为节点个数, $E$ 为边的个数
空间复杂度: $O(V+E)$, 入度数组和队列分别需要 $(V)$, 邻接表需要 $O(V+E)$, 相比于第207题, 多了一个拓扑序列的数组, 大小为 $O(V)$.

和第207题差不多, 不过在判断是否有环的同时, 还要记录正确的拓扑序列并返回.

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 {
public:
vector<int> findOrder(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<vector<int>> graph_c(numCourses, vector<int>()); // 构建图的邻接表
vector<int> in_degree(numCourses);// 构建入度数组
for(auto c_pair : prerequisites){
graph_c[c_pair.second].push_back(c_pair.first);
in_degree[c_pair.first]++;
}

queue<int> q; //入度为0的队列
for(int i=0; i<numCourses; i++){
if(in_degree[i]==0) q.push(i);
}

vector<int> res; // 记录拓扑序列
while(!q.empty()){
int cur_c = q.front(); q.pop();
res.push_back(cur_c);
for(auto &next_c : graph_c[cur_c]){
in_degree[next_c]--; // 后修课的入度减1
if(in_degree[next_c]==0) q.push(next_c);
}
}
if(res.size() == numCourses) return res;
else return vector<int>();
}
};

解法二: DFS

时间复杂度: $O(V+E)$, 复杂度和 BFS 算法近似, 其中 $V$ 为节点个数, $E$ 为边的个数
空间复杂度: $O(V+E)$, visit数组和递归栈分别需要 $(V)$, 邻接表需要 $O(V+E)$, 拓扑序列需要 $O(V)$.

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 {
public:
vector<int> findOrder(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<vector<int>> graph_c(numCourses, vector<int>()); // 构建图的邻接表
vector<int> visit(numCourses, 0);// 构建入度数组
for(auto c_pair : prerequisites){
graph_c[c_pair.second].push_back(c_pair.first);
}
vector<int> res;
for(int i=0; i<numCourses; i++){ //非连通图, 需要遍历所有节点
if(findOrderDFS(graph_c, i, visit, res)==false) return vector<int>();
}
std::reverse(res.begin(), res.end()); //等于dfs来说, 最后的课程会先加入结果数组, 因此, res中的序列逆置后才是最终的拓扑序列.
return res;
}

bool findOrderDFS(vector<vector<int>> &graph_c, int i, vector<int> &visit, vector<int> &res){
if(visit[i]==-1) return false; // 重复访问, 存在环
if(visit[i]==1) return true; // 已经访问过且确认无环, 可直接返回
visit[i] = -1; // 置为正在访问状态
for(auto next_c : graph_c[i]){
if(findOrderDFS(graph_c, next_c, visit, res) == false) return false;
}
visit[i] = 1; //确认无环
res.push_back(i); //
return true;
}
};

211. 添加与搜索单词 - 数据结构设计

题目链接: https://leetcode-cn.com/problems/add-and-search-word-data-structure-design/

解法: 回溯

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
class WordDictionary:

def __init__(self):
"""
Initialize your data structure here.
"""
self.root = dict()
self.end = '#'

def addWord(self, word: str) -> None:
"""
Adds a word into the data structure.
"""
curNode = self.root
for c in word:
if c not in curNode:
curNode[c] = dict()
curNode = curNode[c]
curNode[self.end] = True

def search(self, word: str) -> bool:
"""
Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter.
"""
#print(self.root)
def search_helper(curNode, word):
for i, c in enumerate(word):
if c == '.':
for nextNode in curNode.values():
if isinstance(nextNode, dict) and search_helper(nextNode, word[i+1:]):
return True
return False
else:
if c not in curNode:
return False
curNode = curNode[c]
if self.end not in curNode:
return False
return True

return search_helper(self.root, word)

# Your WordDictionary object will be instantiated and called as such:
# obj = WordDictionary()
# obj.addWord(word)
# param_2 = obj.search(word)

212. Word Search II

Description: 返回字符矩阵中含有的所有单词

Given a 2D board and a list of words from the dictionary, find all words in the board.

Each word must be constructed from letters of sequentially adjacent cell, where “adjacent” cells are those horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.

Example:

Input:
words = [“oath”,”pea”,”eat”,”rain”] and board =
[
[‘o’,’a’,’a’,’n’],
[‘e’,’t’,’a’,’e’],
[‘i’,’h’,’k’,’r’],
[‘i’,’f’,’l’,’v’]
]

Output: [“eat”,”oath”]

解法一: 穷举

时间复杂度: $O(w mn 4^k)$, 暴力求解, $mn$ 为字符矩阵的宽和高, 也即 cell 数量, 对于 dfs 中的每个 cell, 有4个扩展方向, 一共需要扩展 $k$ 次($k$ 为单词的长度). 总共有 $w$ 个单词, 因此复杂度为$O(w mn 4^k)$
空间复杂度: $O(mn)$ , 和79题相同, 回溯时, 用#来记录已经遍历过的点, 无需申请额外空间来记录. 但是递归程序需要占用 $O(mn)$ 的空间复杂度.

该题和79题类似, 只不过给定的是一个单词列表, 而不是一个单词, 因此, 可以对这个单词列表循环调用79题的解. 不过时间复杂度过高, 无法通过 OJ.

解法二: 字典树

时间复杂度: $O(mn 4^k)$, 暴力求解, $mn$ 为字符矩阵的 cell 数量, 对于 dfs 中的每个 cell, 有4个扩展方向, 一共需要扩展 $k$ 次($k$ 为单词的长度).
空间复杂度: $O(mn)$ , 和79题相同, 回溯时, 用#来记录已经遍历过的点, 无需申请额外空间来记录. 但是递归程序需要占用 $O(mn)$ 的空间复杂度. 另外, 还有构建字典树所需的空间复杂度, 这部分复杂度与具体的字符串数组有关, 当字符串公共部分较多时, 复杂度较低, 反之, 复杂度较高, 最差情况下为 $O(wk)$, 即无公共前缀

相对于第79题来说, 本题增加的复杂度主要体现在需要同时查看 $w$ 个单词的字符, 查询这些单词字符的复杂度约为 $O(wk)$, 其中, $k$ 为单词的最大长度, 那么, 我们能否将这里的复杂度降低成 $k$ 呢? 如果降低成 $k$ 的话, 就相当是在查找一个单词, 那么整体的复杂度就和79题相同, 变成了 $O(mn 4^k)$.
实际上, 字典树正是这种数据结构! 在由 $w$ 个字符串构成的字典树中查询某个字符串或者字符子串的复杂度为 $k$. 因此, 我们可以借助字典树来降低整体的时间复杂度. 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class TrieNode{
public:
TrieNode *child[26];
string str;
TrieNode():str(""){
for(auto &node : child) node=nullptr;
}
};

class Trie{
public:
TrieNode *root;

Trie():root(new TrieNode()){};

void insert(string s){
TrieNode *p = root;
for(auto c : s){
int i = c - 'a';
if(p->child[i] == nullptr) p->child[i] = new TrieNode();
p = p->child[i];
}
p->str = s;
}
};

class Solution {
public:
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
vector<string> res;
if(words.size()==0 || board.size()==0 || board[0].size()==0) return res;
vector<vector<bool>> visit(board.size(), vector<bool>(board[0].size(), false));
Trie T;
for(auto word : words) T.insert(word);

for(int i=0; i<board.size(); i++){
for(int j=0; j<board[i].size(); j++){
if(T.root->child[board[i][j] - 'a'] != nullptr){
search(board, T.root->child[board[i][j]-'a'], i, j, visit, res);
}
}
}
return res;

}

void search(vector<vector<char>> &board, TrieNode *node, int i, int j, vector<vector<bool>> &visit, vector<string> &res){
if(!node->str.empty()){
res.push_back(node->str);
node->str.clear(); // 重新置为空 node->str = ""; 防止重复push_back
}

int direct[4][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };
visit[i][j] = true; // 将当前位置设置为已访问, 因为题目要求同一个位置只能在一个字符串中被访问一次
for(auto d : direct){
int new_i = i + d[0]; int new_j = j + d[1];
if(new_i>=0 && new_j>=0 && new_i<board.size() && new_j<board[0].size() && visit[new_i][new_j]==false && node->child[board[new_i][new_j] - 'a'] != nullptr){
search(board, node->child[board[new_i][new_j] - 'a'], new_i, new_j, visit, res);
}
}
visit[i][j] = false;
}
};

213. 打家劫舍 II-中等

题目链接: https://leetcode-cn.com/problems/house-robber-ii/

解法: 动态规划

C++ 实现:
在 198 题打家劫舍的基础上进行扩展, 由题意知, 该题的难点在于第一家和最后一家是紧挨着的, 因此, 我们可以分两种情况进行讨论, 第一种情况是允许偷第一家, 那么就一定不能偷最后一家, 第二种情况是允许偷最后一家, 那么就一定不能偷第一家. 注意, 这里用的是允许偷, 而不是一定偷, 偷与不偷的取舍会在动态规划中自行决定.

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 rob(vector<int>& nums) {
if (nums.size() == 1) return nums[0];
int dp1[2] = {0, 0};
int dp2[2] = {0, 0};
for (int i = 0; i < nums.size(); i++) {
if (i < nums.size()-1) { // 可以偷第一家, 就一定不能偷最后一家
int rob = dp1[1] + nums[i];
int not_rob = std::max(dp1[0], dp1[1]);
dp1[0] = rob;
dp1[1] = not_rob;
}
if (i > 0) { // 可以偷最后一家, 就一定不能偷第一家
int rob = dp2[1] + nums[i];
int not_rob = std::max(dp2[0], dp2[1]);
dp2[0] = rob;
dp2[1] = not_rob;
}
}
int m1 = std::max(dp1[0], dp1[1]);
int m2 = std::max(dp2[0], dp2[1]);
return std::max(m1, m2);
}
};

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1: return nums[0]
dp1 = [0, 0]
dp2 = [0, 0]
for i, num in enumerate(nums):
if i < len(nums)-1: # 允许偷第一家, 不能偷最后一家
rob = dp1[1] + num
not_rob = max(dp1[0], dp1[1])
dp1 = [rob, not_rob]
if i > 0: # 允许偷最后一家, 不能偷第一家
rob = dp2[1] + num
not_rob = max(dp2[0], dp2[1])
dp2 = [rob, not_rob]
return max(dp1[0], dp1[1], dp2[0], dp2[1])

215. Kth Largest Element in an Array

Description: 找出无序数组中第k大的数

Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.

Example 1:

Input: [3,2,1,5,6,4] and k = 2
Output: 5
Example 2:

Input: [3,2,3,1,2,4,5,5,6] and k = 4
Output: 4

解法一: 小顶堆

时间复杂度: $O(nlogk)$, 堆的插入复杂度为 $O(logk)$, 最多需要进行 $n$ 次插入.
空间复杂度: $O(k)$, 堆的大小

构建一个大小为 $k$ 的小顶堆, 对于任意一个新来的元素, 如果该元素大于堆顶, 将则堆顶退出, 并将该元素插入. 最终, 堆内的元素就是数组的最大的前 $k$ 个元素, 而堆顶刚好为第 $k$ 大的元素.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int> > heap_k;
for(auto num : nums){
if(heap_k.size() < k){
heap_k.push(num);
}else if(num > heap_k.top()){
heap_k.pop();
heap_k.push(num);
}
}
return heap_k.top();
}
};

解法二: 部分排序(nth_element)

http://www.voidcn.com/article/p-qyrpnkse-gx.html

最优解法

时间复杂度: 平均为 $O(n)$. nth_element 的时间复杂度为 $T(n) = T(n/2) + O(n) = O(n) + O(n/2) + O(n/4) + …$, 也就是 $O(n)$.
空间复杂度: $O(1)$, 不占用额外空间

直接调用 STL 的部分排序算法nth_element.
nth_element算法将重新排列区间[first, last)的序列元素, 算法执行完毕后, 会使得

  • 第 $k$ 个位置的元素在最终的算法执行完毕后, 和整个区间完全排序后该位置的元素相同.
  • 这个新的nth元素之前的所有元素均 <= (>=) nth元素之后的所有元素.
    但是该算法并不保证位于第 $k$ 个元素两边区间的元素有序. 该算法和 partial_sort 算法之间一个很大的区别在于: nth_element对于除第 $k$ 位置的元素之外的区间元素的顺序不做保证, 而partial_sort排序后会使得前 $m$ 个数的子区间是有序的. 正因为如此, 在需要无序的前 top_k 个值时, nth_element 相对于 partial_sort 要更快.(只需要找第 $k$ 个值, 其前面的元素即为 top_k, 时间复杂度为 $O(n)$). 如果需要有序, 也可以先使用 nth_element, 再对前 k 个数组排序, 总的复杂度为 $O(n+klogk)$
1
2
3
4
5
6
7
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
std::nth_element(nums.begin(), nums.begin()+k-1, nums.end(), std::greater<int>());
return nums[k-1];
}
};

解法三: 基于 Partition

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

该解法和解法二思路相同, 只不过是我们自己手动实现 Partition 的算法逻辑, 而不是调用 STL 函数.

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 findKthLargest(vector<int>& nums, int k) {
int low=0, high=nums.size()-1;
int pth = Partition(nums, low, high);
while(pth != k-1){
if(pth > k-1)
high = pth-1;
else
low = pth+1;
pth = Partition(nums, low, high);
}
return nums[pth];
}

int Partition(vector<int> &nums, int low, int high){
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;
}
};

217. Contains Duplicate

Description: 判断数组中是否有重复元素

Given an array of integers, find if the array contains any duplicates.

Your function should return true if any value appears at least twice in the array, and it should return false if every element is distinct.

Example 1:

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

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

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

解法一: 暴力

时间复杂度: $O(n^2)$, 暴力求解, 双重循环
空间复杂度: $O(1)$, 无需额外空间

时间超限, 无法通过 OJ

解法二: 排序+遍历

时间复杂度: $O(nlogn)$, 先排序, 然后遍历看是否有相邻元素相等, 即 $O(nlogn + n)$, 也就是 $O(nlogn)$.
空间复杂度: $O(1)$, 基于不同的排序算法决定, 使用堆排序则为 $O(1)$.

解法三: unordered_set(哈希)

时间复杂度: $O(n)$, 遍历一遍数组, 在 unordered_set 中查询的复杂度为常数
空间复杂度: $O(n)$, unordered_set占用额外空间

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
std::unordered_set<int> nums_set;
for(auto num : nums){
if(nums_set.find(num) == nums_set.end())
nums_set.insert(num);
else
return true;
}
return false;
}
};

218. The Skyline Problem

Description: 天际线问题

A city’s skyline is the outer contour of the silhouette formed by all the buildings in that city when viewed from a distance. Now suppose you are given the locations and height of all the buildings as shown on a cityscape photo (Figure A), write a program to output the skyline formed by these buildings collectively (Figure B).

The geometric information of each building is represented by a triplet of integers [Li, Ri, Hi], where Li and Ri are the x coordinates of the left and right edge of the ith building, respectively, and Hi is its height. It is guaranteed that 0 ≤ Li, Ri ≤ INT_MAX, 0 < Hi ≤ INT_MAX, and Ri - Li > 0. You may assume all buildings are perfect rectangles grounded on an absolutely flat surface at height 0.

For instance, the dimensions of all buildings in Figure A are recorded as: [ [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] ] .

The output is a list of “key points” (red dots in Figure B) in the format of [ [x1,y1], [x2, y2], [x3, y3], … ] that uniquely defines a skyline. A key point is the left endpoint of a horizontal line segment. Note that the last key point, where the rightmost building ends, is merely used to mark the termination of the skyline, and always has zero height. Also, the ground in between any two adjacent buildings should be considered part of the skyline contour.

For instance, the skyline in Figure B should be represented as:[ [2 10], [3 15], [7 12], [12 0], [15 10], [20 8], [24, 0] ].

解法一: multiset

时间复杂度: $O(nlogn)$, 拆分三元组到二元组为 $O(n)$, 排序为 $O(nlogn)$, 更新轮廓节点为 $O(nlogn)$ (插入高度为 $O(log)$, 总共有 $O(n)$ 组高度).
空间复杂度: $O(n)$, 存储高度的二元组 vector, 以及维护当前建筑物高度顺序的 multiset.

首先我们将表示建筑物的所有三元组(Li, Ri, Hi)进行拆分, 将其分成(Li, -Hi), (Ri, Hi)的两个二元组, 将这些二元组存放在一个数组 vector 中, 然后按照 x 轴的下标进行排序, 注意如果当一个建筑物的右侧和另一个建筑物的左侧重叠时, 我们为了不丢失当前建筑物的高度, 必须先考虑将另一个建筑物的左侧添加进 multiset 里, 然后获取最高高度. 接着在下一次循环时, 再将重合的右侧边界对应的建筑物剔除, 因此我们需要令二元组中的左侧为负, 使其在排序时可以排到前面.
得到有序的建筑物二元组序列以后, 我们遍历该序列, 如果遇到了某个建筑物的左侧边界, 则将该边界对应建筑物的高度加入到 multiset 中, 如果遇到了某个建筑物的右侧边界, 则将对应建筑物的高度剔除. 假设我们已经得到了前 i 个坐标的建筑物组成的轮廓坐标点, 现在来到第 i+1 个坐标, 只有可能对应下面几种情况:

  • i+1 坐标上新来的建筑物(遇到该建筑物左侧就行)完全被之前的建筑物覆盖, 此时不更新 res 轮廓. 说明添加了该建筑物后, 并不改变当前建筑群的最高高度.
  • i+1 坐标上新来的建筑物比当前建筑群最高的高度还要高, 则需要记录当前的点.
  • i+1 坐标上没有新来建筑物, 但是有一个建筑物遇到了右侧边界, 此时建筑群的高度会变成第二高建筑物的高度, 同样需要记录当前的坐标点.
  • i+1 坐标上既没有新来建筑物, 也没有遇到建筑物右侧, 此时无需记录任何值, 可继续探测 i+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
28
29
30
class Solution {
public:
vector<pair<int, int>> getSkyline(vector<vector<int>>& buildings) {
vector<pair<int, int>> heights, res; // height 用于存放建筑物的高度, res存放结果
multiset<int> m; // 用 multiset 数据结构来维护当前x坐标之前的建筑物高度
for(auto &b : buildings){
//用负高度代表当前的边是左侧的边. 因为后面有排序, 所以必须令左侧为负, 而不能令右侧为负.
//因为当x坐标相同时, 当前的building的右侧还不能剔除,
//否则, 有可能"低估" 轮廓高度所以要将左侧的排在前面
heights.push_back({b[0], -b[2]});
heights.push_back({b[1], b[2]});
}
std::sort(heights.begin(), heights.end()); // 按照x坐标排序, 当x一样时, 按照高度排序
int pre = 0; // pre代表之前的最高建筑物的高度, 初始为0
int cur; // cur 代表当前的最高建筑物的高度, 会在for循环中赋值.
m.insert(0); // 开始的时候, m中的最高高度为0
for(auto &h : heights){
if(h.second < 0) m.insert(-h.second); // 如果是左侧边, 则加入当前建筑物高度集合, 并自动排序
else m.erase(m.find(h.second)); // 如果遇到了右侧边, 则将对应的建筑物从当前建筑物集合内剔除
// 注意, 这里在使用erase时, 是先找到key值匹配的某一个元素的迭代器(多个存在多个匹配), 然后再删除
// 如果直接使用 erase(key) 的话, 则会将满足key值的所有元素都擦除, 这样会导致程序出错.
cur = * m.rbegin(); // 获取当前的最大高度
if(cur != pre){ // 说明此时要么新加入了更高的高度, 要么被用于最高高度的建筑物被剔除, 需要更新轮廓点
res.push_back({h.first, cur}); //新更新的轮廓点的x坐标即为当前h的x坐标.
pre = cur; // 更新pre
}
}
return res;
}
};

解法二: priority_queue(堆)

在解法一中, 用了 multiset, 之所以不用 priority_queue 的原因是因为, C++ 的 priority_queue 容器并没有提供erase或者find之类的方法, 因此, 在删除指定高度时, 比较麻烦. 而 multiset 不仅完成堆的功能(最后一个元素就是最大的), 同时还支持在对数复杂度时间内删除指定的高度.

因此, 如果想要使用 priority_queue 的话, 就需要调整算法的逻辑, 下面是使用 priority_queue 的解法:

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 {
public:
vector<pair<int, int>> getSkyline(vector<vector<int>>& buildings) {
std::vector<std::pair<int, int>> res;
int cur = 0, cur_X, cur_H = -1, len = buildings.size();
std::priority_queue<std::pair<int, int>> liveBlg;
while (cur < len or !liveBlg.empty()) {
cur_X = liveBlg.empty() ? buildings[cur][0] : liveBlg.top().second;

if (cur >= len or buildings[cur][0] > cur_X) {
while (!liveBlg.empty() && (liveBlg.top().second <= cur_X)) {
liveBlg.pop();
}
} else {
cur_X = buildings[cur][0];
while (cur < len && buildings[cur][0] == cur_X) {
liveBlg.push({buildings[cur][2], buildings[cur][1]});
cur++;
}
}
cur_H = liveBlg.empty() ? 0 : liveBlg.top().first;
if (res.empty() or (res.back().second != cur_H)) {
res.push_back({cur_X, cur_H});
}
}
return res;
}
};

221. 最大正方形

题目链接: https://leetcode-cn.com/problems/maximal-square/

解法一: 动态规划

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

申请一个长度为矩阵列数的一维数组dp, dp[j]代表以matrix[i][j]为结尾的正方形的边长, 于是当我们计算矩阵中以某个点为右下角的正方形边长时, 就可以利用右上角已经计算过的变量直接获取相应的信息, 这里在使用dp时, 需要注意的一点是, 由于仅仅需要右上角的值, 因此, 每次新的dp生成时, 都要向后移一位, 前面补0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def maximalSquare(self, matrix: List[List[str]]) -> int:
if len(matrix) == 0: return 0
m = len(matrix)
n = len(matrix[0])

dp = [0] * (n+1)
res = 0
for i in range(m):
for j in range(n):
if matrix[i][j] != '0': # 注意, 字符 '0' 在 bool 中是 True 的, 所以不能直接用 if matrix[i][j]
edge = dp[j]
k = 1
while (k <= edge and matrix[i-k][j]!='0' and matrix[i][j-k]!='0'): # 利用之前已经求得的正方形基础上算当前正方形边长
k += 1
dp[j] = k # 更新正方形边长
else:
dp[j] = 0
res = max(res, dp[j]) # 更新 res
dp = [0] + dp[0:-1] # dp 数组最前方加0, 其他元素后移, 最后一个元素再后面用不到, 舍去
return res*res

解法二: 优化的动态规划

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

由于仅仅需要右上角的值, 因此我们可以把dp压缩到一个常数, 此时matrix的便利方式就不能是先行后列了, 而应该是沿着对角线进行遍历才行.

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
class Solution:
def maximalSquare(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
while ii < m and jj < n:
if matrix[ii][jj] == '0':
dp = 0
else:
edge = dp
k = 1
while (k <= edge and matrix[ii-k][jj] == '1' and matrix[ii][jj-k] == '1'):
k += 1
dp = k
res = max(res, dp)
ii += 1
jj += 1
return res*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
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
if (matrix.size() == 0) return 0;
int m = matrix.size();
int n = matrix[0].size();
int res = 0, i = 0, j = 0, ii = 0, jj = 0;
while (i < m or j < n) {
if (i < m) {
ii = i;
jj = 0;
i++;
} else if (j < n) {
ii = 0;
jj = j;
j++;
}
int dp = 0;
while (ii < m and jj < n) {
if (matrix[ii][jj] == '0')
dp = 0;
else {
int k = 1;
while (k <= dp and matrix[ii-k][jj] == '1' and matrix[ii][jj-k] == '1') {
k++;
}
dp = k;
}
ii++; jj++;
res = std::max(res, dp);
}
}
return res*res;
}
};

226. 翻转二叉树

题目链接: https://leetcode-cn.com/problems/invert-binary-tree/

谷歌: 我们90%的工程师使用您编写的软件(Homebrew), 但是您却无法在面试时在白板上写出翻转二叉树这道题, 这太糟糕了.

解法一: 递归

Python 实现:

编写递归函数, 先翻转两个子树, 再把左右子树翻转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:

def invert(root):
if root == None:
return
invert(root.right)
invert(root.left)
root.right, root.left = root.left, root.right
return
invert(root)
return root

先翻转左右节点, 再翻转子树也可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:

def invert(root):
if root == None:
return
root.right, root.left = root.left, root.right
invert(root.right)
invert(root.left)
return
invert(root)
return root

C++ 实现(先根, 其他同理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 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* invertTree(TreeNode* root) {
if (root == nullptr)
return root;
std::swap(root->right, root->left);
invertTree(root->left);
invertTree(root->right);
return root;
}
};

解法二: 迭代

该题用递归非常好解, 所以如果面试问道, 一定会考察迭代解法. 核心思想就是遍历二叉树的每一个节点, 然后把节点的的左右子树交换即可, 故而有先根, 中根, 后根遍历三种解法, 分别如下:

先根遍历
Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:
stack = []
node = root
while node != None or len(stack) > 0:
while node != None:
node.left, node.right = node.right, node.left # 先根遍历
stack.append(node)
node = node.right # 左右子树已经交换了, 所以要入栈原来的左子树, 就需要入栈right

node = stack.pop()
node = node.left # 注意, 由于已经将左右子树交换了, 所以这里的左子树实际是原来未入栈的右子树
return root

实际上, 树的左右孩子是等价的, 因此, 即使这里入栈交换后的left, 也没有问题, 只要保证后面即将入栈的和之前入栈是相反的孩子即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:
stack = []
node = root
while node != None or len(stack) > 0:
while node != None:
node.left, node.right = node.right, node.left # 先根遍历
stack.append(node)
node = node.left # 实际上, 树的左右孩子是等价的, 因此, 即使这里入栈交换后的left, 也没有问题,
node = stack.pop()
node = node.right # 只要保证这里即将入栈的和之前入栈是相反的孩子即可
return 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
/**
* 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* invertTree(TreeNode* root) {
std::stack<TreeNode*> s;
auto node = root;
while (node != nullptr or !s.empty()) {
while (node != nullptr) {
std::swap(node->left, node->right);
s.push(node);
node = node->left;
}
node = s.top(); s.pop();
node = node->right;
}
return root;
}
};

中根遍历
Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:
stack = []
node = root
while node != None or len(stack) > 0:
while node != None:
stack.append(node)
node = node.left
# 中根遍历
node = stack.pop()
node.left, node.right = node.right, node.left
node = node.left # 注意, 由于已经将左右子树交换了, 所以这里的左子树实际是原来未入栈的右子树
return 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
/**
* 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* invertTree(TreeNode* root) {
std::stack<TreeNode*> s;
auto node = root;
while (node != nullptr or !s.empty()) {
while (node != nullptr) {
s.push(node);
node = node->left;
}
node = s.top(); s.pop();
std::swap(node->left, node->right);
node = node->left;
}
return root;
}
};

后根遍历
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 invertTree(self, root: TreeNode) -> TreeNode:
stack = []
node = root
pre = None
while node != None or len(stack) > 0:
while node != None:
stack.append(node)
node = node.left # 实际上, 树的左右孩子是等价的, 因此, 即使这里入栈交换后的left, 也没有问题,

if pre == stack[-1].right or stack[-1].right == None: # 只有当右子树为空或者已经访问过时, 才能访问根
r_node = stack.pop() # 注意, 访问根时, 不能将根的值赋给 node, 否则外部node会陷入死循环
pre = r_node
r_node.left, r_node.right = r_node.right, r_node.left # 交换左右子树
else:
node = stack[-1].right
return 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
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 {
public:
TreeNode* invertTree(TreeNode* root) {
std::stack<TreeNode*> s;
auto node = root;
TreeNode* pre = nullptr;
while (node != nullptr or !s.empty()) {
while (node != nullptr) {
s.push(node);
node = node->left;
}
auto tmpnode = s.top();
if (tmpnode->right == nullptr or tmpnode->right == pre) {
s.pop();
pre = tmpnode;
std::swap(tmpnode->right, tmpnode->left);
} else {
node = tmpnode->right;
}
}
return root;
}
};

227. Basic Calculator II

Description: 基本计算器(二)

Implement a basic calculator to evaluate a simple expression string.

The expression string contains only non-negative integers, +, -, * , / operators and empty spaces . The integer division should truncate toward zero.

Example 1:

Input: “3+2*2”
Output: 7
Example 2:

Input: “ 3/2 “
Output: 1
Example 3:

Input: “ 3+5 / 2 “
Output: 5

解法一: 栈

时间复杂度: $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
33
34
35
36
37
38
39
40
class Solution {
public:
int calculate(string s) {
stack<int> cal_s;
for(int i=0; i<s.size(); ){
while(i!=s.size() && s[i] == ' ') i++; // 跳过空格
if(i==s.size()) break; // 达到字符串尾部, 直接跳出
char op;
if(cal_s.empty()) op = '+';
else op = s[i++];
int num = 0;
while(s[i] == ' ') i++; // 跳过空格
while( i!=s.size() && s[i] <= '9' && s[i] >= '0'){
num = num*10 + s[i++] - '0';
}
int pre_num=0;
switch(op){
case '+': cal_s.push(num); break;
case '-': cal_s.push(-num); break;
case '* ':
pre_num = cal_s.top(); cal_s.pop();
cal_s.push(pre_num * num);
break;
case '/':
pre_num = cal_s.top(); cal_s.pop();
cal_s.push(pre_num / num);
break;
default:
return op; //error
}
}
int res = 0;
while(!cal_s.empty()){
res += cal_s.top();
cal_s.pop();
}
return res;

}
};

解法二: 字符串流

时间复杂度: $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
class Solution {
public:
int calculate(string s) {
std::istringstream in("+"+s+"+");
long long sum = 0, pre_num = 0, num;
char op;
while(in>>op) {
if (op == '+' or op == '-') {
sum += pre_num;
in >> pre_num;
int sign = (op == '+' ? 1 : -1);
pre_num * = sign;
} else {
in >> num;
if (op == '*') {
pre_num * = num;
} else if (op == '/') {
pre_num /= num;
}
}
}
return static_cast<int>(sum);
}
};

230. Kth Smallest Element in a BST

Description: 找出二叉搜索树中的最小元素

Given a binary search tree, write a function kthSmallest to find the kth smallest element in it.

Note:
You may assume k is always valid, 1 ≤ k ≤ BST’s total elements.

Example 1:

1
2
3
4
5
6
7
Input: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
Output: 1

Example 2:

1
2
3
4
5
6
7
8
9
Input: root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
Output: 3

Follow up:
What if the BST is modified (insert/delete operations) often and you need to find the kth smallest frequently? How would you optimize the kthSmallest routine?

解法一: 非递归中根遍历

时间复杂度: $O(k)$, 遍历到第 $k$ 个元素为止
空间复杂度: $O(k)$, 栈中最多存储 $k$ 个元素.

非递归中根遍历二叉搜索树, 当遍历到第k个元素时, 将其返回.

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 {
public:
int kthSmallest(TreeNode* root, int k) {
if(k<1) return INT_MIN;// error
stack<TreeNode*> s;
TreeNode* cur = root;
int count=0;
while(cur!=nullptr || !s.empty()){
while(cur!=nullptr){
s.push(cur);
cur = cur->left;
}
if(!s.empty()){
cur = s.top(); s.pop();
if(++count == k){
return cur->val;
}
cur = cur->right;
}
}
return INT_MIN;// error
}
};

解法二: 递归中根遍历

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int kthSmallest(TreeNode* root, int k) {
int count = 0; int res = 0;
helper(root, count, k, res);
return res;
}

void helper(TreeNode * root, int &count, int &k, int &res){
if(count==k || root == nullptr) return; // 如果已经统计了k个, 则直接返回
helper(root->left, count, k, res);
if(count==k) return; // 如果已经统计了k个, 则直接返回 // 加上该语句可省去后面的过程, 加速迭代结束, 当然不加也可以
else if(++count == k){ // 访问当前节点
res = root->val;
return;
}
if(count!=k) helper(root->right, count, k, res); // 如果已经统计了k个, 则不再遍历右子树
}
};

更简洁的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
void helper(TreeNode* root, int& count, int k, int& res) {
if (root == nullptr) return;
helper(root->left, count, k, res);
count++;
if(k == count) {
res = root->val;
return;
}
helper(root->right, count, k, res);
}

public:
int kthSmallest(TreeNode* root, int k) {
int count = 0; int res = 0;
helper(root, count, k, res);
return res;
}
};

解法三: 二叉搜索

时间复杂度: $O(logn)+ O(n)$, 搜索的复杂度为树的高度, 但是计算count的复杂度为 $O(n)$.
空间复杂度: $O(logn)$, 递归占用的空间, 若采用非递归实现, 则空间复杂度为 $O(1)$.

二叉搜索, 统计当前节点之前的元素个数, 如果大于 $k$, 则继续在左子树中搜索第 $k$ 小的元素, 如果 count 小于 $k$ , 则在右子树中搜索第 $k-count-1$ 小的元素.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int kthSmallest(TreeNode* root, int k) {
int count = countNode(root->left); // 左子树元素个数
if(count+1 > k){
return kthSmallest(root->left, k);
}else if(count+1 < k){
return kthSmallest(root->right, k - count - 1);
}else{
return root->val;
}
}

int countNode(TreeNode* root){
if(root==nullptr) return 0;
return 1+countNode(root->left)+countNode(root->right);
}
};

解答Follow up

方法一:

根据解法三我们可以知道, 在计算子树节点个数的时候 int count = countNode(root->left);, 有很多的重复计算, 因此, 我们可以修改树的结构定义, 使得每个节点都持有其左子树中的节点个数, 那么在查找第 $k$ 小的元素的时候, 就可以用 $O(1)$ 的时间复杂度获取到左子树的节点个数, 因此, 最终查询第 $k$ 小的时间复杂度变为 $O(logn)$.

方法二:

在中根遍历的同时, 用一个大小为 $k$ 的大顶堆(priority_queue), 这些可以将二叉搜索树中最小的 $k$ 个数存储起来, 并且可以用 $O(1)$ 的时间复杂度获取到第 $k$ 小的元素. (二叉搜索树的中根遍历下, 未遍历到的都是较大的元素, 因此无需遍历整个树, 只需要遍历到第 $k$ 个元素即可). 在对树进行修改时, 同步更新大顶堆, 前者时间复杂度为 $O(logn)$, 后者为 $O(logk)$.

234. Palindrome Linked List

Description: 回文链表判断

Given a singly linked list, determine if it is a palindrome.

Example 1:

1
2
Input: 1->2
Output: false

Example 2:

1
2
Input: 1->2->2->1
Output: true

Follow up:
Could you do it in O(n) time and O(1) space?

解法一: 借助辅助数组

时间复杂度: $O(n)$, 两次遍历
空间复杂度: $O(n)$, 额外数组

最简单的做法就是遍历链表, 将其转换成一个可随机访问的数组, 然后进行回文串的判断.

解法二: 不借助辅助数组

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

先利用两个指针变量slowfast找到链表的中点(slow每次走一步, fast每次走两步), 然后将后半段逆置, 接着将前半段和后半段进行比较. 最后根据具体需要将链表后半段复原. (在实际工作中, 不存在 $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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* node) {
ListNode* prev = nullptr;
while(node != nullptr) {
auto tmp = node->next;
node->next = prev;
prev = node;
node = tmp;
}
return prev;
}
bool isPalindrome(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;

while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
if (fast != nullptr) { // 奇数个节点, 始终令slow指向后半段的开始节点
slow = slow->next;
}
slow = reverseList(slow); // 令slow指向后半段逆置后的开始节点
fast = head;
while(slow != nullptr && fast->val == slow->val) {
fast = fast->next;
slow = slow->next;
}
return slow == nullptr ? 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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution {
public:
bool isPalindrome(ListNode* head) {
if(head==nullptr || head->next==nullptr) return true;

ListNode* slow = head;
ListNode* fast = head->next;

while(fast!=nullptr && fast->next!=nullptr){
slow = slow->next;
fast = fast->next->next;
}
ListNode * rail = slow; // 记录前半段的最后一个节点, 以便复原链表
slow = slow->next; // 令slow指向回文串后半段的第一个节点
ListNode *rhead = reverseList(slow); // 令fast 指向回文串后半段逆置后的连接头(奇数回文串时, 中间的节点算作前半段)
slow = head;
fast = rhead;
bool res=true;
while(slow!=nullptr && fast!=nullptr){
if(slow->val != fast->val){
res = false;
break;
}
slow = slow->next;
fast = fast->next;
}
rail->next = reverseList(rhead); // 复原链表
return res;
}

ListNode *reverseList(ListNode *cur){
ListNode* next = cur->next;
ListNode* pre = nullptr;
while(cur != nullptr){
cur->next = pre;
pre = cur;
cur = next;
if(next!=nullptr) next = next->next;
}
return pre;
}
};

236. Lowest Common Ancestor of a Binary Tree

Description: 查找二叉树中任意两个节点的公共祖先

Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.

According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”

Given the following binary tree: root = [3,5,1,6,2,0,8,null,null,7,4]

1
2
3
4
5
6
7
     _______3______
/ \
__5__ __1__
/ \ / \
6 2 0 8
/ \
7 4

Example 1:

1
2
3
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
Output: 3
Explanation: The LCA of nodes 5 and 1 is 3.

Example 2:

1
2
3
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
Output: 5
Explanation: The LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition.

Note:

  • All of the nodes values will be unique.
  • p and q are different and both values will exist in the binary tree.

解法一: 递归

时间复杂度: $O(n)$, 需遍历 $n$ 个节点.(任何情况下都需遍历n个节点)
空间复杂度: $O(n)$, 需进行 $n$ 次递归调用.( $n$ 包含空节点)

对于最小公共祖先来说, 它相对于其他祖先有一个特点, 即节点 pq 只可能是以下面三种情况分布在树中:

  • pq分别处于当前节点的左子树 右子树之中.
  • p为当前节点, q处于当前节点的左子树 右子树之中
  • q为当前节点, p处于当前节点的左子树 右子树之中

而对于其他祖先来说, 绝对不可能出现上面三种情况, 因为 pq一定处于其他祖先的同一侧子树之中., 即要么都处在右子树中, 要么都处在左子树中. 因此我们可以用pq在当前节点构成的子树中的分布情况来判断是否为最小祖先.

**注意, 题目中说了p, q一定存在, 并且树中节点都是唯一的, 因此, 下面的代码无需对p, q进行存在性检查.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 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* res = nullptr;
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
res = nullptr;
recurseHelper(root, p, q);
return res;
}

bool recurseHelper(TreeNode * root, TreeNode * p, TreeNode * q){
if(root == nullptr) return false; // 遇到空节点, 说明没有目标节点
int left = recurseHelper(root->left, p, q) ? 1 : 0; // 左子树中有p或q
int right = recurseHelper(root->right, p, q) ? 1 : 0; // 右子树中有p或q
int mid = (root==p || root==q) ? 1 : 0; // 找到了p或q, 这里相当于做了存在性检查

if( left+right+mid >= 2) res = root; // 如果左,右或当前节点中有两个以上为true, 则说明当前节点为最小公共祖先
return (left+right+mid)>0; // 只要不是空节点, 就可以返回 true.
}
};

上面用了是将res作为成员函数进行赋值, 更好的做法是用指针引用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
TreeNode* res = nullptr;
lcaHelper(root, p, q, res);
return res;
}

bool lcaHelper(TreeNode* root, TreeNode* p, TreeNode* q, TreeNode*& res) {
if (root == nullptr) return false;
int left= lcaHelper(root->left, p, q, res) ? 1 : 0;
int right = lcaHelper(root->right, p, q, res) ? 1 : 0;
int mid = (root == p || root == q) ? 1 : 0;

if (left+right+mid >= 2) res = root;
return (left+right+mid) > 0;
}
};

解法二: 迭代(存储父节点)

时间复杂度: $O(n)$, 最坏需遍历 $n$ 个节点.
空间复杂度: $O(n+n+n) = O(n)$, 栈, 哈希表, 集合的空间复杂度在最坏情况下均为 $O(n)$

如果我们能够获取到父节点, 那么我们就可以反向遍历qp来访问他们的祖先节点. 那么, 第一个公共的祖先节点就一定是 LCA node. 我们可以将节点的父节点指针保存在一个字典(hash)当中. 具体的算法流程如下所示:

  1. 从根节点开始遍历整个树(任意一种遍历算法都可以, 只要能找到pq即可);
  2. 直到找到节点pq之前, 将所有节点的父节点都保存在字典(hash)中;
  3. 一旦我们找到了qq, 我们就将所有p的祖先节点放入了一个集合(set)当中;
  4. 然后, 我们反向遍历q的祖先节点, 当找到一个存在时集合中的祖先节点时, 该节点就是第一个公共的租店节点, 也就是 LCA node, 将其返回.
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:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
std::stack<TreeNode*> s;
std::unordered_map<TreeNode*, TreeNode*> hash;
std::set<TreeNode*> ancestors;

if (root != nullptr) {
s.push(root);
hash.insert({root, nullptr});
}
else return nullptr;
while(hash.find(p) == hash.end() || hash.find(q) == hash.end()) {
TreeNode* node = s.top();
s.pop();
if (node->left != nullptr) {
hash.insert({node->left, node});
s.push(node->left);
}
if (node->right != nullptr) {
hash.insert({node->right, node});
s.push(node->right);
}
}

TreeNode* parent = p;
while (parent != nullptr) {
ancestors.insert(parent);
parent = hash[parent];
}

TreeNode* lcaNode = q;
while (ancestors.find(lcaNode) == ancestors.end()) {
lcaNode = hash[lcaNode];
}
return lcaNode;
}
};

解法三: 迭代(不存储父节点)

时间复杂度: $O(n)$, 最坏需遍历 $n$ 个节点.
空间复杂度: $O(n)$, 采用后序遍历, 只需维护一个栈, 空间复杂度在最坏情况下为 $O(n)$

在解法二中, 我们是通过反向遍历的方法来查找 LCA 的, 实际上我们可以省去这一步, 直接要一个指针时刻指向可能的 LCA, 当我们找到pq两个节点时, 我们可以直接返回当前的 LCA. 具体算法步骤如下:

  1. 从根节点开始;
  2. (root, root_state)压入栈中, root_state定义了根节点的剩余的子节点是否可以被遍历;
  3. 当栈非空时, 查看栈顶元素(parent_node, parent_state);
  4. 在遍历parent_node的任何子节点之前, 首先确认parent_node是否是节点pq;
  5. 当首次找到pq时, 将标志变量one_node_found设置为True. 同时根据栈中的节点跟踪 LCA (栈中的所有元素都是当前节点的祖先);
  6. 当再次找到pq时, 说明我们已经将两个节点都找到了, 此时返回 LCA node.
  7. 无论何时访问parent_node的子节点, 都需要将(parent_node, updated_parent_state)更新到栈中.
  8. A node finally gets popped off from the stack when the state becomes BOTH_DONE implying both left and right subtrees have been pushed onto the stack and processed. If one_node_found is True then we need to check if the top node being popped could be one of the ancestors of the found node. In that case we need to reduce LCA_index by one. Since one of the ancestors was popped off

Whenever both p and q are found, LCA_index would be pointing to an index in the stack which would contain all the common ancestors between p and q. And the LCA_index element has the lowest ancestor common between p and q.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Solution {
enum class State {
BOTH_PENDING = 2, // 代表左右子节点均未访问
LEFT_DONE = 1, // 代表已经访问了一个节点
BOTH_DONE = 0 // 代表两个子节点都已经访问, 当前节点可以出栈
};
public:

TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
std::stack<std::pair<TreeNode*, State> > s;
s.push(std::make_pair(root, State::BOTH_PENDING));

bool one_node_found = false; // 标记是否找到p或q
TreeNode* LCA = nullptr; // 跟踪LCA
TreeNode* child_node = nullptr;

while (!s.empty()) {
auto top = s.top();
TreeNode* parent_node = top.first;
State parent_state = top.second;

if (parent_state != State::BOTH_DONE) {
if (parent_state == State::BOTH_PENDING) {
if (parent_node == p || parent_node == q) {
// 找到了 p 或 q 中的一个, 如果是第二次找到, 则可以返回LCA
// 如果是第一次找到, 则更新 LCA.
if (one_node_found) {
return LCA;
} else {
one_node_found = true;
LCA = parent_node;
}
}
// 当状态为 BOTH_PENDING, 说明左右子树都没遍历, 应先遍历左子树
child_node = parent_node->left;
} else {
// 如果状态为 LEFT_DONE, 说明已经遍历完左子树, 该遍历右子树
child_node = parent_node->right;
}

s.pop();
parent_state = static_cast<State>(static_cast<int>(parent_state) - 1);
s.push(std::make_pair(parent_node, parent_state));

if (child_node != nullptr) {
s.push(std::make_pair(child_node, State::BOTH_PENDING));
}
} else {
// state 为 BOTH_DONE, 说明当前节点可以出栈
// 如果当前节点为LCA, 则需要更新LCA
auto node = s.top().first;
s.pop();
if (LCA == node && one_node_found) {
LCA = s.top().first;
}
}
}
return nullptr;
}
};

237. Delete Node in a Linked List

Description: 删除链表中的某个节点

Write a function to delete a node (except the tail) in a singly linked list, given only access to that node.

Given linked list — head = [4,5,1,9], which looks like following:

1
4 -> 5 -> 1 -> 9

Example 1:

1
2
3
Input: head = [4,5,1,9], node = 5
Output: [4,1,9]
Explanation: You are given the second node with value 5, the linked list should become 4 -> 1 -> 9 after calling your function.

Example 2:

1
2
3
Input: head = [4,5,1,9], node = 1
Output: [4,5,9]
Explanation: You are given the third node with value 1, the linked list should become 4 -> 5 -> 9 after calling your function.

Note:
The linked list will have at least two elements.
All of the nodes’ values will be unique.
The given node will not be the tail and it will always be a valid node of the linked list.
Do not return anything from your function.

解法一: 复制+跳过节点

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

这是一道非常取巧(也可以说是投机)的题, 题目给的参数是需要删除的节点指针, 同时该指针不会是最后一个节点, 因此我们可以利用先复制, 再跳过的方式实现删除.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
void deleteNode(ListNode* node) {
node->val = node->next->val; // 题目假设node 不是最后一个节点
node->next = node->next->next; // 跳过node节点
}
};

238. Product of Array Except Self

Description: 计算数组内其他元素之积(不能使用除法)

Given an array nums of n integers where n > 1, return an array output such that output[i] is equal to the product of all the elements of nums except nums[i].

Example:

1
2
Input:  [1,2,3,4]
Output: [24,12,8,6]

Note: Please solve it without division and in O(n).

Follow up:
Could you solve it with constant space complexity? (The output array does not count as extra space for the purpose of space complexity analysis.)

解法一: 借助辅助数组

时间复杂度: $O(n)$, 遍历两次数组
空间复杂度: $O(n)$, 额外申请 $n$ size 的数组(不计算 res 的空间占用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> from_begin(n);
vector<int> from_end(n);
from_begin[0] = 1;
from_end[n-1] = 1;
for(int i = 1; i<n; i++){
from_begin[i] = from_begin[i-1] * nums[i-1]; // from_begin[i] 为 nums[i] 之前的所有元素的乘积
from_end[ n-i-1] = from_end[n-i] * nums[n-i]; // from_end[i] 为 nums[i] 之后所有元素的乘积
}

for(int i=0 ;i<n ; i++){
from_end[i] = from_begin[i] * from_end[i]; // 用 nums[i] 之前的所有元素的乘积和 nums[i] 之后所有元素的乘积相乘
}
return from_end;
}
};

解法二: 用一个变量代替数组

时间复杂度: $O(n)$, 两次遍历
空间复杂度: $O(1)$, 用变量代替数组

对解法一进行改写, 具体的做法是用一个变量来维护 from_begin 数组中的值(当然也可以选择代替 from_end)

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> productExceptSelf(vector<int>& nums) {
int n = nums.size();
// vector<int> from_begin(n);
vector<int> from_end(n);
// from_begin[0] = 1;
from_end[n-1] = 1;
for(int i = 1; i<n; i++){
// from_begin[i] = from_begin[i-1] * nums[i-1]; // from_begin[i] 为 nums[i] 之前的所有元素的乘积
from_end[ n-i-1] = from_end[n-i] * nums[n-i]; // from_end[i] 为 nums[i] 之后所有元素的乘积
}
int from_begin = 1; // 用一个变量代替 from_begin 数组的作用
for(int i=0 ;i<n ; i++){
from_end[i] = from_begin * from_end[i]; // 用 nums[i] 之前的所有元素的乘积和 nums[i] 之后所有元素的乘积相乘, 作为结果
from_begin = from_begin * nums[i]; // 维护 from_begin的值
}
return from_end;
}
};

解法三: 用两个变量代替数组

时间复杂度: $O(n)$, 一次遍历
空间复杂度: $O(1)$, 不计算结果数组的空间

观察到解法二的做法, 虽然将空间复杂度压缩到 $O(1)$, 但是仍然使用了两次for循环, 实际上, 我们可以同时用变量from_begin和变量from_end替换掉对应的数组, 并且同一个for循环中更新这两个变量, 如下所示.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
int from_begin = 1;
int from_end = 1;
vector<int> res(n,1);
for(int i=0; i<n; i++){ // 同时从前后分别计算, from_begin记录i之前的元素之和, from_end记录i之后的元素之和
res[i] = from_begin * res[i];
from_begin = from_begin * nums[i];
res[n-i-1] = from_end * res[n-i-1];
from_end = from_end * nums[n-i-1];
}
return res;
}
};

239. Sliding Window Maximum

Description: 滑动窗口的最大值

Given an array nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Return the max sliding window.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
Input: nums = [1,3,-1,-3,5,3,6,7], and k = 3
Output: [3,3,5,5,6,7]
Explanation:

Window position Max
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

Note:
You may assume k is always valid, 1 ≤ k ≤ input array’s size for non-empty array.

Follow up:
Could you solve it in linear time?

解法一: 双端队列

时间复杂度: $O(n)$, 一次遍历
空间复杂度: $O(k)$, 双端队列的 size 为 $k$.

使用双端队列deque, 从下标0开始, 一直到n-1, 每次进行如下步骤:

  • 当前元素是否比队列中最后一个元素大, 如果大, 说明队列元素以后也不可能再成为较大值, 直接pop, 如此循环, 直到队列为空或者遇到比当前值大的元素
  • 判断队列中队首的元素是否过期(若队空则直接下一步, 无需判断), 若过期, 则pop, 否则, 不管( 只看队首, 队内的元素是否过期不影响算法, 因为就算过期后面也会将其淘汰)
  • 将当前元素的下标存到队尾
  • 将新的队首元素存到结果向量max_res中

注意: 队列里面存的是下标, 而不是元素本身的值, 后面在提到队列的元素值时, 均是指队列中存储的下标对应的元素值.

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> maxSlidingWindow(vector<int>& nums, int k) {
if(nums.size()==0 || k ==0) return vector<int>{};
int n = nums.size();
deque<int> dq;
vector<int> res;
for(int i=0; i<n; i++){
if(dq.empty())
dq.push_back(i);
else{
if(dq.front() < i-k+1) dq.pop_front(); //过期元素, 出队列
while(!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back(); // 将队列中小于当前元素的都出队列(因为它们不可能成为max)
dq.push_back(i); // 将当前元素入队列.
}
if(i >= k-1) res.push_back(nums[dq.front()]);
}
return res;
}
};

240. Search a 2D Matrix II

Description: 矩阵搜索

Write an efficient algorithm that searches for a value in an m x n matrix. This matrix has the following properties:

Integers in each row are sorted in ascending from left to right.
Integers in each column are sorted in ascending from top to bottom.

Example:

1
2
3
4
5
6
7
8
9
Consider the following matrix:

[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]

Given target = 5, return true.

Given target = 20, return false.

解法一: 从左下角开始

时间复杂度: $O(n+m)$, 最多走 $n+m$ 步, $n$ 和 $m$ 分别为矩阵的宽和高
空间复杂度: $O(1)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
if(matrix.size()==0 || matrix[0].size()==0) return false;
int i=matrix.size()-1;
int j=0; // 从左下角开始搜索
while(i>=0 && j<matrix[0].size()){
if(matrix[i][j] < target) j++;
else if(matrix[i][j] > target) i--;
else
return true;
}
return false;
}
};

242. Valid Anagram

变位词: 改变某个词或短语的字母顺序后构成的新词或短语

Description: 判断变位词

Given two strings s and t , write a function to determine if t is an anagram of s.

Example 1:

1
2
Input: s = "anagram", t = "nagaram"
Output: true

Example 2:

1
2
Input: s = "rat", t = "car"
Output: false

Note:
You may assume the string contains only lowercase alphabets.

Follow up:
What if the inputs contain unicode characters? How would you adapt your solution to such case?

解法一: 排序

时间复杂度: $O(nlogn)$, 对两个字符串进行排序
空间复杂度: $O(1)$, 可以原地排序, 不占用额外空间

对两个字符串排序后, 看是否相等. 该方式可以无缝的解决 Follow up 中的问题.

解法二: 哈希表

时间复杂度: $O(n1+n2)$, $n1$, $n2$ 分别为两个字符串的长度, 二者必须相等, 否则一定不是变位词.
空间复杂度: $O(1)$, 哈希表的 size 为 26, 常数级

构造一个字母哈希表, 先统计

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 isAnagram(string s, string t) {
if(s.size() != t.size()) return false;
int ana_hash[26]={0};
for(auto c : s){
ana_hash[c-'a']++;
}
for(auto c : t){
ana_hash[c-'a']--;
if (ana_hash[c-'a'] < 0)
return false;
}
/*
因为长度相等, 所以一旦不是异构词, 就一定会出现某个哈希位上的值小于0的情况, 因此无需在这里再次判断
for(auto i : ana_hash){
if(i != 0) return false;
}
*/
return true;
}
};

解答 Follow up:

unordered_map 来代替数组哈希表, 此时复杂度与输入的字符种类数目有关, 哈希表的空间复杂度变成 $O(n)$.

251. 展开二维向量

题目链接: https://leetcode-cn.com/problems/flatten-2d-vector/

解法一: 下标访问法

需要特别处理元素为空的情况, 否则会出现下标越界访问错误.

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 Vector2D:

def __init__(self, v: List[List[int]]):
self.v = v
self.i = 0
self.n = len(v)
while self.i < self.n and len(self.v[self.i]) == 0: self.i += 1 # 特别处理元素为空的情况
self.j = 0

def next(self) -> int:
res = self.v[self.i][self.j]
self.j += 1
if self.j == len(self.v[self.i]):
self.i += 1
while self.i < self.n and len(self.v[self.i]) == 0: self.i += 1 # 特别处理元素为空的情况
self.j = 0
return res

def hasNext(self) -> bool:
return True if self.i < self.n else False

# Your Vector2D object will be instantiated and called as such:
# obj = Vector2D(v)
# param_1 = obj.next()
# param_2 = obj.hasNext()

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 Vector2D {
private:
int i, j, n;
vector<vector<int>> v;
public:
Vector2D(vector<vector<int>>& v_) {
i = 0;
j = 0;
v = v_;
n = v.size();
while (i < n and v[i].size() == 0) i++;
}

int next() {
int res = v[i][j];
j++;
if (j == v[i].size()) {
i++;
j = 0;
}
while (i < n and v[i].size() == 0) i++;
return res;
}

bool hasNext() {
if (i < n)
return true;
else
return false;
}
};

/**
* Your Vector2D object will be instantiated and called as such:
* Vector2D* obj = new Vector2D(v);
* int param_1 = obj->next();
* bool param_2 = obj->hasNext();
*/

252. 会议室-简单

题目链接: https://leetcode-cn.com/problems/meeting-rooms/

解法一: 排序

先排序, 然后逐个元素查看是否有交集

Python 实现:

1
2
3
4
5
6
7
class Solution:
def canAttendMeetings(self, intervals: List[List[int]]) -> bool:
intervals.sort()
for i in range(1, len(intervals)):
if intervals[i][0] < intervals[i-1][1]:
return False
return True

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
static bool cmp(vector<int> vec1, vector<int> vec2) {
if (vec1[0] != vec2[0])
return vec1[0] < vec2[0];
else
return vec1[1] < vec1[1];
}
bool canAttendMeetings(vector<vector<int>>& intervals) {
std::sort(intervals.begin(), intervals.end(), cmp);
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] < intervals[i-1][1])
return false;
}
return true;
}
};

253. 会议室 II

题目链接: https://leetcode-cn.com/problems/meeting-rooms-ii/

解法一: 排序

排序后, 申请一个列表rooms, 内部元素代表当前每个会议室的会议结束时间, 对于一个新来的会议时间段, 我们按照会议的开始时间将其对应room的结束时间更新, 如果当前的rooms均不能满足这个会议, 那么就新添加一个会议室, 其结束时间为当前新会议的结束时间.

优化: 我们可以将rooms申请成一个优先队列(小顶堆), 这样我们可以快速判断当前最先处于空闲状态的房间是哪一个, 如果这一个房间不满足条件, 那么其他的房间也一定不满足条件, 因此, 直接为当前的会议新申请一个会议室.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
import heapq
class Solution:
def minMeetingRooms(self, intervals: List[List[int]]) -> int:
if not intervals: return 0
intervals.sort()
rooms = [intervals[0][1]];
heapq.heapify(rooms) # 使用堆结构快速找到当前最早释放的空间
for i in range(1, len(intervals)):
if rooms[0] <= intervals[i][0]:
heapq.heapreplace(rooms, intervals[i][1])
else:
heapq.heappush(rooms, intervals[i][1])
return len(rooms)

C++ 实现:

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:
static bool cmp(vector<int> vec1, vector<int> vec2) {
if (vec1[0] != vec2[0]) {
return vec1[0] < vec2[0];
} else {
return vec1[1] < vec2[1];
}
}
int minMeetingRooms(vector<vector<int>>& intervals) {
if (intervals.empty()) return 0;
std::sort(intervals.begin(), intervals.end(), cmp);
std::priority_queue<int, std::vector<int>, std::greater<int> > rooms; // 利用堆结构可以快速找到当前最先空闲的房间
rooms.push(intervals[0][1]);
for (int i = 1; i < intervals.size(); i++) {
if (rooms.top() <= intervals[i][0]) {
rooms.pop();
}
rooms.push(intervals[i][1]);
}
return rooms.size();
}
};

268. Missing Number

Description: 缺失的数字

Given an array containing n distinct numbers taken from 0, 1, 2, …, n, find the one that is missing from the array.

Example 1:

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

Input: [9,6,4,2,3,5,7,0,1]
Output: 8
Note:
Your algorithm should run in linear runtime complexity. Could you implement it using only constant extra space complexity?

解法一: 排序

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

解法二: 哈希表

时间复杂度: $O(n)$, 两次遍历, 第一次构建哈希, 第二次查询缺失数字
空间复杂度: $O(n)$, 哈希表所占空间

另一种解法: 用下表做哈希, 将数字放置在与下标相同的位置上, 最终放错位置的元素的下标就是缺失的数字, 如果位置都正确, 则缺失 n. 复杂度与哈希表相同, 代码实现如下:

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

解法三: 异或

时间复杂度: $O(n)$, 一次遍历
空间复杂度: $O(1)$, 无需额外空间

因为题目是从 0, 1, 2, ..., n 共 $n+1$ 个数字中选出了 $n$ 个不相同的数字, 因此, 如果将 $n+1$ 大小的数组和 $n$ 大小的数组合并成一个大数组, 那么在大数组中, 除了那个缺失的数字以外, 所有的数字都恰好出现了两次, 因此题目变成了求数组中出现一次的唯一数字, 此时可以利用异或在 $O(n)$ 时间复杂度内解决.

该解法还可以解决丢失两个数字, 丢失三个数字的情况, 具体可参考用异或解决奇数偶数数字的问题.

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int missingNumber(vector<int>& nums) {
int n = nums.size();
int res = n;
for(int i=0; i<n; i++){
res = res ^ i ^ nums[i];
}
return res;
}
};

解法四: 高斯求和公式

时间复杂度: $O(n)$, 一次遍历
空间复杂度: $O(1)$, 无需任何额外空间

前 $n$ 项和的求和公式为: $1+2+3+\cdots+n = \frac{(n+1)n}{2}$
因此, 我们只需要计算出当前数组的和, 然后在计算当前和与高斯和之间的差即可.

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int missingNumber(vector<int>& nums) {
int n = nums.size();
int gauss_sum = n*(n+1)/2;
int sum = 0;
for(auto num : nums)
sum += num;
return gauss_sum - sum;
}
};

277. 搜寻名人-中等

题目链接(加锁): https://leetcode-cn.com/problems/find-the-celebrity/

题目描述:

假设你是一个专业的狗仔,参加了一个 n 人派对,其中每个人被从 0 到 n - 1 标号。在这个派对人群当中可能存在一位 “名人”。所谓 “名人” 的定义是:其他所有 n - 1 个人都认识他/她,而他/她并不认识其他任何人。

现在你想要确认这个 “名人” 是谁,或者确定这里没有 “名人”。而你唯一能做的就是问诸如 “A 你好呀,请问你认不认识 B呀?” 的问题,以确定 A 是否认识 B。你需要在(渐近意义上)尽可能少的问题内来确定这位 “名人” 是谁(或者确定这里没有 “名人”)。

在本题中,你可以使用辅助函数 bool knows(a, b) 获取到 A 是否认识 B。请你来实现一个函数 int findCelebrity(n)。

派对最多只会有一个 “名人” 参加。若 “名人” 存在,请返回他/她的编号;若 “名人” 不存在,请返回 -1。

示例 1:

1
2
3
4
5
6
7
输入: graph = [
  [1,1,0],
  [0,1,0],
  [1,1,1]
]
输出: 1
解析: 有编号分别为 0、1 和 2 的三个人。graph[i][j] = 1 代表编号为 i 的人认识编号为 j 的人,而 graph[i][j] = 0 则代表编号为 i 的人不认识编号为 j 的人。“名人” 是编号 1 的人,因为 0 和 2 均认识他/她,但 1 不认识任何人。

示例 2:

1
2
3
4
5
6
7
输入: graph = [
[1,0,1],
[1,1,0],
[0,1,1]
]
输出: -1
解析: 没有 “名人”

注意:

  1. 该有向图是以邻接矩阵的形式给出的,是一个 n × n 的矩阵, a[i][j] = 1 代表 i 与 j 认识,a[i][j] = 0 则代表 i 与 j 不认识。
  2. 请记住,您是无法直接访问邻接矩阵的。

解法一: 双重循环

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

遍历每一个人, 循环判断他与其他人的关系, 如果他认识任何人, 或者有任意一人不认识他, 他都不是名人, 直接判断下一个人. 判断完所有人后, 若还没有找到名人, 就跳出.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# The knows API is already defined for you.
# @param a, person a
# @param b, person b
# @return a boolean, whether a knows b
# def knows(a, b):

class Solution(object):
def findCelebrity(self, n):
"""
:type n: int
:rtype: int
"""
for i in range(n):
flag = True
for j in range (n):
if i == j: continue # 自己肯定认识自己, 跳过
if knows(i, j) == 1 or knows(j, i) == 0:
flag = False # 该人不是名人, 退出当前循环, 判断下一个
break
if flag: return i
return -1

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Forward declaration of the knows API.
bool knows(int a, int b);

class Solution {
public:
int findCelebrity(int n) {
for (int i = 0; i < n; i++) {
bool flag = true;
for (int j = 0; j < n; j++) {
if (i == j) continue;
if (knows(i, j) == 1 or knows(j, i) == 0) {
flag = false;
break;
}
}
if (flag) return i;
}
return -1;
}
};

解法二: 确立名人候选

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

根据题目描述, 我们知道, 名人不能认识任何人, 并且所有人都要认识他, 因此, 我们可以遍历一遍节点, 确立名人候选人. 然后对该候选人进行检查即可. 确立候选人的方法如下:
假设 celebrity = 0, 对 i = 1~n-1 有两种可能:

  1. celebrity 认识 i, 说明, celebrity 一定不是名人, 而 i 有可能 是名人
  2. celebrity 不认识 i, 说明 i 一定不可能是 名人, 而 celebrity 有可能是
    这样, 当遍历完所有人之后, celebrity 指向到就是 唯一有可能 成为名人的人

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# The knows API is already defined for you.
# @param a, person a
# @param b, person b
# @return a boolean, whether a knows b
# def knows(a, b):

class Solution(object):
def findCelebrity(self, n):
"""
:type n: int
:rtype: int
"""
celebrity = 0
for i in range (1, n):
if (celebrity != i and knows(celebrity, i)):
celebrity = i

for i in range(n):
if i == celebrity: continue
if knows(celebrity, i) or not knows(i, celebrity):
return -1
return celebrity

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Forward declaration of the knows API.
bool knows(int a, int b);

class Solution {
public:
int findCelebrity(int n) {
int celebrity = 0;
for (int i = 1; i < n; i++) {
if (i != celebrity and knows(celebrity, i))
celebrity = i;
}
for (int i = 0; i < n; i++) {
if (i == celebrity) continue;
if (knows(celebrity, i) or !knows(i, celebrity))
return -1;
}
return celebrity;
}
};

279. Perfect Squares

Description: 找到最少的平方和个数

Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, …) which sum to n.

Example 1:

1
2
3
Input: n = 12
Output: 3
Explanation: 12 = 4 + 4 + 4.

Example 2:

1
2
3
Input: n = 13
Output: 2
Explanation: 13 = 4 + 9.

解法一: 四平方和定理(最优)

时间复杂度: $O(\sqrt n)$, 最坏情况为 $O(\sqrt n)$, 最好情况为 $O(1)$.
空间复杂度: $O(1)$, 无需额外空间

四平方和定理: 任何一个正整数, 都可以表示成四个整数的平方和(如果不算 0 的话, 就是可以用小于等于 4 个整数的平方和来表示任意一个整数).

对于题目, 要求我们返回组合平方和的数字的 最少 个数(不算0), 因此, 这里还可以使用到两个特别的性质来加速计算:

  • 如果 $n$ 可以被 4 整除, 那么 $n$ 和 $n/4$ 的最少平方和数字个数相同.
  • 如果 $n \% 8=7$, 那么 $n$ 的最少平方和个数一定为 4.

因此, 本题的解法流程如下:

  1. 循环整除 4, 降低 $n$ 的大小;
  2. 判断是否有 $n \% 8 =7$, 如果有, 则直接返回 4;
  3. 查看 $n$ 是否能够拆分成两个数(其中一个可以为0), 如果可以, 则返回 !!i + !!j, 即返回正整数的个数. 此处需要注意, i 需要从 0 开始遍历, 因为对于 $3^2+4^2 = 0^2 + 5^2 = 25$ 来说, 我们希望返回的是后者(即返回最少的平方和个数);
  4. 如果上面都不行, 则只可能反正 3(因为 $n>0$).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int numSquares(int n) {
while(n%4 == 0) n = n/4;
if(n%8 == 7) return 4;

for(int i=0; i*i<=n; i++){ // i必须从0开始, 否则会找到其他组合, eg: 3^2 + 4^2 = 0^2 + 5^2
int j = sqrt(n - i * i);
if(i*i + j*j == n)
return !!i+!!j; // 返回1(只有一个正整数)或2(两个都是正整数)
}
return 3; //既不是4, 也不是1,2, 返回3(因为n>0, 所以不可能返回0)
}
};

解法二: DP

时间复杂度: $O(n\sqrt n)$, 外层循环约为 $n$ 次, 内层循环约为 $\sqrt n$ 次.
空间复杂度: $O(n)$, 需要额外申请 $n+1$ 大小的 DP 数组.

对于解法一来说, 虽然它的时间和空间复杂度最优, 但是其中使用到了很多不常用的定理和性质, 如果不知道这些定理和性质, 很难想到解法一的实现. 因此, 我们更容易想到的是使用动态规划来解决这道题, 具体解题步骤如下:

  1. 申请 $n+1$ 大小的 DP 数组, 并令 dp[0]=0, 令其他元素为 INT_MAX, dp[i] 的值代表组成数字 $i$ 所需的最少的平方和数字个数;
  2. 由于我们已经求得 dp[0] 的值, 因此, 对于 j=1, 2, ... 来说, 我们可以顺势求得 dp[0+j*j] = dp[0]+1=1.
  3. 对于已经求得的 dp[i], 我们可以求得 dp[i+j*j] = min(dp[i+j*j], dp[i]+1), 这里的 min 是为了保证组成数字的平方和个数最少.
  4. 最终, 返回 dp.back() 即为组成 $n$ 的最少的平方和个数.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1, INT_MAX);
dp[0]=0; // 赋初值
for(int i=0; i<n+1; i++){
for(int j=1; i+j*j < n+1; j++){
dp[i+j*j] = std::min(dp[i+j*j], dp[i]+1);
}
}
return dp.back();
}
};

解法三: DP

时间复杂度: $O(n\sqrt n)$, 外层循环约为 $n$ 次, 内层循环约为 $\sqrt n$ 次.
空间复杂度: $O(n)$, 需要额外申请 $n+1$ 大小的 DP 数组.

复杂度和解法二没有区别, 但是我们可以从另一个角度来实现 DP 算法, 具体流程如下:

  1. 申请只含有一个元素的 DP 数组 dp[0]=0;
  2. 根据 dp[0] 的值计算 dp[1].(计算方法和解法二类似, 具体请看代码)
  3. 根据 dp[0]~dp[i-1] 的值计算 dp[i].
  4. i==n 时, 返回 dp[i].
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int numSquares(int n) {
vector<int> dp(1,0);

while(dp.size()<=n){
int m = dp.size(); int val = INT_MAX;
for(int j=1; j*j <= m; j++){ //这里必须 <= m, 否则会缺少 dp[0]+1 的情况.
val = std::min(val, dp[m - j*j] + 1);
}
dp.push_back(val);
}
return dp.back();
}
};

解法四: 递归

http://www.cnblogs.com/grandyang/p/4800552.html

1
2
3
4
5
6
7
8
9
10
11
12
13
// Recrusion
class Solution {
public:
int numSquares(int n) {
int res = n, num = 2;
while (num * num <= n) {
int a = n / (num * num), b = n % (num * num);
res = min(res, a + numSquares(b));
++num;
}
return res;
}
};

283. Move Zeroes

Description: 将 0 移动到最后, 保持其他元素相对位置不变

Given an array nums, write a function to move all 0’s to the end of it while maintaining the relative order of the non-zero elements.

Example:

Input: [0,1,0,3,12]
Output: [1,3,12,0,0]
Note:

You must do this in-place without making a copy of the array.
Minimize the total number of operations.

解法一: 交换法

时间复杂度: $O(n)$
空间复杂度: $O(1)$, 无需额外空间

利用交换将不符合要求的元素交换, 具体做法如下:

  1. i 指向第一个 0 元素;
  2. j 指向 i 之后的第一个非 0 元素; (注意 j 必须在 i 的后面才能执行交换)
  3. 交换 ij 指向的元素, 更新 ij 的值.
  4. 重复以上步骤, 直到 j 越界.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int i = 0, j = 0;
while(nums[i]!=0) i++;
j = i;
while(nums[j]==0) j++;

while(j < nums.size()){
std::swap(nums[i], nums[j]);
while(nums[i]!=0) i++;
j = i;
while(nums[j]==0) j++;
}
return;
}
};

解法二: 更简洁的交换法

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

这道题可以从另一个角度来理解, 即可以看做是要将所有的非 0 元素保持相对位置不变地移动到数组的前面, 那么我们可以遍历数组, 并用一个变量 i 来记录当前元素之前的非 0 元素的个数, 那么如果当前元素为非 0 元素, 则可以令当前元素与 nums[i] 交换, 同时 i++, 这样便可以同时保证将非 0 元素移动到数组前以及保持相对位置不变两个条件.

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
void moveZeroes(vector<int>& nums) {
for(int i=0, j=0; j<nums.size(); j++){
if(nums[j] != 0){
std::swap(nums[i], nums[j]);
i++; // 非0元素个数加1
}
}
return;
}
};

285. 二叉搜索树中的顺序后继

题目链接: https://leetcode-cn.com/problems/inorder-successor-in-bst/

解法一: 遍历二叉搜索树

遍历的时候要用一个父节点指针, 指向比当前节点大的第一个祖先

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
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def inorderSuccessor(self, root: 'TreeNode', p: 'TreeNode') -> 'TreeNode':
parent = None
while root:
if p.val < root.val:
parent = root # parent 指向当前节点, 即左子树的父亲
root = root.left
elif p.val > root.val: # parent 不变, 是左子树父亲的父亲
root = root.right
else:
if root.right:
root = root.right
while (root.left): # 找到最左子节点
root = root.left
return root
else:
return parent # 返回第一个大于它的祖先
#return None

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
/**
* 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* inorderSuccessor(TreeNode* root, TreeNode* p) {
TreeNode* parent = nullptr;
while (root) {
if (p->val < root->val) {
parent = root;
root = root->left;
} else if (p->val > root->val) {
root = root->right;
} else {
if (root->right) {
root = root->right;
while (root->left)
root = root->left;
return root;
} else {
return parent;
}
}
}
return nullptr;
}
};

解法二: 中序遍历

直接中序遍历, 访问的指定节点时, 返回下一个节点即可

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 inorderSuccessor(self, root: 'TreeNode', p: 'TreeNode') -> 'TreeNode':
def inorder_traverse(root, p):
if root.left: inorder_traverse(root.left, p) # 左子树遍历
if self.p_next: # 如果已经找到 p_next 则直接返回, 终止遍历
return
if self.p_prev and self.p_prev == p: # p_prev == p, 说明前序找到
self.p_next = root
return
self.p_prev = root # 指定前序为当前节点, 遍历下一个
if root.right: inorder_traverse(root.right, p)

self.p_next = None
self.p_prev = None
inorder_traverse(root, p)
return self.p_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
/**
* 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 {
TreeNode* p_prev;
TreeNode* p_next;
void inorder_traverse(TreeNode* root, TreeNode *p) {
if (root->left) inorder_traverse(root->left, p);
if (p_next)
return;
if (p_prev and p_prev == p) {
p_next = root;
return;
}
p_prev = root;
if (root->right) inorder_traverse(root->right, p);
}
public:
TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p) {
p_prev = nullptr;
p_next = nullptr;
inorder_traverse(root, p);
return p_next;
}
};

Python 迭代实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def inorderSuccessor(self, root: 'TreeNode', p: 'TreeNode') -> 'TreeNode':
stack = []
p_prev = None
while root or stack:
while root:
stack.append(root)
root = root.left
root = stack.pop();
if p_prev == p:
return root
p_prev = root
root = root.right
return 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
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:
TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p) {
stack<TreeNode*> stack_tree;
TreeNode* p_prev = nullptr;
while (root or !stack_tree.empty()) {
while (root) {
stack_tree.push(root);
root = root->left;
}
root = stack_tree.top(); stack_tree.pop();
if (p_prev == p) {
return root;
}
p_prev = root;
root = root->right;
}
return root;
}
};

287. Find the Duplicate Number

Description: 寻找重复元素

Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.

Example 1:

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

Example 2:

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

Note:

  • You must not modify the array (assume the array is read only).
  • You must use only constant, O(1) extra space.
  • Your runtime complexity should be less than O(n2).
  • There is only one duplicate number in the array, but it could be repeated more than once.

解法一: 哈希表

时间复杂度: $O(n)$, 一次遍历
空间复杂度: $O(n)$, 哈希表额外空间

这道题用哈希表很容易解, 但是这是最简单的解法之一(更简单的还有暴力法), 因此这里贴出来只用做参考.

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int findDuplicate(vector<int>& nums) {
unordered_set<int> nums_set;
for(auto num : nums){
if(nums_set.find(num) != nums_set.end())
return num;
nums_set.insert(num);
}
}
};

另一种解法是不建立哈希表, 而是利用数组的元素值和元素下标建立对应关系, 即将所有的数字放置在数字对应的下标位置上, 这样, 最终重复的元素就会出现的下标为 0 的位置上, 当然, 期间如果已经发现重复, 则可以直接返回, 代码如下:

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

int n = nums.size() - 1;
for (int i = 1; i < n + 1; i++) {
if (nums[0] != nums[nums[0]]) {
std::swap(nums[0], nums[nums[0]]);
} else {
return nums[0];
}
}
return nums[0];
}
};

解法二: 排序

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

先对数组排序, 然后遍历查找重复元素, 但是这种解法会改变原有数组中的元素分布, 题目要是数组是只读的, 因此该解法也只作为参考贴出

1
2
3
4
5
6
7
8
9
class Solution {
public:
int findDuplicate(vector<int>& nums) {
std::sort(nums.begin(), nums.end());
for(int i=0; i<nums.size(); i++){
if(nums[i] == nums[i+1]) return nums[i]; // 一定存在重复元素, 因此 i+1 不会越界
}
}
};

解法三: Floyd 的乌龟和兔子(Floy 判圈算法)

Floyd’s Tortoise and Hare, 该算法是用来判断链表中是否含有环的. 对于此题, 我们换一个角度来解读, 数组中总共有 $n+1$ 个数, 这些数都是 $[1,n]$ 中的正整数, 因此, 至少会存在一个重复的数, 根据题目的假设, 有且仅有一个重复的数字, 那么, 我们假设该数字为 $k$, 于是, 我们可以将该数组表示成下面的形式(表中的 $x$ 代表该元素的值不为 $k$ ):

下标 $0$ $1$ $2$ $k$ $n$
元素 $x$ $k$ $k$ $x$ $x$

如果我们将上面的 (下标, 元素) 看做是链表结构中的 (val, next), 那么可以看出, 当某一个节点(上面假设为节点 1)的 next 指向 k 以后, k 又会重新指向另一个元素, 但是, 经过一定步数以后, 一定 又会重新指向 k (因为元素存在重复), 这在链表中称之为 “环”, 因此, 这道题就变成了求链表中环的开始节点, 该题正好是剑指offer第55题LeetCode第142题

这道题有一个很关键的条件就是, 元素的值是在1~n之间, 因此, 下标 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
23
24
25
26
27
28
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = 0; //
int fast = 0; // 实际上 fast 和 slow 可以指向环前的任意节点, 不影响最终结果.
do{ // 因为一定存在环, 所以fast不会越界
slow = nums[slow];
fast = nums[fast];
fast = nums[fast];
}while(slow!=fast);
int len=1; // 求环长度
fast = nums[fast];
while(slow!=fast){
fast = nums[fast];
len ++;
}
slow = 0;
fast = 0;
while(len--){
fast = nums[fast]; // 先让fast走环长的距离
}
while(slow!=fast){ // 再次相遇时即为环的开始节点
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
};

更简洁的写法:
上面在求环的开始节点时, 是先求环长, 再让 fast 走环长距离, 然后 slowfast 同步前进, 最终相遇点即为开始点, 这么写比较容易理解, 但难免有些繁琐. 实际上, 我们只需要令 slow 从头开始, 即 slow=0, 接着令 fastslow 同步前进, 那么相遇点就是开始节点. 原因是因为, 二者是从同一点出发的, fast 的步长较快, 当二者相遇时, 他们一定是在环中的某一点相遇, 这个时候再把slow重新放回起点, 那么fast领先slow的距离就等于: 环外的距离 + 若干圈 + 当前圈内已经走的距离. 而此时 fast 距离环入口还有一段距离, 因为第一次相遇点的位置, 因此, 我们如果此时从起点出发, 最终正好可以弥补这一部分距离, 因此, 最终会在环入口相遇.

一句话总结: 令fast和slow一起开始, fast步长是slow步长的二者, 找到二者相遇的点, 然后令slow重新回到起点, 此时步长一致, 再次相遇时即为环的入口点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = 0; // head;
int fast = 0; // head->next; 指向head也没错, 因为, 最终仍会slow=fast
do{ // 因为一定存在环, 所以fast不会越界
slow = nums[slow];
fast = nums[fast];
fast = nums[fast];
}while(slow!=fast);

slow = 0;
while(slow != fast){
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
};

289. Game of Life

Description: 游戏人生

According to the Wikipedia’s article: “The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970.”

Given a board with m by n cells, each cell has an initial state live (1) or dead (0). Each cell interacts with its eight neighbors (horizontal, vertical, diagonal) using the following four rules (taken from the above Wikipedia article):

  1. Any live cell with fewer than two live neighbors dies, as if caused by under-population.
  2. Any live cell with two or three live neighbors lives on to the next generation.
  3. Any live cell with more than three live neighbors dies, as if by over-population..
  4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

Write a function to compute the next state (after one update) of the board given its current state. The next state is created by applying the above rules simultaneously to every cell in the current state, where births and deaths occur simultaneously.

Example:

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

Follow up:

  1. Could you solve it in-place? Remember that the board needs to be updated at the same time: You cannot update some cells first and then use their updated values to update other cells.
  2. In this question, we represent the board using a 2D array. In principle, the board is infinite, which would cause problems when the active area encroaches the border of the array. How would you address these problems?

解法一: 状态机

时间复杂度: $O(mn)$, 遍历两次二维数组
空间复杂度: $O(1)$, 无需额外空间

根据细胞的更新规则, 我们可以设计出下面的状态转移:
0: 从 0 到 0;
1: 从 1 到 1:
2: 从 1 到 0;
3: 从 0 到 1;

因此, 本解法需要遍历两边 board 矩阵, 第一遍先计算每个 cell 的状态, 第二遍根据状态赋予 cell 不同的值, 具体来说就是如果当前状态 board[i][j]%2==0, 那么就令 board[i][j]=0, 反之, 令 board[i][j]=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
class Solution {
public:
void gameOfLife(vector<vector<int>>& board) {
if(board.size()==0 || board[0].size()==0) return;
int n = board.size();
int m = board[0].size();
int direct[8][2]={{-1,-1}, {-1, 0}, {-1, 1},
{ 0,-1}, { 0, 1},
{ 1,-1}, { 1, 0}, { 1, 1}};
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
int count_1 = 0;
for(int k=0; k<8; k++){
int i_k = i+direct[k][0];
int j_k = j+direct[k][1];
if(i_k>=0 && i_k<n && j_k>=0 && j_k<m
&& (board[i_k][j_k]==1 || board[i_k][j_k]==2) )
count_1++;
}
if( (count_1<2 || count_1>3) && board[i][j]==1)
board[i][j] = 2; // 2:1->0, 0:0->0
else if(count_1==3 && board[i][j]==0)
board[i][j] = 3; // 3:0->1
// 剩余情况维持不变
}
}
for(auto &cells : board){ // 如果要对board进行修改, 需要使用引用号 &
for(auto &cell : cells)
if(cell%2==1) cell=1;
else cell=0;
}
}
};

Follow up

  1. 常数空间复杂度: 正如解法一
  2. 无边界限制: 修改边界空间条件, 使其变成 “循环” 二维矩阵.

295. Find Median from Data Stream

Description: 返回数据流的中位数

Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.

For example,
[2,3,4], the median is 3
[2,3], the median is (2 + 3) / 2 = 2.5

Design a data structure that supports the following two operations:

  • void addNum(int num) - Add a integer number from the data stream to the data structure.
  • double findMedian() - Return the median of all elements so far.

Example:

1
2
3
4
5
addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2

Follow up:

  1. If all integer numbers from the stream are between 0 and 100, how would you optimize it?
  2. If 99% of all integer numbers from the stream are between 0 and 100, how would you optimize it?

解法一: 传统排序

时间复杂度: $O(nlogn)$, 添加数字时不排序, 返回中位数时排序
空间复杂度: $O(n)$, 排序需要额外空间

添加数字时, 直接添加, 时间复杂度为 $O(1)$, 每次需要输出中位数时, 都对数组内当前所有元素排序, 时间复杂度为 $O(nlogn)$, 该方法超时.

解法二: 插入排序

时间复杂度: $O(n)$, 二分搜索位置需要 $O(logn)$, 插入需要 $O(n)$.
空间复杂度: $O(n)$

每次新来一个数字时, 都执行插入排序, 先利用二分搜索(因为当前数组已经有序)找到应该插入的位置, 时间复杂度为 $O(logn)$, 然后将数字插入到该位置, 插入的时间复杂度是 $O(n)$, 由于已经排序好, 因此返回中位数的时间复杂度是 $O(1). 该解法 同样超时.

解法三: 大顶堆+小顶堆

时间复杂度: $O(5\times logn) = O(logn)$
空间复杂度: $O(n)$, 大顶堆和小顶堆的大小之和为 $n$.

元素首先加入大顶堆($O(logn)$), 得到前面数字的最大值, 然后将该最大值弹出($O(logn)$)并加入到小顶堆当中($O(logn)$), 以维护当前小顶堆的元素合法(例如, 新的大顶堆堆顶的元素大于当前小顶堆堆顶元素, 这就不合法了), 然后, 看看当前大顶堆和小顶堆的元素个数是否符合要求, 如果不符合的话, 就小顶堆的堆顶弹出($O(logn)$)并加入大顶堆($O(logn)$). 由此可知, 添加元素的时间复杂度为: $O(5\times logn)$. 返回中位数时可以直接获取堆顶, 而无需更改堆结构, 故而为 $O(1)$. 所以, 最终的时间复杂度就为 $O(5\times logn) = O(logn)$

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 MedianFinder {
private:
priority_queue<int> max_heap; // 大顶堆, 维护前 (n+1)/2 个元素
priority_queue<int, vector<int>, std::greater<int>> min_heap; //小顶堆, 维护后n/2个元素
public:
/** initialize your data structure here. */
MedianFinder() {

}

void addNum(int num) {
max_heap.push(num); // 先将当前元素添加到大顶堆中, 找到前半段最大元素
min_heap.push(max_heap.top()); max_heap.pop(); // 调节最小堆, 这一步是必须的, 是为了同时确保大顶堆和小顶堆的元素正确

// 数字会随着元素size的变化而不断在大顶堆和小顶堆之间切换
if(max_heap.size() < min_heap.size()){ //调节后, 平衡大顶堆和小顶堆的size
max_heap.push(min_heap.top());
min_heap.pop();
}
}

double findMedian() {
return (max_heap.size() + min_heap.size())%2==1 ? double(max_heap.top()) : (max_heap.top() + min_heap.top())*0.5;
}
};

/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/

解法四: multiset+指示器

时间复杂度: $O(logn+1) = O(logn)$
空间复杂度: $O(n)$, multiset 容器需要 $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
class MedianFinder {
private:
std::multiset<int> data;
std::multiset<int>::iterator low_mid, high_mid; // 迭代指示器会随着容器的变动而变动, 这个性质是该解法可行的重要因素之一
public:
/** initialize your data structure here. */
MedianFinder():low_mid(data.end()), high_mid(data.end()) {

}

void addNum(int num) {
const size_t n = data.size();
data.insert(num);
if(n == 0){
low_mid = data.begin();
high_mid = data.begin();
}else if(n & 1==1){ // 插入之前元素数量为奇数, low_mid=high_mid
if(num < *low_mid) // 会插入到 low_mid/high_mid 之前, 因此, 前半段元素增加
low_mid--;
else // 如果 >=, 则会插入到low_mid/high_mid 之后
high_mid++;
}else{ // 插入之前元素数量为偶数, low_mid+1 = high_mid

if(num >= *low_mid && num < *high_mid){ //插入的元素刚好在中间, 注意前面要用 >=, 后面用 <, 因为相等时, 会插在后面
low_mid++;
high_mid--; // 两个指针都想中间靠拢.
}else if(num < *low_mid){ // 插入元素会插在前面, 则前面元素数量增加
high_mid--; // 令high_mid=low_mid;
}else{ // 插在了后面
low_mid++; // 令low_mid=high_mid;
}
}
}

double findMedian() {
return (*low_mid + *high_mid) * 0.5;
}
};

上面的指示器实际上可以简化成一个(因为两个指示器只能互相挨着或者重叠), 因此, 我们可以只维护一个指示器, 简化代码如下(但是不太好理解):

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 MedianFinder {
multiset<int> data;
multiset<int>::iterator mid;

public:
MedianFinder()
: mid(data.end())
{
}

void addNum(int num)
{
const int n = data.size();
data.insert(num);

if (!n) // first element inserted
mid = data.begin();
else if (num < *mid) // median is decreased
mid = (n & 1 ? mid : prev(mid));
else // median is increased
mid = (n & 1 ? next(mid) : mid);
}

double findMedian()
{
return data.size() & 1 == 1 ? (*mid) : (*mid + *next(mid)) * 0.5 ;
}
};

Follow Up

  1. If all integer numbers from the stream are between 0 and 100, how would you optimize it?

用bucket?

  1. If 99% of all integer numbers from the stream are between 0 and 100, how would you optimize it?

297. Serialize and Deserialize Binary Tree

Description: 序列化和反序列化二叉树

Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment.

Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure.

Example:

1
2
3
4
5
6
7
8
9
You may serialize the following tree:

1
/ \
2 3
/ \
4 5

as "[1,2,3,null,null,4,5]"

Clarification: The above format is the same as how LeetCode serializes a binary tree. You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself.

Note: Do not use class member/global/static variables to store states. Your serialize and deserialize algorithms should be stateless.

解法一: DFS

时间复杂度: $O(n)$, 在序列化和反序列化递归中, 各遍历每个节点一次
空间复杂度: $O(n\times v + n) = O(n)$, 其中, $n$ 为节点个数, $v$ 为节点上的值所占空间大小, 最后的一个 $n$ 代表递归调用所占的空间大小.

使用 ostringstreamistringstream 来缓存字符串, 中间用空格分隔, 利用流操作 <<>> 可以方便的对字符串进行存入和读取, 而无需额外进行分词操作.

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
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Codec {
private:
void serialize(TreeNode *root, ostringstream &out){ // 函数重载
if(root==nullptr) out << "# ";
else{
out << root->val << " ";
serialize(root->left, out);
serialize(root->right, out);
}
}

TreeNode *deserialize(istringstream &in){
string cur_val;
in >> cur_val;
if(cur_val=="#") return nullptr;
else{
TreeNode *node = new TreeNode(std::stoi(cur_val));
node->left = deserialize(in);
node->right = deserialize(in);
return node;
}
}
public:

// Encodes a tree to a single string.
string serialize(TreeNode* root) {
ostringstream out;
serialize(root, out);
return out.str();
}

// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
istringstream in(data);
return deserialize(in);
}
};

// Your Codec object will be instantiated and called as such:
// Codec codec;
// codec.deserialize(codec.serialize(root));

解法二: BFS

时间复杂度: $O(n)$, 在序列化和反序列化中, 每个节点都遍历一次
空间复杂度:

BFS 的会按照层次遍历的顺序将树的节点序列化, 序列化的代码比较好写, 只需对普通的层次遍历稍加改动即可. 反序列化的代码有一点麻烦, 需要控制树节点的左右子节点的值, 具体如下.

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
class Codec {

public:

// Encodes a tree to a single string.
string serialize(TreeNode* root) {
ostringstream out;
if(root==nullptr) return "";
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
TreeNode *node = q.front(); q.pop();
if(node!=nullptr){
out << node->val << " ";
q.push(node->left);
q.push(node->right);
}else
out << "# ";
}
return out.str();
}

// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
if(data.empty()) return nullptr;
istringstream in(data);
queue<TreeNode *> q;
string val;
in >> val;
TreeNode *res = new TreeNode(std::stoi(val));
TreeNode *cur = res;
q.push(cur);
while(!q.empty()){
TreeNode *node = q.front(); q.pop();
if(!(in>>val)) break;
if(val!="#"){
cur = new TreeNode(std::stoi(val));
node->left = cur;
q.push(cur);
}
if(!(in>>val)) break;
if(val!="#"){
cur = new TreeNode(std::stoi(val));
node->right = cur;
q.push(cur);
}
}
return res;
}
};

300. 最长递增子序列/最长上升子序列

Description: 求最长递增子序列(可以不连续)的长度

给定一个无序的整数数组,找到其中最长上升子序列的长度。 注意两点:

  • 数组无序
  • 子序列不必是连续的

Example:

1
2
3
Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.

Note:

  • There may be more than one LIS combination, it is only necessary for you to return the length.
  • Your algorithm should run in O(n2) complexity.

Follow up:
Could you improve it to O(n log n) time complexity?

解法一: 暴力

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

对于任意一个数字, 只有两种情况, 即处于最长递增数组内, 或者不处于最长递增数组内, 需要同时将这两种情况考虑, 然后选择最长的情况. 该方法时间超限.

解法二: Recursion with memorization [Memory Limit Exceeded]

解法三: DP

分析题目可以得出, 第 $i$ 个下标对应的数字是否存在于递增序列中, 与该下标之后的元素是无关的, 因此, 很自然的想到利用 DP 的方法来解决这道题. 我们令 dp[i] 代表 包含第 $i$ 个下标对应元素的递增序列的长度. 在求取 dp[i+1] 时, 我们需要遍历前面 dp[0~i] 个数组元素才能决定 dp[i+1] 的值, 因此, 时间复杂度为 $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:
int lengthOfLIS(vector<int>& nums) {
if(nums.size()==0) return 0;
vector<int> dp(nums.size(), 1);
int res_max=1; // 记录最长的递增序列长度, 因为最少有一个元素, 所以长度最少为1
for(int i=1; i<nums.size(); i++){
int max_val = 0;
for(int j=0; j<i; j++){
if(nums[i] > nums[j]){ // 只有当当前元素大于前面的元素时, 才能构成递增序列
max_val = std::max(max_val, dp[j]);//当前元素与nums[j]可以组成递增序列
}
}
dp[i] = max_val+1; // 将当前元素加入, 因此, 长度增1
res_max = std::max(res_max, dp[i]); //用当前长度更新最长长度的值
}
return res_max;
}
};

解法四: DP+二分搜索(最优)

时间复杂度: $O(nlogn)$, 每次搜索的复杂度为 $O(logn)$, 总共需要搜索 $n$ 次
空间复杂度: $O(m)$, $m$ 为最长递增序列的长度.

同样还是 DP 解法, 但是我们重新赋予 dp[] 数组另一个含义, 我们令 dp[] 数组内储存的元素的数量刚好等于当前最长递增序列的数量, 注意, dp[] 数组内的值不一定是递增序列的值. 这样, 对于任意的一个数, 我们都将其与dp中的元素比较, 找到第一个大于该数的值, 并将其替换成该数, 此时, 虽然dp中的元素不是递增序列的值, 但是dp中的元素依然是有序的, 不仅维持了dp的长度, 同时还记录了新的有可能产生更长递增序列的值.
核心算过过程如下所示:

  1. 初始时, 令 dp[] 数组为空, 即 dp=[];
  2. 对于每一个元素 num, 我们查找 numdp 数组中的 upper_bound 迭代器(首个大于 num 的元素的迭代器), 假设取名为 upper;(注意, dp 数组是有序的, 所以这里的查询复杂度为 $O(logn)$)
  3. 查看 upper-1 指向的元素是否和 num 相等, 如果相等, 则说明该元素已经存在, 那么就跳过该元素, 重新回到步骤2;
  4. 如果 num 大于 dp 数组内的所有元素, 则将 num 添加进 dp 数组; 否则, 就将 dp 数组中大于 num 的第一个元素的值赋为 num.
  5. 重复步骤2,3,4, 直到遍历完数组为止.

为了更好的解释这种解法, 我们通过举例进行说明, 假定输入的数字序列为: [4,10,3,4,10,3,2], 那么我们的 dp[] 数组的变化情况如下:

dp=[],初始时, 数组为空;
dp=[4], 遍历元素4, 加入到数组中;
dp=[4,10], 遍历元素10, 10大于所有元素, 将其添加到数组中;
dp=[3,10], 遍历元素3, 发现第一个大于3的值为4, 将其赋值为3;
dp=[3,4], 遍历元素4, 发现第一个大于4的的值为10, 将其赋值为4;
dp=[3,4,10], 遍历元素10, 10大于所有元素, 将其添加到数组中;
dp=[3,4,10], 遍历元素3, 3在数组中已经存在, 跳过该元素;
dp=[2,4,10], 遍历元素2, 发现第一个大于2个值为3, 将其赋值为2.

综上, 我们可以看到, dp 数组的长度始终等于当前数组的最长子序列的长度, 故而, 直接返回 dp.size() 即为最终的结果. 注意, dp 内的值不一定为递增子序列的值.

Python 实现

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 lengthOfLIS(self, nums: List[int]) -> int:
def upper_bound(dp, num): # 找到 upper_bound, 即首个大于 num 的值
low = 0
high = len(dp)
while (low < high):
mid = (low + high) // 2
if dp[mid] <= num:
low = mid+1
else: # dp[mid] > num
high = mid
return high

dp = []
for num in nums:
index = upper_bound(dp, num)
if (index!=0 and dp[index-1] == num): continue
if index == len(dp): # 如果dp内的元素均小于该元素, 则递增子序列长度+1
dp.append(num)
else: # 否则, 将首个大于该值的位置变成该值
dp[index] = num
return len(dp)

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size()==0) return 0;
vector<int> dp;
for(auto num : nums){
auto upper = std::upper_bound(dp.begin(), dp.end(), num);
if(upper!=dp.begin() && *(upper-1) == num) continue; // 如果num在dp数组中已经存在, 则跳过该num.
if(upper==dp.end()){
dp.push_back(num); // 当前num比dp数组内的所有值都大, 则添加进dp数组
}else{
*upper = num; // 用更小的值替代当前dp数组内的值
}
}
return dp.size(); // 最终, dp数组的长度即为最长递增序列的长度
}
};

306. 累加数

累加数是一个字符串,组成它的数字可以形成累加序列。

一个有效的累加序列必须至少包含 3 个数。除了最开始的两个数以外,字符串中的其他数都等于它之前两个数相加的和。

给定一个只包含数字 ‘0’-‘9’ 的字符串,编写一个算法来判断给定输入是否是累加数。

说明: 累加序列里的数不会以 0 开头,所以不会出现 1, 2, 03 或者 1, 02, 3 的情况。

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 isAdditiveNumber(self, num: str) -> bool:
def dfs(res, num1, num2, num): # dfs 判断, num1 和 num2 是否能构成累加数
new_num = str(int(num1) + int(num2))
res += new_num
if len(res) <= len(num) and res == num[:len(res)]:
if res == num:
return True
else:
return dfs(res, num2, new_num, num)
else:
return False

n = len(num)
for i in range(1, n):
for j in range(i+1, n):
num1 = num[:i]
num2 = num[i:j]
if (len(num1) > 1 and num1[0] == '0') or (len(num2) > 1 and num2[0] == '0'):
continue # 除了 0 自身外, 其它数不能以 0 开始
res = num1 + num2
if dfs(res, num1, num2, num):
return True
return False

309. 买卖股票的最佳时机含冷冻期

题目链接: https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/

解法一: 动态规划

时间: $O(n)$, 空间: $O(1)$

难点主要在于想到动态规划的方程, 将每一天的状态分成持有股票和不持有股票两种, 则每一天状态的更新方式如下:

  • 持有(取较大者):
    • 保持昨天的持有
    • 前天 不持有(包含前天卖出的情况), 今天买入;
  • 不持有(取较大者):
    • 则为昨天持有, 今天卖出
    • 保持昨天的不持有

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() <= 1) return 0;
// 对于任意一天的股票, 均由持有和不持有两种状态, 记录两种状态下当前的余额
// 由于存在冷冻期, 因此, 我们要先求出第一天和第二天的状态, 然后从第三天开始根据前天和昨天的状态进行判断
int dp_q[2] = {-prices[0], 0}; // 第一天, 若持有, 则需要买入, 若不持有, 则余额为 0(没有支出)
// 第二天, 若持有, 则为保持前一天的持有, 或者前一天不持有, 第二天买入; 若不持有, 则为第一天买入, 第二天卖出, 或者保持第一天的不持有
int dp_z[2] = {std::max(-prices[0], -prices[1]), std::max(prices[1] - prices[0], 0)};
for (int i = 2; i < prices.size(); i++) {
// 对于任意一天的状态来说, 若持有, 则为保持昨天的持有, 或者前天不持有(包含前天卖出的情况), 今天买入; 若不持有, 则为昨天持有, 今天卖出, 或者保持昨天的不持有
int dp[2] = {std::max(dp_z[0], dp_q[1]-prices[i]), std::max(dp_z[0]+prices[i], dp_z[1])};
dp_q[0] = dp_z[0]; dp_q[1] = dp_z[1]; // 进入下一天, 昨天变前天
dp_z[0] = dp[0]; dp_z[1] = dp[1]; // 进入下一天, 今天变昨天
}
return std::max(dp_z[0], dp_z[1]);
}
};

Python 实现:

1
2
3
4
5
6
7
8
9
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices) <= 1: return 0;
dp_q = [-prices[0], 0] # first day, [y, n]
dp_z = [max(-prices[0], -prices[1]), max(0, prices[1] - prices[0])] # second day, [y, n]
for price in prices[2:]:
dp = [max(dp_z[0], dp_q[1]-price), max(dp_z[1], dp_z[0]+price)]
dp_q, dp_z = dp_z, dp
return max(dp_z)

315. Count of Smaller Numbers After Self

Description: 统计右边比当前数字小的个数

You are given an integer array nums and you have to return a new counts array. The counts array has the property where counts[i] is the number of smaller elements to the right of nums[i].

Example:

1
2
3
4
5
6
7
Input: [5,2,6,1]
Output: [2,1,1,0]
Explanation:
To the right of 5 there are 2 smaller elements (2 and 1).
To the right of 2 there is only 1 smaller element (1).
To the right of 6 there is 1 smaller element (1).
To the right of 1 there is 0 smaller element.

解法一: multiset

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

先介绍一下利用 multiset 的解法, multiset 的底层实现使用了红黑树, 所以在插入和查找的时候复杂度都为 $O(logn)$, 但是求 distance 时, 由于 multiset 的迭代器不是随机访问的, 因此复杂度为 $O(n)$, 故而最后的时间复杂度为 $O(n^2)$. 该方法在 OJ 上超时, 此处仅用于记录.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 class Solution {
public:
vector<int> countSmaller(vector<int>& nums) {
multiset<int> nums_set;
int n = nums.size();
vector<int> res(nums.size(), 0);
for(int i=n-1; i>=0; i--){
auto itlow = nums_set.lower_bound(nums[i]);

res[i] = std::distance(nums_set.begin(), itlow);
// multiset 求distance的复杂度为线性, 因此, 总复杂度为 O(n^2)

nums_set.insert(nums[i]);
}
return res;
}
};

解法二: 有序数组

时间复杂度: $O(n\times (logn+n) = O(n^2)$, 在有序数组中找指定位置需要 $O(logn)$, 将当前元素插入到数组的指定位置需要 $O(n)$, 这个过程需要进行 $n$ 次.
空间复杂度: $O(n+n) = O(n)$, 一个有序数组, 一个结果数组, 大小都为 $n$.

我们从后往前遍历, 将遍历过的数字维护成一个有序数组, 然后对于任意一个新来的数字, 我们可以在有序数组中查询小于该数字的元素个数, 查询的时间复杂度为 $O(logn)$, 然后我们需要将该数字也插入到有序数组中并保持有序, 插入操作需要的时间复杂度为 $O(n)$, 总共有 $n$ 个数字, 因此需要执行 $n$ 次, 故时间复杂度约为 $O(n^2)$, (虽然是 $O(n^2)$, 但是仍然没超时, 考虑是因为只有一个 $logn$, 而解法一具有两个 $logn$.) 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<int> countSmaller(vector<int>& nums) {
int n = nums.size();
vector<int> order_nums;
vector<int> res(n, 0);
for(int i=n-1; i>=0; i--){
//计算第一个不小于nums[i]的数字之前的数字个数
int d = std::lower_bound(order_nums.begin(), order_nums.end(), nums[i]) - order_nums.begin();
res[i] = d; //将数字个数填进结果数字
order_nums.insert(order_nums.begin()+d, nums[i]); // 当 nums[i] 插入到合适位置, 保持order_nums有序
}
return res;
}
};

解法三: 二叉搜索树(BST)

时间复杂度: $O(nlogn)$, 内部只有一个 $logn$ 复杂度的插入操作, 没有其他操作, 但是由于不是平衡的, 所以在最坏情况下的复杂度为 $O(n^2)$, 最好情况即为平衡树, 复杂度为 $O(nlogn)$.
空间复杂度: $O(n+n)$, res 数组和二叉树结构各占 $n$ 大小的空间. 如果采用递归实现插入, 则可能额外需要 $n$ 大小的递归空间.

在解法一中, 通过 multiset 红黑树的结构使得插入时的复杂度为 $logn$, 但是最终需要进行的操作过多, 导致时间超时, 为此, 我们可以自己实现一个二叉搜索树, 从后往前的遍历数组, 并且在插入元素的时候就统计出小于当前元素的节点的个数(为此我们需要在树的结构中额外添加一个变量 smaller, 只是小于当前节点的元素个数), 故而只需要一次 $logn$, 且没有其他多于操作, 代码如下:

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 {
struct TreeNode{
int val;
int smaller;
TreeNode *left;
TreeNode *right;
TreeNode(int v, int s):val(v), smaller(s), left(nullptr), right(nullptr){};
};

int insert(TreeNode *&root, int val){ // 注意, 这要insert函数中, root的值要影响函数外的指针, 所以要用引用&
if(root==nullptr){
root = new TreeNode(val, 0);
return 0; //
}
if(val < root->val){
root->smaller++; // 如果新来的数比当前root的的值还小, 则smaller增1
return insert(root->left, val); // 递归插入到左子树中
}else{ // 递归插入到右子树中, 返回的小于元素的数量为: 根左侧的数量+右子树的数量+根(0:1)
return root->smaller + insert(root->right, val) + (root->val==val ? 0 : 1);
// 这里要千万注意三目运算符的优先级, 一定要用括号整个括起来才行!!!
}
}
public:

vector<int> countSmaller(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, 0);
TreeNode *root = nullptr;
for(int i=n-1; i>=0; i--){ // 如果题目问的是左侧, 则i从0开始
res[i] = insert(root, nums[i]);
}
return res;
}
};

解法四: 归并排序

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

由于解法三构造的二叉树并不是一个平衡的二叉树, 导致在树的极端情况下, 时间复杂度为 $O(n^2)$, 而要手动实现二叉树的平衡逻辑, 又有些复杂, 不适合解此题. 所以, 我们可以考虑此题的另一种解法, 即利用归并排序来解决.

【链接】Loading…
https://leetcode.com/problems/count-of-smaller-numbers-after-self/discuss/76607/C%2B%2B-O(nlogn)-Time-O(n)-Space-MergeSort-Solution-with-Detail-Explanation

322. Coin Change

Description: 硬币凑面额

You are given coins of different denominations and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.

Example 1:

1
2
3
Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1

Example 2:

1
2
Input: coins = [2], amount = 3
Output: -1

解法一: DP

时间复杂度: $O(nm)$, $n$ 为总面额的大小, $m$ 为硬币的数量.
空间复杂度: $O(n)$, DP 数组的大小为总面额的大小.

当我们求组成面额 $i$ 时所需的最少硬币数时, 我们可以用面额 $j$ 和面额 $i-j$ ($j\in[0,i]$)所需的硬币数之和来代替, 因此, 也就是说只与 $i$ 之前的面额数有关, 所以我们可以考虑使用 DP 算法来求解. 我们令 dp[i] 代表组成面额 $i$ 时所需的最少的硬币数, 要求 dp[i], 我们可以根据硬币的面额来求解, 假设硬币的面额是 $j$, 那么就有 dp[i] = min(dp[j] + dp[i-j]), 其中 dp[j]=1, 因为组成这种面额只需要一个硬币就可以了, 我们根据此公式就可以写出相应的 DP 代码, 如下所示.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// 因为不可能为负值, 所以使用无符号整数, 防止溢出
// 额外多了一个0面额, 初值也可以设置为 amount+1, 因为最多的硬币数就是amount个1元.
vector<unsigned int> dp(amount+1, amount+1);
dp[0] = 0; // 为面额0赋初值
for(int i=1; i<amount+1; i++){
for(int ci=0; ci<coins.size(); ci++){
int j = coins[ci];
if(i >= j) dp[i] = std::min(dp[i], 1+dp[i-j]); // 注意不能少了if语句, 否则会运行时错误
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};

你可能会觉得我们进行了一些无用计算, 例如如果 $i$ 为 11, 而 coins 为 [1,5], 那么我们是否只需要计算 dp[6] 就可以了呢? 实际上, 如果有面额为 1 的硬币存在, 那么我们就必须计算所有的小于 $i$ 的dp值, 因为这些都是解, 至于是否为最小数量, 则需要利用 min 来不断筛选.

解法二: DP 递归实现

时间复杂度: $O(nm)$, $n$ 为总面额的大小, $m$ 为硬币的数量.
空间复杂度: $O(n+n)=O(n)$, DP 数组的大小为总面额的大小, 另外, 递归还需额外占用一定空间.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, INT_MAX);
dp[0] = 0;
return coin_dfs(coins, amount, dp);
}
int coin_dfs(vector<int> &coins, int target, vector<int> &dp){
if(target < 0) return -1; // invalid combination
if(dp[target] != INT_MAX) return dp[target]; // already computed, return it
for(int i=0; i<coins.size(); i++){
int tmp = coin_dfs(coins, target-coins[i], dp);
if(tmp>=0) dp[target] = min(dp[target], 1+tmp);
}
dp[target] = (dp[target] == INT_MAX) ? -1 : dp[target];
return dp[target];
}
};

解法三: 对暴力解法剪枝

时间复杂度: $O(logn+mlogm)$, 每次都用当前面额除以硬币面额, 故时间复杂度为 $O(logn)$, $O(mlogm)$ 为对硬币面额的排序复杂度, 当 $m<<n$ 时, 可忽略不计.
空间复杂度: $O(logn)$, 无需申请额外空间, 仅仅是递归过程需要占用空间.

下面的方法利用余数对暴力解法进行剪枝, 剪枝后的程序运行速度十分快, 远远快于前两个算法.

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 coinChange(vector<int>& coins, int amount) {
int res = INT_MAX; // results count
int n = coins.size();
int cur = 0; // current count
std::sort(coins.begin(), coins.end()); // sort from small to large
helper(coins, amount, n-1, cur, res);
return res==INT_MAX ? -1 : res;
}

void helper(vector<int> &coins, int target, int start, int cur, int &res){
if(target%coins[start]==0){ // 如果可以整除, 说明找到了合适的组合
res = min(res, cur+target/coins[start]);
return;
}
for(int i=target/coins[start]; i>=0; i--){
if(cur+i >= res-1)
// 如果当前的硬币数已经超过了 res-1, 说明之后肯定需要更多的硬币,
// 因为后面的硬币面额变小了, 所以需要至少cur+i+1个硬币才能凑齐
// 因此, 无需再进行循环, 直接跳出即可
break;
if(start>0) // start不能为负值, 因此start要大于0才能继续递归
helper(coins, target-i*coins[start], start-1, cur+i, res);
}
}
};

关于此算法的更详细解释(http://www.cnblogs.com/grandyang/p/5138186.html):
难道这题一定要DP来做吗, 我们来看网友hello_world00提供的一种解法, 这其实是对暴力搜索的解法做了很好的优化, 不仅不会TLE, 而且击败率相当的高!对比Brute Force的方法, 这里在递归函数中做了很好的优化. 首先是判断start是否小于0, 因为我们需要从coin中取硬币, 不能越界. 下面就是优化的核心了, 看target是否能整除coins[start], 这是相当叼的一步, 比如假如我们的目标值是15, 如果我们当前取出了大小为5的硬币, 我们做除法, 可以立马知道只用大小为5的硬币就可以组成目标值target, 那么我们用cur + target/coins[start] 来更新结果res. 之后的for循环也相当叼, 不像暴力搜索中的那样从start位置开始往前遍历coins中的硬币, 而是遍历 target/coins[start] 的次数, 由于不能整除, 我们只需要对余数调用递归函数, 而且我们要把次数每次减1, 并且再次求余数. 举个例子, 比如coins=[1,2,3], amount=11, 那么 11除以3, 得3余2, 那么我们的i从3开始遍历, 这里有一步非常有用的剪枝操作, 没有这一步, 还是会TLE, 而加上了这一步, 直接击败百分之九十九以上, 可以说是天壤之别. 那就是判断若 cur + i >= res - 1 成立, 直接break, 不调用递归. 这里解释一下, cur + i 自不必说, 是当前硬币个数cur 加上新加的i个硬币, 我们都是知道cur+i如果大于等于res的话, 那么res是不会被更新的, 那么为啥这里是大于等于res-1呢?因为能运行到这一步, 说明之前是无法整除的, 那么余数一定存在, 所以再次调用递归函数的target不为0, 那么如果整除的话, cur至少会加上1, 所以又跟res相等了, 还是不会使得res变得更小.

323. 无向图中连通分量的数目

题目链接: https://leetcode-cn.com/problems/number-of-connected-components-in-an-undirected-graph/

解法一: DFS

Python 实现:

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 countComponents(self, n: int, edges: List[List[int]]) -> int:
def dfs(graph, visited, node):
for adj_node in graph[node]:
if not visited[adj_node]:
visited[adj_node] = True
dfs(graph, visited, adj_node)

graph = dict()
for i in range (n):
graph[i] = dict()
for edge in edges:
graph[edge[0]][edge[1]] = 1;
graph[edge[1]][edge[0]] = 1;
visited = [False] * n
connected = 0
for node in range(n):
if not visited[node]:
visited[node] = True
connected += 1
dfs(graph, visited, node)
return connected

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
class Solution {
void dfs(std::unordered_map<int, std::unordered_set<int>> &graph, std::vector<bool> &visited, int node) {
for (auto &adj_node : graph[node]) {
if (!visited[adj_node]) {
visited[adj_node] = true;
dfs(graph, visited, adj_node);
}
}
}
public:
int countComponents(int n, vector<vector<int>>& edges) {
std::unordered_map<int, std::unordered_set<int>> graph;
for (int i = 0; i < n; i++) graph[i] = std::unordered_set<int>{};
for (auto &edge : edges) {
graph[edge[0]].insert(edge[1]);
graph[edge[1]].insert(edge[0]);
}

std::vector<bool> visited(n, false); // 访问数组, 记录访问过的节点
int connect = 0; // 记录连通分量个数
for (auto &node : graph) {
if (!visited[node.first]) {
connect++;
visited[node.first] = true;
dfs(graph, visited, node.first); // 深度遍历
}
}
return connect;
}
};

解法二: BFS

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 countComponents(self, n: int, edges: List[List[int]]) -> int:
graph = dict()
for i in range (n):
graph[i] = dict()
for edge in edges:
graph[edge[0]][edge[1]] = 1;
graph[edge[1]][edge[0]] = 1;
visited = [False] * n
connected = 0
for node in range(n):
if not visited[node]:
visited[node] = True
connected += 1
graph_q = [node] # 初始化 BFS 队列
while (graph_q): # 循环直至队列为空
cur_node = graph_q.pop(0)
for adj_node in graph[cur_node]: # 所有未遍历过的邻接节点入队列
if not visited[adj_node]:
visited[adj_node] = True
graph_q.append(adj_node)
return connected
`

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

public:
int countComponents(int n, vector<vector<int>>& edges) {
std::unordered_map<int, std::unordered_set<int>> graph;
for (int i = 0; i < n; i++) graph[i] = std::unordered_set<int>{};
for (auto &edge : edges) {
graph[edge[0]].insert(edge[1]);
graph[edge[1]].insert(edge[0]);
}

std::vector<bool> visited(n, false); // 访问数组, 记录访问过的节点
int connected = 0; // 记录连通分量个数

for (auto &node : graph) {
if (!visited[node.first]) {
connected++;
visited[node.first] = true;
std::queue<int> graph_q; // BFS 队列
graph_q.push(node.first); // 初始化节点入队列
while (!graph_q.empty()) { // BFS 直至队列为空
int cur_node = graph_q.front(); graph_q.pop();
for (auto &adj_node : graph[cur_node]) { // 邻接节点入队列
if (!visited[adj_node]) {
visited[adj_node] = true;
graph_q.push(adj_node);
}
}
}
}
}
return connected;
}
};

解法三: 并查集

直接用并查集,对每一条边进行union操作,最后统计还有多少个不同的部分。

并查集的两种优化方法:

  • rank: 在进行 union 操作时, 根据当前树的高度进行合并, 使得合并后的树高尽可能的矮
  • 路径压缩: 每次 find 的时候, 将遇到的所有节点的父亲节点都指向根节点, 这样可以大大降低树的高度.

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
37
38
39
40
41
class UnionFind:
def __init__(self, n):
self.parent = [i for i in range(n)] # 父亲节点, 初始时每个节点的父亲节点指向自己
self.rank = [1] * n # 每个节点的 rank, 也即树高, 初始时树高均为 1
self.count = n # 并查集中集合的个数, 初始化每个节点构成一个集合, 集合个数为 n

def find_root(self, node): # 递归方法进行路径压缩
if (node == self.parent[node]): # 如果该节点的父亲节点为自身, 则该节点为根节点
return node
else:
self.parent[node] = self.find_root(self.parent[node]) # 递归查找根节点, 同时进行路径压缩, 让当前节点的所有祖先节点最终的父亲都指向根节点
return self.parent[node] # 返回根节点

def find_root2(self, node): # 迭代方法进行路径压缩
ancestors = [] # 记录从当前节点到根节点的路径上的所有节点, 之后将这些节点的父亲指向根节点, 完成路径压缩
while (node != self.parent[node]): # 循环直至找到根节点
ancestors.append(node) # 添加路径上的节点(包括自己)
node = self.parent[node] # 向上找到父亲节点
for anc in ancestors:
self.parent[anc] = node # 令所有节点的父亲指向根节点, 完成路径压缩
return node # 返回根节点

def union_item(self, p, q): # 基于 rank 的 union, 尽量保证树的高度较小
root_p = self.find_root(p)
root_q = self.find_root(q)
if (root_p == root_q): return # 说明已经存在于同一个集合内, 直接返回
if self.rank[root_p] < self.rank[root_q]: # 如果p根节点的高度小于q的高度, 那么就把p的父亲指向q, q的高度不变
self.parent[root_p] = root_q
elif self.rank[root_q] < self.rank[root_p]: # 反之, 将q的父亲指向p, 高度不变
self.parent[root_q] = root_p
else: # 二者相等时, 随意选择一个即可, 此时, 作为根节点的 rank 增加 1, 因为树高增加了 1
self.parent[root_p] = root_q
self.rank[root_q] += 1
self.count -= 1 # 完成了一次 union, 则集合数量少了一个

class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
uf = UnionFind(n)
for edge in edges:
uf.union_item(edge[0], edge[1])
return uf.count

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
60
61
62
63
64
class UnionFind {
private:
std::vector<int> parent;
std::vector<int> rank;
int count;
public:
UnionFind(int n) {
for (int i = 0; i < n; i++) {
parent.emplace_back(i); // 初始时每个节点单独构成一个集合
rank.emplace_back(1); // 初始高度均为1
}
count = n; // 总共有 n 个集合
}

int find_root(int node) { // 递归路径压缩
if (parent[node] == node) {
return node;
} else {
parent[node] = find_root(parent[node]);
return parent[node];
}
}

int find_root2(int node) { // 迭代路径压缩
std::vector<int> ancestors;
while (node != parent[node]) {
ancestors.emplace_back(node);
node = parent[node];
}
for (auto &anc : ancestors) { // 路径压缩
parent[anc] = node;
}
return node;
}

void union_item(int p, int q) {
int root_p = find_root(p);
int root_q = find_root(q);
if (root_p == root_q) return;
if (rank[root_p] < rank[root_q]) {
parent[root_p] = root_q;
} else if (rank[root_q] < rank[root_p]) {
parent[root_q] = root_p;
} else {
parent[root_q] = root_p;
rank[root_p]++;
}
count--;
}

int get_count() {
return count;
}
};
class Solution {
public:
int countComponents(int n, vector<vector<int>>& edges) {
UnionFind uf(n);
for (auto &edge : edges) {
uf.union_item(edge[0], edge[1]);
}
return uf.get_count();
}
};

324. Wiggle Sort II

Description: “驼峰” 排序

Given an unsorted array nums, reorder it such that nums[0] < nums[1] > nums[2] < nums[3]….

Example 1:

1
2
Input: nums = [1, 5, 1, 1, 6, 4]
Output: One possible answer is [1, 4, 1, 5, 1, 6].

Example 2:

1
2
Input: nums = [1, 3, 2, 2, 3, 1]
Output: One possible answer is [2, 3, 1, 3, 1, 2].

Note:
You may assume all input has valid answer.

Follow Up:
Can you do it in O(n) time and/or in-place with O(1) extra space?

解法一: 排序

时间复杂度: $O(nlogn + n)$, 排序的时间复杂度为 $O(nlogn)$, 构造 “驼峰” 数组的复杂度为 $O(n)$
空间复杂度: $O(n)$, 额外数组需要占用 $O(n)$ 空间

该问题的解法可能有多个, 我们只需要找到其中一个即可, 核心思路是将一个数组分成两半, 其中前一半的元素都小于后一半的元素, 然后我们只需要依次从两个数组中取值组成新数组, 就可以满足 “驼峰” 排序.
首先, 对数组中的元素排序, 这样, 任意的相邻元素, 都满足 nums[i] <= nums[i+1], 我们将数组分成两半, 这样, 前半段的元素都小于等于后半段的元素, 注意, 题目中已经指明数组是合法的有效数组, 所以一定可以组成驼峰, 因此, 我们先取前半段的最后一个元素, 再取后半段的最后一个元素, 这两个元素一定满足绝对小于关系(否则无法形成 “驼峰”), 然后我们再取倒数第二个, 依次类推, 直至取完. 注意, 我们不能从前往后取, 因为不能保证前半段的第二个元素绝对小于后半段的第一个元素, 例如[4,5,5,6], 从前往后取就会变成[4,5,5,6], 不符合驼峰, 从后往前取为[5,6,4,5], 符合驼峰.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
void wiggleSort(vector<int>& nums) {
int low = 0, high = nums.size()-1;
std::sort(nums.begin(), nums.end());
int mid = (nums.size()+1)/2; // 令mid指向中间的位置
vector<int> tmp;
for(int i=mid-1, j=nums.size()-1; i>=0 ; i--, j--){ // 从后往前选择元素, 分别放到tmp中
tmp.push_back(nums[i]);
if(j>=mid) tmp.push_back(nums[j]);
}
nums = tmp;
}
};

解法二: partition

时间复杂度: $O(n+n)= O(n)$, 查找中位数需要 $O(n)$, 填充数组需要 $O(n)$.
空间复杂度: $O(n)$, 填充时使用了额外的数组空间来辅助.

如果当数组中的元素不含有重复时, 此题很容易就用基于 partition 的方法解决, 因为, 我们可以找到将数组分成两个具有绝对小于关系的数组, 然后依次用两个数组填充即可, 但是, 此题的元素是可重复的, 所以必须考虑重复元素的影响.
首先我们利用 nth_element() 找到中位数, 虽然 nth_element() 的时间复杂度已经不是 $O(n)$, 但是这里我们为了简化代码, 仍然使用 nth_element() 来查找中位数 mid(后面也会更多稍复杂一点的 partition 算法, 面试时建议使用 nth_element, 注意要向面试官说明复杂度问题), 之后, 对于其他的任意一个数组元素, 都有三种不同的情况:

  • 大于 mid, 将大于 mid 的元素放在数组开始的奇数位上面;
  • 小于 mid, 将小于 mid 的元素放在数组的偶数位上面;
  • 等于 mid, 用所有等于 mid 的元素填充剩下的位置.

由于题目指明输入的数组一定是有效的, 因此当我们进行了上面遍历后, 数组一定会变成 “驼峰” 数组, 因为当和 mid 相等的元素处于 “驼峰” 底部时, 它一定位于偶数位(奇数位都是大于 mid 的元素), 同理, 当 mid 处于 “驼峰” 顶部时, 它一定位于奇数位, 因为偶数位都被小于 mid 的元素填充. 故最终的数组是 “驼峰” 数组.

nth_element()(该函数在 C++17 后不是 $O(n)$, 而是 $O(nlogn)$, 但是在 C++11 中仍然是 $O(n)$):

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:
void wiggleSort(vector<int>& nums) {
int n = nums.size();
std::nth_element(nums.begin(), nums.begin()+n/2, nums.end());
int mid = nums[n/2]; // 找到中位数

vector<int> res(n, mid); // 先将所有元素置为中位数
int even_i = (n-1)/2*2; // 令 even_i 指向数组的最后一个偶数位
int odd_i = 1;
for(int i=0; i<n; i++){
if(nums[i] > mid){ // 将大于中位数的放到前面的奇数位上
res[odd_i] = nums[i];
odd_i += 2;
}else if(nums[i] < mid){ //将小于中位数的放到后面的偶数位上
res[even_i] = nums[i];
even_i -= 2;
}
} // 剩下的位置都是中位数
nums = res;
}
};

自己利用partition实现 $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
class Solution {
private:
int partition(vector<int> &nums, int low, int high){
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;
}
public:
void wiggleSort(vector<int>& nums) {
int n = nums.size();
int low = 0, high = n-1;
int target = n/2;
while(1){
int m = partition(nums, low, high);
if(m < target) low = m + 1;
else if(m > target) high = m - 1;
else break;
}
int mid = nums[target]; // 找到中位数

vector<int> res(n, mid); // 先将所有元素置为中位数
int even_i = (n-1)/2*2; // 令 even_i 指向数组的最后一个偶数位
int odd_i = 1; // 指向第一个奇数位
for(int i=0; i<n; i++){
if(nums[i] > mid){ // 将大于中位数的放到前面的奇数位上
res[odd_i] = nums[i];
odd_i += 2;
}else if(nums[i] < mid){ //将小于中位数的放到后面的偶数位上
res[even_i] = nums[i];
even_i -= 2;
}
} // 剩下的位置都是中位数
nums = res;
}
};

Follow up: three-way partition

时间复杂度: $O(n+n) = O(n)$, 找中位数时的复杂度为 $O(n)$, 调整数组的复杂度为 $O(n)$.
空间复杂度: $O(1)$, 无需占用额外空间

解法二的时间复杂度满足要求, 问题在于我们如何能够在 $O(1)$ 的空间复杂度限制下, 完成数组的填充工作, 很自然的我们可以想到利用 swap 来实现, 具体流程如下所示:

  1. 先令 even_i 指向数组的最后一个偶数位(从0位开始, 0算作偶数位), 令 odd_i 指向第一个奇数位(下标为1). 我们从最后一个偶数位元素(用下标 i 指示)开始进行判断;
  2. 如果 nums[i]<mid, 则将 nums[i]nums[even_i] 交换, 交换后, even_i 不可再被访问, 令 even_i -= 2, 同时注意, 由于刚开始的时候 ieven_i 是相等的, 故也要令 i -= 2, 当 i<0 以后, 要令 i 指向最后一个奇数位.
  3. 如果 nums[i]>mid, 则将 nums[i]nums[odd_i] 交换, 同时令 odd_i += 2, 注意, 此时, i 指向的数字是交换后的原来 odd_i 指向的数字, 因此, 我们需要对该数字进行判断, 故不能改变 i 的值.
  4. 如果和 mid 相等, 则无需进行交换填充, 令其保存原值即可, 判断下一个元素, 令 i -=2, 同时还要判断 i 是否小于 0, 若小于, 则需令 i 指向最后的奇数位.

nth_element():

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 {
public:
void wiggleSort(vector<int>& nums) {
int n = nums.size();
std::nth_element(nums.begin(), nums.begin()+n/2, nums.end());
int mid = nums[n/2]; // 找到中位数

// O(1) 空间复杂度填充数组
int even_i = (n-1)/2*2;
int odd_i = 1;
int i = even_i; // 令i指向最后一个偶数位
int count = n;
while(count--){ //每次都会判断一个元素
if(nums[i] < mid){
std::swap(nums[i], nums[even_i]);
even_i -= 2;
i -= 2;
if(i<0) i = n/2*2 - 1; // 令 i 指向最后一个奇数位
}else if(nums[i] > mid){
std::swap(nums[i], nums[odd_i]);
odd_i += 2; // 奇数位增加
}else{ // 保持原值不变, 判断下一个值
i -= 2;
if(i<0) i = n/2*2 - 1; // 令 i 指向最后一个奇数位
}
}
}
};

partition:

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
class Solution {
private:
int partition(vector<int> &nums, int low, int high){
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;
}
public:
void wiggleSort(vector<int>& nums) {
int n = nums.size();
int low = 0, high = n-1;
int target = n/2;
while(1){
int m = partition(nums, low, high);
if(m < target) low = m + 1;
else if(m > target) high = m - 1;
else break;
}
int mid = nums[target]; // 找到中位数

// O(1) 空间复杂度填充数组
int even_i = (n-1)/2*2;
int odd_i = 1;
int i = even_i; // 令i指向最后一个偶数位
int count = n;
while(count--){ //每次都会判断一个元素
if(nums[i] < mid){
std::swap(nums[i], nums[even_i]);
even_i -= 2;
i -= 2;
if(i<0) i = n/2*2 - 1; // 令 i 指向最后一个奇数位
}else if(nums[i] > mid){
std::swap(nums[i], nums[odd_i]);
odd_i += 2; // 奇数位增加
}else{ // 保持原值不变, 判断下一个值
i -= 2;
if(i<0) i = n/2*2 - 1; // 令 i 指向最后一个奇数位
}
}
}
};

326. Power of Three

Description: 三的幂次

Given an integer, write a function to determine if it is a power of three.

Example 1:

1
2
Input: 27
Output: true

Example 2:

1
2
Input: 0
Output: false

Example 3:

1
2
Input: 9
Output: true

Example 4:

1
2
Input: 45
Output: false

解法一: 自下而上(超时)

时间复杂度: $O(logn)$, 计算3的幂次, 总共需要计算 $log_3n$ 次
空间复杂度: $O(1)$

该方法从 3 开始, 逐渐计算 3 的幂次, 但是由于对于任何数都要计算 $log3n$ 次, 故当数很大时会超时

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
bool isPowerOfThree(int n) {
int pow = 1;
while(pow < n){
pow = pow*3;
}
return pow==n ? true : false;
}
};

解法二: 自上而下

时间复杂度: $O(logn)$, 利用除法判断是否能整除 3, 当不能整除时, 可以提前退出, 起到剪枝效果, 最多需要计算 $log_3n$ 次
空间复杂度: $O(1)$

解法一采用的自下而上的乘法方法对于任何的数字都需要进行 $log_3n$ 次乘法才能判断是否为 3 的幂次, 这显然是不需要的, 我们只需要利用除法, 不断判断是否能被 3 整除即可, 一旦发现不能整除, 则肯定不是 3 的幂次, 可提前退出, 代码如下:

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
bool isPowerOfThree(int n) {
if(n<1) return false;
while (n%3 == 0){
n /= 3;
}
return n==1;
}
};

解法三: 进制转换(不使用循环或迭代)

十进制的 pow 形式为: 10, 100, 1000 (分别代表十, 一百, 一千)
二进制的 pow 形式为: 10, 100, 1000 (分别代表二, 四, 八)
因此我们可以推出三进制的形式为: 10, 100, 1000 (分别代表三, 九, 二十七)

故此, 我们可以将十进制先转换成三进制, 然后判断三进制形式是否首位为一, 其他位均为零, 如果满足, 则说明当前的数字是三的幂次. 该方法不需要循环和迭代(实际上在转换的过程仍然使用了循环和迭代).

1
2


328. Odd Even Linked List

Description: 奇偶链表

Given a singly linked list, group all odd nodes together followed by the even nodes. Please note here we are talking about the node number and not the value in the nodes.

You should try to do it in place. The program should run in O(1) space complexity and O(nodes) time complexity.

Example 1:

1
2
Input: 1->2->3->4->5->NULL
Output: 1->3->5->2->4->NULL

Example 2:

1
2
Input: 2->1->3->5->6->4->7->NULL
Output: 2->3->6->7->1->5->4->NULL

Note:
The relative order inside both the even and odd groups should remain as it was in the input.
The first node is considered odd, the second node even and so on …

解法一: 一次遍历

时间复杂度: $O(n)$, 遍历每个节点一次
空间复杂度: $O(1)$, 未使用任何额外空间

我们利用两个变量分别来维护奇数链表和偶数链表, 最后令奇数链表的最后一个节点的 next 指针指向偶数链表的头结点, 代码如下:

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

ListNode *odd_head = head; // 奇数链表头
ListNode *even_head = head->next; // 偶数链表头
ListNode *odd_node = odd_head; // 奇数链表节点
ListNode *even_node = even_head; // 偶数链表节点
ListNode *node = head->next->next; // 令当前节点指向第三个节点
int i = 3; // 记录节点编号(从1开始)
while(node!=nullptr){
if(i&1 == 1){ // 奇数链表
odd_node->next = node;
odd_node = odd_node->next;
node = node->next;
}else{ // 偶数链表
even_node->next = node;
even_node = even_node->next;
node = node->next;
}
i++;
}
odd_node->next = even_head;
even_node->next = nullptr; // 少了这句话会超时, 原因是even_node会指向前面的某个节点, 形成环, 使得程序判断时无法终止
return odd_head;
}
};

更简洁的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Solution {
public ListNode oddEvenList(ListNode head) {
if (head == null) return null;
ListNode odd = head, even = head.next, evenHead = even;
while (even != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = evenHead;
return head;
}
}

329. Longest Increasing Path in a Matrix

Description: 寻找矩阵中的最长递增序列

Given an integer matrix, find the length of the longest increasing path.

From each cell, you can either move to four directions: left, right, up or down. You may NOT move diagonally or move outside of the boundary (i.e. wrap-around is not allowed).

Example 1:

1
2
3
4
5
6
7
8
Input: nums =
[
[9,9,4],
[6,6,8],
[2,1,1]
]
Output: 4
Explanation: The longest increasing path is [1, 2, 6, 9].

Example 2:

1
2
3
4
5
6
7
8
Input: nums =
[
[3,4,5],
[3,2,6],
[2,2,1]
]
Output: 4
Explanation: The longest increasing path is [3, 4, 5, 6]. Moving diagonally is not allowed.

解法一: DP + dfs

时间复杂度: $O(mn)$, 每个节点都会遍历一次, 当遍历一次后, 下次再访问时可以直接通过 dp 数组得知答案.
空间复杂度: $O(mn+mn=mn)$, $n$ 行 $m$ 列的 DP 数组所占用的空间大小, 另外还有递归所占用的空间($mn?$)

申请和矩阵相同大小的 DP 数组, 令 dp[i][j] 代表从 (i,j) 位置为起点的绝对递增数列的长度, 每遍历一个位置后, 下一次再访问该位置时就无需重复计算, 可以直接通过 dp 数组获取到相应长度. 在查找当前节点的最大长度时, 我们利用 dfs 算法, 依次从四个方向进行查找, 最终取最大值作为本位置的最长递增序列长度

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 {
private:
int dirs[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};

int dfs(vector<vector<int>> &matrix, vector<vector<int>> &dp, int i, int j, int &n, int &m){
if(dp[i][j]!=0) return dp[i][j];
dp[i][j] = 1; //长度至少为1
for(auto d : dirs){
int x = i+d[0], y = j+d[1];
if(x>=0 && x<n && y>=0 && y<m && matrix[x][y] > matrix[i][j]){ // 绝对递增, 因此不能有 =
int len = 1+dfs(matrix, dp, x, y, n, m);
dp[i][j] = std::max(dp[i][j], len);
}
}
return dp[i][j];
}
public:
int longestIncreasingPath(vector<vector<int>>& matrix) {
if(matrix.size()==0 || matrix[0].size()==0) return 0;
int n = matrix.size(), m = matrix[0].size();
vector<vector<int>> dp(n, vector<int>(m, 0));
int res=1;
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
res = std::max(res, dfs(matrix, dp, i, j, n, m));
}
}
return res;
}
};

解法二: DP + BFS

TODO: http://www.cnblogs.com/grandyang/p/5148030.html

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:
int longestIncreasingPath(vector<vector<int>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int m = matrix.size(), n = matrix[0].size(), res = 1;
vector<vector<int>> dirs{{0,-1},{-1,0},{0,1},{1,0}};
vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j ) {
if (dp[i][j] > 0) continue;
queue<pair<int, int>> q{{{i, j}}};
int cnt = 1;
while (!q.empty()) {
++cnt;
int len = q.size();
for (int k = 0; k < len; ++k) {
auto t = q.front(); q.pop();
for (auto dir : dirs) {
int x = t.first + dir[0], y = t.second + dir[1];
if (x < 0 || x >= m || y < 0 || y >= n || matrix[x][y] <= matrix[t.first][t.second] || cnt <= dp[x][y]) continue;
dp[x][y] = cnt;
res = max(res, cnt);
q.push({x, y});
}
}
}
}
}
return res;
}
};

334. 递增的三元子序列

Description: 递增的三元子序列

给定一个未排序的数组,判断这个数组中是否存在长度为 3 的递增子序列。

数学表达式如下:

如果存在这样的i, j, k, 且满足0 ≤ i < j < k ≤ n-1
使得arr[i] < arr[j] < arr[k],返回true; 否则返回false
说明: 要求算法的时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。

Example 1:

1
2
Input: [1,2,3,4,5]
Output: true

Example 2:

1
2
Input: [5,4,3,2,1]
Output: false

解法一: 用辅助变量指向 min 和 mid

时间复杂度: $O(n)$, 每个元素之遍历一次
空间复杂度: $O(1)$, 无需额外空间

我们利用两个变量 minmid 分别指向三元子序列中的最小元素和中间元素, 最开始时, 二者赋初值为 INT_MAX, 然后遍历数组, 对于数组中的每一个数 num, 进行如下判断:

  1. 是否小于等于 min, 若满足, 则令 min=num;
  2. 若不满足(1), 则说明 num > min, 判断 num 是否小于等于 mid, 若满足, 责令 mid=num;(此时 mid 一定大于 min, 且下标也大于 min 下标)
  3. 若不满足(1)(2), 则说明 num 不仅大于 min, 而且大于 mid, 同时 num 的下标也大于前两者, 由此, 我们找到了一个满足条件的递增三元组子序列, 可直接返回 true. 否则, 重复以上步骤直至遍历完数组
  4. 如果遍历完整个数组后, 仍然找不到符合条件的序列, 则说明不存在这样的序列, 返回 false.

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def increasingTriplet(self, nums: List[int]) -> bool:
low = float('inf')
mid = float('inf')
for num in nums:
if num <= low: # 如果 num <= low 则令其为 low
low = num
elif num <= mid: # 否则, 令其为 mid
mid = num
else: # 如果 num > low 且 > mid, 说明存在这样的三元组, 因为如果不存在的话, mid 或 low 肯定为 inf
return True
return False

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
if(nums.size() < 3) return false;
int min=INT_MAX, mid=INT_MAX;
for(auto num : nums){
if(num <= min) // 等于号不能少, 否则会跳到最后的else中, 直接返回true
min = num;
else if(num <= mid) // 如输入为 11111111 时, 若没有等于号, 则会跳到else中返回true
mid = num;
else
return true; //当前数字比min和mid都大, 所以找到了一个三元组
}
return false;
}
};

337. 打家劫舍 III-中等

题目链接: https://leetcode-cn.com/problems/house-robber-iii/

二叉树型的偷家

解法: 树形动态规划

动态方程:

  • 某个树的最大收益 = max(包含根节点的最大收益,以及不包含根节点的最大收益);
  • 不包含根节点的最大收益 = 左子树的最大收益 + 右子树最大收益
  • 包含根节点的最大收益 = 不包含左子节点的左子树最大收益 + 根节点 + 不包含右子节点的最大收益
  • maxSum = max(maxSum,当前树的最大收益)

复杂度分析:
时间复杂度:O(n)。只遍历一遍所有节点
空间复杂度:O(n)。递归栈的调用,如果树极度不平衡,空间复杂度为O(n);如果树平衡,为O(log 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
/**
* 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 {
std::vector<int> traverse(TreeNode* root) {
int rob = 0, not_rob = 0;
if (root == nullptr) return std::vector<int> {rob, not_rob};
auto left = traverse(root->left);
auto right = traverse(root->right);
rob = left[1] + right[1] + root->val;
not_rob = std::max(left[0], left[1]) + std::max(right[0], right[1]);
return std::vector<int> {rob, not_rob};
}
public:
int rob(TreeNode* root) {
auto res = traverse(root);
return std::max(res[0], res[1]);
}
};

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def rob(self, root: TreeNode) -> int:
def helper(root):
if root == None: return 0, 0;
l_rob, l_not_rob = helper(root.left)
r_rob, r_not_rob = helper(root.right)
rob = l_not_rob + r_not_rob + root.val
not_rob = max(l_rob, l_not_rob) + max(r_rob, r_not_rob)
return rob, not_rob
return max(helper(root))

341. Flatten Nested List Iterator

Description: 将嵌套的多维列表展开成一维

Given a nested list of integers, implement an iterator to flatten it.

Each element is either an integer, or a list — whose elements may also be integers or other lists.

Example 1:

1
2
3
Input: [[1,1],2,[1,1]]
Output: [1,1,2,1,1]
Explanation: By calling next repeatedly until hasNext returns false, the order of elements returned by next should be: [1,1,2,1,1].

Example 2:

1
2
3
Input: [1,[4,[6]]]
Output: [1,4,6]
Explanation: By calling next repeatedly until hasNext returns false, the order of elements returned by next should be: [1,4,6].

解法一: 栈

PS: 这道题可以在初始化时将列表全部展开并存储, 这样 hasNext() 就可以达到 $O(1)$ 的时间复杂度, 但是, 这是很不好的! 因为实际实现迭代器时, 我们往往只在需要的时候才会对元素进行展开, 这样可以获得最大的平均效率

时间复杂度: $O(n)$, 每个节点至多遍历一次, 其中, next() 复杂度为 $O(1)$, 初始化和 hasNext() 的复杂度均为 $O(n)$
空间复杂度: $O(n)$, 栈所需空间

先将数组中的所有元素从后往前的放进栈中, 这样栈顶元素即为数组中的第一个元素, 然后对栈顶元素进行判断, 如果 isInteger() 为真, 则直接返回 true, 否则, 就获取栈顶对应的 vector<NestedInteger> 数组, 并将栈顶 pop(), 然后将数组从后往前再放到栈中, 重复以上操作直至栈为空, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
/**
* // This is the interface that allows for creating nested lists.
* // You should not implement it, or speculate about its implementation
* class NestedInteger {
* public:
* // Return true if this NestedInteger holds a single integer, rather than a nested list.
* bool isInteger() const;
*
* // Return the single integer that this NestedInteger holds, if it holds a single integer
* // The result is undefined if this NestedInteger holds a nested list
* int getInteger() const;
*
* // Return the nested list that this NestedInteger holds, if it holds a nested list
* // The result is undefined if this NestedInteger holds a single integer
* const vector<NestedInteger> &getList() const;
* };
*/
class NestedIterator {
private:
stack<NestedInteger> s;
public:
NestedIterator(vector<NestedInteger> &nestedList) {
for(int i=nestedList.size()-1; i>=0; i--){
s.push(nestedList[i]);
}
}

int next() {
auto res = s.top();
s.pop();
return res.getInteger();
}

bool hasNext() {

while(!s.empty()){
NestedInteger top = s.top();
if(top.isInteger()) return true;
else{
s.pop();
vector<NestedInteger> list = top.getList();
for(int i=list.size()-1; i>=0; i--){
s.push(list[i]);
}

}
}
return false;
}
};

/**
* Your NestedIterator object will be instantiated and called as such:
* NestedIterator i(nestedList);
* while (i.hasNext()) cout << i.next();
*/

解法二: deque

时间复杂度: $O(n)$, 每个节点至多遍历一次, 其中, next() 复杂度为 $O(1)$, 初始化和 hasNext() 的复杂度均为 $O(n)$
空间复杂度: $O(n)$, 双端队列所需空间

同样的思路, 也可以用双端队列解决.(栈有的功能双端队列也有)

344. Reverse String

Description: 反转字符串

Write a function that takes a string as input and returns the string reversed.

Example 1:

1
2
Input: "hello"
Output: "olleh"

Example 2:

1
2
Input: "A man, a plan, a canal: Panama"
Output: "amanaP :lanac a ,nalp a ,nam A"

解法一: 使用 reverse 函数

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

1
2
3
4
5
6
7
class Solution {
public:
string reverseString(string s) {
std::reverse(s.begin(), s.end());
return s;
}
};

解法二: 基于 swap

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

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
string reverseString(string s) {
int len = s.size();
for(int i=0; i<len/2; i++){
std::swap(s[i], s[len-1-i]);
}
return s;
}
};

347. Top K Frequent Elements

Description: 寻找频率最高的 k 个数字

Given a non-empty array of integers, return the k most frequent elements.

Example 1:

1
2
Input: nums = [1,1,1,2,2,3], k = 2
Output: [1,2]

Example 2:

1
2
Input: nums = [1], k = 1
Output: [1]

Note:
You may assume k is always valid, 1 ≤ k ≤ number of unique elements.
Your algorithm’s time complexity must be better than O(n log n), where n is the array’s size.

解法一: 哈希+大顶堆

时间复杂度: $O(n+nlogn)=O(nlogn)$, 遍历复杂度为 $O(n)$, 堆排序复杂度为 $O(nlogn)$
空间复杂度: $O(n+n) = O(n)$, unordered_mappriority_queue 各占 $O(n)$ 大小的空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> hash;
priority_queue<pair<int, int>> q;
vector<int> res;
for(auto num : nums)
hash[num]++; //对于不存在的关键字, 其值默认为0
for(auto it:hash)
q.push({it.second, it.first}); // 注意, sceond在前作为排序依据
for(int i=0 ; i<k; i++){
res.push_back(q.top().second); q.pop();
// 注意, 因为插入的时候将first插在了第二位, 因此, 获取时应该用second获取数字
}
return res;
}
};

解法二: 哈希+小顶堆

时间复杂度: $O(n+nlogk)=O(nlogk)$, 遍历复杂度为 $O(n)$, 堆排序时, 用小顶堆, 只保存最大的 k 个元素即可.
空间复杂度: $O(n+n) = O(n)$, unordered_mappriority_queue 各占 $O(n)$ 大小的空间

整体思路和解法一相同, 只不过我们需要得到最大的 $k$ 个元素即可, 因此无需维护 $n$ 大小的大顶堆. 相反, 我们选择维护 $k$ 大小的小顶堆, 对于任意一个新来的元素, 如果它大于堆顶, 则将堆顶退出, 然后将新来元素加入堆中. 因为小顶堆的堆顶是最小的元素, 因此堆中用于 $k-1$ 个比堆顶大的元素, 故这 $k$ 个元素就是最大的 $k$ 个元素, 最终我们只需要将堆中数据依次取出, 然后执行一次 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
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> hash;

// 注意这里小顶堆的定义, 其元素是 pair 类型
priority_queue<pair<int, int>, vector<pair<int,int>>, std::greater<pair<int,int>>> q; // 小顶堆
vector<int> res;
for(auto num : nums)
hash[num]++; //对于不存在的关键字, 其值默认为0
for(auto it : hash){ // 注意, 必须是遍历哈希表, 而不能遍历原数组, 因为原数组存在重复数字
if(q.size() < k)
q.push({it.second, it.first});
else if(q.top().first < it.second){
q.pop();
q.push({it.second, it.first});
}
}
for(int i=0 ; i<k; i++){
res.push_back(q.top().second); q.pop();
}
std::reverse(res.begin(), res.end()); //因为结果是从小顶堆中得到的, 所以需要逆置一下, 也可以不逆置
return res;
}
};

解法三: 哈希+桶

时间复杂度: $O(n+n+k)=O(n)$, 构建哈希表, 构建桶, 从桶找到 $k$ 个最大数字的复杂度分别为: $O(n)$, $O(n)$, 和 $O(k)$.
空间复杂度: $O(n+n) = O(n)$, 哈希表和桶各占 $O(n)$

当我们拥有关于元素频率的哈希表以后, 我们可以利用此表构建桶结构, 桶的 “关键字” 为元素频率, 之后, 我们可以用 $O(n)$ 的复杂度对桶进行遍历, 当找到 $k$ 个最大元素时, 跳出遍历循环, 代码如下:

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> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> hash; // 哈希
for(auto &num : nums) hash[num]++;

vector<vector<int>> buckets(nums.size()+1); // 根据数组的大小申请桶的空间, 多申请一个是为了方便下标对齐
for(auto h : hash) buckets[h.second].push_back(h.first); // 用频率来做桶的索引, 并且对应数字放入桶中

vector<int> res;
for(int i=buckets.size()-1; i>=0; i--){ // 最后往前遍历, 寻找频率最高的k个元素
vector<int> bucket = buckets[i];
for(auto & num : bucket){
res.push_back(num);
if(res.size() >= k) return res; // 找到k个元素, 直接返回并退出
}

}
}
};

348. 判定井字棋胜负

题目链接: https://leetcode-cn.com/problems/design-tic-tac-toe/

解法一: 直接判断

直接对横向, 纵向, 以及两个对角线进行判断

时间复杂度: $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
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
class TicTacToe:

def __init__(self, n: int):
"""
Initialize your data structure here.
"""
self.chessboard = [['#'] * n for _ in range(n)]
self.n = n

def move(self, row: int, col: int, player: int) -> int:
"""
Player {player} makes a move at ({row}, {col}).
@param row The row of the board.
@param col The column of the board.
@param player The player, can be either 1 or 2.
@return The current winning condition, can be either:
0: No one wins.
1: Player 1 wins.
2: Player 2 wins.
"""
if (player == 1):
self.chessboard[row][col] = 'X'
flag_row = True # 横向
flag_col = True # 纵向
flag_dig1 = True # 左对角线
flag_dig2 = True # 右对角线
for i in range(self.n):
if (self.chessboard[row][i] != 'X'): flag_row = False
if (self.chessboard[i][col] != 'X'): flag_col = False
if (self.chessboard[i][i] != 'X'): flag_dig1 = False
if (self.chessboard[i][self.n-i-1] != 'X'): flag_dig2 = False
if (flag_row or flag_col or flag_dig1 or flag_dig2):
return 1
else:
return 0
else:
self.chessboard[row][col] = 'O'
flag_row = True
flag_col = True
flag_dig1 = True
flag_dig2 = True
for i in range(self.n):
if (self.chessboard[row][i] != 'O'): flag_row = False
if (self.chessboard[i][col] != 'O'): flag_col = False
if (self.chessboard[i][i] != 'O'): flag_dig1 = False
if (self.chessboard[i][self.n-i-1] != 'O'): flag_dig2 = False
if (flag_row or flag_col or flag_dig1 or flag_dig2):
return 2
else:
return 0

# Your TicTacToe object will be instantiated and called as such:
# obj = TicTacToe(n)
# param_1 = obj.move(row,col,player)

解法二: count 计数

利用额外的rowscols数组, 以及两个变量分别对各个方向的棋子数量计数, 一旦数量达到n, 则说明胜负已分

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class TicTacToe:

def __init__(self, n: int):
"""
Initialize your data structure here.
"""
self.chessboard = [['#'] * n for _ in range(n)]
self.n = n
self.rows = { 'X': [0] * n, 'O': [0] * n}
self.cols = { 'X': [0] * n, 'O': [0] * n}
self.digs = { 'X': [0] * 2, 'O': [0] * 2}

def move(self, row: int, col: int, player: int) -> int:
"""
Player {player} makes a move at ({row}, {col}).
@param row The row of the board.
@param col The column of the board.
@param player The player, can be either 1 or 2.
@return The current winning condition, can be either:
0: No one wins.
1: Player 1 wins.
2: Player 2 wins.
"""
if (player == 1):
self.chessboard[row][col] = 'X'
self.rows['X'][row] += 1
self.cols['X'][col] += 1
if (row == col):
self.digs['X'][0] += 1
if (row == self.n - col - 1):
self.digs['X'][1] += 1
n = self.n
if (self.rows['X'][row] == n or self.cols['X'][col] == n or self.digs['X'][0] == n or self.digs['X'][1] == n):
return 1
else:
return 0
else:
self.chessboard[row][col] = 'O'
self.rows['O'][row] += 1
self.cols['O'][col] += 1
if (row == col):
self.digs['O'][0] += 1
if (row == self.n - col - 1):
self.digs['O'][1] += 1
n = self.n
if (self.rows['O'][row] == n or self.cols['O'][col] == n or self.digs['O'][0] == n or self.digs['O'][1] == n):
return 2
else:
return 0

# Your TicTacToe object will be instantiated and called as such:
# obj = TicTacToe(n)
# param_1 = obj.move(row,col,player)

349. 两个数组的交集

给定两个数组,编写一个函数来计算它们的交集。

示例 1:

1
2
输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2]

示例 2:

1
2
输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [9,4]

说明:

输出结果中的每个元素一定是唯一的。
我们可以不考虑输出结果的顺序。

解法一: 哈希

1
2
3
4
5
6
7
8
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
set1 = set(nums1)
res = set()
for num in nums2:
if num in set1:
res.add(num)
return list(res)

350. 两个数组的交集 II

Description: 求两数组的交集

给定两个数组,编写一个函数来计算它们的交集。

示例 1:
输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2,2]

示例 2:
输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [4,9]

说明:
输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。
我们可以不考虑输出结果的顺序。

Follow up:

  • 如果给定的数组已经排好序呢?你将如何优化你的算法?
  • 如果 nums1 的大小比 nums2 小很多,哪种方法更优?
  • 如果 nums2 的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?

解法一: 哈希

时间复杂度: $O(n1+n2)$, 构建哈希表和查询哈希表, 需要将两数组的元素都遍历一次
空间复杂度: $O(n1)$, 用 nums1 构建哈希表, 然后用 nums2 进行查询.(也可以多做一步判断, 选择用数组长度较小数组来构建哈希表, 减少空间复杂度)

用一个数组构建哈希表, 哈希表的键为元素值, 哈希表的值为元素的出现次数, 然后用另一个数组的元素对哈希表进行查询, 如果能找到, 则将该元素加入结果数组 res, 并将哈希表对应键的值减一, 如果减到零, 则删除该键.

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
d = dict()
for num in nums1:
d.setdefault(num, 0)
d[num] += 1
res = []
for num in nums2:
if num in d and d[num] > 0:
d[num] -= 1
res.append(num)
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int> hash; // 构建哈希
for(auto &num : nums1) hash[num]++;

vector<int> res;
for(auto &num : nums2){
if(hash.find(num) != hash.end()){
res.push_back(num);
hash[num]--;
if(hash[num]==0) hash.erase(num); // 当键对应值为0时, 将该键擦除
}
}
return res;
}
};

解法二: 排序

时间复杂度: $O(n1logn1 + n2logn2 + n1 + n2) = max(n1, n2)\times log(max(n1, n2))$
空间复杂度: $O(1)$

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<int> intersect(vector<int>& nums1, vector<int>& nums2) {
std::sort(nums1.begin(), nums1.end());
std::sort(nums2.begin(), nums2.end());

int i1=0, i2=0; // 设置两个指示变量
vector<int> res;
while(i1<nums1.size() && i2<nums2.size()){
if(nums1[i1] == nums2[i2]){
res.push_back(nums1[i1]);
i1++;
i2++;
}else if(nums1[i1] < nums2[i2])
i1++;
else
i2++;
}
return res;
}
};

Follow up

当给定数组已经有序时

可以设置两个指示变量, 分别指向两个数组, 然后按照指向元素的大小关系进行判断并递进, 这样, 时间复杂度为 $O(n1+n2)$, 空间复杂度为 $O(1)$, 代码可见解法二.

nums1 远远小于 nums2

正如前面所说, 选用元素数量较少的数组来构建哈希表, 可以降低空间复杂度

如果 nums2 存放在磁盘上, 同时内存不足以加载整个 nums2 数组

nums2 分片, 逐个求交集, 最后再合并

371. Sum of Two Integers

Description: 不用加减乘除做加法

Calculate the sum of two integers a and b, but you are not allowed to use the operator + and -.

Example 1:

1
2
Input: a = 1, b = 2
Output: 3

Example 2:

1
2
Input: a = -2, b = 3
Output: 1

解法一: 位操作(递归)

对于两个数相加, 例如 759+674, 在计算机中我们可以按照如下步骤求解:

  1. 不考虑进位, 相加得到 323;
  2. 只考虑进位, 进位为 1110;
  3. 将上面两个数字相加, 得到 1433, 即为最终结果

因此, 我们可以用 异或 求得不考虑进位的加, 用 与操作 来得到当前数字的进位, 由于进位与数字相加后, 有可能产生新的进位, 所以我们还要假设将新的进位加上, 直到进位位为0, 此时可以此时返回当前的和, 代码如下所示

1
2
3
4
5
6
7
8
9
class Solution {
public:
int getSum(int a, int b) {
if(b==0) return a ; // 如果进位为0, 则可直接返回
int sum = a ^ b; // 计算不带进位的加法
int carry = (a & b) << 1; // 计算进位
return getSum(sum, carry); // 结合并返回
}
};

上面的代码可以简化成一行:

1
2
3
4
5
6
class Solution {
public:
int getSum(int a, int b) {
return b==0 ? a : getSum(a^b, (a&b)<<1);
}
};

解法二: 位操作(迭代)

思路和解法一相同, 只不过写成了迭代的形式

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int getSum(int a, int b) {
while(b!=0){
int tmp = a ^ b; // 不考虑进位的加
b = (a&b) << 1; // 进位
a = tmp;
}
return a;
}
};

378. Kth Smallest Element in a Sorted Matrix

Description: 找到半有序数组中的第 k 小的元素

Given a n x n matrix where each of the rows and columns are sorted in ascending order, find the kth smallest element in the matrix.

Note that it is the kth smallest element in the sorted order, not the kth distinct element.

Example:

1
2
3
4
5
6
7
8
matrix = [
[ 1, 5, 9],
[10, 11, 13],
[12, 13, 15]
],
k = 8,

return 13.

Note:
You may assume k is always valid, 1 ≤ k ≤ n2.

解法一: 堆

基于堆的 baseline 解法:
最简单的堆解法就是不使用矩阵的有序性质, 直接当成无序数组来做, 我们申请一个 $k$ 大小的大顶堆, 然后遍历矩阵中的所有元素, 如果某元素小于堆顶就将堆顶弹出, 并压入该元素, 最终, 大顶堆的堆顶就是整个矩阵中第 $k$ 小的元素. 该解法的时间复杂度为 $O(nmlogk)$, 空间复杂度为 $O(k)$, 由于没有使用到有序矩阵的性质, 故不做讨论.

更优的基于堆的解法(超屌的解法!):

时间复杂度: $O(klogn)$, $k$ 代表 kth, $n$ 代表矩阵的行数
空间复杂度: $O(n)$, 堆的大小, $n$ 代表矩阵的行数

我们需要利用矩阵行列分别有序的性质, 首先, 具体思路如下:

  1. 利用将矩阵中每一行的首元素(也就是第一列元素, 同理, 这里也可以用第一行元素)构造一个最小堆(这一步的复杂度小于 $O(nlogn)$), 堆中的元素是一个 pair, 其中 first 为元素的值, second 又是一个 pair, 存储着值的行列坐标 (i, j)
  2. 将最小堆中的一个元素弹出(弹出的是当前堆最小的元素), 然后再将弹出元素的同一行的下一个元素(通过元素坐标获取)压入堆, 压入后, 堆会自动排序, 使得最小的元素位于堆顶.
  3. 重复步骤(2) k-1 次以后. 我们已经弹出了整个矩阵的最小的 k-1 个元素, 那么现在堆顶中的元素就是第 k 小的元素, 将其返回即可
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:
struct cmp{
bool operator()(pair<int, pair<int,int>> &a, pair<int, pair<int,int>> &b){
return a.first > b.first; // 小顶堆
}
};
int kthSmallest(vector<vector<int>>& matrix, int k) {
if(matrix.size()==0 || matrix[0].size()==0) return 0;
int n = matrix.size(), m=matrix[0].size();
priority_queue< pair<int, pair<int, int>>, vector<pair<int, pair<int, int>>>, cmp> min_heap;

for(int i=0; i<n; i++){ // 用矩阵每一行的首元素构建堆, 堆的元素组成为<(val, (i,j))>
min_heap.push(make_pair(matrix[i][0], make_pair(i, 0)));
}

int res;
while(k--){
int val = min_heap.top().first;
int i = min_heap.top().second.first;
int j = min_heap.top().second.second;
min_heap.pop(); // 弹出堆
res = val;
if(j+1<m) // 将同行的下一个元素放入堆
min_heap.push(make_pair(matrix[i][j+1], make_pair(i, j+1)));
}
return res;
}
};

解法二: 二分查找

时间复杂度: $O(nlogm\times logD$, $n$ 为矩阵的行数, $m$ 为矩阵的列数, $D$ 为矩阵中最大元素与最小元素之间的差值.
空间复杂度: $O(1)$, 没有利用额外空间

算法利用了每一行中, 元素都是有序的这个性质(但是没有用到列有序的性质), 步骤如下:

  1. 获取矩阵中元素的最小值 low 和最大值 high
  2. mid = (high+low)/2, 然后我们利用 upper_bound() 函数来查找矩阵中第一个大于 mid 的元素(耗时 $O(logn)$), 接着计算这个元素之前的元素数量. 对矩阵的每一行重复这个步骤, 并将所有的元素数量累加起来
  3. 如果累加元素数 count < k, 说明, mid 的值较小, 我们令 low=mid+1, 否则, 说明 count>=k, 我们令 high=mid, 注意, 这里的赋值关系最好不要改动, 并且要知道为什么令 high=mid, 而不是 mid-1.
  4. 重复上述过程直至 low=high, 此时, lowhigh 的值就是矩阵中第 k 小的值
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 kthSmallest(vector<vector<int>>& matrix, int k) {
if(matrix.size()==0 || matrix[0].size()==0) return 0;
int n = matrix.size(), m = matrix[0].size();
int low = matrix[0][0];
int high = matrix[n-1][m-1]; //题目中是方阵, 这里故意写成nm的矩阵, 以适应更普通的情况

while(low < high){
int mid = (low+high) / 2;
int count = 0;
for(int i=0; i<n; i++){
// 找到第一个大于 mid 的数, 然后计算这之前的元素个数
int row_count = std::upper_bound(matrix[i].begin(), matrix[i].end(), mid) - matrix[i].begin();
count += row_count;
}

if(count < k){ // 注意, 这里不能令小于号来包括等于号时的情况, 因为 (low+high)/2 是偏向左边的, 这样会造成死循环
low = mid + 1;
}else{ // 当 count>=k 时, 说明 mid之前就能满足 k 个元素, 故令 high=mid; 注意, 这里不要尝试令low=mid
high = mid;
} // 这里的二分查找不同于普通的数组, 因为 mid 有可能不是数组中的值, 所以即使count=k时, 也不能直接返回mid
}

return low; // 最终, 当 low=high时, 即为第k小的元素. 因为当, high指向第k小的元素时, 它就不可能再减小, 而只能是low一点点靠近high, 直至相等
}
};

解法三: 二分查找

时间复杂度: $O((n+m)logD)$, $n$ 为矩阵行数, $m$ 为矩阵列数, $D$ 为矩阵中元素的最大差值
空间复杂度: $O(1)$

解法二中并没有完全使用到矩阵所有的性质, 考虑到矩阵在列上也是有序的, 我们可以进一步优化算法. 我们应该还记得在剑指offer的第一题中, 考察了这种行列有序数组的元素查找算法, 我们可以在 $O(n+m)$ 的时间里找到指定的元素, 因此, 我们可以利用该算法替换解法二中对每一行执行二分查找的算法, 故而时间复杂度就变成了 $O((n+m)logD)$, 其中, $n$ 为矩阵行数, $m$ 为矩阵列数, $D$ 为矩阵中元素的最大差值, 代码如下.

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:
int kthSmallest(vector<vector<int>>& matrix, int k) {
if(matrix.size()==0 || matrix[0].size()==0) return 0;
int n = matrix.size(), m = matrix[0].size();
int low = matrix[0][0], high = matrix[n-1][m-1];

while(low < high){
int mid = (low+high) / 2;
int count = search(matrix, mid); // 查找小于等于mid的元素数量
if(count < k)
low = mid + 1;
else
high = mid;
}
return low;
}

int search(vector<vector<int>> &matrix, int target){
int n = matrix.size(), m = matrix[0].size();
int i = n-1, j=0; // 从左下角开始
int count=0; // 记录小于等于 target 的元素数量
while(i>=0 && j<m){
if(matrix[i][j] <= target){
j++;
count += i+1;
}else{
i--;
}
}
return count;
}
};

380. Insert Delete GetRandom O(1)

Description: 常数时间复杂度的插入,删除,和随机获取

Design a data structure that supports all following operations in average O(1) time.

  • insert(val): Inserts an item val to the set if not already present.
  • remove(val): Removes an item val from the set if present.
  • getRandom: Returns a random element from current set of elements. Each element must have the same probability of being returned.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Init an empty set.
RandomizedSet randomSet = new RandomizedSet();

// Inserts 1 to the set. Returns true as 1 was inserted successfully.
randomSet.insert(1);

// Returns false as 2 does not exist in the set.
randomSet.remove(2);

// Inserts 2 to the set, returns true. Set now contains [1,2].
randomSet.insert(2);

// getRandom should return either 1 or 2 randomly.
randomSet.getRandom();

// Removes 1 from the set, returns true. Set now contains [2].
randomSet.remove(1);

// 2 was already in the set, so return false.
randomSet.insert(2);

// Since 2 is the only number in the set, getRandom always return 2.
randomSet.getRandom();

解法一: 哈希表+数组

时间复杂度: $O(1)$, 符合题意
空间复杂度: $O(n)$, 数组和哈希表的大小各为 $O(n)$.

解题思路:

  • 插入: 用数组的 push_back() 存储新来的元素, 同时存入哈希表, key 为元素值, val 为元素在数组中的下标;
  • 删除: 先用哈希表获取元素的下标, 然后将数组中的该元素和数组的最后一个元素交换, 接着用 pop_back() 删除该元素, 然后用 erase() 从哈希表中删除该元素, 最后在哈希表中更新被交换元素的下标;
  • 获取随机元素: 利用 C++ 的内置随机函数 rand() 来获取随机数. 但是注意, rand() 对生成的随机数质量无法保证, 在 C++11 中, 已经建议使用随机数生成设施来替换 rand(). 另外注意: 如果想要使用 srand() 来播种, 那么不能将该语句放在 getRandom() 函数中, 因为重复播种会使得每次生成的随机数都一样, 正确的做法是将其放在构造函数中, 只进行一次播种.
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 RandomizedSet {
private:
vector<int> vec;
unordered_map<int, int> hash;
public:
/** Initialize your data structure here. */
RandomizedSet() {
srand(time(0));
}

/** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
bool insert(int val) {
if(hash.find(val) != hash.end()) return false;
vec.push_back(val);
hash[val] = vec.size()-1;
return true;
}

/** Removes a value from the set. Returns true if the set contained the specified element. */
bool remove(int val) {
if(hash.find(val) == hash.end()) return false;
int i = hash[val];
int j = vec.size() - 1;
swap(vec[i], vec[j]);
vec.pop_back(); // 将元素和最后一位元素交换, 然后在删除, 满足 O(1) 复杂度
hash[vec[i]] = i;
hash.erase(val); // 在哈希表中删除指定键值
return true;
}

/** Get a random element from the set. */
int getRandom() {
// srand(time(0)); // 不能放在这里, 要放只能放在构造函数中
return vec[rand()%vec.size()];
// rand 无法保证生成的随机数质量, C++11推荐用随机数生成设施来替换该函数
}
};

/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet obj = new RandomizedSet();
* bool param_1 = obj.insert(val);
* bool param_2 = obj.remove(val);
* int param_3 = obj.getRandom();
*/

384. Shuffle an Array

Description: 打乱数组

Shuffle a set of numbers without duplicates.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
// Init an array with set 1, 2, and 3.
int[] nums = {1,2,3};
Solution solution = new Solution(nums);

// Shuffle the array [1,2,3] and return its result. Any permutation of [1,2,3] must equally likely to be returned.
solution.shuffle();

// Resets the array back to its original configuration [1,2,3].
solution.reset();

// Returns the random shuffling of array [1,2,3].
solution.shuffle();

解法一: 随机交换

时间复杂度: $O(n)$, 打乱需要 $O(n)$, reset 为 $O(1)$
空间复杂度: $O(n)$

  • shuffle: 打乱时, 遍历数组的下标, 然后随机生成一个下标, 令二者指向的元素交换. 更多分析请看Knuth shuffle算法
  • reset: 直接返回缓存的原始数组
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 {
private:
vector<int> v;
public:
Solution(vector<int> nums): v(nums){
std::srand(std::time(0));
}

/** Resets the array to its original configuration and return it. */
vector<int> reset() {
return v;
}

/** Returns a random shuffling of the array. */
vector<int> shuffle() {
vector<int> sv(v);
for(int i=0; i<sv.size(); i++){
int j = i + rand() % (sv.size()-i); //这里生成的 j 只可能在 i 之后
swap(sv[i], sv[j]);
}
return sv;
}
};

/**
* Your Solution object will be instantiated and called as such:
* Solution obj = new Solution(nums);
* vector<int> param_1 = obj.reset();
* vector<int> param_2 = obj.shuffle();
**/

387. First Unique Character in a String

Description: 寻找字符串中的首个不重复字符

Given a string, find the first non-repeating character in it and return it’s index. If it doesn’t exist, return -1.

Examples:

1
2
3
4
5
s = "leetcode"
return 0.

s = "loveleetcode",
return 2.

Note: You may assume the string contain only lowercase letters.

解法一: 哈希表

时间复杂度: $O(n+n)=O(n)$, 第一个 $n$ 用于建立哈希表, 第二个 $n$ 用于查询首个出现次数为 1 的字符, $n$ 为字符串的长度
空间复杂度: $O(26)$, 哈希表的大小为字符集的大小 26 (如果是 unicode 字符, 就为 256).

遍历两边字符串, 第一遍构建哈希表, 第二遍按照字符串序列查询, 遇到值 1 的字符出现时, 就将其下标返回

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int firstUniqChar(string s) {
unordered_map<char, int> hash;
for(auto c : s){
hash[c]++;
}
for(int i=0; i<s.size(); i++){
if(hash[s[i]]==1) return i;
}
return -1;
}
};

394. 字符串解码

题目链接: https://leetcode-cn.com/problems/decode-string/

解法一: 遍历

逐次的遍历字符串, 每次解码一层, 知道所有的数字都被消除为止

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:
def decodeString(self, s: str) -> str:
while True:
length = len(s)
i = 0
flag = False
tmp_s = ''
while i < length:
if s[i] <= '9' and s[i] >= '0':
flag = True # whether have num
dup_num = ''
while s[i] <= '9' and s[i] >='0':
dup_num += s[i]
i += 1
dup_num = int(dup_num)
i += 1 # skip '['
dup_str = ''
pre_num = 0
while s[i] != ']' or pre_num > 0: # get duplicate str in [...]
if s[i] == '[':
pre_num += 1
elif s[i] == ']':
pre_num -= 1
dup_str += s[i]
i += 1
for _ in range(dup_num):
tmp_s += dup_str
else:
tmp_s += s[i]
i += 1
if flag: # flag = True, have num, continue loop
s = tmp_s
else: # have no num, compelete decode
return s

解法二: 栈

涉及到括号匹配类的问题, 一般用栈解决起来比较方便

Python 实现:

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 decodeString(self, s: str) -> str:
stack = []
res = ''
for c in s:
if c != ']': # 不是右括号就一直进栈
stack.append(c)
else:
string = '' # 先收集要加倍的字符串
while not stack[-1].isdigit():
string = stack.pop() + string
times = '' # 再收集加倍倍数
while stack and stack[-1].isdigit():
times = stack.pop() + times
if times: # 如果有倍数则加倍
string = string[1:] * int(times)
if stack: # 还有没处理完的上级,把处理好的字符串入栈等待处理
#stack += list(string)
stack.append(string)
else: # 前面的字符串处理完毕了,直接把字符串加入答案
res += string
return res + ''.join(stack) # 最后可能有没有右括号收尾的字符串

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:
string decodeString(string str) {
std::stack<std::string> s;
std::string res = "";
for (auto c : str) {
if (c != ']') {
std::string tmp_s = "x";
tmp_s[0] = c;
s.push(tmp_s);
} else {
std::string dup_str = "";
while (!s.empty() and s.top() < "0" or s.top() > "9") {
dup_str = s.top() + dup_str; s.pop();
}
if (dup_str != "") dup_str = dup_str.substr(1);
std::string times = "";
while (!s.empty() and s.top() <= "9" and s.top() >= "0") {
times = s.top() + times; s.pop();
}
std::string times_str = "";
for (int i = 0; !times.empty() and i < std::stoi(times); i++) {
times_str += dup_str;
}
if (s.empty()) {
res += times_str;
} else {
s.push(times_str);
}
}
}
std::string rear_s = "";
while (!s.empty()) {
rear_s = s.top() + rear_s; s.pop();
}
return res + rear_s;
}
};

395. Longest Substring with At Least K Repeating Characters

Description

Find the length of the longest substring T of a given string (consists of lowercase letters only) such that every character in T appears no less than k times.

Example 1:

1
2
3
4
5
6
7
Input:
s = "aaabb", k = 3

Output:
3

The longest substring is "aaa", as 'a' is repeated 3 times.

Example 2:

1
2
3
4
5
6
7
Input:
s = "ababbc", k = 2

Output:
5

The longest substring is "ababb", as 'a' is repeated 2 times and 'b' is repeated 3 times.

解法一: 哈希表+位标志

时间复杂度: 平均情况下为 $O(n)$, 最坏情况(待查找子串不存在)下为 $O(n^2)$
空间复杂度: $O(26 + 1)$, 26 为哈希表的大小, 1 为 mask 的大小.

对于字母集, 可以利用哈希表来实现 $O(n)$ 复杂度的字符数量统计, 我们设置一个变量 mask, 该变量每一个比特位上的值有两种含义: 当某比特位为 1 时, 代表该比特位对应的字母在当前字符子串中的数量小于 k, 反之, 则该比特位为 0. 那么, 只要当 mask=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
class Solution {
public:
int longestSubstring(string s, int k) {
int n = s.size();
int res = 0;
for(int i=0; i+k <= n; ){ // i 代表其实字符的位置
int max_end = i; // 注意要把这三行放在第一个for循环内部, 每次都要初始化一次
unsigned int mask = 0;
int hash[26] = {0};


for(int j=i; j<n; j++){ // j 代表终止字符的位置, 从 i 开始
int t = s[j] - 'a';
hash[t]++;

if(hash[t] < k) mask |= (1<<t); // set t bit to 1
else mask &= (~(1<<t)); //这里外边的括号可以省, 但是位操作最好显式加括号

if(mask == 0){ // 如果mask=0, 说明所有的字符要么没有出现, 要么数量>=k.
int length = j-i+1;
res = std::max(res, length);
max_end = j;
}
}
i = max_end + 1; // 下一个最长的子串的开始一定不会在 i 与 max_end 之间,
// 因为如果在这之间, 那么就一定会在 max_end 处终止
}
return res;
}
};

解法二: 分而治之, 递归

时间复杂度: $O(n)$, 最坏情况下为 $O(n)$, 因为递归调用的深度最多为 26, 而每一层的复杂度约为 $O(n)$. (这种说法是网上的说法, 但是这里我个人觉得最坏情况是 $O(n^2), 只不过有的递归调用很快退出, 是的程序运行时间很短)
空间复杂度: $O(26+log_{26}n)$, 哈希表空间为, 递归占用空间为 $O(log_{26}n)$.

对于任意的字符串, 我们都执行下面的算法步骤:

  1. 根据当前的字符串, 构建相应的哈希表, 表内数据为没一个字符的出现次数, 所以哈希表的大小为 26(或 256);
  2. 如果哈希表内所有字符的出现次数都满足条件(出现 0 次出现 k 次以上), 那么当前字符串满足条件, 可直接输出长度
  3. 如果字符串中存在不满足条件的字符, 那么就以这些字符
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 {
int helper(string &s, int l, int r, int k){
int hash[256]={0};

for(int i=l; i<=r; i++){ // 构建 l,r 范围内的字符数哈希表
hash[s[i]]++;
}

bool flag = true;
for(int i=l; i<=r; i++){ // 如果当前 l, r范围内的字符满足要求, 则直接返回
if(hash[s[i]] && hash[s[i]] < k){
flag = false;
break;
}
}
if(flag) return (r-l+1);

int res=0;
int i = l;
for(int j=l; j<=r; j++){
//以所有不满足条件的字符为分界线, 递归获取前半段和后半段的最长子串长度
if(hash[s[j]] && hash[s[j]] < k){
res = std::max(res, helper(s, i, j-1, k));
i = j+1;
// 这里虽然对于多个相同的不满足条件的字符会进行多次调用
// 但是由于传入的子串很短, 所以会很快接结束调用, 故可忽略不计此次调用
}
}
return std::max(res, helper(s, i, r, k));// i, r 为最后一段
}
public:
int longestSubstring(string s, int k) {
int l = 0, r = s.size()-1;
return helper(s, l, r, k);
}
};

解法三: 更简洁的递归

时间复杂度: $O(n)$, 最差情况下为 $O(kn)$, 详细见下面的分析
空间复杂度: $O(n)$, 哈希+递归

真正的 $O(n)$ 复杂度的实现: 和上面的思路一致, 也是利用不满足条件的字符作为分隔(因为只有符合条件的字符组成的字符串从 有可能 具有正确的长度), 但是不同于上面程序的是, 此次我们只对满足条件的子串进行递归, 故而那些重复的不满足条件的字符不会被重复用于递归(上面的代码就是重复调用了, 因为是在发现 <k 时就进行调用), 下面的代码更加精炼易懂, 我们首先会跳过所有不满足条件的字符, 然后从满足条件的字符开始, 找到连续的满足条件的子串的最后一个字符, 然后对这个子串进行递归调用, 也就是说, 我们最多会进行不超过 k 次递归调用, 因为最坏的情况是 26 个字符中, 只有一个字符不满足条件, 而这个字符最多将字符串分割成 k 段, 如果分割成 k+1 段, 那么就必须用 k 个字符, 此时与假设矛盾.

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 {
int helper(string &s, int l, int r, int k){
int hash[26] = {0};
for(int i=l; i<=r; i++) hash[s[i]-'a']++; // 构建哈希

int res=0;
for(int i=l; i<=r; ){
while(i<=r && hash[s[i]-'a']<k) i++; // 跳过不符合的字符, 注意也要跳过未出现的字符, 所以=0也要跳过
if(i>r) break; // 如果所有字符都不符合, 则直接break

int j = i;
while(j<=r && hash[s[j]-'a']>=k) j++; // 找到当前子串中符合条件的最后一个连续字符
j--; // 此时 j 指向的是符合条件字符的下一个位置, 因此, 我们要令 j--
//if(j>r) j=r; // j如果超限, 说明所有字符都符合, 则令 j 指向尾部字符即可

if(i==l && j==r) return r-l+1; // 当前范围所有字符满足条件, 直接返回长度
res = std::max(res, helper(s, i, j, k)); // 对符合条件的子串进行调用, 最多会进行不超过 k 次调用
i = j+1; // 开始下一个子串的查询
}
return res;
}
public:
int longestSubstring(string s, int k) {
int l = 0, r = s.size()-1;
return helper(s, l, r, k);
}
};

上面的边界控制比较麻烦, 下面我们用超尾的方式来进行边界控制, 会使程序更加简洁, 如下所示:

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 {
int helper(string &s, int begin, int end, int k){
int hash[26] = {0};
for(int i=begin; i<end; i++) hash[s[i]-'a']++; // 构建哈希

int res=0;
for(int i=begin; i<end; ){
while(i<end && hash[s[i]-'a']<k) i++; // 跳过不符合的字符, 注意也要跳过未出现的字符, 所以=0也要跳过
if(i==end) break; // 如果所有字符都不符合, 则直接break

int j = i;
while(j<end && hash[s[j]-'a']>=k) j++; // 找到当前子串中符合条件的最后一个连续字符
//当使用超尾时, 无需对j特殊处理

if(i==begin && j==end) return end-begin; // 当前范围所有字符满足条件, 直接返回长度
res = std::max(res, helper(s, i, j, k)); // 对符合条件的子串进行调用, 最多会进行不超过 k 次调用
i = j+1; // 开始下一个子串的查询
}
return res;
}
public:
int longestSubstring(string s, int k) {
int begin = 0, end = s.size();
return helper(s, begin, end, k);
}
};

399. 除法求值

题目链接: https://leetcode-cn.com/problems/evaluate-division/

解法一: 构建有向图

先构造图,使用dict实现,其天然的hash可以在in判断时做到O(1)复杂度。

对每个equation如”a/b=v”构造a到b的带权v的有向边和b到a的带权1/v的有向边,

之后对每个query,只需要进行dfs并将路径上的边权重叠乘就是结果了,如果路径不可达则结果为-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
26
27
28
29
30
31
32
33
class Solution:
def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
graph = dict() # construct a directed graph
for (x, y), v in zip(equations, values):
if x in graph:
graph[x][y] = v
else:
graph[x] = {y: v}
if y in graph:
graph[y][x] = 1 / v
else:
graph[y] = {x: 1/v}

def dfs(start, end, graph, visited):
if start not in graph or end not in graph: # 如果开始节点或者结束节点不在图中, 则返回 -1
return -1.0
elif start == end: # 开始节点就是结束节点, 则可认为权重为 1
return 1.0
else:
for node in graph[start].keys(): # 遍历当前节点的所有下一个节点
if node == end:
return graph[start][end]
elif node not in visited: # 未被访问
visited[node] = 1 # 加入访问字典
v = dfs(node, end, graph, visited)
if v != -1: # 如果能从 node 抵达 end, 则返回该node
return graph[start][node] * v
return -1.0 # 没有找到路径 返回 -1
res = []
for query in queries:
visited = {(query[0]): 1} # 注意要先将开始节点加入到已访问字典中
res.append(dfs(query[0], query[1], graph, visited))
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
39
40
41
42
43
class Solution {
double dfs(string start, string end, unordered_map<string, unordered_map<string, double>> graph, unordered_set<string> visited) {
if (graph.find(start) == graph.end() or graph.find(end) == graph.end()) {
return -1.0;
} else if (start == end) {
return 1.0;
} else {
for (auto const& node : graph[start]) {
if (node.first == end) {
return node.second;
break;
} else if (visited.find(node.first) == visited.end()) {
visited.insert(node.first);
auto v = dfs(node.first, end, graph, visited);
if (v != -1) {
return node.second * v;
}
}
}
}
return -1.0;
}
public:
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
// 构建有向图
unordered_map<string, unordered_map<string, double>> graph;
int n = values.size();
for (int i = 0; i < n; i++) {
auto node1 = equations[i][0];
auto node2 = equations[i][1];
auto weight = values[i];
graph[node1][node2] = weight;
graph[node2][node1] = 1.0 / weight;
}

std::vector<double> res;
for (auto const &query : queries) {
std::unordered_set<string> visited{query[0]}; // 已访问节点
res.emplace_back(dfs(query[0], query[1], graph, visited));
}
return res;
}
};

解法二: 并查集

406. 根据身高重建队列

题目链接: https://leetcode-cn.com/problems/queue-reconstruction-by-height/

解法一: 排序

根据题目要求, 排序,然后插入。

假设候选队列为 A,已经站好队的队列为 B.

从 A 里挑身高最高的人 x 出来,插入到 B. 因为 B 中每个人的身高都比 x 要高,因此 x 插入的位置,就是看 x 前面应该有多少人就行了。比如 x 前面有 5 个人,那 x 就插入到队列 B 的第 5 个位置。

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
bool my_sort2(std::vector<int> vec1, std::vector<int> vec2) { // 自定义函数必须写在最外面, 否则无法通过
if (vec1[0] == vec2[0]) {
return vec1[1] < vec2[1];
}
else
return vec1[0] > vec2[0];
}
class Solution {
public:
struct {
bool operator() (std::vector<int> vec1, std::vector<int> vec2) {
if (vec1[0] == vec2[0]) {
return vec1[1] < vec2[1];
}
else
return vec1[0] > vec2[0];
}
} my_sort; // 匿名结构体变量
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
std::sort(people.begin(), people.end(), my_sort); // 排序, 按照 h 降序, k 升序
//std::sort(people.begin(), people.end(), my_sort2); //与上面一行等价
std::vector<std::vector<int>> res;
for (auto vec : people) {
res.insert(res.begin()+vec[1], vec); // 按照 k 进行插入
}
return res;
}
};

Python 实现:

1
2
3
4
5
6
7
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
people.sort(key = lambda x: [-x[0], x[1]])
res = []
for p in people:
res.insert(p[1], p)
return res

412. Fizz Buzz

Description: 输出指定字符串

Write a program that outputs the string representation of numbers from 1 to n.

But for multiples of three it should output “Fizz” instead of the number and for the multiples of five output “Buzz”. For numbers which are multiples of both three and five output “FizzBuzz”.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
n = 15,

Return:
[
"1",
"2",
"Fizz",
"4",
"Buzz",
"Fizz",
"7",
"8",
"Fizz",
"Buzz",
"11",
"Fizz",
"13",
"14",
"FizzBuzz"
]

解法一: 条件判断直接输出

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<string> fizzBuzz(int n) {
vector<string> res;
for(int i=1; i<=n; i++){
if(i%15==0)
res.push_back("FizzBuzz");
else if(i%5==0)
res.push_back("Buzz");
else if(i%3==0)
res.push_back("Fizz");
else
res.push_back(std::to_string(i));
}
return res;
}
};

416. 分割等和子集

题目链接: https://leetcode-cn.com/problems/partition-equal-subset-sum/

解法: 动态规划

先求得需要划分的子集的元素和target, 构建长度为len(target)dp数组, 进行两层循环, 第一层循环nums, 第二层循环dp, dp[j]表示在当前num之前, 是否可以选取一部分数字, 其和为j.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def canPartition(self, nums: List[int]) -> bool:
s = sum(nums)
if (s % 2 == 1):
return False
target = s // 2
dp = [False] * (target+1)
dp [0] = True # base case
for num in nums:
for j in range(target, 0, -1):
if j >= num:
dp[j] = dp[j] or dp[j-num]
else:
break
return dp[-1]

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
bool canPartition(vector<int>& nums) {
int s = 0;
for (auto num : nums) s += num;
if (s % 2 == 1) return false;
int target = s / 2;
std::vector<bool> dp(target+1, false);
dp[0] = true;
for (auto num : nums) {
for (int j = target; j > 0; j--) {
if (j >= num)
dp[j] = dp[j] or dp[j-num];
else
break;
}
}
return dp[target];
}
};

437. 路径总和 III-简单

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

解法一: 双递归(DFS+递归)

外层递归执行树的遍历, 内层递归统计当前节点能够组成符合条件路径的数量.

注意, 这一道题有一点题目没有交代清楚, 那就是这里的路径中其节点的左子树和右子树不能同时作为路径中的元素出现, 也就是说, 对于每一个节点, 我们只需要分别考虑左子树构成的路径和右子树构成的路径即可, 无需考虑同时含有左子树和右子树的情况.

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 pathSum(self, root: TreeNode, sum: int) -> int:

def path_num(node, target):
if node is None: return 0
count = 1 if node.val == target else 0
return count + path_num(node.left, target-node.val) + path_num(node.right, target-node.val)

def traverse(node):
self.count += path_num(node, sum)
if node.left: traverse(node.left)
if node.right: traverse(node.right)

self.count = 0
if root: traverse(root)
return self.count

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
/**
* 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:
int count = 0;
int path_sum(TreeNode* node, int target) { // 找到以当前节点为根的路径数量
if (!node) return 0;
int node_sum = path_sum(node->left, target - node->val) + path_sum(node->right, target - node->val);
if (node->val == target)
return 1 + node_sum;
else
return 0 + node_sum;
}

void traverse(TreeNode* node, int sum) { // 遍历树
if (!node) return;
count += path_sum(node, sum);
traverse(node->left, sum);
traverse(node->right, sum);
}
public:
int pathSum(TreeNode* root, int sum) {
traverse(root, sum);
return count;
}
};

解法二: 队列(BFS)+递归

思路和解法一相同, 只不过遍历二叉树的部分换成了 BFS, 用队列来实现遍历

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 pathSum(self, root: TreeNode, sum: int) -> int:

def path_num(node, target):
if node is None: return 0
count = 1 if node.val == target else 0
return count + path_num(node.left, target-node.val) + path_num(node.right, target-node.val)

if not root: return 0
count = 0
queue = [root]
while queue:
node = queue.pop(0)
count += path_num(node, sum)
if (node.left): queue.append(node.left)
if (node.right): queue.append(node.right)
return count

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
/**
* 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:
int count = 0;
int path_sum(TreeNode* node, int target) { // 找到以当前节点为根的路径数量
if (!node) return 0;
int node_sum = path_sum(node->left, target - node->val) + path_sum(node->right, target - node->val);
if (node->val == target)
return 1 + node_sum;
else
return 0 + node_sum;
}

public:
int pathSum(TreeNode* root, int sum) {
if (!root) return 0;
std::queue<TreeNode*> bfs_q;
bfs_q.push(root);
while (!bfs_q.empty()) {
auto node = bfs_q.front(); bfs_q.pop();
count += path_sum(node, sum);
if (node->left) bfs_q.push(node->left);
if (node->right) bfs_q.push(node->right);
}
return count;
}
};

解法三: DFS+回溯, 避免重复计算

在上面的两种解法中, 对于每一个节点, 都要遍历它所有的可能路径, 这样势必会产生大量的重复计算. 因此, 我们从另一个角度出发解决该问题. 由于该问题中, 路径的开始节点必须是结束节点的祖先. 因此我们可以在遍历每一个节点时, 存储 从根节点到该节点 的路径的总长path_sum. 那么, 我们只需要知道, 在这一条路径上, 从根节点到另一个节点 的长度等于path_sum - sum的路径数量, 就可以知道以该节点为结束节点的路径数量. 实际上就是采用了前 n 项和的思想, 即 (sum[4] - sum[1]) 就代表了第 2, 3, 4 项的和.

字典的 key 为从根节点到当前节点的长度, value 为当前路径上能够形成该长度的子路径总数

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def pathSum(self, root: TreeNode, sum: int) -> int:

def dfs(node, sum_dict, path_sum):
path_sum += node.val
count = sum_dict.get(path_sum - sum, 0) # 必须先获取count, 再将 sum_dict 增1, 否则会获取多余的 count
sum_dict[path_sum] = sum_dict.get(path_sum, 0) + 1 # 该语句与上一条语句的位置不可颠倒
if node.left: count += dfs(node.left, sum_dict, path_sum)
if node.right: count += dfs(node.right, sum_dict, path_sum)
sum_dict[path_sum] -= 1 # 访问完该节点, 一定要进行回溯
return count

if root is None: return 0
sum_dict = dict({0: 1}) # 初始时需要有 0, 因为当 path_sum - sum 为 0 时, 说明从根节点到该节点刚好是一条路径
return dfs(root, sum_dict, 0)

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 {

int dfs(TreeNode* node, std::unordered_map<int, int> sum_dict, int sum, int path_sum) {
if (node == nullptr) return 0;
path_sum += node->val;
int count = sum_dict[path_sum - sum];
sum_dict[path_sum]++;
count += dfs(node->left, sum_dict, sum, path_sum) + dfs(node->right, sum_dict, sum, path_sum);
sum_dict[path_sum]--;
return count;
}
public:
int pathSum(TreeNode* root, int sum) {
std::unordered_map<int, int> sum_dict {{0, 1}};
return dfs(root, sum_dict, sum, 0);
}
};

438. 找到字符串中所有字母异位词

题目链接: https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/

解法一: 滑动窗口

将目标字符串的值加起来, 组成target, 只有窗口内部的元素和等于该target, 说明就是字母异位词. 维持窗口大小不变, 同步移动窗口, 继续进行判断. 时间复杂度为 $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
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
std::unordered_map<char, int> hash;
int target = 0;
for (auto c : p) {
target += int(c);
hash[c]++;
}
int start = 0;
int end = 0;
int sum = 0;
while (end < p.size() and end < s.size()) { // 先判断到窗口大小处, 注意, s 有可能长度小于 p, 因此也要做判断
if (hash.find(s[end]) != hash.end()) {
sum += int(s[end]);
}
end++;
}
std::vector<int> res;
if (sum == target) res.emplace_back(start); // 第一个窗口内元素符合要求, 加入结果
while (end < s.size()) { // 移动窗口, 并判断窗口内元素是否符合要求
if (hash.find(s[end]) != hash.end()) {
sum += int(s[end]);
}
if (hash.find(s[start]) != hash.end()) {
sum -= int(s[start]);
}
end++;
start++;
if (sum == target) res.emplace_back(start);
}
return res;
}
};

Python 实现: 注意在 Python 中如果想要获取一个字符的 ASCII 编码, 则需要使用ord(), 直接使用int强制类型转换, 无法得到 ASCII 编码, 因为在 Python 中, int('5') = 5

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 findAnagrams(self, s: str, p: str) -> List[int]:
d = {c:1 for c in p}
target = 0
for c in p: target += ord(c)
start = 0
end = 0
res = []
cur_sum = 0
while (end < len(p) and end < len(s)):
if s[end] in d:
cur_sum += ord(s[end])
end += 1
if (target == cur_sum): res.append(start)
while (end < len(s)):
if s[end] in d:
cur_sum += ord(s[end])
if s[start] in d:
cur_sum -= ord(s[start])
end += 1
start += 1
if (target == cur_sum): res.append(start)
return res

442. 数组中重复的数据

数组中出现两次的数据

题目链接: https://leetcode-cn.com/problems/find-all-duplicates-in-an-array/

解法一: 两次遍历

“桶排序”的思想是“抽屉原理”,即“一个萝卜一个坑”,8 个萝卜要放在 7 个坑里,则至少有 1 个坑里至少有 2 个萝卜。

这里由于数组元素限定在数组长度的范围内,因此,我们可以通过一次遍历:

让数值 1 就放在索引位置 0 处;
让数值 2 就放在索引位置 1 处;
让数值 3 就放在索引位置 2 处;

一次遍历以后,那些“无处安放”的元素就是我们要找的“出现两次的元素”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def findDuplicates(self, nums: List[int]) -> List[int]:

i = 0
while i < len(nums):
num = nums[i]
if i != num-1 and nums[num-1] != num:
nums[i], nums[num-1] = nums[num-1], nums[i]
else:
i+=1
res = []
for i, num in enumerate(nums):
if i != num-1:
res .append(num)
return res

解法二: 用当前的值作为索引, 一次遍历

对于任意一个数字num, 我们可以用起作为索引, 然后在nums数组内部构建相应的哈希表, 数字第一次出现时, 将目标位置的值置为负数, 置成负数的好处是, 我们仍然可以用绝对值的方式无损失的获取的该值的原始值, 这会, 第二次如果遇到已经是负数的, 说明是之前已经出现过的, 于是可以直接将其加入到结果中, 这样, 只需进行一次遍历, 同时不使用额外的空间.

Python 实现:

1
2
3
4
5
6
7
8
9
10
class Solution:
def findDuplicates(self, nums: List[int]) -> List[int]:
res = []
for num in nums:
key = abs(num)-1 # 由于下面会修改 nums 里面的值, 因此这里需要用abs获取其绝对值
if nums[key] > 0: # 如果 "哈希" 中不存在该值
nums[key] = -nums[key] # 向 "哈希" 中添加新的键
else: # 如果已经存在该值, 说明出现了两次
res.append(abs(num))
return res

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<int> findDuplicates(vector<int>& nums) {
std::vector<int> res;
for (auto num : nums) {
int key = std::abs(num)-1;
if (nums[key] > 0) {
nums[key] = -nums[key];
} else {
res.emplace_back(std::abs(num));
}
}
return res;
}
};

448. 找到所有数组中消失的数字

题目链接: https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/

数字归位

遍历第一遍把数字放到与它自身值对应的下标上, 如 3 放到 nums[2] 处, 遍历第二次, 如果值与下标不对应, 则说明该数字丢失.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
i = 0
res = []
while (i < len(nums)):
if nums[i] == i+1 or nums[nums[i]-1] == nums[i]: # 如果当前位置以及符合, 或者目标位置以及符合, 则不进行交换
i += 1
continue
else: # 否则进行交换
target_i = nums[i] -1 # 交换时, 先求得目标位置, 然后再交换, 错误写法: nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]
nums[i], nums[target_i] = nums[target_i], nums[i]
#return [i for i, num in enumerate(nums, 1) if i != num] # 与下面三行等价
for i, num in enumerate(nums, 1):
if i != num: res.append(i)
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> findDisappearedNumbers(vector<int>& nums) {
std::vector<int> res;
int i = 0;
while (i < nums.size()) {
if (nums[i] == i+1 or nums[nums[i]-1] == nums[i]) {
i++;
continue;
} else {
int target_i = nums[i]-1;
std::swap(nums[i], nums[target_i]);
}
}
for (int i = 0; i < nums.size(); i++) {
if (nums[i] != i+1) res.emplace_back(i+1);
}
return res;
}
};

解法二: 置负数

我们将当前值所对于的下标置为负数, 这样, 一次遍历过后, 数组中正数的地方就是缺少的数字.

Python 实现:

1
2
3
4
5
6
7
8
class Solution:
def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
i = 0
res = []
for i in range(len(nums)):
target_i = abs(nums[i])-1
nums[target_i] = -abs(nums[target_i])
return [i for i, num in enumerate(nums, 1) if num > 0]

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
vector<int> findDisappearedNumbers(vector<int>& nums) {
std::vector<int> res;
for (int i = 0; i < nums.size(); i++) {
int target_i = std::abs(nums[i]) - 1;
nums[target_i] = -std::abs(nums[target_i]);
}
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0) res.emplace_back(i+1);
}
return res;
}
};

扩展题型

扩展: 数组中每个元素出现的可能次数是 n 次,求出数组中出现此次为偶数(奇数)次的元素(出现 0 次也算偶数次)。

上述的解法二可以扩展到解此题, 只需修改一行, 注意出现0次也算是出现偶数次.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution(object):
def findDisappearedNumbers(self, nums):
"""
:type nums: List[int]
:rtype: List[int]
"""
# 将所有正数作为数组下标,置对应数组值为相反数。那么,仍为正数的位置即为出现偶数次(未出现是0次,也是偶数次)数字。
# 举个例子:
# 原始数组:[1, 1, 1, 1, 2, 3, 4, 5]
# 重置后为:[1, -1, -1, -1, -2, 3, 4, 5]
# 结论:[1,3,5,6] 分别对应的index为[1,6,7,8](消失的数字)
for num in nums:
index = abs(num) - 1
# 保持nums[index]为相反数,唯一和上面的解法不同点就是这里,好好体会
nums[index] = -nums[index]
#偶数次
return [i + 1 for i, num in enumerate(nums) if num > 0]
#奇数次
return [i + 1 for i, num in enumerate(nums) if num < 0]

454. 4Sum II

Description: 4 数之和为零的可能组合数

Given four lists A, B, C, D of integer values, compute how many tuples (i, j, k, l) there are such that A[i] + B[j] + C[k] + D[l] is zero.

To make problem a bit easier, all A, B, C, D have same length of N where 0 ≤ N ≤ 500. All integers are in the range of -228 to 228 - 1 and the result is guaranteed to be at most 231 - 1.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
Input:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]

Output:
2

Explanation:
The two tuples are:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

解法一: 先求两两之和

时间复杂度: $O(n^2+n^2)=O(n^2)$, 前者为 A, B 两两和的复杂度, 后者为 C, D 两两和的复杂度.
空间复杂度: $O(n^2)$, 哈希表占用的空间

先求 A 与 B 的两两之和, 并将和作为键存于哈希表中, 哈希表中的值为和的出现次数, 然后再求 C, D 的两两之和, 同时查询哈希表中是否存在 C, D 和的负数, 若存在, 则说明可以组成零. 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map <int, int> hash;
for(auto a : A){
for(auto b : B){
hash[a+b]++;
}
}
int res = 0;
for(auto c : C){
for(auto d : D){
int target = -(c+d);
res += hash[target];
}
}
return res;
}
};

543. 二叉树的路径

题目链接: https://leetcode-cn.com/problems/diameter-of-binary-tree/

解法一: 递归, 后序遍历

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
/**
* 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 {
int traverseTree(TreeNode* root, int &res) {
if (root == nullptr) return 0;
int left_len = traverseTree(root->left, res);
int right_len = traverseTree(root->right, res);
res = std::max(res, left_len+right_len);
return std::max(left_len, right_len) + 1;
}
public:
int diameterOfBinaryTree(TreeNode* root) {
int res = 0;
traverseTree(root, res);
return res;
}
};

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
def traverseTree(root, res):
if root == None: return 0
l = traverseTree(root.left, res)
r = traverseTree(root.right, res)
res[0] = max(res[0], l+r)
return max(l, r) + 1
res = [0] # 这里使用列表是为了将 res 作为可变对象进行传递
traverseTree(root, res)
return res[0]

解法二: 迭代, 后序遍历

貌似不是很好写, 较好的办法是修改树节点的结构体, 使之能够保存左右子树的最深深度. 否则, 非递归后序遍历时, 无法知道当前节点的左右子树深度, 就无法直接获取路径.

560. 和为 K 的子数组/连续子数组

题目链接: https://leetcode-cn.com/problems/subarray-sum-equals-k/

解法一: 累加和+哈希表

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

注意, 这里的子数组是连续的子数组, 因此使用 累加和.

只需遍历一次数组, 遍历的时候, 计算从第 0 个元素到当前元素的 累加和, 同时用哈希表保存出现过的累加和的次数, 然后利用 sum-k 得到 target, 即为 dp[j], 那么就有 dp[i] - dp[j] = sum[i, j], 所以可以直接将哈希表中 target 的出现次数加到最终的结果变量中, 代码实现如下:

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
if len(nums) == 0: return 0
dp = 0
sum_dict = {0: 1} # 首先在字典中存放一个0, 这样可以保证 0~i 的数组被考虑到
res = 0
for i, num in enumerate(nums):
dp = dp + num # 计算 [0~i] 的数组和
target = dp - k # 计算 dp[i] 需要减去多少才能变成 k, 也就是找到 dp[j], dp[i] - dp[j] = [j+1, i]
if target in sum_dict: # 如果字典中有 dp[j], 那么就将 dp[j] 的个数加到最终的结果中
res += sum_dict[target]
sum_dict[dp] = sum_dict.get(dp, 0) + 1 # 将 dp[i] 存入到字典中, 值为 dp[i] 的出现次数
return res

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
std::unordered_map<int, int> hash{{0, 1}};
int dp = 0;
int res = 0;
for (auto num : nums) {
dp += num;
int target = dp - k;
if (hash.find(target) != hash.end()) {
res += hash[target];
}
hash[dp] += 1;
}
return res;
}
};

581. 最短无序连续子数组

题目链接: https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/

解法一: 确定每个元素位置是否正确

我们要找出的就是无序数列中的最左端和无序数列中的最右端. 那么, 对于最右端, 我们可以 从左往右 的寻找 最后一个 不满足 “比之前元素都大” 性质的元素. 对于最左端, 我们可以 从右往左 的寻找 最后一个 不满足 “比之前元素都小” 性质的元素.

另一种理解方式: 每次从左往右判断i(下标)位置是否比前i个的最大值大,如果是则说明位置在这i + 1中正确,假设不是,则说明 位置不对,因为从小到大,i位置应该这i + 1中最大才符合要求. 从右往左同理

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def findUnsortedSubarray(self, nums: List[int]) -> int:
if len(nums) < 2: return 0
min_num, max_num = nums[-1], nums[0]
right = 0
left = 1
n = len(nums)
for i in range(n):
max_num = max(max_num, nums[i])
min_num = min(min_num, nums[n-i-1])
if max_num != nums[i]: right = i
if min_num != nums[n-i-1]: left = n-i-1
return right - left + 1

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
int n = nums.size();
if (n < 2) return 0;
int left = 1, right = 0;
int min_num = nums[n-1], max_num = nums[0];
for (int i = 0; i < nums.size(); i++) {
max_num = std::max(max_num, nums[i]);
min_num = std::min(min_num, nums[n-i-1]);
if (max_num != nums[i]) right = i;
if (min_num != nums[n-i-1]) left = n-i-1;
}
return right - left + 1;
}
};

621. 任务调度器

题目链接: https://leetcode-cn.com/problems/task-scheduler/

解法一: 以最多的任务作为依据

首先, 找到出现频率最高的任务, 然后在完成该任务时穿插完成其他任务, 根据任务的冷却时间不同, 分为两种情况:

  1. 次数最多的任务 A 的冷却时间大于其他任务的循环时间, 那么最小时间就是任务 A 全部执行完毕的时间: $(A - 1)\times(n + 1) + count_max$
  2. 任务 A 的冷却时间小于其他任务的循环时间, 那么这个时间连次数最多的任务 A 都没有等待时间了, 所以其他所有的任务都可以完美紧密执行, 而不需要进行等待, 因此总时间就是len(tasks)

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
task_dict = {}
for task in tasks:
task_dict[task] = task_dict.get(task, 0) + 1
time = 0
max_num = max(task_dict.values())
count = 0
for v in task_dict.values():
if v == max_num: count += 1
min_time = (max_num-1)*(n+1) + count
return max(min_time, len(tasks))

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int leastInterval(vector<char>& tasks, int n) {
std::unordered_map<char, int> hash;
int max_num = 0;
for (auto c : tasks) {
hash[c]++;
max_num = std::max(hash[c], max_num);
}
int count = 0;
for (auto item : hash) {
if (item.second == max_num) {
count++;
}
}
return std::max((max_num-1) * (n+1) + count, int(tasks.size()));
}
};

647. 回文子串

题目链接: https://leetcode-cn.com/problems/palindromic-substrings/

数有多少个回文子串

解法一: 扩展中心法

时间复杂度 $O(n^2)$, 题目字符串长度不超过 1000, 因此复杂度满足题目要求, 我们将字符首尾插入两个不同的字符, 然后在所有字符中间插入相同的字符, 这样就可以避免判断回文串的奇偶长度了, 具体看下面的代码实现

Python 实现:

1
2
3
4
5
6
7
8
9
class Solution:
def countSubstrings(self, s: str) -> int:
ss = '#'.join(list('$' + s + '@')) # 前后字符不一样可以帮助判断是否已经到了边界, 用 # 连接字符可以省去奇偶判断
count = 0
for i in range(1, len(ss)-1):
l = 1
while (ss[i+l] == ss[i-l]): l += 1 # 先求导回文串的长度+1
count += l // 2 # 取整除法, 获得包含的回文子串个数
return count

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int countSubstrings(string s) {
s = '$' + s;
std::vector<char> ss(2*s.size()+1, '@');
for (int i = 0; i < ss.size()-1; i+=2) {
ss[i] = s [i/2];
ss[i+1] = '#';
}
int count = 0;
for (int i = 1; i < ss.size()-1; i++) {
int l = 1;
while (ss[i+l] == ss[i-l]) l++;
count += l / 2;
}
return count;
}
};

解法二: Manacher 算法

Manacher 算法的时间复杂度接近于 $O(n)$

这道题实际上就是求最长回文串的变形, 不同之处仅在于当求出每一个回文串的长度之后, 需要将相应的回文子串的数量添加到最终的结果中, 其余地方相同. 关于 Manacher 算法的详细讲解可以查看第 005 题, 本题的代码实现如下所示:

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def countSubstrings(self, s: str) -> int:
ss = '#'.join(list('$' + s + '@')) # 前后字符不一样可以帮助判断是否已经到了边界, 用 # 连接字符可以省去奇偶判断
p_len = [0] * len(ss) # 记录每个字符的回文串长度
count = 0 # 记录回文子串的个数
P = 0 # 记录回文串最右边所能到达的边界
center = 0 # 记录最右边回文串对应的中心
for i in range(1, len(ss)-1):
if i < P:
l = min(P-i, p_len[center-(i-center)]) # l 为 P-i, 及center对应i的长度的较小者
while (ss[i-l] == ss[i+l]): l+=1
else: # 如果 i > P, 就只能老老实实的使用中心扩展法
l = 1
while (ss[i-l] == ss[i+l]): l += 1
if (P < i+l-1): # 更新 P, center
P = i+l-1
center = i
p_len[i] = l-1 # 更新 len
count += l // 2
return count

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
class Solution {
public:
int countSubstrings(string s) {
s = '$' + s;
std::vector<char> ss(2*s.size()+1, '@');
std::vector<int> p_len(ss.size(), 0);
int P = 0, center = 0, count = 0;
for (int i = 0; i < ss.size()-1; i+=2) {
ss[i] = s [i/2];
ss[i+1] = '#';
}
for (int i = 1; i < ss.size()-1; i++) {
int l;
if (i < P) { // i < P, 可以利用之前的计算结果
l = std::min(P-i, p_len[center - (i-center)]);
while (ss[i+l] == ss[i-l]) l++;
} else { // 否则只能使用中心扩展法
l = 1;
while (ss[i+l] == ss[i-l]) l++;
}
if (P < l-1) { // 更新 P 和 center
P = i+l-1;
center = i;
}
p_len[i] = l-1; // 更新 len
count += l / 2;
}
return count;
}
};

673. 最长递增子序列的个数

给定一个未排序的整数数组,找到最长递增子序列的个数。

示例 1:

输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:

输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

解法一: 动态规划

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

length[i]存储以第 i 个字符结尾的递增子序列的最长长度, 用count[i]表示该长度下的子序列个数.

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def findNumberOfLIS(self, nums: List[int]) -> int:
if not nums: return 0
n = len(nums)
length = [1] * n # 每个字符的最短长度为 1
count = [1] * n # 每个子序列的最少数量为 1
for i in range(n):
for j in range(i):
if nums[j] < nums[i]: # 用第 i 个字符为结尾时, 看与前面的字符是否能组成递增子序列
new_len = length[j] + 1
if new_len > length[i]: # 如果组成后的长度 > 当前长度, 则更新长度和该长度的数量
length[i] = new_len
count[i] = count[j]
elif new_len == length[i]: # 如果 = 当前长度, 将更新长度数量
count[i] += count[j]

max_len = max(length) # 统计 max_len 的总数量
return sum(c for i, c in enumerate(count) if length[i] == max_len)

解法二: 线段树

时间复杂度: $O(nlogn)$
待补充

674. 最长连续递增序列

给定一个未经排序的整数数组,找到最长且连续的的递增序列。

示例 1:

输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。
示例 2:

输入: [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。
注意:数组长度不会超过10000。

解法一: 滑动窗口(最优)

因为此题要求子序列连续, 每个连续递增子序列之间是不相交的, 因此, 我们可以逐个遍历, 当遇到nums[i-1] >= nums[i]时, 说明一段递增子序列已经结束, 应该开启下一段, 期间我们不断更新最大的连续递增子序列的长度即可

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

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
max_len = 0
cur_len = 0
for i, num in enumerate(nums):
if i != 0 and nums[i] > nums[i-1]:
cur_len += 1
else:
cur_len = 1
max_len = max(max_len, cur_len)
return max_len

703. 数据流中的第 K 大元素

题目链接: https://leetcode-cn.com/problems/kth-largest-element-in-a-stream/

解法一: 小顶堆

注意: 这道题要求的是第 K 大的元素, 而不是第 K 个元素, 也就是说是升序排列后, 倒数 第 K 个元素. 所以要用大顶堆.

对于任意的元素, 只有当其大于堆顶时, 才将之加入堆, 堆得大小为 K, 这样, 所有大于堆顶的元素都在堆内.

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import heapq
class KthLargest:
#heap = []
#k = 0
def __init__(self, k: int, nums: List[int]):
self.heap = []
self.k = k
self.heap = heapq.nlargest(k, nums) # 先找出 nums 中最大的 k 个元素
heapq.heapify(self.heap) # 将其构建成堆, 注意此时堆的大小仍然有可能小于 k
def add(self, val: int) -> int:
if (len(self.heap) < self.k): # 若堆的大小不够 k, 则直接入堆
heapq.heappush(self.heap, val)
elif (val > self.heap[0]): # 否则, 只有当前元素大于堆顶时, 才替换堆顶
heapq.heapreplace(self.heap, val)
return self.heap[0] # 返回堆顶

# Your KthLargest object will be instantiated and called as such:
# obj = KthLargest(k, nums)
# param_1 = obj.add(val)

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
class KthLargest {
private:
std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap; // 小顶堆
int k = 0;

public:
KthLargest(int kk, vector<int>& nums) {
k = kk;
for (int i = 0; i < nums.size(); i++) { // 初始化堆, 上限为 k
if (min_heap.size() < k)
min_heap.push(nums[i]);
else if (min_heap.top() < nums[i]) {
min_heap.pop();
min_heap.push(nums[i]);
}
}
}

int add(int val) {
if (min_heap.size() < k) // 添入新元素, 上限为 k
min_heap.push(val);
else if (min_heap.top() < val) {
min_heap.pop();
min_heap.push(val);
}
return min_heap.top();
}
};

/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest* obj = new KthLargest(k, nums);
* int param_1 = obj->add(val);
*/

714. 买卖股票的最佳时机含手续费

题目链接: https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/

解法: 通用 DP 解法

需要注意的是每次买入时需要多花费一笔手续费, 其他地方与无限次的买卖股票完全一致.

Python 实现:

1
2
3
4
5
6
7
class Solution:
def maxProfit(self, prices: List[int], fee: int) -> int:
if len(prices) <= 1: return 0
dp = [-prices[0]-fee, 0] # base case: hold+fee, not_hold
for price in prices[1:]:
dp = [max(dp[0], dp[1]-price-fee), max(dp[1], dp[0]+price)] # 不要忘了买入时的手续费
return dp[1]

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
if (prices.size() <= 1) return 0;
std::pair<int, int> dp = {-prices[0]-fee, 0};
for (int i=1; i<prices.size(); i++) {
int hold = std::max(dp.first, dp.second-prices[i]-fee);
int not_hold = std::max(dp.second, dp.first+prices[i]);
dp.first = hold; dp.second = not_hold;
}
return dp.second;
}
};

739. 每日温度

题目链接: https://leetcode-cn.com/problems/daily-temperatures/

解法一: 单调递增栈

维护一个单调递增栈, 栈内元素越深, 值越大, 判断某一个的升温天数时, 逐个查看栈顶, 若栈顶小于当前温度, 则退栈, 直到栈为空, 若为空, 则说明后面的温度不会升高.

C++ 解法:

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:
vector<int> dailyTemperatures(vector<int>& T) {
std::stack<std::pair<int, int>> stk;
std::vector<int> res(T.size(), 0);
for (int i = int(T.size())-1; i >= 0; i--) {
while (!stk.empty()) {
if (T[i] < stk.top().first) { // 栈顶温度高于当天, 则直接计算相差的天数, 并把当天情况入栈
res[i] = stk.top().second - i;
stk.push({T[i], i});
break;
} else { // 栈顶温度低于当天, 则退栈, 因为栈顶的元素不可能成为前面的解
stk.pop();
}
}
if (stk.empty()) { // 如果栈本身为空或一直退栈导致栈空, 则应该将当天情况入栈
stk.push({T[i], i});
}
}
return res;
}
};

Python 解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def dailyTemperatures(self, T: List[int]) -> List[int]:
stack = []
res = [0] * len(T)
for i in range(len(T)-1, -1, -1):
while len(stack) > 0:
if T[i] < stack[-1][0]:
res[i] = stack[-1][1] - i
stack.append([T[i], i])
break
else:
stack.pop()
if len(stack) == 0:
stack.append([T[i], i])
return res

解法二: 利用已经求得的部分结果加速循环

实际上我们不需要额外的维护这个栈, 我们可以直接利用已经求得的部分结果来进行计算, 这样可以空间复杂度降低.

从后往前判断, 对于任意一天, 逐个遍历该天后的温度, 由于我们已经求得了该天后的温度提升数组res, 因此, 如果某天温度比该天低, 我们可以直接跳到某天温度的升高天数, 这样, 降低了循环次数, 使其少于 $O(n^2)$

C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& T) {
std::vector<int> res(T.size(), 0);
for (int i = int(T.size())-1; i >= 0; i--) {
for (int j = i+1; j < T.size(); ) {
if (T[i] < T[j]) { // 如果温度高于当天, 则直接计算天数, 并退出该层循环
res[i] = j - i;
break;
} else if (res[j] == 0) { // 如果温度低于当天, 且后面没有比此温度更高的, 则不用判断, 直接置零
break;
} else{ // 否则, 直接跳过 res 求得的天数, 加速循环
j += res[j];
}
}
}
return res;
}
};

Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def dailyTemperatures(self, T: List[int]) -> List[int]:
res = [0] * len(T)
for i in range(len(T)-1, -1, -1):
j = i + 1
while (j < len(T)):
if T[i] < T[j]:
res[i] = j - i
break
elif res[j] == 0:
res[i] = 0
break
else:
j += res[j]
return res

743. 网络延迟时间

题目链接: https://leetcode-cn.com/problems/network-delay-time/

解法一: 迪杰斯特拉

迪杰斯特拉算法可以求得有向图中一个顶点到其余各个顶点的最短路径, 然后在所有的最短路径中取最大值即为该题的解.

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 networkDelayTime(self, times: List[List[int]], N: int, K: int) -> int:
graph = dict()
for u, v, w in times:
graph[u] = graph.get(u, dict())
graph[u][v] = w
if K not in graph:
return -1
dists = [1] + [float('inf')] * N # -1 是为了将节点编号是图的编号对齐
dists[K] = 0 # 初始化到 K 的距离为 0
points = [i for i in range(1, N+1)] # 记录还未确定最短路径的节点
while points:
min_path, node, index = float('inf'), 0, 0
for _index, _node in enumerate(points): # 找到剩余节点中, 具有最短路径的节点
if dists[_node] < min_path:
min_path = dists[_node]
node = _node
index = _index
points.pop(index) # 确定该节点的最短路径, 将该节点弹出
if node in graph: # 当前的节点必须有出度
for adj_node, weight in graph[node].items(): # 将该节点指向的其他节点, 更新到 dists 中
if dists[node]+graph[node][adj_node] < dists[adj_node]:
dists[adj_node] = dists[node]+graph[node][adj_node]
path = max(dists) # 找到最长的最短路径
return path if path < float('inf') else -1

上式中, 我们使用的是列表来存储dists, 于是在每次获取当前具有最短路径的剩余节点时, 需要 $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
import heapq
class Solution:
def networkDelayTime(self, times: List[List[int]], N: int, K: int) -> int:
graph = dict()
for u, v, w in times: # 构建图
graph[u] = graph.get(u, dict())
graph[u][v] = w
if K not in graph:
return -1
#dists = [-1] + [graph[K].get(i, float('inf')) for i in range(1, N+1)] # -1 是为了将节点编号是下标对齐
dists = [-1] + [float('inf')] * N # 注意这里初始时全部为 inf, 主要是因为下面的堆是从 (0, K) 开始的
dists[K] = 0 # 将 K 到自身的距离提前置为0, 否则会一直是 inf

points = {i: 1 for i in range(1, N+1)} # 记录还未确定最短路径的节点
points_pq = [] # 构建最小堆, 堆中元素第一个节点为距离, 第二个代表具体的节点
heapq.heappush(points_pq, (0, K)) # 首先将 K 节点放入堆, 此时堆中最短的路径就是到 K 的路径
while points: # 遍历直到 points 为空, 遍历n次
while points_pq:
min_path, node = heapq.heappop(points_pq)
if node in points: # 由于堆只能对堆顶访问, 因此下面更新节点的值时, 实际上不是更新, 而是直接插入了新的路径
break # 因此, 有可能存在当前的最短节点之前已经处理过, 此时需要知道找到存在于未处理字典中 points 的节点
if node not in points: break # 如果找到的节点依然不在 points 中, 那么说明 points_pq 已经为空, 退出所有循环
points.pop(node) # 在 points 弹出 node, 将其处理
if node in graph: # 当前的节点必须有出度
for adj_node, weight in graph[node].items(): # 将该节点指向的其他节点, 更新到 dists 中
if dists[node]+graph[node][adj_node] < dists[adj_node]:
dists[adj_node] = dists[node]+graph[node][adj_node]
heapq.heappush(points_pq, (dists[adj_node], adj_node)) # 由于我们不能对堆中的节点访问, 因此直接将新的更短路径插入到堆中

path = max(dists) # 找到最长的最短路径
return path if path < float('inf') else -1

C++ 实现:

标准实现(vector):

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:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
std::unordered_map<int, std::unordered_map<int, int>> graph;
for (auto &time: times) { // 构建图
graph[time[0]][time[1]] = time[2];
}
std::vector<int> dists;
for (int i = 0; i < N+1; i++) {
dists.emplace_back(INT_MAX); // 初始化 dists 数组, 均为 INT_MAX
}
dists[0] = -1; // dists[0] 主要是为了让节点编号和下标对齐
dists[K] = 0; // K 到 K 的距离更新为 0
std::vector<int> points; // 记录未处理的节点
for (int i = 1; i < N+1; i++) {
points.emplace_back(i);
}
while (!points.empty()) { // 循环直到所有节点均已处理
int min_path = INT_MAX, node = 0, index = 0;
for (int i = 0; i < points.size(); i++) { // 找到当前未处理节点中的最短路径
if (dists[points[i]] < min_path) {
min_path = dists[points[i]];
node = points[i];
index = i;
}
}
points.erase(points.begin()+index); // 对找到的节点进行处理, 将其弹出未处理数组
for (auto &adj_node : graph[node]) {
if (dists[node] + adj_node.second < dists[adj_node.first]) {
dists[adj_node.first] = dists[node] +adj_node.second; // 更新 dists 数组
}
}
}

int max_path = 0; // 查找最短路径中的最大者, 将其返回
for (auto d : dists) {
max_path = std::max(max_path, d);
if (max_path == INT_MAX) return -1;
}
return max_path;
}
};

堆优化实现:

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
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
std::unordered_map<int, std::unordered_map<int, int>> graph;
for (auto &time: times) { // 构建图
graph[time[0]][time[1]] = time[2];
}
std::vector<int> dists;
for (int i = 0; i < N+1; i++) {
dists.emplace_back(INT_MAX); // 初始化 dists 数组, 均为 INT_MAX
}
dists[0] = -1; // dists[0] 主要是为了让节点编号和下标对齐
dists[K] = 0; // K 到 K 的距离更新为 0
std::set<int> points; // 记录未处理的节点
for (int node = 1; node < N+1; node++) {
points.insert(node);
}
std::priority_queue<std::pair<int, int>,
std::vector<std::pair<int, int>>,
std::greater<std::pair<int, int>> > points_pq; // 构建堆结构, 方便找到当前未处理节点中的最短路径
points_pq.push({0, K});
while (!points.empty()) { // 循环直到所有节点均已处理
while (!points_pq.empty()) {
if (points.count(points_pq.top().second)) {
break;
}
points_pq.pop();
}
int node = points_pq.top().second;
if (!points.count(points_pq.top().second)) break;
points.erase(node); //对找到的节点进行处理, 将其弹出未处理数组
points_pq.pop();

for (auto &adj_node : graph[node]) {
if (dists[node] + adj_node.second < dists[adj_node.first]) {
dists[adj_node.first] = dists[node] +adj_node.second; // 更新 dists 数组
points_pq.push({dists[adj_node.first], adj_node.first});
}
}
}

int max_path = 0; // 查找最短路径中的最大者, 将其返回
for (auto d : dists) {
max_path = std::max(max_path, d);
if (max_path == INT_MAX) return -1;
}
return max_path;
}
};

856. 括号的分数

https://leetcode-cn.com/problems/score-of-parentheses/
给定一个平衡括号字符串 S,按下述规则计算该字符串的分数:

() 得 1 分。
AB 得 A + B 分,其中 A 和 B 是平衡括号字符串。
(A) 得 2 * A 分,其中 A 是平衡括号字符串。

解法一: 栈

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def scoreOfParentheses(self, S: str) -> int:
stack = []
for c in S:
if c == '(':
stack.append('(')
elif c == ')':
res = 0
while stack[-1] != '(': # 将中间的结果计算, 直到遇到 '('
res = res + stack.pop()
stack.pop() # 弹出 '('
if res == 0:
stack.append(1)
else:
stack.append(2*res)
ans = 0 # 统计栈中的所有元素数值和
while stack:
ans += stack.pop()
return ans

885. 螺旋矩阵 III

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

解法一: 总结每个方向的步长规律

检查我们在每个方向的行走长度,我们发现如下模式:1,1,2,2,3,3,4,4,… 即我们先向东走 1 单位,然后向南走 1 单位,再向西走 2 单位,再向北走 2 单位,再向东走 3 单位,等等。因为我们的行走方式是自相似的,所以这种模式会以我们期望的方式重复。

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 spiralMatrixIII(self, R: int, C: int, r0: int, c0: int) -> List[List[int]]:
direct = [(0, 1), (1, 0), (0, -1), (-1, 0)]
step = 1
#matrix = [[0]*C for _ in range(R)] #若题目要求填充对应矩阵, 则将与 matrix 有关的代码取消注释即可
i = r0
j = c0
#matrix[i][j] = 1
cur_val = 2
flag = 0
ans = [(i, j)]
while cur_val <= R*C: # 终止条件为 cur_val 超过了 R*C, 说明矩阵已经访问完毕
for x in range(2): # 1, 1, 2, 2, 3, 3, 4, 4, ...
for _ in range(step):
i += direct[flag%4][0]
j += direct[flag%4][1]
if i >= 0 and i < R and j >= 0 and j < C:
#matrix[i][j] = cur_val #
ans.append([i, j])
cur_val += 1
flag += 1 # 方向调换
step += 1
return ans # 返回坐标列表

958. 二叉树的完全性检验

判断是否是完全二叉树

解法一: 用 flag 标记

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.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def isCompleteTree(self, root: TreeNode) -> bool:
from queue import Queue
q = Queue()
if not root: return True
q.put(root)
flag = False # 判断是否已经出现没左或右孩子的的节点
while not q.empty():
node = q.get()
if node.left: # left 在前
if flag: # 如果 flag 已经出现, 那么应该返回 False
return False
else:
q.put(node.left)
else:
flag = True
if node.right:
if flag:
return False
else:
q.put(node.right)
else:
flag = True
return True

959. 由斜杠划分区域

题目链接: https://leetcode-cn.com/problems/regions-cut-by-slashes/

解法一: 并查集

将每个 $1\times1$ 的小方格, 利用斜杠划分成上下左右四个三角形区域, 并按照 “上, 右, 下, 左” 的顺序分别给每个区域编号为 “0, 1, 2, 3”. 初始时假设这 $n\times n\times 4$ 个三角形区域都是不连通的, 然后根据每一个 $1\times 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
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
class UnionFind:
def __init__(self, n):
self.parent = [i for i in range(n)] # 每个小三角形独立构成一个连通域
self.rank = [1] * n # 树高度均为 1
self.count = n

def find_root(self, node):
if node == self.parent[node]:
return node
else:
self.parent[node] = self.find_root(self.parent[node])
return self.parent[node]

def union_items(self, p, q):
root_p = self.find_root(p)
root_q = self.find_root(q)
if (root_p == root_q): return
if (self.rank[root_q] < self.rank[root_p]):
self.parent[root_q] = root_p
elif (self.rank[root_p] < self.rank[root_q]):
self.parent[root_p] = root_q
else:
self.parent[root_q] = root_p
self.rank[root_p] += 1
self.count -= 1


class Solution:
def regionsBySlashes(self, grid: List[str]) -> int:
n = len(grid)
uf = UnionFind(n*n*4)

for i in range(n):
for j in range(n):
start = 4*(i*n+j)
if grid[i][j] == '/':
uf.union_items(start+0, start+3)
uf.union_items(start+1, start+2)
elif grid[i][j] == '\\':
uf.union_items(start+0, start+1)
uf.union_items(start+2, start+3)
else:
uf.union_items(start+0, start+1)
uf.union_items(start+1, start+2)
uf.union_items(start+2, start+3)
if i > 0:
top_start = 4*((i-1)*n+j)
uf.union_items(top_start+2, start)
if j > 0:
left_start = 4*(i*n+j-1)
uf.union_items(left_start+1, start+3)
return uf.count

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class UnionFind {
private:
std::vector<int> parent;
std::vector<int> rank;
int count;
public:
UnionFind(int n) {
for (int i = 0; i < n; i++) {
parent.emplace_back(i);
rank.emplace_back(1);
}
count = n;
}

int find_root(int node) {
if (node == parent[node]) {
return parent[node];
} else {
parent[node] = find_root(parent[node]);
return parent[node];
}
}

void union_items(int p, int q) {
int root_p = find_root(p);
int root_q = find_root(q);
if (root_p == root_q)
return;
if (rank[root_q] < rank[root_p])
parent[root_q] = root_p;
else if (rank[root_p] < rank[root_q])
parent[root_p] = root_q;
else {
parent[root_q] = root_p;
rank[root_p]++;
}
count--;
}

int get_count() {
return count;
}
};

class Solution {
public:
int regionsBySlashes(vector<string>& grid) {
int n = grid.size();
UnionFind uf(n*n*4);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
int start = 4*(i*n+j);
if (grid[i][j] == '/') {
uf.union_items(start+0, start+3);
uf.union_items(start+1, start+2);
} else if (grid[i][j] == '\\') {
uf.union_items(start+0, start+1);
uf.union_items(start+2, start+3);
} else {
uf.union_items(start+0, start+1);
uf.union_items(start+1, start+2);
uf.union_items(start+2, start+3);
}
if (i > 0) {
int top_start = 4*((i-1)*n+j);
uf.union_items(start, top_start+2);
}
if (j > 0) {
int left_start = 4*(i*n+j-1);
uf.union_items(start+3, left_start+1);
}
}
}
return uf.get_count();
}
};

股票问题

框架:

1
2
3
4
5
def stock():
# 定义 base case
dp[0] = ...
for price in prices:
#更新 dp 状态

核心思想依然是 DP, 只不过可以套用下面的状态转移方程(也可以看做是 DP 更新方程):

1
2
3
4
5
6
7
8
9
10
11
12
13
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )

解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )

解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。

这个解释应该很清楚了,如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的。这样就相当于控制了可以交易的最大次数, 如果可以交易无限次, 那么就可以不记录 k.

现在,我们已经完成了动态规划中最困难的一步:状态转移方程。如果之前的内容你都可以理解,那么你已经可以秒杀所有问题了,只要套这个框架就行了。不过还差最后一点点,就是定义 base case,即最简单的情况。

1
2
3
4
5
6
7
8
dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。