Hard题目总结

Binary Search

Search in Rotated Sorted Array: https://leetcode.com/problems/search-in-rotated-sorted-array/

两种方法:

1)直接binary search,先判断mid在哪一段,然后判断mid和target的关系,如果结果可能在两段,再判断这段边界(left或right)和target的关系

2)先用binary search找出最小的元素(即从左边开始第一个小于最后一个元素的元素),然后判断target在哪一段,对那一段用普通的binary search即可




方法技巧题:

Binary Tree Maximum Path Sum: https://leetcode.com/problems/binary-tree-maximum-path-sum/

对一个节点来说,以该节点为根的树的最大path sum可能是:

a) 左子树最大的path sum

b) 右子树最大的path sum

c) 根节点 + max(0,左子树包含左儿子的最大path sum) + max(0,右子树包含右儿子的最大path sum)

所以设定一个helper(TreeNode* root, int &max_sum, int &max_root)。注意这里的max_root不能跨过根节点,只能是(根节点+左子树) / (根节点+右子树)


Binary Tree Postorder Traversal: https://leetcode.com/problems/binary-tree-postorder-traversal/

1)按照root,right,left的顺序traverse,然后将结果reverse

2)设置一个cur指向当前访问的节点,pre指针,指向上一次访问的节点。将cur初始为root,将从cur开始的左儿子依次push入stack,直到NULL;令cur = s.top(),如果cur->right为NULL或pre,表示它没有右儿子或右子树已经遍历结束,则将cur加入result并将其从stack中pop出,令pre = cur并令cur = NULL;否则令cur = cur->right。只要cur != NULL或stack不为空则重复这个过程


Jump Game II: https://leetcode.com/problems/jump-game-ii/

思想是BFS。计算走1步最远到哪,走2步最远到哪…… 直到第一次>= nums.size() - 1,返回步数。计算时,设一个start表示当前步数的起点,end表示当前步数能到达的最远距离,遍历从start到end的所有元素,如果他们能到达最后一个元素,则返回步数;否则令下一轮的最远距离max_end = max(max_end, i + nums[i]);每次循环结束后将start更新为end + 1,将end更新为max_end。注意要特殊处理一下只有一个元素的情况


Merge k Sorted List: https://leetcode.com/problems/merge-k-sorted-lists/

三种方法:

1)divide & conquer,只要前一半和后一半已经merge到一起了,则只要将这两半merge到一起即可,递归

2)先两两merge,直到最后只剩一个链表。这种方法可以使用iterative而避免递归,即 i 每次循环到 (size + 1) / 2,将lists[2*i]和lists[2 * i + 1]merge到一起放在lists[i],然后size = (size + 1) / 2直到size == 1,返回lists[0]即可(可以先令size = (size + 1) / 2,然后将list[i]和list[i + size]merge到一起放到list[i])

3)设一个priority queue存放所有list的第一个节点,然后每次拿出最小的,如果该节点的下一个节点不为空则将这下一个节点也放入priority queue,直到priority queue为空


Median of Two Sorted List: https://leetcode.com/problems/median-of-two-sorted-arrays/

可以转换成寻找两个数列从小到大第 k 个数的问题。比较两个数列第 k / 2 - 1个元素的大小,将小的那个数列的前k / 2 - 1个元素丢掉,然后再寻找两个数列的第 k - k / 2个元素。注意两点:1)在比较两个数列的[start + k / 2 - 1]时要注意判断是否越界,如果越界则将进行比较的值设为INT_MAX;2)注意下一次寻找的是第 k - k / 2个元素,而不是 k / 2个元素


Best Time to Buy and Sell Stock III: https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/

首先用一个名为profit的vector记录每一天的最大profit,然后令 i = size - 2 ~ 0,表示第二次购买发生在第 i 天,则result = max(result, max_price - prices[i] + (i != 0 ? profit[i - 1] : 0),其中max_price初始化为prices[size - 1],表示从 i 开始到最后的最大价格。有一个空间为O(1)的算法,但是不太好理解,看这里:https://leetcode.com/discuss/18330/is-it-best-solution-with-o-n-o-1 


Largest Rectangle in Histogram: https://leetcode.com/problems/largest-rectangle-in-histogram/

设置一个单调递增的栈index存储下标,当(!index.empty() && height[i] <= index.top())时,将栈顶pop出存放在tmp_index中,则以height[tmp_index]为高的最大面积为 heights[tmp_index] * (i - index.top() - 1),(左边到index.top(),右边到 i - 1),注意这里要特别处理一下index为空的情况


LRU Cache: https://leetcode.com/problems/lru-cache/

使用双向链表和hash table。双向链表的节点是一个类Node,存储key和value;用hash table存储key和Node*。在LRU类的private部分定义Node *newest指向最新的节点。调用get()时,如果it->second == newest则不用进行处理,否则将it->second移动到链表尾部;调用set()时,注意在删除最旧节点时要判断一下它的next是否为NULL,同时要将被删除节点的key从hash table中erase掉


Find Median from Data Stream: https://leetcode.com/problems/find-median-from-data-stream/

用一个最大堆存储小于前一半元素,用一个最小堆存储后一半元素,add()时通过pivot判断应该加入哪个堆,然后对两个堆进行调整,保证small == big或small == big + 1,并更新pivot


Longest Consecutive Sequence: https://leetcode.com/problems/longest-consecutive-sequence/

将所有元素放入一个unordered_set,对nums[i],设置一个upper = nums[i] + 1,在hash table中寻找upper,如果找到则erase,直到upper不存在,于是得到一个上界;同理可以得到下界


Substring with Concatenation of Words: https://leetcode.com/problems/substring-with-concatenation-of-all-words/

用一个hash table记录words中每个word出现的次数。将s分成word_size组,每一组表示可能是正确结果的起始位置,第i组是i, i + 1* word_size, i + 2 * word_size...,按照组对s进行遍历,这样在每一组内,下一个位置可以使用之前位置得到的结果,而避免了重复的工作。对每一组内,令j = i,想象有一个window,window内是一些字符串,用变量start表示window的起始位置,用另一个hash table记录window从start到现在已访问的word及出现次数,用count记录已访问的有效的word数。每次检查一个新的word名为tmp时,如果它不在hash中,则是无效word,此时清空tmp_hash,count = 0并令start += word_size;如果它在hash中则tmp_hash[tmp]++, count++,如果tmp_hash[tmp] > hash[tmp],则将start右移并将移出window的word再tmp_hash中--,直到tmp_hash <= hash[tmp]。然后检查count,如果等于总的word数,则加入result。对于j,每次增加word_size,并且注意循环的终止条件应该是j + word_size <= s.size(),有等号


First Missing Positive: https://leetcode.com/problems/first-missing-positive/

将nums进行partition,<0的放到前面,>=0的放到后面,然后返回第一个>=0的下标n。因为缺失的数肯定在范围[1, n + 1]内,所以只考虑abs(nums[i]) <= n的情况即可。然后遍历所有的正数(0~n),如果一个数存在且abs(nums[i]) <= n,则将nums[abs(nums[i]) - 1]取反,最后再从头检查nums,第一个大于0的index对应的数是不存在的,返回这个index + 1即可(这里的partition是2-ways partition,注意2-way partition和3-way partition的区别)

另外一种方法是,类似的,也是保证将出现的nums[i]放到nums[nums[i] - 1]的位置。遍历所有的元素,在nums[i] > 0 && nums[i] <= size && nums[nums[i] - 1] != nums[i]时,swap(nums[i], nums[nums[i] - 1])。因为每次swap都能保证至少一个数被放在正确的位置,而一个数一旦被放在了正确位置就不会重复放置,所以虽然有嵌套循环,每个[1, n]的数只会被访问一次,所以时间复杂度是O(n)


Longest Substring with At Most Two Distinct Characters: https://leetcode.com/problems/longest-substring-with-at-most-two-distinct-characters/

扩展到k distinct characters的情况:用一个window记录满足条件的substring的起止位置,再用一个大小为256的vector num记录每个字符出现的的次数,每遇到s[i]就num[s[i]]++,如果num[s[i]] == 1则说明出现了新的字符,检查并保证当前window中只有k个字符


Minimum Window Substring: https://leetcode.com/problems/minimum-window-substring/

记录t中每个字符出现的次数,然后用window对s进行遍历,用count记录遇到的t中元素的个数。如果num[s[end]] > 0,说明s[end]是t中的元素,则count++,然后num[s[end]]--;在count == t.size()时首先判断当前的end - start + 1 < window_size是否成立,如果成立则更新result,然后增加start以缩小window的大小,如果num[s[start]] == 0,说明s[start]是t中的一个元素(因为s[start]在之前肯定被访问过,如果它不是t中的元素,则对它--时它会小于0,只有是t中的元素时自减完的结果才是0),则count--,然后start++。最后返回result即可

注意比较一下这道题和上面那道题,一个是求最长的substring,一个是求最短的substring。对于求最长,应该在移动完start的循环之后判断是否更新结果,因为循环中的是invalid的,移动完变成valid;而对于求最短,应该在移动start的循环中判断是否更新结果,因为循环中是valid的,移动完后变成invalid


Wildcard Matching: https://leetcode.com/problems/wildcard-matching/

1)可以用dp做,dp[i + 1][j + 1]表示s[i]和p[j]匹配,注意初始化dp[0][j]

2)用两个变量match和asterick,asterick表示p中上一个星号出现的位置,match表示s中这个星号可能要匹配的下一个位置。在i < s.size()时,如果单字符能匹配,则i++,j++;如果p[j] == '*',则令match = i,asterick = j++;如果之前出现了星号,则i = ++match,j = asterick + 1;否则返回false(以上判断注意有的要保证j < p.size())。s检查完后,剩下的p必须全部是星号则返回true,否则返回false


Trapping Rain Water: https://leetcode.com/problems/trapping-rain-water/

设置left_max和right_max分别表示当前左边和右边的最大高度,每次循环开始先更新二者,如果left_max < right_max,则说明左边的高度成为了限制,计算当前left能容纳的水量并令left++;否则,计算当前right能容纳的水量并令right--






BSF & DSF:

Word Ladder II: https://leetcode.com/problems/word-ladder-ii/

简单的方法:

先用bfs,一边进行搜索一边构建图(即建立一个名为neighbor的unordered_map<string, vector<string>>记录每一个节点所能到达的下一个节点),在得到endWord后,利用neighbor通过dfs产生答案。在bfs的过程中,要使用两个unordered_set<string>记录当前层的节点(cur)和这次新产生的节点(next),然后每次令cur = next,注意每一个节点被访问后需要从wordList中将其删除,避免重复访问

高效的方法:

从beginWord和endWord两端交替进行bfs同时构建图(记录neighbor),将产生的中间结果作为新的beginWord进行下一层寻找,主要要设一个flip来判断此次是从beginWord->endWord还是endWord->beginWord以保证能正确地产生neighbor,当找到一条路径后就退出,利用neighbor通过dfs构造答案。需要注意的一点技巧是,之所以双端bfs比较快是因为路径是从两端向中间增长的,我们要做的就是尽量减少无关结果,所以每次在调用bfs时,如果head.size() > tail.size(),说明用head作为beginWord会产生很多无关结果,所以这时我们不对head进行增长,而直接进行下一次逆向增长,这样可以极大地提高算法的效率





Dynamic Programming:

Palindrome Partitioning II: https://leetcode.com/problems/palindrome-partitioning-ii/

对于每一个位置 i,令j = i ~ 0,检查s[j] ~ s[i]是不是palindrome,如果是则cut[i + 1] = min(cut[i + 1], cut[j] + 1)。这里要将cut初始化为size + 1,也就是说位置 i 的cut数是存在cut[i + 1]中的,因为对j == 0,如果0 ~ i 是palindrome,则不需要切,也就是说对应的值应该为0,因此为了这种情况下不进行特别处理,所以在最前面添加一个cut[0] = -1。为了不必重复判断一个substring是不是palindrome,用一个的二维vector isPalindrome[j][i]表示j ~ i 是不是palindrome。这个二维vector初始化为false,当确定某段为palindrome时将其置为true。在判断 j ~ i 是不是palindrome时,只要满足s[i] == s[j] && (i - j < 2 || isPalindrome[j + 1][i - 1])即可


Edit Distance: https://leetcode.com/problems/edit-distance/

设置一个名为count的二维vector,[i][j]表示将word1的前 i 个字符变成word2的前 j 个字符所需要的最小操作数。

若word1[i] == word2[j]:count[i + 1][j + 1] = min(count[i][j], min(count[i][j + 1] + 1, count[i + 1][j] + 1))

若word1[i] != word2[j]:count[i + 1][j + 1] = min(count[i][j] + 1, min(count[i][j + 1] + 1, count[i + 1][j] + 1))

特别需要注意的是,要初始化count[i][0] = i 和count[0][j] = j


Distinct Subsequences: https://leetcode.com/problems/distinct-subsequences/

这道题的意思是,S的subsequence中有几个是和T相同的。设一个名为count的二维vector,count[i + 1][j + 1]表示S的前 i 个字符的subsequence中包含与T的前 j 个字符相同的个数。则对于S[i] != T[j],count[i][j] = count[i - 1][j];对于S[i] == T[j],count[i][j] = count[i - 1][j] + count[i - 1][j - 1]。注意如果T为空,则只有S的空subsequece才等于T,所以初始化时count[i][0] = 1


Interleaving String: https://leetcode.com/problems/interleaving-string/

注意这里要求s1和s2恰好能组成s3,不能有剩余的字符。设一个名为can的二维数组,can[i + 1][j + 1]表示s1的前 i 个字符和s2的前 j 个字符能组成s3的前 i + j 个字符,则can[i + 1][j + 1] = (s1[i] == s3[i + j + 1] && can[i][j + 1]) || (s2[j] == s3[i + j + 1] && can[i + 1][j])。注意两点:1)判断时是s1[i]/s2[j] == s3[i + j + 1];2)初始化时,先将can[0][0]初始化为true,然后对i/j == 0的情况进行特殊处理:can[i + 1][0] = s1[i] == s3[i] && can[i][0];can[0][j + 1] = s2[j] == s3[j] && can[0][j]


Regular Expression Matching: https://leetcode.com/problems/regular-expression-matching/

声明长度+1的二维vector名为match,match[i + 1][j + 1]表示s[i]和p[j]能够match。首先初始化match[0][j],表示p[j]能否match空string,然后进行遍历更新match[i + 1][j + 1],主要有以下几种情况:

1)p[j] != '*':match[i + 1][j + 1] = match[i][j] && (s[i] == p[j] || p[j] == '.')

2)p[j] == '*':match[i + 1][j + 1] = match[i + 1][j - 1] || (match[i][j + 1] && (s[i] == p[j - 1] || p[j - 1] == '.'))

a)但p[j - 1]和p[j]实际为空,则match[i + 1][j + 1] = match[i + 1][j - 1]

b)p[j - 1]出现了至少一次,则match[i + 1][j + 1] = match[i][j + 1] && (s[i] == p[j - 1] || p[j - 1] == '.')

如果仅出现一次,则match[i][j + 1]表示s[i - 1]和p[j]匹配(此时p[j - 1]和p[j]实际为空);如果出现了多次,则match[i][j + 1]自然应该为true


Longest Valid Parentheses: https://leetcode.com/problems/longest-valid-parentheses/
用一个名为dp的vector存储以当前字符为结尾的最长valid输入的长度,如果s[i] == ')'并且s[i - dp[i - 1] - 1] == '(',

则dp[i] = dp[i - 1] + 2 + (i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] : 0)


Word Break II: https://leetcode.com/problems/word-break-ii/

思想类似word break,设一个名为cut_index的vector<vector<int> >(size + 1),cut_index[i + 1]记录了以 s[i]为结尾,上一个合法cut的位置。例如输入"abcde",["abc","de"],则对'e'来说,cut_index[5] = {2}。用一个max_len记录字典中的最大长度,则对于s[i],只要检查j = i - max_len ~ i - 1即可。只有当cut_index[j + 1]不为空且s.substr(j, i - j)在字典中时,表明以s[i]为结尾有合法的cut,就将 j 加入cut_index[i + 1]。注意在初始化时要在cut_index[0]中加入-1使其不为空(并且-1能保证在生成最后结果时第一个substring能从0开始)。最后检查cut_index[size],如果它不为空,则产生结果。在产生结果时使用backtracking,用一个名为space_index的vector<int>记录一个合法cut的结尾,适当加入空格即可


Best Time to Buy and Sell Stock IV: https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/

用一个名为dp的二维vector<vector<int> > (k + 1, vector<int>(size, 0)),dp[i][j]表示在第 j 天,最多有 i 次transaction时的最大利润,则dp[i][j] = max(dp[i - 1][j], prices[j] + tmpMax),其中第一项使用至多有[i - 1]次transaction的结果,prices[j] + tmpMax表示在第 j 天卖出的最大可能结果。tmpMax表示在当前时间之前有至多 i - 1次transaction且最后进行buy时的最大利润,初始化为-prices[0],每次更新为tmpMax = max(tmpMax, dp[i - 1][j - 1] - prices[j]),以供下次循环使用

当k >= size / 2时,表示可以进行任意多次transaction,则此时进行一下处理以加速程序。这时就是Best Time to Buy and Sell Stock II,用dp做即可,空间也可优化为O(1)


你可能感兴趣的:(Hard题目总结)