4. 编程算法
4.1. 找出数组中的特异数字(Single Number)
1 个数字出现奇数次,其余数字出现偶数次。Hint:异或运算。
2 个数字出现奇数次,其余数字出现偶数次。Hint:先做异或运算,得到的是这两个数的异或结果;找到该结果的二进制表示中为 1 的某一位,根据该位为 0/1 将数组分为两组,分别做异或运算。
1 个数字出现 \(1\) 次,其余数字出现 \(k\) 次。Hint:利用大小为 32 的数组,统计二进制各位出现 1 的次数,对 \(k\) 取模;最终 32 位数组的值就是 Single Number 的二进制表示。
一般情形:1 个数字出现 \(p\) 次,其余数字出现 \(k\) 次。
https://blog.csdn.net/wlwh90/article/details/89712795
4.2. 均匀分布生成其他分布的方法
Hint:中心极限定理。
4.3. 海量数据处理
Hint:哈希方法,把大文件划分成小文件,读进内存依次处理,如果需要统计频率/个数,再用哈希表;Bitmap,用一个(或几个)比特位来标记某个元素对应的值。
\(\color{darkgreen}{Code}\)
1// Bitmap 2 3#include <iostream> 4#include <bitset> 5using namespace std; 6 7const int BITSPERWORD = 32; 8const int SHIFT = 5; // i / 32 = i >> 5 9const int MASK = 0x1f; // i % 32 = i & 0x1f 10const int N = 10000; 11int a[1 + N/BITSPERWORD]; 12 13// 设置第 i 位为 1 14inline void set(int i) 15{ 16 a[i >> SHIFT] |= 1 << (i & MASK); 17} 18// 设置第 i 位为 0 19inline void clear(int i) 20{ 21 a[i >> SHIFT] &= ~ (1 << (i & MASK)); 22} 23// 检查第 i 位的值是否为 0 24inline int test(int i) 25{ 26 return a[i >> SHIFT] & (1 << (i & MASK)); 27} 28 29int main() 30{ 31 set(40); 32 cout << bitset<BITSPERWORD>(a[40 >> SHIFT]) << endl; // 00000000000000000000000100000000 33 cout << test(40) << endl; 34 return 0; 35}
4.4. 链表
对每一个节点操作之前,应先考虑该节点是否为空。
[LeetCode] Reverse Linked List 反转链表。Hint:方法一,逐个反转;方法二,递归;方法三,使用栈保存节点的值,反向赋给所有节点。
\(\color{darkgreen}{Code}\)
1struct ListNode 2{ 3 int val; 4 ListNode *next; 5 ListNode() : val(0), next(nullptr) {} 6 ListNode(int x) : val(x), next(nullptr) {} 7 ListNode(int x, ListNode *next) : val(x), next(next) {} 8};
1// 方法一,逐个反转 2ListNode* reverseList(ListNode* head) 3{ 4 if(!head || !head->next) return head; 5 ListNode* curr = head->next; 6 head->next = nullptr; 7 while(curr) 8 { 9 ListNode* post = curr->next; 10 curr->next = head; 11 head = curr; 12 curr = post; 13 } 14 return head; 15}
1// 方法二,递归 2ListNode* reverseList(ListNode* head) 3{ 4 if(head == nullptr || head->next == nullptr) return head; 5 else 6 { 7 ListNode* newHead = reverseList(head->next); 8 head->next->next = head; // head 指向的下一个节点是 newHead 的最后一个节点 9 head->next = nullptr; 10 return newHead; 11 } 12}
1// 方法三,使用栈保存节点的值,占用 O(n) 额外空间 2ListNode* reverseList(ListNode* head) 3{ 4 if(head == nullptr || head->next == nullptr) return head; 5 stack<int> stk; 6 ListNode* p = head; 7 while(p) 8 { 9 stk.emplace(p->val); 10 p = p->next; 11 } 12 p = head; 13 while(p) 14 { 15 p->val = stk.top(); 16 stk.pop(); 17 p = p->next; 18 } 19 return head; 20}
[LeetCode] Reverse Nodes in k-Group 从头节点开始,每 \(k\) 个节点为一组进行反转。Hint:对每一组节点调用反转函数。延伸:从尾节点开始,每 \(k\) 个节点为一组进行反转。Hint:先反转整个链表;按上述方法反转每一组;再反转整个链表。
https://leetcode.com/problems/reverse-nodes-in-k-group/
\(\color{darkgreen}{Code}\)
1// 从头节点开始分组 2 3class Solution 4{ 5public: 6 ListNode* reverseKGroup(ListNode* head, int k) 7 { 8 return reverseK(head, k); 9 } 10private: 11 ListNode* reverseAll(ListNode* head) 12 { 13 if(!head || !head->next) return head; 14 ListNode* newHead = reverseAll(head->next); 15 head->next->next = head; 16 head->next = nullptr; 17 return newHead; 18 } 19 ListNode* reverseK(ListNode* head, int k) 20 { 21 if(!head || !head->next) return head; 22 ListNode* p = head; 23 for(int i = 1; i < k; ++i) 24 { 25 p = p->next; 26 if(!p) return head; 27 } 28 ListNode* secondHead = reverseK(p->next, k); 29 p->next = nullptr; // 第一组的尾节点置为 NULL,便于直接调用 reverseAll 30 ListNode* newHead = reverseAll(head); 31 head->next = secondHead; // 反转之后,head 成为第一组的尾节点 32 return newHead; 33 } 34};
1// 从尾节点开始分组 2 3ListNode* reverseKGroup(ListNode* head, int k) 4{ 5 ListNode* newHead = reverseAll(head); 6 newHead = reverseK(newHead, k); 7 newHead = reverseAll(newHead); 8 return newHead; 9}
求有环单链表中的环长、环起点、链表长。Hint:快慢指针。
https://www.cnblogs.com/xudong-bupt/p/3667729.html
\(\color{darkgreen}{Code}\)
1// 判断链表是否有环 2 3class Solution 4{ 5public: 6 bool hasCycle(ListNode *head) 7 { 8 if(!head || !head->next) return false; 9 ListNode* slow = head; 10 ListNode* fast = head; 11 while(fast && fast->next) 12 { 13 slow = slow->next; 14 fast = fast->next->next; 15 if(slow == fast) return true; 16 } 17 return false; 18 } 19};
判断两个链表是否相交并找出交点。Hint:方法一,先求两个链表的长度差,双指针法;方法二,分别用栈保存两个链表的节点的地址(指针),从后往前比较。如果只需要判断两个链表是否相交,只需判断两个链表最后一个节点是否相同。
单链表 \(\mathcal{O}(1)\) 时间删除给定节点。Hint:交换当前节点与下一个节点的值,删除下一个节点。
https://blog.csdn.net/qq_35546040/article/details/80341136
\(\color{darkgreen}{Code}\)
1bool removeNode(ListNode* pNode) 2{ 3 if(pNode == nullptr) return true; 4 if(pNode->next == nullptr) return false; 5 pNode->val = pNode->next->val; 6 pNode->next = pNode->next->next; 7 return true; 8} 9// 注:如果需要删除最后一个节点,直接令 pNode->next = nullptr 是无法改变实参的(传值调用),可以将形参定义成指向指针的指针 10// 必须从链表头节点开始遍历,找到该节点的前驱节点 11// 还要考虑该链表只有一个节点的情形 12// 另外,可以在该函数内 delete 该指针,但是需要确保在其他地方不再需要访问 pNode 指向的内容
输出该链表中倒数第 \(k\) 个节点。Hint:双指针法,第一个指针先走 \(k-1\) 步,然后第二个指针从头节点开始,与第一个指针同步往后移;当第一个指针移到最后一个节点,第二个指针即指向倒数第 \(k\) 个节点。延伸:删除倒数第 \(k\) 个节点,需要注意删除头节点的情况。
\(\color{darkgreen}{Code}\)
1ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) 2{ 3 if(!pListHead || k == 0) return nullptr; 4 5 unsigned int tk = 1; 6 ListNode* p = pListHead; 7 while(tk < k) 8 { 9 p = p->next; 10 if(!p) return nullptr; 11 tk += 1; 12 } 13 14 ListNode* pk = pListHead; 15 while(p->next) 16 { 17 p = p->next; 18 pk = pk->next; 19 } 20 return pk; 21}
1// 删除倒数第 k 个节点 2ListNode* removeNthFromEnd(ListNode* head, int n) 3{ 4 if(!head || n <= 0) return head; 5 ListNode* pre = head; 6 ListNode* post = head; 7 for(int i = 0; i < n; ++i) 8 { 9 post = post->next; 10 if(!post) 11 { 12 if(i < n-1) return head; 13 else 14 // 删除头节点 15 { 16 pre = head->next; 17 delete(head); 18 return pre; 19 } 20 } 21 } 22 while(post->next) 23 { 24 pre = pre->next; 25 post = post->next; 26 } 27 post = pre->next->next; 28 delete(pre->next); 29 pre->next = post; 30 return head; 31}
[LeetCode] Sort List 链表排序。Hint:方法一,快速排序或归并排序;方法二,遍历链表把值存入数组,使用数组的排序方法,再把值赋回链表。
https://leetcode.com/problems/sort-list/
\(\color{darkgreen}{Code}\)
1// 快速排序 2 3class Solution 4{ 5public: 6 ListNode* sortList(ListNode* head) 7 { 8 quickSort(head, nullptr); 9 return head; 10 } 11private: 12 // 由于链表无法反向遍历,需要重新考虑如何交换两个位置的数值 13 // pre 指向 curr 的前一个数,或者指向第一个比 key 大的数的前一个数 14 // 当 curr 指向的数比 key 小,pre 移到下一位,交换两者的值 15 ListNode* partion(ListNode* head, ListNode* tail) 16 { 17 int key = head->val; 18 ListNode* pre = head; 19 ListNode* curr = head->next; 20 while(curr != tail) 21 { 22 if(curr->val < key) 23 { 24 pre = pre->next; 25 swap(pre->val, curr->val); 26 } 27 curr = curr->next; 28 } 29 swap(head->val, pre->val); 30 return pre; 31 } 32 void quickSort(ListNode* head, ListNode* tail) 33 { 34 if(head == tail || (head->next) == tail) return; 35 ListNode* mid = partion(head, tail); 36 quickSort(head, mid); 37 quickSort(mid->next, tail); 38 } 39};
1// 归并排序 2 3class Solution 4{ 5private: 6 ListNode* getMid(ListNode* head) 7 { 8 if(!head || !head->next) return head; 9 ListNode* slow = head; 10 ListNode* fast = head->next; 11 while(fast && fast->next) 12 { 13 slow = slow->next; 14 fast = fast->next->next; 15 } 16 return slow; 17 } 18 ListNode* merge(ListNode* head1, ListNode* head2) 19 { 20 // 可以 new 一个节点作为临时头节点,代码会更简洁,但是会增加空间开销、降低时间效率 21 if(!head1) return head2; 22 if(!head2) return head1; 23 ListNode* tmp_head; 24 if(head1->val <= head2->val) 25 { 26 tmp_head = head1; 27 head1 = head1->next; 28 } 29 else 30 { 31 tmp_head = head2; 32 head2 = head2->next; 33 } 34 ListNode* p = tmp_head; 35 while(head1 && head2) 36 { 37 if(head1->val <= head2->val) 38 { 39 p->next = head1; 40 head1 = head1->next; 41 } 42 else 43 { 44 p->next = head2; 45 head2 = head2->next; 46 } 47 p = p->next; 48 } 49 if(head1) p->next = head1; 50 if(head2) p->next = head2; 51 return tmp_head; 52 } 53 ListNode* mergeSort(ListNode* head) 54 { 55 if(!head || !head->next) return head; 56 ListNode* mid = getMid(head); 57 ListNode* head_post = mid->next; 58 mid->next = nullptr; 59 head = mergeSort(head); 60 head_post = mergeSort(head_post); 61 return merge(head, head_post); 62 } 63public: 64 ListNode* sortList(ListNode* head) 65 { 66 return mergeSort(head); 67 } 68};
将二叉搜索树转换为升序排序的双向链表。Hint:中序遍历。
\(\color{darkgreen}{Code}\)
1struct TreeNode 2{ 3 int val; 4 struct TreeNode *left; 5 struct TreeNode *right; 6 TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} 7}; 8 9class Solution 10{ 11public: 12 TreeNode* Convert(TreeNode* pRootOfTree) 13 { 14 pRootOfTree = converTree2List(pRootOfTree); 15 return pRootOfTree; 16 } 17private: 18 // 返回的是转换之后的链表的头节点 19 TreeNode* converTree2List(TreeNode* root) 20 { 21 if(!root) return nullptr; 22 23 TreeNode* l = converTree2List(root->left); 24 while(l && l->right) l = l->right; // 根节点应该接在左子树链表的尾节点之后 25 if(l) l->right = root; 26 root->left = l; 27 28 TreeNode* r = converTree2List(root->right); 29 if(r) r->left = root; 30 root->right = r; // 根节点应该接在右子树链表的头节点之前 31 32 while(root->left) root = root->left; // 找到头节点 33 return root; 34 } 35};
删除链表中的重复节点。Hint:可能会删除头节点;注意尾节点处是否有重复元素。
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 ListNode* deleteDuplicates(ListNode* head) 5 { 6 if(!head || !head->next) return head; 7 ListNode* tmp_head = new ListNode(-1); 8 tmp_head->next = head; 9 ListNode* pre = tmp_head; 10 ListNode* curr = head; 11 while(curr && curr->next) 12 { 13 if(curr->val == curr->next->val) 14 { 15 while(curr->next && curr->val == curr->next->val) curr = curr->next; 16 curr = curr->next; 17 if(!curr || !curr->next) pre->next = curr; 18 } 19 else 20 { 21 pre->next = curr; 22 pre = curr; 23 curr = curr->next; 24 } 25 } 26 head = tmp_head->next; 27 delete(tmp_head); tmp_head = nullptr; 28 return head; 29 } 30};
重组链表,首尾交错,L0→L1→…→Ln-1→Ln 转换为 L0→Ln→L1→Ln-1→L2→Ln-2→…。Hint:首先,链表中间截断;然后,第二段链表翻转;最后,合并两个子链表。
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 void reorderList(ListNode* head) 5 { 6 if(!head || !head->next || !head->next->next) return; 7 8 // 第一步:找到中间节点 9 ListNode* slow = head; 10 ListNode* fast = head; 11 while(fast && fast->next) 12 { 13 slow = slow->next; 14 fast = fast->next->next; 15 } 16 17 // 第二步:翻转第二段链表 18 ListNode* secondHead = slow->next; 19 slow->next = nullptr; // 第一段链表的尾节点 20 ListNode* p = secondHead->next; 21 secondHead->next = nullptr; // 第二段链表的尾节点 22 ListNode* q; 23 while(p) 24 { 25 q = p->next; 26 p->next = secondHead; 27 secondHead = p; 28 p = q; 29 } 30 31 // 第三步:交叉合并两个子链表 32 ListNode* h1 = head; 33 ListNode* h2 = secondHead; 34 while(h1 && h2) 35 { 36 ListNode* h1Post = h1->next; 37 ListNode* h2Post = h2->next; 38 h1->next = h2; 39 h2->next = h1Post; 40 h1 = h1Post; 41 h2 = h2Post; 42 } 43 } 44};
[LeetCode] Partition List 分割链表,小于 \(x\) 的排前面,不小于 \(x\) 的排后面。Hint:建立两个新的链表,一个链表连接小于 \(x\) 的节点,另一个链表连接其他节点,最后把这两个链表串起来。
https://leetcode.com/problems/partition-list/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 ListNode* partition(ListNode* head, int x) 5 { 6 if(!head || !head->next) return head; 7 ListNode* h1 = new ListNode(x); 8 ListNode* p1 = h1; 9 ListNode* h2 = new ListNode(x); 10 ListNode* p2 = h2; 11 ListNode* q = head; 12 while(q) 13 { 14 if(q->val < x) 15 { 16 p1->next = q; 17 p1 = p1->next; 18 } 19 else 20 { 21 p2->next = q; 22 p2 = p2->next; 23 } 24 q = q->next; 25 } 26 p2->next = nullptr; // 尾节点置为空 27 p1->next = h2->next; 28 head = h1->next; 29 delete h1; 30 delete h2; 31 return head; 32 } 33};
4.5. [LeetCode] Sort Colors 三颜色排序
https://blog.csdn.net/princexiexiaofeng/article/details/79645511
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 void sortColors(vector<int>& nums) 5 { 6 if(nums.size() <= 1) return; 7 int left = 0; 8 int right = nums.size() - 1; 9 for(int mid = left; mid <= right; ++mid) 10 { 11 while(nums[mid] == 2 && mid < right) 12 { 13 swap(nums[mid], nums[right]); 14 right--; 15 } 16 while(nums[mid] == 0 && mid > left) 17 { 18 swap(nums[mid], nums[left]); 19 left++; 20 } 21 } 22 } 23}; 24 25// 注:要先判断 nums[mid] == 2,再判断 nums[mid] == 0,否则会出错,如 [1,2,0] 26// 因为 2 是往后交换,0 是往前交换;2 交换得到的可能是 0,但可以保证 0 交换得到的不会是 2,因为 2 在 0 之前被处理了 27// 如果判断顺序反过来,2 交换得到的 0 不会被处理
4.6. 找到数组第 \(k\) 大的数
https://leetcode.com/problems/kth-largest-element-in-an-array/
排序。时间复杂度 \(\mathcal{O}(N \log N)\) 。
伪排序:\(k\) 次遍历数组,每次从剩余数字中找一个最大值。时间复杂度 \(\mathcal{O}(kN)\) 。
借助大小为 \(k\) 的最小堆。时间复杂度 \(\mathcal{O}(N \log k)\) 。
快排思想。时间复杂度 \(\mathcal{O}(N)\) 。
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int partition(vector<int>& nums, int i, int j) 5 { 6 int pivot = nums[i]; 7 int l = i+1; 8 int r = j; 9 while(true) 10 { 11 while(l <= j && nums[l] < pivot) l++; 12 while(r > i && nums[r] > pivot) r--; 13 if(l >= r) break; 14 swap(nums[l], nums[r]); 15 l++; 16 r--; 17 } 18 swap(nums[i], nums[r]); 19 return r; 20 } 21 // partition 可用如下更简洁的形式 22 int partition(vector<int>& nums, int i, int j) 23 { 24 int pivot = nums[i]; 25 int l = i; 26 int r = j+1; 27 while(true) 28 { 29 while(nums[++l] < pivot && l < j); 30 while(nums[--r] > pivot); 31 if(l >= r) break; 32 swap(nums[l], nums[r]); 33 } 34 swap(nums[i], nums[r]); 35 return r; 36 } 37 38 // T(n) = T(n/2) + O(n),时间复杂度 O(n) 39 int quicksort(vector<int>& nums, int a, int b, int k) 40 { 41 int p = partition(nums, a, b); 42 if(b - p + 1 == k) return p; 43 if(b - p + 1 < k) return quicksort(nums, a, p-1, k - (b - p + 1)); 44 else return quicksort(nums, p+1, b, k); 45 } 46 int findKthLargest(vector<int>& nums, int k) 47 { 48 int k_id = quicksort(nums, 0, nums.size()-1, k); 49 return nums[k_id]; 50 } 51};
4.7. [LeetCode] Best Time to Buy and Sell Stock 买卖股票的最佳时间
最多一次交易
http://www.cnblogs.com/grandyang/p/4280131.html
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int maxProfit(vector<int>& prices) 5 { 6 if(prices.size() <= 1) return 0; 7 int profit = 0; 8 int minimal = INT_MAX; 9 for(int p: prices) 10 { 11 profit = max(profit, p - minimal); 12 minimal = min(p, minimal); 13 } 14 return profit; 15 } 16};
无限次交易
http://www.cnblogs.com/grandyang/p/4280803.html
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int maxProfit(vector<int>& prices) 5 { 6 if(prices.size() <= 1) return 0; 7 int profit = 0; 8 for(int i = 0; i < prices.size() - 1; ++i) profit += max(prices[i+1] - prices[i], 0); 9 return profit; 10 } 11};
最多两次交易
最多k次交易
交易冷却
https://www.cnblogs.com/grandyang/p/4997417.html
\(\color{darkgreen}{Code}\)
1// buy[i] = max(buy[i-1], cool[i-1] - prices[i]) 2// sell[i] = max(sell[i-1], buy[i-1] + prices[i]) 3// cool[i] = sell[i-1] => buy[i] = max(buy[i-1], sell[i-2] - prices[i]) 4 5class Solution 6{ 7public: 8 int maxProfit(vector<int>& prices) 9 { 10 if(prices.size() <= 1) return 0; 11 int pre_sell = 0; 12 int sell = 0; 13 int pre_buy = INT_MIN; 14 int buy = 0; 15 for(int p : prices) 16 { 17 buy = max(pre_buy, pre_sell - p); // 这里的 pre_sell 其实是 pre_pre_sell 18 pre_sell = sell; // pre_sell 更新晚一步 19 sell = max(pre_sell, pre_buy + p); 20 pre_buy = buy; 21 } 22 return sell; 23 } 24};
4.8. [LeetCode] Partition Equal Subset Sum 数组分成两个子集,和相等
https://leetcode.com/problems/partition-equal-subset-sum/
\(\color{darkgreen}{Code}\)
1class Solution(object): 2 def backtrack(self, nums, sum_nums, sum_current, i): ## self 3 if sum_current == sum_nums / 2: 4 return True 5 if i == len(nums): 6 return False 7 if self.backtrack(nums, sum_nums, sum_current+nums[i], i+1): ## self 8 return True 9 if self.backtrack(nums, sum_nums, sum_current, i+1): ## self 10 return True 11 return False 12 13 def canPartition(self, nums): 14 """ 15 :type nums: List[int] 16 :rtype: bool 17 """ 18 if len(nums) <= 1: 19 return False 20 sum_nums = sum(nums) 21 if sum_nums % 2: 22 return False 23 return self.backtrack(nums, sum_nums, 0, 0) ## self
4.9. [LeetCode] Find All Anagrams in a String 统计变位词出现的位置
Hint:采用滑动窗口和 计数器 进行比较。
https://leetcode.com/problems/find-all-anagrams-in-a-string/
\(\color{darkgreen}{Code}\)
1/* https://leetcode.com/problems/find-all-anagrams-in-a-string/discuss/92027/C%2B%2B-O(n)-sliding-window-concise-solution-with-explanation */ 2 3class Solution 4{ 5public: 6 vector<int> findAnagrams(string s, string p) 7 { 8 vector<int> vec; 9 if(s.size()<p.size() || (s.empty() && p.empty())) return vec; 10 vector<int> p_counter(26, 0), s_counter(26, 0); 11 for(int i = 0; i < p.size(); ++i) 12 { 13 ++p_counter[p[i]-'a']; 14 ++s_counter[s[i]-'a']; 15 } 16 if(p_counter == s_counter) vec.push_back(0); 17 for(int i = p.size(); i < s.size(); ++i) 18 { 19 --s_counter[s[i-p.size()]-'a']; 20 ++s_counter[s[i]-'a']; 21 if(s_counter == p_counter) vec.push_back(i-p.size()+1); 22 } 23 return vec; 24 } 25};
4.10. 寻找重复数
数值范围为 \(\{ 1,2,3,...,n \}\) ,有的出现 2 次,有的出现 1 次。Hint:把数组元素的值当做下标,由于元素存在重复,因此必然会 重复多次访问同一个位置 。 从另一个角度讲,访问序列中存在“环”。排序的时间复杂度高,哈希不满足空间复杂度为 \(\mathcal{O}(1)\) 的要求。
[LeetCode] Find the Duplicate Number 找到一个重复数字(共有 \(n+1\) 个数)。
https://leetcode.com/problems/find-the-duplicate-number/
http://www.cnblogs.com/grandyang/p/4843654.html
\(\color{darkgreen}{Code}\)
1// 解法一:快慢指针,寻找某个“环”的入口 2class Solution 3{ 4public: 5 int findDuplicate(vector<int>& nums) 6 { 7 int slow = 0, fast = 0, t = 0; 8 while (true) 9 { 10 slow = nums[slow]; // 注意,这里下标没有减 1 11 fast = nums[nums[fast]]; 12 if (slow == fast) break; 13 } 14 while (true) 15 { 16 slow = nums[slow]; 17 t = nums[t]; 18 if (slow == t) break; 19 } 20 return slow; 21 } 22};
1// 解法二:不断交换位置,找到第一个重复访问的元素 2class Solution 3{ 4public: 5 int findDuplicate(vector<int>& nums) 6 { 7 int duplicate = -1; 8 for(int k = 0; k < nums.size(); ++k) 9 { 10 while(nums[k]-1 != k) 11 { 12 if(nums[k] == nums[nums[k]-1]) 13 { 14 duplicate = nums[k]; 15 break; 16 } 17 swap(nums[k], nums[nums[k]-1]); 18 // 一次交换之后,下标为 nums[k]-1 的元素就等于 nums[k] 了。 19 } 20 if(duplicate != -1) break; 21 } 22 return duplicate; 23 } 24};
[LeetCode] Find All Duplicates in an Array 找到所有重复数字(共有 \(n\) 个数)。
https://leetcode.com/problems/find-all-duplicates-in-an-array/
http://www.cnblogs.com/grandyang/p/6209746.html
\(\color{darkgreen}{Code}\)
1// 解法一:将访问过的元素置为相反数(负数),如果下次访问到一个负数,说明这个元素被重复访问 2class Solution 3{ 4public: 5 vector<int> findDuplicates(vector<int>& nums) 6 { 7 vector<int> res; 8 for (int i = 0; i < nums.size(); ++i) 9 { 10 int idx = abs(nums[i]) - 1; 11 if (nums[idx] < 0) res.push_back(idx + 1); 12 else nums[idx] = -nums[idx]; 13 } 14 return res; 15 } 16};
1// 解法二:不断交换位置使得 i == nums[i]-1 2class Solution 3{ 4public: 5 vector<int> findDuplicates(vector<int>& nums) 6 { 7 vector<int> duplicate; 8 for(int i = 0; i < nums.size(); ++i) 9 { 10 while(nums[nums[i] - 1] != nums[i]) swap(nums[i], nums[nums[i] - 1]); 11 } 12 for(int i = 0; i < nums.size(); ++i) 13 { 14 if(i != nums[i] - 1) duplicate.push_back(nums[i]); 15 } 16 return duplicate; 17 } 18};
[LeetCode] First Missing Positive 找到第一个消失的正整数。Hint:假设数组长度为 \(n\) ,则第一个消失的正整数所在区间是 \([1, n+1]\) ,注意:输入数组中可能存在负数和0。延伸:找到第一个大于 \(K\) 的正整数。Hint:可知目标数所在区间是 \([K+1, K+n+1]\) ;先删除数组中不在该区间的整数;其余数都减 \(K\) ,范围变成 \([1, n+1]\) ,后续解法同上。
https://leetcode.com/problems/first-missing-positive/
\(\color{darkgreen}{Code}\)
1// 解法一:将访问过的元素置为相反数(负数) 2class Solution 3{ 4public: 5 int firstMissingPositive(vector<int>& nums) 6 { 7 int n = nums.size(); 8 for(auto& m: nums) if(m <= 0) m = n + 1; // 先处理非正整数,全部置为 n+1 9 for(auto& m: nums) 10 { 11 int i = abs(m) - 1; 12 if(i < n && nums[i] > 0) nums[i] = -nums[i]; 13 } 14 for(int i = 0; i < n; ++i) 15 { 16 if(nums[i] > 0) return i+1; 17 } 18 return n + 1; 19 } 20};
1// 解法二:不断交换位置使得 i == nums[i]-1 2class Solution 3{ 4public: 5 int firstMissingPositive(vector<int>& nums) 6 { 7 int n = nums.size(); 8 for(auto& m: nums) 9 { 10 while(m > 0 && m <= n && m != nums[m-1]) swap(m, nums[m-1]); 11 } 12 for(int i = 0; i < n; ++i) 13 { 14 if(nums[i] != i + 1) return i+1; 15 } 16 return n+1; 17 } 18};
4.11. [LeetCode] Spiral Matrix 环形打印矩阵
https://leetcode.com/problems/spiral-matrix/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 void tranverseMatrixAccorindTo4Directions(vector<vector<int>> &matrix, const unsigned int row, const unsigned int col, int start, vector<int>& vec) 5 { 6 // 特别注意 7 // 如果把 start, endX, endY, k 声明为 unsigned int 类型,在减到 0 的时候可能会死循环,因为 unsigned int 类型不会小于 0。 8 9 int endX = row - 1 - start; 10 int endY = col - 1 - start; 11 12 // 1 向右 13 for(int k = start; k <= endY; ++k) vec.push_back(matrix[start][k]); 14 15 // 2 向下 16 for(int k = start+1; k <= endX; ++k) vec.push_back(matrix[k][endY]); 17 18 // 3 向左:要求至少存在两行(不加判断会重复扫描同一行) 19 if(endX > start) for(int k = endY-1; k >= start; --k) vec.push_back(matrix[endX][k]); 20 21 // 4 向上:要求至少存在两列(不加判断会重复扫描同一列) 22 if(endY > start) for(int k = endX-1; k > start; --k) vec.push_back(matrix[k][start]); 23 24 } 25 vector<int> spiralOrder(vector<vector<int>>& matrix) 26 { 27 vector<int> vec; 28 unsigned int row = matrix.size(); 29 if(row == 0) return vec; 30 unsigned int col = matrix[0].size(); 31 if(col == 0) return vec; 32 int start = 0; 33 // 循环中止条件:圈数判断( (start,start) 是每一圈的入口坐标) 34 while(start * 2 < row && start * 2 < col) 35 { 36 tranverseMatrixAccorindTo4Directions(matrix, row, col, start, vec); 37 ++start; 38 } 39 return vec; 40 } 41};
4.12. [LeetCode] Longest Consecutive Sequence 最长连续序列
Hint:方法一,排序;方法二,对于每个元素 \(n\) ,搜索 \(n+1\) 是否在数组中,使用 hash/set 可以获得 \(\mathcal{O}(1)\) 的查找复杂度。
https://leetcode.com/problems/longest-consecutive-sequence/
\(\color{darkgreen}{Code}\)
1class Solution(object): 2 def longestConsecutive(self, nums): 3 """ 4 :type nums: List[int] 5 :rtype: int 6 """ 7 8 longest = 0 9 num_set = set(nums) 10 11 for num in nums: 12 if num-1 not in num_set: 13 current_long = 1 14 while num + 1 in num_set: 15 current_long += 1 16 num += 1 17 longest = max(longest, current_long) 18 19 num_set.clear() 20 21 return longest
4.13. 跳跃的蚂蚱
从 0 点出发,往正或负向跳跃,第一次跳跃一个单位,之后每次跳跃距离比上一次多一个单位,跳跃多少次可到到达坐标 \(x\) 处? Hint:走 \(n\) 步之后能到达的坐标是一个公差为 2 的等差数列(如 \(n=2\) ,可到达 \(\{-3,-1,1,3\}\) )。 只需找到最小的 \(n\) 使得
是非负偶数。跳到 \(x\) 和跳到 \(-x\) 的次数相同, 因此只考虑 \(x\) 为正的情况。
https://www.zhihu.com/question/50790221
\(\color{darkgreen}{Code}\)
1// 作者:Rukia 2// 链接:https://www.zhihu.com/question/50790221/answer/125213696 3 4int minStep(int x) 5{ 6 if (x == 0) return 0; 7 if (x < 0) x = -x; 8 int n = sqrt(2*x); // 快速找到一个接近答案的 n 9 while ((n+1)*n/2-x & 1 || (n+1)*n/2 < x) // & 的优先级低 10 ++n; 11 return n; 12}
4.14. 求 \(n\) 的阶乘末尾有多少个 \(0\)
Hint:1 个 \(5\) 和1个 \(2\) 搭配可以得到 1 个 \(0\) ;\(2\) 的个数比 \(5\) 多, 因此只关心 \(5\) 的个数;\(25\) 包含 2 个 \(5\) ,\(125\) 包含 3 个 \(5\) …。
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int trailingZeroes(int n) 5 { 6 if(n <= 0) return 0; 7 int res = 0; 8 while(n) 9 { 10 res += n / 5; 11 n /= 5; 12 } 13 return res; 14 } 15};
4.15. 求一个整数的二进制表示中 \(1\) 的个数
Hint:移位操作;负数可能造成死循环。
C++中,指定移位次数大于或等于对象类型的比特数(如 int 型的 32 位),或者对负数进行左移操作,结果都是未定义的 。
例如:n >> 32
是未定义的,但是允许 n >>= 1
执行无限次,这是安全的。
\(\color{darkgreen}{Code}\)
1// 方法一:不断右移 n。如果 n 是负数,需要保持最高位为 1,不断移位后这个数字会变成 0xFFFFFFFF 而陷入死循环。 2int numberOf1(int n) 3{ 4 int cnt = 0; 5 while(n) 6 { 7 if(n & 1) cnt++; 8 n >>= 1; 9 } 10 return cnt; 11}1// 方法二:n 不动,左移一个比较子。 2int numberOf1(int n) 3{ 4 int cnt = 0; 5 unsigned int flag = 1; 6 while(flag) // 连续左移32次之后为0 7 { 8 if(n & flag) cnt++; 9 flag <<= 1; 10 } 11 return cnt; 12}1// 方法三:把一个整数减 1,再和原整数做逻辑与运算,会把该整数最右边的一个 1 变成 0。 2int numberOf1(int n) 3{ 4 int cnt = 0; 5 while(n) 6 { 7 cnt++; 8 n = (n - 1) & n; 9 } 10 return cnt; 11}
4.16. [LeetCode] Subarray Sum Equals K 子数组和为 \(K\)
Hint:依次求数组的前 \(n\) 项和 \(s[n]\) ,\(n \in [0, N]\) (注意:0 也在内), 将和作为哈希表的 key,和的值出现次数作为 value;如果存在 \(s[i]−s[j]=K \ (i \gt j)\) ,则 \(s[i]\) 和 \(s[j]\) 都应该在哈希表中。
https://leetcode.com/problems/subarray-sum-equals-k/
\(\color{darkgreen}{Code}\)
1## https://leetcode.com/problems/subarray-sum-equals-k/solution/ : Approach #4 Using hashmap 2 3from collections import defaultdict 4class Solution(object): 5 def subarraySum(self, nums, k): 6 """ 7 :type nums: List[int] 8 :type k: int 9 :rtype: int 10 """ 11 12 if len(nums) == 0: 13 return 0 14 15 N = len(nums) 16 17 sum_to_num = defaultdict(int) 18 sum_to_num[0] = 1 ## 前 0 项和 19 20 cnt = 0 21 tmp_sum = 0 22 for n in nums: 23 tmp_sum += n 24 diff = tmp_sum - k 25 cnt += sum_to_num[diff] 26 sum_to_num[tmp_sum] += 1 27 28 return cnt
延伸:和不小于 \(K\) 的最短子数组。Hint:滑动窗口 + 单调 deque,时间复杂度和空间复杂度都是 \(\mathcal{O}(N)\) 。
https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k
\(\color{darkgreen}{Code}\)
1from collections import deque 2class Solution: 3 def shortestSubarray(self, nums: List[int], k: int) -> int: 4 ans = float('inf') 5 s = 0 6 dq = deque() ## (s, i) 7 for i in range(len(nums)): 8 s += nums[i] 9 if s >= k: 10 ans = min(ans, i + 1) 11 ## 以 i 为窗口右边界,收拢左边界 12 while dq and s - dq[0][0] >= k: 13 ans = min(ans, i - dq[0][1]) 14 dq.popleft() 15 ## 保持 dq 是单调增队列 16 ## dq[-1][0] > s 说明数组在区间 [dq[-1][0] + 1, i] 的和是负数 17 ## 因此 dq[-1][0] + 1 不可能是右边界在 i 之后的任何最短子数组的左边界 18 while dq and dq[-1][0] > s: 19 dq.pop() 20 dq.append((s, i)) 21 return ans if ans != float('inf') else -1
4.17. 使用位运算进行加法运算
Hint:原位加法运算等效为 ^
运算,进位等效为 &
和 移位
的复合。 注:C++不允许对负数进行左移运算。
https://leetcode.com/problems/sum-of-two-integers/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int getSum(int a, int b) 5 { 6 int sum, carry; 7 do 8 { 9 sum = (a ^ b); 10 carry = (a & b & INT_MAX) << 1; // & INT_MAX 操作保证移位前的数是正数,否则结果是未定义的。 11 a = sum; 12 b = carry; 13 }while(b != 0); 14 return a; 15 } 16};1from numpy import int32 2 3class Solution(object): 4 def getSum(self, a, b): 5 """ 6 :type a: int 7 :type b: int 8 :rtype: int 9 """ 10 a, b = int32(a), int32(b) 11 12 while True: 13 a, b = a ^ b, (a & b) << 1 14 print a, b 15 if b == 0: 16 break 17 18 return int(a) 19 20## 注意,这里并没有与 0x7fffffff 做 & 运算 21## 假设 a & b = -16,-16 & 0x7fffffff = 2147483632 22## C++ 中,对 2147483632 左移1位使得最高位符号位为 1,得到 -32 23## python中,2147483632的符号位为 0,继续左移1位,会直接做大整数运算,得到 4294967264L,导致不能得到正确结果 24## python 中,使用type()查看数据类型时发现,有时候系统会把 int32 转化为 int64,或者 int64 转为 int32,疑惑中。。。
4.18. [LeetCode] Longest Substring with At Least K Repeating Characters 包含重复字符的最长子串
Hint:由于该字符串只包含小写字母,因此直接使用长度为26的静态数组来统计字符频率更为简洁高效,不需要使用map。
https://leetcode.com/problems/longest-substring-with-at-least-k-repeating-characters/
\(\color{darkgreen}{Code}\)
1// https://www.cnblogs.com/grandyang/p/5852352.html 2// 使用一个int型(32位)的mask,指示各字符频率是否到达k 3// 以每一个字符作为起点,往后统计。时间复杂度 O(N^2) 4// mask第 idx 位从 0 -> 1,表示对应字符出现了,但是未达到k次 5// mask第 idx 位从 1 -> 0,表示对应字符已经出现了k次 6// mask变成 0,表示这段子串满足要求 7 8class Solution 9{ 10public: 11 int longestSubstring(string s, int k) 12 { 13 int ans = 0; 14 int start = 0; 15 while(start + k <= s.size()) 16 { 17 int hash[26] = {0}; 18 int mask = 0; 19 int next_start = start + 1; 20 for(int end = start; end < s.size(); ++end) 21 { 22 int idx = s[end] - 'a'; 23 ++hash[idx]; 24 if(hash[idx] < k) mask |= (1 << idx); // 0 -> 1 25 else mask &= ~(1 << idx); // 1 -> 0 26 if(mask == 0) 27 { 28 ans = max(ans, end - start + 1); 29 next_start = end + 1; 30 } 31 } 32 start = next_start; 33 } 34 return ans; 35 } 36};
4.19. 几个数的和
[LeetCode] Two Sum 两数之和为目标值。Hint:哈希,时间复杂度 \(\mathcal{O}(N)\) 。
https://leetcode.com/problems/two-sum/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 vector<int> twoSum(vector<int>& nums, int target) 5 { 6 vector<int> res; 7 map<int, int> hash; 8 for(size_t k = 0; k < nums.size(); k++) hash[nums[k]] = k; 9 for(size_t k = 0; k < nums.size(); k++) 10 { 11 if(hash.find(target - nums[k]) != hash.end()) 12 { 13 if(hash[target - nums[k]] > k) // 避免重复统计同一对 14 { 15 res.push_back(k); 16 res.push_back(hash[target - nums[k]]); 17 } 18 } 19 } 20 return res; 21 } 22};
[LeetCode] 3Sum 3 个数之和为 0。Hint:先排序;双指针;时间复杂度 \(\mathcal{O}(N^2)\) 。
https://leetcode.com/problems/3sum/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 vector<vector<int>> threeSum(vector<int>& nums) 5 { 6 vector<vector<int>> result; 7 if(nums.size()<3) return result; 8 sort(nums.begin(), nums.end()); 9 unsigned int n = nums.size(); 10 int target = 0; 11 for(unsigned int i = 0; i + 2 < n; ++i) 12 { 13 if(i > 0 && nums[i] == nums[i-1]) continue; // 忽略重复值 14 if(nums[i] + nums[i+1] + nums[i+2] > target) break; // 下界 15 if(nums[i] + nums[n-2] + nums[n-1] < target) continue; // 上界 16 unsigned int left = i + 1; 17 unsigned int right = n - 1; 18 while(left < right) 19 { 20 if(nums[i]+nums[left]+nums[right] == target) 21 { 22 result.push_back(vector<int>{nums[i], nums[left], nums[right]}); 23 // 找到之后,两个指针都需要移动,并忽略重复值 24 do{++left;}while(nums[left] == nums[left-1] && left < right); 25 do{--right;}while(nums[right] == nums[right+1] && left < right); 26 } 27 else if(nums[i]+nums[left]+nums[right] < target) 28 { 29 do{++left;}while(nums[left] == nums[left-1] && left < right); 30 } 31 else 32 { 33 do{--right;}while(nums[right] == nums[right+1] && left < right); 34 } 35 } 36 } 37 return result; 38 } 39};
[LeetCode] 4Sum 4 个数之和为目标值。Hint:先排序;双指针;时间复杂度 \(\mathcal{O}(N^3)\) 。
https://leetcode.com/problems/4sum/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 vector<vector<int>> fourSum(vector<int>& nums, int target) 5 { 6 vector<vector<int>> quad; 7 if(nums.size() < 4) return quad; 8 unsigned int n = nums.size(); 9 sort(nums.begin(), nums.end()); 10 for(unsigned int i = 0; i + 3 < n; ++i) 11 { 12 if(i > 0 && nums[i] == nums[i-1]) continue; // 忽略重复值 13 if(nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target) break; // 下界 14 if(nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target) continue; // 上界 15 for(unsigned int j = i + 1; j + 2 < n; ++j) 16 { 17 if(j > i + 1 && nums[j] == nums[j-1]) continue; // 忽略重复值 18 if(nums[i] + nums[j] + nums[j+1] + nums[j+2] > target) break; // 下界 19 if(nums[i] + nums[j] + nums[n-2] + nums[n-1] < target) continue; // 上界 20 unsigned int left = j + 1; 21 unsigned int right = n - 1; 22 while(left < right) 23 { 24 int sum = nums[i] + nums[j] + nums[left] + nums[right]; 25 if(sum == target) 26 { 27 quad.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]}); 28 // 找到之后,两个指针都需要移动,并忽略重复值 29 do 30 { 31 ++left; 32 } 33 while(nums[left] == nums[left-1] && left < right); 34 do 35 { 36 --right; 37 } 38 while(nums[right] == nums[right+1] && left < right); 39 } 40 else if(sum < target) 41 { 42 do 43 { 44 ++left; 45 } 46 while(nums[left] == nums[left-1] && left < right); 47 } 48 else 49 { 50 do 51 { 52 --right; 53 } 54 while(nums[right] == nums[right+1] && left < right); 55 } 56 } 57 } 58 } 59 return quad; 60 } 61};
[LeetCode] 4Sum II 4 个数和为 0 的组合数。Hint:两两之和存入哈希表,时间复杂度和空间复杂度 \(\mathcal{O}(N^2)\) 。
https://leetcode.com/problems/4sum-ii/
\(\color{darkgreen}{Code}\)
1def fourSumCount(self, A, B, C, D): 2 AB = collections.Counter(a+b for a in A for b in B) 3 return sum(AB[-c-d] for c in C for d in D)
4.20. [LeetCode] Maximum Product Subarray 求连续子数组的最大乘积
Hint:数组中存在负数,负负得正,因此相比于连续子数组最大和问题,不仅需要记录以每个元素结尾的连续乘积的最大值,还需要记录最小值。
https://leetcode.com/problems/maximum-product-subarray/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int maxProduct(vector<int>& nums) 5 { 6 int pre_min = nums[0]; 7 int pre_max = nums[0]; 8 int curr_min = nums[0]; 9 int curr_max = nums[0]; 10 int maxproduct = nums[0]; 11 for(int k = 1; k < nums.size(); ++k) 12 { 13 curr_min = min(nums[k], min(pre_min*nums[k], pre_max*nums[k])); 14 curr_max = max(nums[k], max(pre_min*nums[k], pre_max*nums[k])); 15 maxproduct = max(maxproduct, curr_max); 16 pre_min = curr_min; 17 pre_max = curr_max; 18 } 19 return maxproduct; 20 } 21};
4.21. 统计 1 的数目
给定一个十进制整数 \(N\) ,统计从 \(1\) 到 \(N\) 所有的整数各位出现的 \(1\) 的数目。Hint: \(1\) 的数目 = 个位出现 \(1\) 的数目 + 十位出现 \(1\) 的数目 + 百位出现 \(1\) 的数目 + ……。以百位为例:如果百位数字为0,则百位出现1的次数只由更高位决定,如12013,次数为12 * 100;如果百位数字为1,则百位出现1的次数由更高位和更低位同时决定,如12113,次数为12 * 100 + (13 + 1);如果百位数字大于1,则百位出现1的次数只由更高位决定,如12213,次数为(12 + 1) * 100。时间复杂度 \(\mathcal{O}(\log_{10}(N))\) 。
https://leetcode.com/problems/number-of-digit-one
http://www.cnblogs.com/jy02414216/archive/2011/03/09/1977724.html
\(\color{darkgreen}{Code}\)
1typedef unsigned long long Ull; 2class Solution 3{ 4public: 5 int countDigitOne(int n) 6 { 7 Ull factor = 1; 8 Ull low, curr, high; 9 Ull res = 0; 10 while(n / factor) 11 { 12 low = n % factor; 13 curr = (n / factor) % 10; 14 high = n / (factor * 10); 15 switch(curr) 16 { 17 case 0: 18 res += high * factor; 19 break; 20 case 1: 21 res += high * factor + low + 1; 22 break; 23 default: 24 res += (high + 1) * factor; 25 } 26 factor *= 10; 27 } 28 return res; 29 } 30};
4.22. 数组循环移位
循环右移 \(K\) 位,时间复杂度 \(\mathcal{O}(N)\) 。Hint:三次翻转。
\(\color{darkgreen}{Code}\)
1void reverse(int *arr, int begin, int end) 2{ 3 for(; begin < end; begin++, end--) swap(arr[begin], arr[end]); 4} 5 6void right_shift(int *arr, int N, int K) 7{ 8 K %= N; 9 reverse(arr, 0, N-K-1); 10 reverse(arr, N-K, N-1); 11 reverse(arr, 0, N-1); 12}
4.23. [LeetCode] Divide Two Integers 整数除法
Hint:先取绝对值,做正整数之间的除法;防止溢出。
https://leetcode.com/problems/divide-two-integers/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int divide(int dividend, int divisor) 5 { 6 if(dividend == INT_MIN && divisor == -1) return INT_MAX; // 越界则输出最大值 7 if(dividend == INT_MIN && divisor == 1) return INT_MIN; 8 if(dividend == INT_MIN && divisor == INT_MIN) return 1; // 枚举分子为最小整数时的情形 9 if(divisor == INT_MIN) return 0; 10 11 bool sign = (dividend>0) ^ (divisor>0) ? false : true; 12 13 int res = 0; 14 15 bool max_flow = false; 16 if(dividend == INT_MIN) 17 { 18 dividend = abs(1 + INT_MIN); // 防止取绝对值之后溢出 19 max_flow = true; 20 } 21 else dividend = abs(dividend); 22 divisor = abs(divisor); 23 24 while(dividend >= divisor) 25 { 26 int diff = divisor; 27 int n = 1; 28 while(diff <= (dividend >> 1)) 29 { 30 diff <<= 1; 31 n <<= 1; 32 } 33 dividend -= diff; 34 res += n; 35 } 36 if(max_flow && dividend == divisor-1) res += 1; 37 38 return sign? res : -res; 39 } 40};
4.24. [LeetCode] Fraction to Recurring Decimal 循环小数
Hint:小数除法:余数乘以10再求余;如果余数出现重复,则说明是循环小数。
https://leetcode.com/problems/fraction-to-recurring-decimal/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 string fractionToDecimal(int numerator, int denominator) 5 { 6 if(numerator == 0 || denominator == 0) return "0"; 7 int sign_num = numerator > 0? 1:-1; 8 int sign_den = denominator > 0? 1:-1; 9 10 long long num = abs((long long)numerator); 11 long long den = abs((long long)denominator); 12 13 long long integer = num / den; 14 long long rem = num % den; 15 16 string int_part = to_string(integer); 17 if(rem) int_part += "."; 18 19 string frac_part = ""; 20 unordered_map<long long, int> mp; 21 int idx = 0; 22 23 while(rem) 24 { 25 if(mp.find(rem) != mp.end()) // 余数重复 26 { 27 frac_part.insert(mp[rem], "("); 28 frac_part += ")"; 29 break; 30 } 31 mp[rem] = idx++; 32 frac_part += to_string((10*rem) / den); 33 rem = (10*rem) % den; 34 } 35 36 string res = ""; 37 if(sign_num * sign_den < 0) res += "-"; 38 res += int_part + frac_part; 39 return res; 40 } 41};
4.25. 旋转数组查找
Hint:采用二分查找的思路。
二分查找
\(\color{darkgreen}{Code}\)
1// preliminary: binary search,时间复杂度 O(logN) 2template<class T> 3int binarySearch(T* arr, int n, const T& target) 4{ 5 if(arr == nullptr || n <= 0) return -1; 6 int low = 0; 7 int high = n - 1; // 查找区间: [0, n) 8 while(low <= high) 9 { 10 int mid = low + (high - low) / 2; // mid = (low + high)/2 可能导致溢出 11 if(arr[mid] == target) return mid; 12 if(arr[mid] < target) low = mid + 1; 13 else high = mid - 1; 14 } 15 return -1; 16}
1// 浮点数二分,不存在区间取整,要求达到某个精度 2 3// 例:在区间 [low, high] 二分查找开方数 4 5#define eps 1e-5 6 7bool judge(double mid, double x) 8{ 9 return mid >= x / mid; 10} 11 12double search(double low, double high, double x) 13{ 14 while(high - low > eps) 15 { 16 double mid = low + (high - low) / 2; 17 if(judge(mid, x)) high = mid; 18 else low = mid; 19 } 20 return low + (high - low) / 2; // 此时 low 和 high 比较接近,取它们的均值作为最终结果 21}
1## 返回区间 [first, last) 内第一个不小于 target 的位置 2## 如果所有数都小于 target,则返回 last 3def lower_bound(a, first, last, target): 4 if first > last: 5 return None 6 while first < last: ## [first, last)不为空 7 mid = first + (last - first) // 2 8 if a[mid] < target: 9 first = mid + 1 10 else: 11 last = mid 12 return first 13 ## 返回 last 也行,因为 [first, last) 为空的时候它们相等
查找旋转数组最小值(含重复元素)
https://leetcode.com/problems/find-minimum-in-rotated-sorted-array-ii/
\(\color{darkgreen}{Code}\)
1// 方法一 2// 第一个指针总指向前面递增数组的元素 3// 第二个指针总指向后面递增数组的元素 4// 最终两个指针指向相邻元素:第一个指针指向前面递增数组的最后一个元素,第二个指针指向后面递增数组的第一个元素(也就是最小元素) 5template<class T> 6int findRotateMin(T* arr, int n) 7{ 8 if(arr == nullptr || n <= 0) return -1; 9 int low = 0; 10 int high = n - 1; 11 while(arr[low] >= arr[high]) 12 { 13 if(high - 1 == low) return high; 14 15 int mid = low + (high - low) / 2; 16 17 // 如果这三个元素相等,则在区间 [low, high] 内顺序查找 18 if(arr[low] == arr[mid] && arr[mid] == arr[high]) return (min_element(arr + low, arr + high + 1) - arr); 19 20 if(arr[mid] <= arr[high]) high = mid; 21 else low = mid; 22 } 23 // 如果数组本身是有序的,即 arr[0] < arr[n-1],则第一个元素就是最小值 24 return 0; 25}
1# 方法二 2# 每次比较 nums[mid] 与 nums[high],如果两者相等,则 --high 3class Solution: 4 def findMin(self, nums: List[int]) -> int: 5 n = len(nums) 6 low = 0 7 high = n - 1 8 while low < high: 9 mid = low + (high - low) // 2 10 if nums[mid] == nums[high]: 11 high -= 1 12 else: 13 if nums[mid] > nums[high]: 14 low = mid + 1 15 else: 16 high = mid 17 return nums[low]
在旋转数组查找目标值(无重复元素)
https://leetcode.com/problems/search-in-rotated-sorted-array/
\(\color{darkgreen}{Code}\)
1// 每次比较 nums[mid] 与 nums[high] 2class Solution 3{ 4public: 5 int search(vector<int>& nums, int target) 6 { 7 int n = nums.size(); 8 if(n == 0) return -1; 9 int low = 0; 10 int high = n - 1; 11 while(low <= high) 12 { 13 int mid = low + (high - low) / 2; 14 if(nums[mid] == target) return mid; 15 16 if(nums[mid] < nums[high]) // 注:只有当 low == high,才会出现: mid == high,nums[mid] == nums[high] 17 { 18 if(nums[mid] < target && target <= nums[high]) low = mid + 1; 19 else high = mid - 1; 20 } 21 else 22 { 23 if(nums[mid] > target && target >= nums[low]) high = mid - 1; 24 else low = mid + 1; 25 } 26 } 27 return -1; 28 } 29};
在旋转数组查找目标值(含重复元素)
https://leetcode.com/problems/search-in-rotated-sorted-array-ii/
\(\color{darkgreen}{Code}\)
1// https://www.cnblogs.com/grandyang/p/4325840.html 2// 相对于上例,需要增加一个判断:如果 nums[mid] 与 nums[high] 相等,则 --high 3class Solution 4{ 5public: 6 bool search(vector<int>& nums, int target) 7 { 8 int n = nums.size(); 9 if(n == 0) return false; 10 int low = 0; 11 int high = n - 1; 12 while(low <= high) 13 { 14 int mid = low + (high - low) / 2; 15 if(nums[mid] == target) return true; 16 17 if(nums[mid] == nums[high]) --high; // 增加这个判断。注:只有当 low == high,才会出现: mid == high 。 18 19 else if(nums[mid] < nums[high]) 20 { 21 if(nums[mid] < target && target <= nums[high]) low = mid + 1; 22 else high = mid - 1; 23 } 24 else 25 { 26 if(nums[mid] > target && target >= nums[low]) high = mid - 1; 27 else low = mid + 1; 28 } 29 } 30 return false; 31 } 32};
在旋转数组查找目标值(含重复元素,若存在多个目标值需返回最小的下标)
https://leetcode.cn/problems/search-rotate-array-lcci
\(\color{darkgreen}{Code}\)
1class Solution: 2 def search(self, arr: List[int], target: int) -> int: 3 n = len(arr) 4 low, high = 0, n - 1 5 while low <= high: 6 mid = (low + high) // 2 7 ## 最左侧找到直接返回 8 if arr[low] == target: 9 return low 10 ## 这里将右边界移到 mid 11 if arr[mid] == target: 12 high = mid 13 elif arr[mid] == arr[high]: 14 high -= 1 15 elif arr[mid] < arr[high]: 16 if arr[mid] < target <= arr[high]: 17 low = mid + 1 18 else: 19 high = mid - 1 20 else: 21 if arr[low] <= target < arr[mid]: 22 high = mid - 1 23 else: 24 low = mid + 1 25 return -1
4.26. [LeetCode] Maximum Gap 最大间隔
Hint:方法一,普通排序,逐个比较;方法二,桶排序。将 \(n\) 个数放到 \(n+1\) 个桶中,最小值放第一个桶, 最大值放最后一个桶,每个桶的大小为 \(\frac{max-min}{n}\) 。根据鸽巢原理,至少存在一个桶为空。最大间隔必然出现在空桶两侧,且只与左侧桶的最大值、 右侧桶的最小值有关。(事实上,可以将 \(n\) 个数放到 \(n\) 个桶中,如果没有空桶,则刚好每个桶有且仅有一个数,最大间隔出现在相邻桶中;如果某个桶有2个数以上, 说明存在有空桶,最大间隔出现在非空的相邻桶中。总之,最大间隔不会出现在一个桶中。)
https://leetcode.com/problems/maximum-gap/
\(\color{darkgreen}{Code}\)
1// 建立 n 个桶 2class Solution 3{ 4public: 5 int maximumGap(vector<int>& nums) 6 { 7 size_t n = nums.size(); 8 if(n < 2) return 0; 9 10 int MIN = *min_element(nums.begin(), nums.end()); 11 int MAX = *max_element(nums.begin(), nums.end()); 12 if(MIN == MAX) return 0; 13 14 vector<vector<int>> bucket(n, vector<int>(2, 0)); // 大小为 n * 2 15 for(size_t k = 0; k < n; ++k) 16 { 17 bucket[k][0] = INT_MAX; 18 bucket[k][1] = INT_MIN; 19 } 20 21 22 double delta = (MAX - MIN) / double(n - 1); 23 for(size_t k = 0; k < n; ++k) 24 { 25 int idx = (nums[k] - MIN) / delta; 26 bucket[idx][0] = min(nums[k], bucket[idx][0]); 27 bucket[idx][1] = max(nums[k], bucket[idx][1]); 28 } 29 30 int gap = 0; 31 size_t pre = 0; 32 size_t curr = 1; 33 while(curr < bucket.size()) 34 { 35 if(bucket[curr][0] == INT_MAX && bucket[curr][1] == INT_MIN) curr++; // 空桶 36 else 37 { 38 if(curr - pre >= 1) 39 { 40 int pre_max = bucket[pre][1]; 41 int curr_min = bucket[curr][0]; 42 gap = max(gap, curr_min - pre_max); 43 } 44 pre = curr; 45 curr++; 46 } 47 } 48 return gap; 49 } 50};
4.27. 数组操作模拟大数乘法
Hint:从低位到高位,采用竖式计算,记录所有位的乘积,再将对应位的结果相加,最后进位。假设数组 \(a\) 和 \(b\) 从低位到高位存储了两个大数(可能存在小数点),则乘积为 \(\mathrm{ans}[k] = \sum_{i+j=k} a[i] \times b[j]\) 。
\(\color{darkgreen}{Code}\)
1def preProcess(a): 2 ## input: str 3 ## output: list, l 4 pf = a.find('.') 5 lf = 0 6 if pf != -1: 7 lf = len(a) - 1 - pf ## 小数位数 8 a = a[:pf] + a[pf+1:] ## 去掉小数点 9 a = list(a) 10 a = a[::-1] ## 翻转数组,a[0] 表示最低位 11 return a, lf 12 13def strMul(a, b): 14 a, la = preProcess(a) 15 b, lb = preProcess(b) 16 lf = la + lb 17 18 ans = [0 for _ in range(len(a) + len(b))] 19 for ia in range(len(a)): 20 for ib in range(len(b)): 21 ans[ia+ib] += int(a[ia]) * int(b[ib]) 22 carry = 0 23 for i in range(len(ans)): 24 tmp = ans[i] + carry 25 ans[i] = tmp % 10 26 carry = tmp / 10 27 ans = ans[::-1] ## 翻转数组 28 29 if lf > 0: 30 ans.insert(len(ans) - lf, '.') ## 插入小数点 31 if ans[0] == 0: 32 ans = ans[1:] ## 最高位是 0 则去掉 33 iz = len(ans)-1 34 while lf > 0 and ans[iz] == 0: ## 去掉小数点末尾的 0 35 iz -= 1 36 37 s = '' 38 for e in ans[:iz+1]: 39 s += str(e) 40 41 return s
4.28. [LeetCode] Number of Islands 孤岛个数
Hint:使用队列,广度优先遍历(BFS)。
延伸:从坐标 \((0, 0)\) 到 \((n-1, m-1)\) 的最短时间,只能走四邻域,\(\mathrm{map}[i][j] = 1\) 表示有障碍。Hint:BFS,第一个到达的就是时间最短的。
https://leetcode.com/problems/number-of-islands/
\(\color{darkgreen}{Code}\)
1// 孤岛个数 2class Solution 3{ 4public: 5 void traverseIsland(vector<vector<char>>& grid, int m, int n, const int M, const int N) 6 { 7 queue<pair<int, int>> que; 8 9 que.push(make_pair(m, n)); 10 grid[m][n] = '0'; 11 12 while (!que.empty()) 13 { 14 pair<int, int> p = que.front(); 15 que.pop(); 16 17 for (int i = 0; i < 4; ++i) 18 { 19 int next_x = p.first + directions[i][0]; 20 int next_y = p.second + directions[i][1]; 21 if (0 <= next_x && next_x < M && 0 <= next_y && next_y < N && grid[next_x][next_y] == '1') 22 { 23 // 入队需要改变标志位,避免后续过程中同一坐标重复入队 24 grid[next_x][next_y] = '0'; 25 que.push(make_pair(next_x, next_y)); 26 } 27 } 28 } 29 } 30 31 int numIslands(vector<vector<char>>& grid) 32 { 33 if (grid.size()==0) return 0; 34 int M = grid.size(); 35 int N = grid[0].size(); 36 int island = 0; 37 for (int m = 0; m < M; ++m) 38 { 39 for (int n = 0; n < N; ++n) 40 { 41 if (grid[m][n]=='1') 42 { 43 island += 1; 44 traverseIsland(grid, m, n, M, N); 45 } 46 } 47 } 48 return island; 49 } 50private: 51 static const int directions[4][2]; 52}; 53 54const int Solution::directions[4][2] = {{-1,0},{0,-1},{1,0},{0,1}};1// 最短时间 2// https://www.nowcoder.com/practice/365493766c514d0da0cd774d3d40fd49?tpId=8&tqId=11040&tPage=1&rp=1&ru=/ta/cracking-the-coding-interview&qru=/ta/cracking-the-coding-interview/question-ranking 3// https://leetcode.com/problems/shortest-path-in-binary-matrix/ 4 5struct point 6{ 7 int x; 8 int y; 9 int time; 10 point(int _x, int _y, int _time): x(_x), y(_y), time(_time){} 11}; 12 13class Solution 14{ 15public: 16 int shortestPathBinaryMatrix(vector<vector<int>>& grid) 17 { 18 int n = grid.size(); 19 queue<point> q; 20 if(grid[0][0] != 1) 21 { 22 q.push(point(0, 0, 1)); 23 grid[0][0] = 1; 24 } 25 while(!q.empty()) 26 { 27 auto p = q.front(); 28 q.pop(); 29 if(p.x == n-1 && p.y == n-1) return p.time; 30 for(int i = 0; i < 8; ++i) 31 { 32 int next_x = p.x + directions[i][0]; 33 int next_y = p.y + directions[i][1]; 34 if(0 <= next_x && next_x < n && 0 <= next_y && next_y < n && grid[next_x][next_y] != 1) 35 { 36 // 入队需要改变标志位,避免后续过程中同一坐标重复入队 37 grid[next_x][next_y] = 1; 38 q.push(point(next_x, next_y, p.time+1)); 39 } 40 } 41 } 42 return -1; 43 } 44private: 45 static const int directions[8][2]; 46}; 47 48const int Solution::directions[8][2] = { 49 {-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1} 50}; 51 52// 注意:当点 p 的近邻都满足条件入队之后,它们的标志位全部同时改变 53// 因为当最短路径包含点 p 时,只会再包含点 p 的一个近邻,最短路径不可能多次经过点 p 的不同近邻
4.29. 回文(Palindrome)
[LeetCode] Longest Palindromic Substring 最长回文子串(子串连续)。Hint:方法一,中心扩展法,回文中心的两侧互为镜像,将每一个位置作为中心进行扩展,回文分偶数和奇数;方法二,动态规划,类似于矩阵连乘问题,逐渐增大步长。
https://leetcode.com/problems/longest-palindromic-substring/
$$ dp[i][i] = \mathrm{true} $$ $$ dp[i][j] = \begin{cases} \mathrm{true} & &\ s[i] = s[j]\ \&\&\ (i \leqslant j \leqslant i+1\ ||\ dp[i+1][j-1] = \mathrm{true}) \\ \mathrm{false} & &\ \mathrm{otherwise} \end{cases} $$\(\color{darkgreen}{Code}\)
1// 方法一,中心扩展法 2class Solution { 3public: 4 void palindrome(int i, int j, string s, int& start, int& longest) 5 { 6 while(i >= 0 && j < s.size() && s.at(i) == s.at(j)) 7 { 8 i--; 9 j++; 10 } 11 i += 1; 12 j -= 1; 13 if(j - i + 1 > longest) 14 { 15 longest = j-i+1; 16 start = i; 17 } 18 } 19 string longestPalindrome(string s) { 20 int len = s.size(); 21 if(len <= 1) return s; 22 int start = 0; 23 int longest = 1; 24 for(int i = 0; i < len-1; ++i) 25 { 26 palindrome(i, i, s, start, longest); 27 palindrome(i, i+1, s, start, longest); 28 } 29 string str; 30 str.assign(s, start, longest); 31 return str; 32 } 33};
1// 方法二,动态规划 2class Solution 3{ 4public: 5 string longestPalindrome(string s) 6 { 7 if(s.size() <= 1) return s; 8 size_t len = s.size(); 9 vector<vector<bool>> dp(len, vector<bool>(len, false)); 10 size_t start = 0; 11 size_t longest = 1; 12 for(size_t i = 0; i < len; ++i) dp[i][i] = true; 13 for(size_t gap = 0; gap < len; ++gap) 14 { 15 for(int i = 0; i + gap < len; ++i) 16 { 17 int j = i + gap; 18 if(s[i] == s[j]) 19 { 20 if(gap <= 1 || dp[i+1][j-1]) 21 { 22 dp[i][j] = true; 23 longest = j - i + 1; // 由于步长是逐渐增大的,因此最后得到的回文子串一定是最长的 24 start = i; 25 } 26 else dp[i][j] = false; 27 } 28 } 29 } 30 vector<vector<bool>>().swap(dp); 31 return s.substr(start, longest); 32 } 33};
[LeetCode] Longest Palindromic Subsequence 最长回文子序列(子序列可以不连续)。Hint:寻找原字符串与翻转字符串的最长公共子序列,动态规划。
https://leetcode.com/problems/longest-palindromic-subsequence/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 // 寻找字符串 str 与其翻转字符串的最长公共子序列 5 int lcsLength(string& str) 6 { 7 int len = str.size(); 8 vector<vector<int>> dp(len+1, vector<int>(len+1, 0)); 9 // dp[i][j] 表示前 i 个字符和后 j 个字符翻转后的最长公共子序列的长度 10 for(int i = 1; i <= len; ++i) 11 { 12 for(int j = 1; j <= len; ++j) 13 { 14 if(str[i-1] == str[len-j]) dp[i][j] = dp[i-1][j-1] + 1; 15 else dp[i][j] = max(dp[i-1][j], dp[i][j-1]); 16 } 17 } 18 int ans = dp[len][len]; 19 vector<vector<int>>().swap(dp); 20 return ans; 21 } 22 23 int longestPalindromeSubseq(string s) 24 { 25 if(s.size() <= 1) return s.size(); 26 return lcsLength(s); 27 } 28};
[LeetCode] Count Different Palindromic Subsequences 统计不同回文子序列的个数(子序列可以不连续)。
https://leetcode.com/problems/count-different-palindromic-subsequences/
https://blog.csdn.net/lili0710432/article/details/78659583
\(\color{darkgreen}{Analysis}\)
用 \(dp[i][j]\) 表示字符串 \([i,j]\) 区间内的的回文子序列个数。
\(S[i] \ne S[j]\) 。下式的第三项是前两项重复计算的部分。
\[dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]\]\(S[i] = S[j]\)
如果相同的回文子序列可以多次统计,递推式如下。其中 \(+1\) 统计的是长度为 2 的回文子序列 “ \(S[i]S[j]\) ”; \(+ dp[i+1][j-1]\) 统计的是以 “ \(S[i]\) ”开头,以 “ \(S[j]\) ”结尾,且中间部分取自区间 \([i+1,j-1]\) 的回文子序列。
\[\begin{split}dp[i][j] &=\ dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] + 1 + dp[i+1][j-1] \\ &=\ dp[i+1][j] + dp[i][j-1] + 1\end{split}\]如果只统计不同回文子序列的个数,分三种情况。
若 \(S[i]\) 不再出现在区间 \([i+1,j-1]\) 内,递推式如下。其中 \(\times 2\) 统计了两类回文子序列:一类是以 “ \(S[i]\) ”开头,以 “ \(S[j]\) ”结尾,且中间部分取自区间 \([i+1,j-1]\) 的回文子序列,另一类是只取自区间 \([i+1,j-1]\) 的回文子序列; \(+2\) 统计的是长度为 1 的回文子序列 “ \(S[i]\) ”和长度为 2 的回文子序列 “ \(S[i]S[j]\) ”。
\[dp[i][j] = dp[i+1][j-1] \times 2 + 2\]若 \(S[i]\) 在区间 \([i+1,j-1]\) 内又出现 1 次,递推式如下。 \(+1\) 统计的是长度为 2 的回文子序列 “ \(S[i]S[j]\) ”,长度为 1 的回文子序列 “ \(S[i]\) ”在区间 \([i+1,j-1]\) 内已经统计过了。
\[dp[i][j] = dp[i+1][j-1] \times 2 + 1\]若 \(S[i]\) 在区间 \([i+1,j-1]\) 内又出现多次,设出现的第一个位置是 \(l\) ,最后一个位置是 \(r\) ,递推式如下。这种情况下,以 “ \(S[i]\) ”开头,以 “ \(S[j]\) ”结尾,且中间部分取自区间 \([i+1,j-1]\) 的回文子序列也会被重复统计。
\[dp[i][j] = dp[i+1][j-1] \times 2 - dp[l+1][r-1]\]
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int countPalindromicSubsequences(string S) 5 { 6 int n = S.size(); 7 if(n <= 1) return n; 8 vector<vector<long long>> dp(n, vector<long long>(n, 0)); // long long 防止溢出 9 for(int i = 0; i < n; ++i) dp[i][i] = 1; 10 11 long long modulo = 1000000007; 12 for(int gap = 1; gap < n; ++gap) 13 { 14 for(int i = 0; i + gap < n; ++i) 15 { 16 int j = i + gap; 17 if(S[i] != S[j]) 18 { 19 dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]; 20 } 21 else 22 { 23 dp[i][j] = dp[i+1][j-1] * 2; // 先计算这部分,避免后面重复计算 24 int left = i + 1; 25 int right = j - 1; 26 while(left < j && S[left] != S[i]) left++; 27 while(right > i && S[right] != S[i]) right--; 28 29 if(left > right) dp[i][j] += 2; 30 else if(left == right) dp[i][j] += 1; 31 else dp[i][j] -= dp[left+1][right-1]; 32 } 33 dp[i][j] = (dp[i][j] + modulo) % modulo; // 前面有减法操作,因此 dp[i][j] 可能是负数 34 } 35 } 36 37 int res = dp[0][n-1]; 38 dp.clear(); 39 dp.shrink_to_fit(); 40 return res; 41 } 42};
[LeetCode] Palindrome Partitioning 分割字符串使所有的子串都是回文子串。Hint:回溯,从字符串起始位置往后判断回文,如果满足回文,加入子串集合,并从回文结束位置往后遍历。
https://leetcode.com/problems/palindrome-partitioning/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 vector<vector<string>> partition(string s) 5 { 6 vector<vector<string>> res; 7 if(s.empty()) return res; 8 9 // isPalindrome[i][j] 表示 s 的区间 [i,j] 是否是回文 10 vector<vector<bool>> isPalindrome(s.size(), vector<bool>(s.size(), false)); 11 for(int gap = 0; gap < s.size(); ++gap) 12 { 13 for(int i = 0; i+gap < s.size(); ++i) 14 { 15 int j = i + gap; 16 if(s[i] == s[j]) 17 { 18 if(gap <= 1) isPalindrome[i][j] = true; 19 else isPalindrome[i][j] = isPalindrome[i+1][j-1]; 20 } 21 else isPalindrome[i][j] = false; 22 } 23 } 24 25 vector<string> tmp; 26 dfs(s, 0, tmp, res, isPalindrome); 27 28 isPalindrome.clear(); 29 isPalindrome.shrink_to_fit(); 30 31 return res; 32 } 33private: 34 void dfs(string& s, int t, vector<string>& tmp, vector<vector<string>>& res, vector<vector<bool>>& isPalindrome) 35 { 36 if(t == s.size()) 37 { 38 res.push_back(tmp); 39 return; 40 } 41 for(int i = t; i < s.size(); ++i) 42 { 43 if(isPalindrome[t][i]) 44 { 45 tmp.push_back(s.substr(t, i-t+1)); // 如果满足回文,加入当前子串集合 46 dfs(s, i+1, tmp, res, isPalindrome); // 回文结束位置为 i,因此下一个起始位置是 i+1 47 tmp.pop_back(); 48 } 49 } 50 } 51};
[LeetCode] Palindrome Partitioning II 找出最短回文分割。Hint:如果采用上题方法,会超时;使用动态规划,类似于最长上升子序列的解法。
https://leetcode.com/problems/palindrome-partitioning-ii/
\(\color{darkgreen}{Code}\)
1class Solution { 2public: 3 int minCut(string s) { 4 if(s.size() <= 1) return 0; 5 6 vector<vector<bool>> isPalindrome(s.size(), vector<bool>(s.size(), false)); 7 for(int gap = 0; gap < s.size(); ++gap) 8 { 9 for(int i = 0; i+gap < s.size(); ++i) 10 { 11 int j = i + gap; 12 if(s[i] == s[j]) 13 { 14 if(gap <= 1) isPalindrome[i][j] = true; 15 else isPalindrome[i][j] = isPalindrome[i+1][j-1]; 16 } 17 else isPalindrome[i][j] = false; 18 } 19 } 20 21 vector<int> dp(s.size(), 0); // dp[i] 表示区间 [0, i] 的最短回文分割 22 for(int i = 1; i < s.size(); ++i) 23 { 24 if(isPalindrome[0][i]) dp[i] = 0; 25 else 26 { 27 dp[i] = dp[i-1] + 1; // 直接划分 s[i] 为一个子串 28 for(int j = 1; j < i; ++j) 29 { 30 if(isPalindrome[j][i]) dp[i] = min(dp[i], dp[j-1] + 1); // [j, i] 为一个子串 31 } 32 } 33 } 34 35 int res = dp[s.size()-1]; 36 37 isPalindrome.clear(); isPalindrome.shrink_to_fit(); 38 dp.clear(); dp.shrink_to_fit(); 39 40 return res; 41 } 42 43};
4.30. 判断字符串 s2 是否可由 s1 旋转得到
Hint:对 s1 做循环移位,所得字符串都将是字符串 s1s1 的子串。
\(\color{darkgreen}{Code}\)
1bool checkReverseEqual(string s1, string s2) 2{ 3 if(s1.size()==0 || s2.size()==0) return false; 4 if(s1.size() < s2.size()) return false; // s1 = "abc", s2 = "abcabc" 5 string s1s1 = s1 + s1; 6 if(s1s1.find(s2) == string::npos) return false; 7 return true; 8}
4.31. [LeetCode] Validate Binary Search Tree 检查一棵二叉树是否为二叉查找树
Hint:不仅要求左节点比当前节点小,右节点比当前节点大,而是要求左子树所有节点都小于当前节点,右子树所有节点都大于当前节点;利用二叉树的中序遍历,BST 得到的序列是升序排列的。
https://leetcode.com/problems/validate-binary-search-tree/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 bool isValidBST(TreeNode* root) 5 { 6 // 节点的值 val 是 int 型 7 long long pre = (long long)(INT_MIN) - 1; 8 return checkBST(root, pre); 9 } 10private: 11 // 中序遍历,检查上一个遍历的数是否小于当前数, O(1) 空间复杂度 12 bool checkBST(TreeNode* root, long long& pre) 13 { 14 if(!root) return true; 15 if(!checkBST(root->left, pre)) return false; 16 if(pre >= (long long)(root->val)) return false; 17 pre = (long long)(root->val); 18 return checkBST(root->right, pre); 19 } 20};
4.32. 判断一个数是否是奇数
Hint:考虑负数的情形;方法一,判断模 2 结果不为 0;方法二,位运算判断最低位为 1。
延伸:判断两个数是否相等(或判断某个数是否为 0), 如果是浮点数,应该判断两者差的绝对值是否小于一个阈值,而不是直接使用 ==。
\(\color{darkgreen}{Code}\)
1bool isOdd1(int x) 2{ 3 return (x % 2) != 0; 4} 5 6bool isOdd2(int x) 7{ 8 return (x & 1) == 1; 9} 10 11bool isEqual(double x, double y) 12{ 13 return fabs(x - y) < 1e-6; 14}
延伸:检查一个数是否是 2 的整次幂,Hint:二进制表示只有一个 1;检查一个数是否是 4 的整次幂,Hint:4 的整次幂的二进制表示中, 1 都在奇数位;检查一个数是否是 3 的整次幂,Hint:质数的整次幂的质因子只有该质数本身。
\(\color{darkgreen}{Code}\)
1// 检查一个数是否是 2 的整次幂 2bool checkPower2(int n) 3{ 4 return n > 0 && (n & (n - 1)) == 0; 5}1// 检查一个数是否是 4 的整次幂 2bool checkPower4(int n) 3{ 4 if(n > 0 && (n & (n - 1)) == 0) // 先确保是 2 的整次幂(只有一个 1) 5 { 6 if((n & 0x55555555) == n) return true; // 0x55555555 = 0101 0101 0101 0101 0101 0101 0101 0101 7 } 8 return false; 9}1// 检查一个数是否是 3 的整次幂 2bool checkPower3(int n) 3{ 4 return n > 0 && 1162261467 % n == 0; // 3^19 = 1162261467 是 int 型中最大的 3 的整次幂 5}
4.33. [LeetCode] Valid Number 验证一个字符串是否表示某个有效数字
Hint:完整的数字表达是“空格+正负号+整数+小数点+整数+e+正负号+整数+空格”;小数点的相邻两边至少要有一边是整数;如果出现 e,其两边都必须出现整数,但不要求相邻;如 05.e-3 是一个有效数字。
延伸:将字符串转换为整数,需要考虑:空串、正负号、无效字符、溢出。
https://leetcode.com/problems/valid-number/
\(\color{darkgreen}{Code}\)
1// 验证一个字符串是否表示某个有效数字 2class Solution 3{ 4public: 5 bool isNumber(string s) 6 { 7 size_t idx = 0; 8 bool hasDigit = false; 9 10 scanSpace(s, idx); 11 scanSign(s, idx); 12 hasDigit = scanDigit(s, idx); 13 scanPoint(s, idx); 14 hasDigit |= scanDigit(s, idx); 15 if(hasDigit) // 小数点的相邻两边至少要有一边是整数;e 的左边必须出现整数;如果既没有小数点,又没有 e,则要求该字符串中必须包含整数。总而言之,这里必须是 true 才有可能是有效数字 16 { 17 if(scanExp(s, idx)) 18 { 19 scanSign(s, idx); 20 hasDigit = scanDigit(s, idx); // e 的右边必须出现整数 21 } 22 scanSpace(s, idx); 23 if(idx == s.size() && hasDigit) return true; 24 } 25 return false; 26 } 27private: 28 void scanSpace(string& s, size_t& idx) 29 { 30 while(idx < s.size() && s.at(idx) == ' ') ++idx; 31 } 32 void scanSign(string& s, size_t& idx) 33 { 34 if(idx < s.size() && (s.at(idx) == '+' || s.at(idx) == '-')) ++idx; 35 } 36 bool scanDigit(string& s, size_t& idx) 37 { 38 if(idx >= s.size()) return false; 39 if(s.at(idx) < '0' || s.at(idx) > '9') return false; 40 while(idx < s.size() && '0' <= s.at(idx) && s.at(idx) <= '9') ++idx; 41 return true; 42 } 43 void scanPoint(string& s, size_t& idx) 44 { 45 if(idx < s.size() && s.at(idx) == '.') ++idx; 46 } 47 bool scanExp(string& s, size_t& idx) 48 { 49 if(idx < s.size() && s.at(idx) == 'e') 50 { 51 ++idx; 52 return true; 53 } 54 return false; 55 } 56};1// 将字符串转换为整数 2class Solution 3{ 4public: 5 int myAtoi(string str) 6 { 7 unsigned int idx = 0; 8 scanSpace(str, idx); 9 10 bool sign = true; 11 if(idx < str.size() && str[idx] == '-' || str[idx] == '+') 12 { 13 if(str[idx] == '-') sign = false; 14 ++idx; 15 } 16 17 long long ans = 0; 18 bool hasDigit = false; 19 while(idx < str.size() && '0' <= str[idx] && str[idx] <= '9') 20 { 21 hasDigit = true; 22 ans = 10 * ans + str[idx] - '0'; 23 if(sign && ans > INT_MAX) 24 { 25 validInt = false; 26 return INT_MAX; 27 } 28 if(!sign && -ans < INT_MIN) 29 { 30 validInt = false; 31 return INT_MIN; 32 } 33 ++idx; 34 } 35 scanSpace(str, idx); 36 if(idx == str.size() && hasDigit) 37 { 38 if(!sign) ans = - ans; 39 validInt = true; 40 return static_cast<int>(ans); 41 } 42 43 validInt = false; 44 return 0; 45 } 46private: 47 bool validInt; // 标志符,输出 0 / INT_MAX / INT_MIN 时,有可能是异常情形 48 void scanSpace(string str, unsigned int& idx) // 扫描首尾空格 49 { 50 while(idx < str.size() && str[idx] == ' ') ++idx; 51 } 52};
4.34. 求 \(1+2+3+ \cdots +n\) ,不使用:乘除法,判断,循环,库函数。
\(\color{darkgreen}{Code}\)
1// 方法一,构造函数 2class A 3{ 4public: 5 A() 6 { 7 id++; 8 sum += id; 9 } 10 static void reset() 11 { 12 id = 0; 13 sum = 0; 14 } 15 static unsigned int getSum() 16 { 17 return sum; 18 } 19private: 20 static unsigned int id; 21 static unsigned int sum; 22}; 23 24unsigned int A::id = 0; 25unsigned int A::sum = 0; 26 27unsigned int sumFrom1ToN(unsigned int N) 28{ 29 A::reset(); 30 31 A* arr = new A[N]; 32 delete[] arr; 33 34 return A::getSum(); 35}1// 方法二,虚函数 2 3class A; // 前向声明 4A* arr[2]; // 这里可以声明类 A 的指针,但是不能声明类 A 的变量,类 A 还未定义 5 6class A 7{ 8public: 9 virtual unsigned int getSum(unsigned int n) 10 { 11 return 0; 12 } 13}; 14 15class B: public A 16{ 17public: 18 unsigned int getSum(unsigned int n) override 19 { 20 return n + arr[!!n] -> getSum(n - 1); // !!n:当 n>0,arr[1] 调用 B::getSum(n);当 n=0,arr[0] 调用 A::getSum(n) 21 } 22}; 23 24unsigned int sumFrom1ToN(unsigned int N) 25{ 26 A a; 27 B b; 28 arr[0] = &a; 29 arr[1] = &b; 30 return arr[1] -> getSum(N); 31}
4.35. [LeetCode] Lexicographical Numbers 按字典序排列 \(1 \sim n\)
Hint:方法一,定义排序规则,按字符串的字典序排序;方法二,回溯,递归深度只与 \(n\) 的位数有关。
https://leetcode.com/problems/lexicographical-numbers/
\(\color{darkgreen}{Code}\)
1// 方法一,定义排序规则 2 3class Solution 4{ 5public: 6 vector<int> lexicalOrder(int n) 7 { 8 vector<int> res; 9 if(n < 1) return res; 10 res.resize(n); 11 iota(res.begin(), res.end(), 1); 12 sort(res.begin(), res.end(), comparator); 13 return res; 14 } 15private: 16 static bool comparator(int x, int y) 17 { 18 return strcmp(to_string(x).c_str(), to_string(y).c_str()) < 0 ? true: false; 19 } 20};1// 方法二,回溯,从高位往低位进行 2 3class Solution 4{ 5public: 6 vector<int> lexicalOrder(int n) 7 { 8 vector<int> res; 9 for(int high = 1; high <= 9; ++high) dfs(high, n, res); // 最高位不能为 0 10 return res; 11 } 12private: 13 void dfs(int high, int n, vector<int>& res) 14 { 15 if(high > n) return; 16 res.push_back(high); // 只有高位,没有低位。这是同一前缀的数字中最小的数 17 for(int low = 0; low <= 9; ++low) dfs(high * 10 + low, n, res); // 高位 + 低位 18 } 19};
延伸:找到字典序排列的第 \(k\) 个数。
\(\color{darkgreen}{Code}\)
1## 在字典树的区间 [p, p+1) 及其子区间查找 2## 下一层子区间为 [p*10, (p+1)*10) 3## 如果子区间内没找到,则进入兄弟区间 [p+1, p+2) 4 5def dictOrder(n, k): 6 pos = 1 7 while True: 8 left = pos 9 right = pos + 1 10 cnt = 0 11 while n >= left: 12 cnt += min(n+1, right) - left ## 区间大小 13 left *= 10 14 right *= 10 15 if cnt < k: ## 不在区间 [pos, pos+1) 及其子区间内 16 k -= cnt 17 pos += 1 ## 进入兄弟区间 18 else: ## 在区间 [pos, pos+1) 或其子区间内 19 k -= 1 20 if k == 0: 21 return pos 22 pos *= 10 ## 进入子区间
4.36. [LeetCode] Merge k Sorted Lists 合并 \(k\) 条有序链表
Hint:建立大小为 \(k\) 的小顶堆,每次弹出一个节点,并把该节点的下一个节点插入小顶堆中。时间复杂度 \(\mathcal{O}(n \log k)\) ,\(n\) 是节点个数。
https://leetcode.com/problems/merge-k-sorted-lists/
\(\color{darkgreen}{Code}\)
1struct ListNode 2{ 3 int val; 4 ListNode* next; 5 ListNode(int x) : val(x), next(nullptr) {} 6}; 7 8struct comparator 9{ 10 bool operator()(ListNode* a, ListNode* b) 11 { 12 return a->val > b->val; // 小顶堆 13 } 14}; 15 16class Solution 17{ 18public: 19 ListNode* mergeKLists(vector<ListNode*>& lists) 20 { 21 if(lists.size() == 0) return nullptr; 22 if(lists.size() == 1) return lists[0]; 23 24 ListNode* head = new ListNode(0); // 合并链表的临时头节点 25 26 priority_queue<ListNode*, vector<ListNode*>, comparator> pq; 27 for(auto & list : lists) 28 { 29 if(list) pq.emplace(list); // 建堆 30 } 31 ListNode* curr = head; 32 while(!pq.empty()) 33 { 34 ListNode* p = pq.top(); 35 pq.pop(); 36 curr->next = p; 37 curr = p; 38 if(p->next) pq.push(p->next); 39 } 40 41 curr = head->next; 42 delete head; 43 return curr; 44 } 45};
4.37. [LeetCode] Max Points on a Line 统计共线的最多点数
Hint:直线需要考虑三种斜率:水平,垂直,斜线,还要考虑点重合的情形;由于浮点运算的精度问题,将斜率表示为两个整数的分数形式,保存到哈希表中。
https://leetcode.com/problems/max-points-on-a-line/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int maxPoints(vector<vector<int>>& points) 5 { 6 int res = 0; 7 for(size_t i = 0; i < points.size(); ++i) // points.size() == 0,返回 0;points.size() == 1,返回 1 8 { 9 unordered_map<string, int> mp; // 对每个点 i 统计其与其他点所成直线的斜率。由于这些直线都通过点 i,因此斜率相同就表示共线 10 int samePointNum = 0; 11 int verticalLineNum = 0; 12 int horizontalLineNum = 0; 13 int slantLineNum = 0; 14 for(size_t j = i + 1; j < points.size(); ++j) // 往后遍历每个点 15 { 16 if(points[i][0] == points[j][0] && points[i][1] == points[j][1]) ++samePointNum; // 点重合 17 else if(points[i][0] == points[j][0]) ++verticalLineNum; // 垂直线 18 else if(points[i][1] == points[j][1]) ++horizontalLineNum; // 水平线,可以计算斜率,但是由于垂直方向差异为 0,不好计算公约数 19 else // 斜线 20 { 21 int dx = points[j][0] - points[i][0]; 22 int dy = points[j][1] - points[i][1]; 23 int g = _gcd(dy, dx); 24 dx /= g; 25 dy /= g; 26 if(dy < 0) // 符号统一令 dy > 0 27 { 28 dy = -dy; 29 dx = -dx; 30 } 31 stringstream ss; 32 ss << dx << " " << dy; 33 string slope = ss.str(); 34 ss.clear(); 35 if(mp.find(slope) == mp.end()) mp[slope] = 1; 36 else ++mp[slope]; 37 slantLineNum = max(slantLineNum, mp[slope]); 38 } 39 } 40 41 int currMax = max(slantLineNum, max(verticalLineNum, horizontalLineNum)); 42 currMax += samePointNum + 1; // + 1 表示点 i 本身 43 res = max(res, currMax); 44 } 45 return res; 46 } 47private: 48 int _gcd(int a, int b) // 辗转相除,计算最大公约数 49 { 50 a = abs(a); 51 b = abs(b); 52 if(a < b) swap(a, b); 53 while(a % b) 54 { 55 int tmp = a; 56 a = b; 57 b = tmp % b; 58 } 59 return b; 60 } 61};
4.38. [LeetCode] Word Break 字符串按字典切分
Hint:回溯;动态规划;字典树/哈希表。
https://leetcode.com/problems/word-break
\(\color{darkgreen}{Code}\)
1// 方法一,回溯 2// 测试用例超时 3// "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" ["a","aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa"] 4 5#include <unordered_set> 6class Solution 7{ 8public: 9 bool wordBreak(string s, vector<string>& wordDict) 10 { 11 if(s == "") return true; 12 if(wordDict.size() == 0) return false; 13 unordered_set<string> wordSet(wordDict.begin(), wordDict.end()); 14 return wordFind(s, wordSet, 0); 15 } 16private: 17 bool wordFind(string& s, const unordered_set<string>& wordSet, int k) 18 { 19 if(k == s.size()) return true; 20 for(int i = k; i < s.size(); ++i) 21 { 22 if(wordSet.find(s.substr(k, i - k + 1)) != wordSet.end()) 23 { 24 if(wordFind(s, wordSet, i + 1)) return true; 25 } 26 } 27 return false; 28 } 29};1// 方法二,动态规划,空间复杂度 O(n^2) 2// dp[i][j] 表示字符串区间 [i, j] 的切分情况 3// 解法类似于矩阵连乘问题 4 5class Solution 6{ 7public: 8 bool wordBreak(string s, vector<string>& wordDict) 9 { 10 if(s.empty() || wordDict.empty()) return false; 11 int n = s.size(); 12 vector<vector<bool>> dp(n, vector<bool>(n, false)); 13 for(int gap = 0; gap < n; ++gap) 14 { 15 for(int i = 0; i + gap < n; ++i) 16 { 17 int j = i + gap; 18 for(string& word: wordDict) 19 { 20 // 这里用 ||,只要有一个 word 匹配就行 21 if(gap + 1 == word.size()) dp[i][j] = dp[i][j] || (s.substr(i, word.size()) == word); 22 else if(gap + 1 > word.size()) dp[i][j] = dp[i][j] || (s.substr(i, word.size()) == word && dp[i+word.size()][j]); 23 } 24 } 25 } 26 return dp[0][n-1]; 27 } 28};1// 方法三,动态规划,空间复杂度 O(n) 2// dp[i] 表示字符串区间 [0, i-1] 的切分情况 3 4#include <unordered_set> 5class Solution { 6public: 7 bool wordBreak(string s, vector<string>& wordDict) { 8 if(s.empty() || wordDict.empty()) return false; 9 10 unordered_set<string> wordSet(wordDict.begin(), wordDict.end()); 11 12 int n = s.size(); 13 vector<bool> dp(n+1, false); 14 dp[0] = true; // 初始化 15 16 for(unsigned int i = 1; i <= n; ++i) 17 { 18 for(unsigned int j = 0; j < i; ++j) 19 { 20 if(dp[j]) // 两段子串:[0, j-1], [j, i] 21 { 22 string str = s.substr(j, i-j); 23 if(wordSet.find(str) != wordSet.end()) 24 { 25 dp[i] = true; 26 break; 27 } 28 } 29 } 30 } 31 return dp[n]; 32 } 33};
延伸:返回所有的切分方式。
https://leetcode.com/problems/word-break-ii
\(\color{darkgreen}{Code}\)
1## 返回所有的切分方式 2## 使用字典树 Trie 3 4from collections import defaultdict 5 6class TrieNode(object): 7 def __init__(self): 8 self.dict = defaultdict(TrieNode) 9 self.word = False 10 11class Trie(object): 12 def __init__(self): 13 self.root = TrieNode() 14 15 def insert(self, word): 16 child = self.root 17 for letter in word: 18 child = child.dict[letter] 19 child.word = True 20 21 def search(self, word): 22 child = self.root 23 for letter in word: 24 child = child.dict.get(letter) 25 if child is None: 26 return False 27 return child.word 28 29 30class Solution: 31 def wordBreak(self, s: str, wordDict: List[str]) -> List[str]: 32 n = len(s) 33 trie = Trie() 34 ## 构建字典树 35 for word in wordDict: 36 trie.insert(word) 37 ## 前一个切分点的下标 38 pre = [[] for i in range(n+1)] 39 pre[0].append(-1) 40 ## 前 i 个字符的切分 41 for i in range(1, n+1): 42 for j in range(i): 43 if pre[j] != []: 44 ## 当前子串:s[j:i] 45 if trie.search(s[j:i]): 46 pre[i].append(j) 47 res = [] 48 seg = "" 49 ## 递归获取所有切分方式 50 self.combineWords(s, pre, n, seg, res) 51 return res 52 53 def combineWords(self, s, pre, t, seg, res): 54 if t == 0: 55 res.append(seg[:-1]) 56 return 57 for j in pre[t]: 58 self.combineWords(s, pre, j, s[j:t] + " " + seg, res)
4.39. [LeetCode] Gas Station 加油站回路
Hint:只要 gas 总和不小于 cost 总和,一定存在可以完成回路的出发点。
https://leetcode.com/problems/gas-station/
https://leetcode.com/problems/gas-station/discuss/191463/topic
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int canCompleteCircuit(vector<int>& gas, vector<int>& cost) 5 { 6 if(gas.size() == 0) return -1; 7 int totalDiff = 0; 8 int currDiff = 0; 9 int start = 0; 10 for(int i = 0; i < gas.size(); ++i) 11 { 12 totalDiff += gas[i] - cost[i]; 13 currDiff += gas[i] - cost[i]; 14 if(currDiff < 0) 15 { 16 start = i + 1; // 第 0 ~ i 加油站都不可能是可以完成回路的起始点 17 currDiff = 0; 18 } 19 } 20 return totalDiff >= 0 ? start: -1; 21 } 22};
延伸:从起点到终点的最少加油次数。Hint:贪心算法,把路过的每个加油站的油量存入优先队列,当需要加油时, 弹出队列中的最大油量。
https://leetcode.com/problems/minimum-number-of-refueling-stops/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int minRefuelStops(int target, int startFuel, vector<vector<int>>& stations) 5 { 6 priority_queue<int, vector<int>, less<int>> que; 7 int cnt = 0; 8 int maxDist = startFuel; 9 int i = 0; 10 while(maxDist < target) 11 { 12 while(i < stations.size() && maxDist >= stations[i][0]) 13 { 14 que.push(stations[i][1]); 15 ++i; 16 } 17 if(que.empty()) return -1; 18 maxDist += que.top(); 19 que.pop(); 20 cnt += 1; 21 } 22 return cnt; 23 } 24};
4.40. 最短过桥时间
\(n\) 个人过桥,每次最多通过两个人(过桥时间按较长者计算),只有一个手电筒,每次过桥之后需要一个人把手电筒送回来,求最短过桥时间。Hint:耗时第 \(k\) 小的人过桥有两种方案,第一,由耗时最短的人送回手电筒,并陪同过河; 第二,耗时最短的人送回手电筒之后,耗时第 \(k\) 小的人与耗时第 \(k-1\) 小的人一起过桥,他们过桥之后,手电筒再由耗时第二短的人送回,最后耗时最短的人和耗时第二短的人一起过桥。
https://blog.csdn.net/u014113686/article/details/82464635
\(\color{darkgreen}{Code}\)
1int minTimeCrossBridge(int* time, int n) 2{ 3 assert(n > 0); 4 sort(time, time + n); 5 int* dp = new int[n]; 6 dp[0] = time[0]; 7 dp[1] = time[1]; 8 for (size_t i = 2; i < n; ++i) 9 { 10 dp[i] = min(dp[i - 1] + time[0] + time[i], dp[i - 2] + time[i] + time[0] + 2 * time[1]); 11 } 12 int res = dp[n - 1]; 13 delete[] dp; 14 return res; 15}
4.41. [LeetCode] Minimum Window Substring 字符串 S 中包含字符串 T 中所有字符的最短子串
Hint:用两个指针表示滑动窗口的起始和结尾;当窗口满足要求,则计算当前窗口的长度,然后两个指针都往后移动一步;终止条件是尾指针移动到了字符串 S 的结尾(’\0’)。
延伸:不考虑字符串 T 中重复的字符,求字符串 S 中包含字符串 T 中出现的字符的最短子串。
https://leetcode.com/problems/minimum-window-substring/
\(\color{darkgreen}{Code}\)
1// 考虑重复 2 3typedef unsigned int uint; 4class Solution 5{ 6public: 7 string minWindow(string s, string t) 8 { 9 uint lenS = s.size(); 10 uint lenT = t.size(); 11 if(lenS < lenT || lenT == 0) return ""; 12 13 uint hashT[256] = {0}; 14 for(size_t k = 0; k < lenT; ++k) 15 { 16 hashT[t[k]] += 1; // 统计 T 中的字符,考虑重复 17 } 18 uint hashWindow[256] = {0}; // 统计 S 的滑动窗口中出现在 T 中的字符 19 20 uint start = 0; 21 uint minLen = lenS + 1; 22 uint pBegin = 0; 23 uint pEnd = -1; // 双指针初始化 24 uint matchCnt = 0; // 统计当前滑动窗口中匹配的字符对 25 while(true) 26 { 27 if(matchCnt == lenT) // T 中所有字符都出现 28 { 29 while(hashT[s[pBegin]] == 0 || hashWindow[s[pBegin]] > hashT[s[pBegin]]) // 收紧窗口的左端,第二个条件表示窗口中包含了多余的重复字符 30 { 31 --hashWindow[s[pBegin]]; 32 ++pBegin; 33 } 34 if(pEnd - pBegin + 1 < minLen) 35 { 36 minLen = pEnd - pBegin + 1; 37 start = pBegin; 38 } 39 --matchCnt; 40 --hashWindow[s[pBegin]]; 41 ++pBegin; // 起始指针往后移动,相应统计量 -1 42 } 43 ++pEnd; 44 if(pEnd == lenS) break; 45 ++hashWindow[s[pEnd]]; 46 if(hashT[s[pEnd]] > 0 && hashWindow[s[pEnd]] <= hashT[s[pEnd]]) ++matchCnt; // 新增匹配 47 } 48 if(minLen == lenS + 1) return ""; 49 else return s.substr(start, minLen); 50 } 51};1// 不考虑重复 2 3typedef unsigned int uint; 4class Solution 5{ 6public: 7 string minWindow(string s, string t) 8 { 9 uint lenS = s.size(); 10 uint lenT = t.size(); 11 if (lenS < lenT || lenT == 0) return ""; 12 13 uint hashT[256] = { 0 }; 14 uint uniqueChar = 0; 15 for (size_t k = 0; k < lenT; ++k) 16 { 17 if (hashT[t[k]] == 0) uniqueChar += 1; // 统计 T 中的字符,不考虑重复 18 hashT[t[k]] = 1; // 不管出现多少次,都是 1 19 } 20 uint hashWindow[256] = { 0 }; 21 22 uint start = 0; 23 uint minLen = lenS + 1; 24 uint pBegin = 0; 25 uint pEnd = -1; 26 uint matchCnt = 0; 27 while (true) 28 { 29 if (matchCnt == uniqueChar) // 匹配了 T 中所有字符 30 { 31 while (hashT[s[pBegin]] == 0 || hashWindow[s[pBegin]] > 1) // 收紧窗口的左端,第二个条件表示窗口中包含了多余的重复字符 32 { 33 --hashWindow[s[pBegin]]; 34 ++pBegin; 35 } 36 if (pEnd - pBegin + 1 < minLen) 37 { 38 minLen = pEnd - pBegin + 1; 39 start = pBegin; 40 } 41 --matchCnt; 42 --hashWindow[s[pBegin]]; 43 ++pBegin; // 起始指针往后移动,相应统计量 -1 44 } 45 ++pEnd; 46 if (pEnd == lenS) break; 47 ++hashWindow[s[pEnd]]; 48 if(hashT[s[pEnd]] > 0 && hashWindow[s[pEnd]] == 1) ++matchCnt; // 新增匹配 49 } 50 if (minLen == lenS + 1) return ""; 51 else return s.substr(start, minLen); 52 } 53};
相关题:从字符串首/末取字符,至少包含 \(k\) 个 a、b、c。Hint:双指针,时间复杂度 \(\mathcal{O}(N)\) 。
https://leetcode.com/problems/take-k-of-each-character-from-left-and-right
\(\color{darkgreen}{Code}\)
1from collections import Counter 2class Solution: 3 def takeCharacters(self, s: str, k: int) -> int: 4 c = Counter(s) 5 if c['a'] < k or c['b'] < k or c['c'] < k: 6 return -1 7 n = len(s) 8 l = 0 9 res = n 10 ## 区间 [l, r] 不取 11 ## 遍历 r 12 for r in range(n): 13 c[s[r]] -= 1 14 while c['a'] < k or c['b'] < k or c['c'] < k: 15 c[s[l]] += 1 16 l += 1 17 res = min(res, n - (r - l + 1)) 18 return res
4.42. [LeetCode] Continuous Subarrays 连续数组(元素之差不超过 2)的数量
Hint:双指针。字典插入、删除、查找最大值/最小值的时间复杂度是 \(\mathcal{O}(\log k)\) 。
https://leetcode.com/problems/continuous-subarrays
\(\color{darkgreen}{Code}\)
1from collections import defaultdict 2class Solution: 3 def continuousSubarrays(self, nums: List[int]) -> int: 4 freq = defaultdict(int) 5 left, right = 0, 0 6 cnt = 0 7 n = len(nums) 8 while right < n: 9 freq[nums[right]] += 1 10 ## min/max 复杂度 O(log k), k <= 3 11 while left <= right and max(freq) - min(freq) > 2: 12 freq[nums[left]] -= 1 13 ## 出队,避免影响 min/max 的统计 14 if freq[nums[left]] == 0: 15 freq.pop(nums[left]) 16 left += 1 17 ## 以 right 结尾的子数组个数 18 cnt += right - left + 1 19 right += 1 20 return cnt
4.43. [LeetCode] Nth Digit 第 \(N\) 个数字
Hint:\(k\) 位数的个数是 \(9 \times 10^{k-1}\) ,例如,两位数有 \(90\) 个;先确定第 \(N\) 个数字是几位数,再定位到具体的数,取出相应数字。
https://leetcode.com/problems/nth-digit/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int findNthDigit(int n) 5 { 6 int sum = 0; 7 int k = 1; 8 while(sum + k * 9 * pow(10, k-1) < n) 9 { 10 sum += k * 9 * pow(10, k-1); 11 k++; 12 } 13 int a = (n - sum) / k; 14 int b = (n - sum) % k; 15 int num = pow(10, k-1) + a - 1; // 定位到具体的数 16 if(b == 0) return num % 10; // 当前数的最后一位数字(个位) 17 else return ((num + 1) / static_cast<int>(pow(10, k-b))) % 10; // 下一个数的第 b 位数字 18 } 19};
4.44. [LeetCode] Pow(x, n) 求幂
Hint:\(x^{2k} = x^k \times x^k,\ x^{2k+1} = x \times x^k \times x^k\) 。
https://leetcode.com/problems/powx-n/
\(\color{darkgreen}{Code}\)
1// 递归 2 3double myPow(double x, int n) 4{ 5 if(fabs(x) < 1e-7) 6 { 7 if(n < 0) throw logic_error("ZeroDivisionError"); // 底数为 0, 指数为负 8 return 1.0; 9 } 10 if(n == 0) return 1; 11 if(n == 1) return x; 12 if(n < 0) 13 { 14 if(n == INT_MIN) return 1/x * myPow(1/x, - (n + 1)); // - INT_MIN 溢出 15 else return myPow(1/x, - n); 16 } 17 double tmp = myPow(x, n/2); 18 if(n % 2) return x * tmp * tmp; 19 else return tmp * tmp; 20}1// 迭代 2 3double myPow(double x, int n) 4{ 5 if(fabs(x) < 1e-7) 6 { 7 if(n < 0) throw logic_error("ZeroDivisionError"); // 底数为 0, 指数为负 8 return 1.0; 9 } 10 11 if(n == 0) return 1.0; 12 if(n == 1) return x; 13 14 double ans = 1.0; 15 16 if(n < 0) 17 { 18 x = 1/x; 19 if(n == INT_MIN) // - INT_MIN 溢出 20 { 21 ans *= x; 22 n = INT_MAX; 23 } 24 else n = - n; 25 } 26 27 while(n > 0) 28 { 29 int k = 1; 30 double tmp = x; 31 while(k <= n/2) 32 { 33 tmp *= tmp; 34 k <<= 1; 35 } 36 n -= k; 37 ans *= tmp; 38 } 39 40 return ans; 41}
4.45. [LeetCode] Longest Valid Parentheses 最长有效匹配括号长度
https://leetcode.com/problems/longest-valid-parentheses/
\(\color{darkgreen}{Code}\)
1// 方法一:动态规划 2// 设 dp[i] 表示以 s[i] 结尾的最长匹配长度 3// 当 s[i] = '(' ,dp[i] = 0 4// 当 s[i] = ')' 且 s[i-1] = '(' ,dp[i] = dp[i-2] + 2 5// 当 s[i] = ')' 且 s[i-1] = ')' ,需要找到与 s[i] 匹配的左括号的位置,而以 s[i-1] 结尾的最长匹配组的长度为 dp[i-1], 6// 因此与 s[i] 匹配的位置为 i - 1 - dp[i-1] 7 8class Solution 9{ 10public: 11 int longestValidParentheses(string s) 12 { 13 if(s.size() <= 1) return 0; 14 vector<int> dp(s.size(), 0); 15 int res = 0; 16 for(int i = 1; i < s.size(); ++i) 17 { 18 if(s[i] == ')') 19 { 20 if(s[i-1] == '(') dp[i] = i-2 >= 0 ? dp[i-2] + 2 : 2; 21 else 22 { 23 int left = i - 1 - dp[i-1]; 24 if(left >= 0 && s[left] == '(') 25 { 26 dp[i] = left > 0 ? dp[left-1] + dp[i-1] + 2 : dp[i-1] + 2; 27 } 28 } 29 } 30 res = max(res, dp[i]); 31 } 32 vector<int>().swap(dp); 33 return res; 34 } 35};1// 方法二:栈 2// 将达成匹配的括号的flag置为true 3// 求flag为true的最长连续子数组 4class Solution 5{ 6public: 7 int longestValidParentheses(string s) 8 { 9 const int LEN = s.size(); 10 if(LEN <= 1) return 0; 11 stack<int> id_stk; 12 vector<bool> match_flag(LEN, false); 13 for(int k = 0; k < LEN; ++k) 14 { 15 if(s.at(k) == '(') id_stk.push(k); 16 else if(!id_stk.empty()) 17 { 18 match_flag[id_stk.top()] = true; // left parentheses 19 match_flag[k] = true; // right parentheses 20 id_stk.pop(); 21 } 22 } 23 int res = 0; 24 int begin = -1; 25 // longest continuous 'true' 26 for(int end = 0; end < LEN; ++end) 27 { 28 if(match_flag[end] && begin == -1) begin = end; 29 else if(begin > -1 && (end+1 == LEN or !match_flag[end+1])) 30 { 31 res = max(res, end - begin + 1); 32 begin = -1; 33 } 34 } 35 return res; 36 } 37};
4.46. 丢弃的数
从 \(1,2,...,n\) 中取丢弃 \(k\) 个数,剩余的数形成一个数组 \(v\) ,求出丢弃的数。
\(k=1\)
\(a = \sum_{i=1}^n i - \sum_{j=1} v_j\) 。
\(k=2\)
\(a + b = \sum_{i=1}^n i - \sum_{j=1} v_j,\ a \times b = n! / \prod_{j=1} v_j\) 。
考虑到连乘可能溢出,可以使用平方和 \(a^2 + b^2 = \sum_{i=1}^n i^2 - \sum_{j=1} v_j^2\) 。
4.47. [LeetCode] Trapping Rain Water 接雨水
Hint:方法一,水从地面往上溢,统计每一层的积水,时间复杂度 \(\mathcal{O}(NH_{max})\) ;方法二,双指针,当左高右低,推进右边的指针,当左低右高,推进左边的指针。
https://leetcode.com/problems/trapping-rain-water/
\(\color{darkgreen}{Code}\)
1// 方法一 2 3class Solution 4{ 5public: 6 int trap(vector<int>& height) 7 { 8 if(height.size() <= 1) return 0; 9 10 int maxh = *max_element(height.begin(), height.end()); 11 int ans = 0; 12 for(int h = 1; h <= maxh; ++h) 13 { 14 vector<int> idx; 15 for(int i = 0; i < height.size(); ++i) 16 { 17 if(height[i] >= h) idx.push_back(i); // 找到所有会积水的区间 18 } 19 if(idx.size() < 2) break; 20 for(int j = 0; j < idx.size() - 1; ++j) 21 { 22 ans += idx[j+1] - idx[j] - 1; // 第 h 层的积水量 23 } 24 } 25 return ans; 26 } 27};1// 方法二 2 3class Solution 4{ 5public: 6 int trap(vector<int>& height) 7 { 8 if(height.size() <= 1) return 0; 9 10 int ans = 0; 11 12 int left = 0; 13 int leftmax = height[0]; 14 int right = height.size() - 1; 15 int rightmax = height[right]; 16 while(left < right) 17 { 18 if(height[left] < height[right]) // 左低右高 19 { 20 if(height[left] <= leftmax) 21 { 22 ans += leftmax - height[left]; 23 ++left; 24 } 25 else leftmax = height[left]; 26 } 27 else // 左高右低 28 { 29 if(height[right] <= rightmax) 30 { 31 ans += rightmax - height[right]; 32 --right; 33 } 34 else rightmax = height[right]; 35 } 36 } 37 return ans; 38 } 39};
4.48. [LeetCode] Sudoku Solver 数独
https://leetcode.com/problems/sudoku-solver/
\(\color{darkgreen}{Code}\)
1// code 用于编码数字出现的位置 2struct code 3{ 4 int a; 5 int b; 6 char digit; 7 code(int _a, int _b, char _digit): a(_a), b(_b), digit(_digit){} 8 bool operator<(const code& rhs) const // 两个 const 9 { 10 if(digit < rhs.digit) return true; 11 if(digit > rhs.digit) return false; 12 if(a < rhs.a) return true; 13 if(a > rhs.a) return false; 14 if(b < rhs.b) return true; 15 return false; 16 } 17 /* 这里重载运算符 < 把 a,b,digit 都作为关键字,这对于后面 set 的 find 操作很关键。 */ 18 /* 假如只使用 digit 作为关键字,set 将把 (1,2,'1') 和 (3,5,'1') 视为相同的元素。 */ 19}; 20 21class Solution 22{ 23public: 24 void solveSudoku(vector<vector<char>>& board) 25 { 26 if(board.size() != 9 || board[0].size() != 9) return; 27 set<code> used; 28 for(int r = 0; r < 9; ++r) 29 { 30 for(int c = 0; c < 9; ++c) 31 { 32 if(board[r][c] != '.') 33 { 34 used.emplace(code(r, -1, board[r][c])); // 第 r 行出现了 board[r][c] 35 used.emplace(code(-1, c, board[r][c])); // 第 c 列出现了 board[r][c] 36 used.emplace(code(r/3, c/3, board[r][c])); // 第 (r/3, c/3) 块出现了 board[r][c] 37 } 38 } 39 } 40 solveSudoku(board, used, 0); 41 } 42private: 43 bool solveSudoku(vector<vector<char>>& board, set<code>& used, int t) 44 { 45 while(t < 81 && board[t/9][t%9] != '.') ++t; 46 if(t == 81) return true; 47 int r = t / 9; 48 int c = t % 9; 49 for(int k = 1; k <= 9; ++k) 50 { 51 char digit = '0' + k; 52 code rowcode(r, -1, digit); 53 code colcode(-1, c, digit); 54 code blockcode(r/3, c/3, digit); 55 if(used.find(rowcode) == used.end() && used.find(colcode) == used.end() && used.find(blockcode) == used.end()) 56 { 57 board[r][c] = digit; 58 used.emplace(rowcode); 59 used.emplace(colcode); 60 used.emplace(blockcode); 61 if(solveSudoku(board, used, t + 1)) return true; 62 used.erase(rowcode); 63 used.erase(colcode); 64 used.erase(blockcode); 65 board[r][c] = '.'; // 擦除 66 } 67 } 68 return false; 69 } 70};
4.49. [LeetCode] Median of Two Sorted Arrays 两个排序数组的中位数
Hint:方法一,归并,时间复杂度 \(\mathcal{O}(m+n)\) ;方法二,二分查找,时间复杂度 \(\mathcal{O}(\log (m+n))\) 。
https://leetcode.com/problems/median-of-two-sorted-arrays/
https://windliang.cc/2018/07/18/leetCode-4-Median-of-Two-Sorted-Arrays/
\(\color{darkgreen}{Code}\)
1// 方法一 2 3class Solution 4{ 5public: 6 double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) 7 { 8 if(nums1.empty() && nums2.empty()) return 0.0; 9 int m = nums1.size(); 10 int n = nums2.size(); 11 int i = 0; 12 int j = 0; 13 int k = 0; 14 double median; 15 double premedian; 16 while(true) 17 { 18 if(j == n || (i < m && nums1[i] <= nums2[j])) 19 { 20 median = nums1[i]; 21 ++i; 22 } 23 else if(i == m || (j < n && nums1[i] > nums2[j])) 24 { 25 median = nums2[j]; 26 ++j; 27 } 28 if((m+n)%2 == 1 && k == (m+n)/2) return median; 29 if((m+n)%2 == 0 && k == (m+n)/2-1) premedian = median; 30 if((m+n)%2 == 0 && k == (m+n)/2) return (premedian + median) / 2.0; 31 ++k; 32 } 33 } 34};1# 方法二 2 3class Solution: 4 def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float: 5 n1, n2 = len(nums1), len(nums2) 6 n = n1 + n2 7 if n & 1: 8 return self.findKth(nums1, 0, n1, nums2, 0, n2, (n+1)//2) 9 else: 10 return (self.findKth(nums1, 0, n1, nums2, 0, n2, n//2) + 11 self.findKth(nums1, 0, n1, nums2, 0, n2, n//2+1)) / 2.0 12 13 def findKth(self, l1, i1, n1, l2, i2, n2, k): 14 while True: 15 if i1 == n1: return l2[i2+k-1] # 区间为空 16 if i2 == n2: return l1[i1+k-1] # 区间为空 17 if k == 1: return min(l1[i1], l2[i2]) 18 half = k // 2 19 j1, j2 = min(i1+half-1, n1-1), min(i2+half-1, n2-1) 20 if l1[j1] > l2[j2]: 21 k -= j2 - i2 + 1 22 i2 = j2 + 1 23 else: 24 k -= j1 - i1 + 1 25 i1 = j1 + 1
4.50. 滑动窗口
[LeetCode] Sliding Window Maximum 滑动窗口最大值。Hint:使用双端队列;新加入元素如果比队尾元素小,则直接入队,否则删除队尾元素直到队空或队尾元素比新加入元素大; 如果队首元素在滑动窗口之外,则删除之;队首元素就是当前窗口的最大值。
https://leetcode.com/problems/sliding-window-maximum/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 vector<int> maxSlidingWindow(vector<int>& nums, int k) 5 { 6 vector<int> win_max; 7 if(nums.size() == 0 || k <= 0) return win_max; 8 deque<int> que; // 双端队列,存的是元素下标 9 for(int i = 0; i < k && i < nums.size(); ++i) 10 { 11 while(!que.empty() && nums[que.back()] <= nums[i]) que.pop_back(); 12 que.push_back(i); 13 } 14 win_max.push_back(nums[que.front()]); // 队首的是滑动窗口最大值 15 for(int i = k; i < nums.size(); ++i) 16 { 17 while(!que.empty() && nums[que.back()] <= nums[i]) que.pop_back(); 18 if(!que.empty() && que.front() <= i - k) que.pop_front(); // 不属于当前滑动窗口 19 que.push_back(i); 20 win_max.push_back(nums[que.front()]); 21 } 22 return win_max; 23 } 24};
[LeetCode] Sliding Window Median 滑动窗口中位数。Hint:使用 multiset(包含重复元素、默认排序),加入/删除元素时调整 mid 的位置。
https://leetcode.com/problems/sliding-window-median/
\(\color{darkgreen}{Code}\)
1// https://leetcode.com/problems/sliding-window-median/discuss/96340/O(n-log-k)-C%2B%2B-using-multiset-and-updating-middle-iterator 2 3class Solution 4{ 5public: 6 vector<double> medianSlidingWindow(vector<int>& nums, int k) 7 { 8 multiset<int> window(nums.begin(), nums.begin()+k); 9 auto mid = next(window.begin(), k/2); // #include<iterator>,next 返回一个迭代器,指向 window.begin() + k/2 10 vector<double> medians; 11 for(int i = k; i < nums.size(); ++i) 12 { 13 medians.push_back((double(*mid) + *prev(mid, 1 - k%2)) / 2.0); // #include<iterator>,prev 返回一个迭代器,指向 mid - (1 - k%2) 14 15 // 比较插入/删除值与 *mid 的大小关系,共 4 种情况,相应调整 mid 16 17 window.insert(nums[i]); 18 if(nums[i] < *mid) --mid; 19 20 if(nums[i-k] <= *mid) ++mid; 21 window.erase(window.lower_bound(nums[i-k])); // 不能直接 erase(nums[i-k]),会删除所有重复元素 22 } 23 medians.push_back((double(*mid) + *prev(mid, 1 - k%2)) / 2.0); // 最后一个窗口的中位数 24 25 return medians; 26 } 27};
[LeetCode] Find Median from Data Stream 数据流中的中位数。Hint:使用一个最大堆和一个最小堆;保证数据平均分配到两个堆中,两个堆的数据个数之差不超过1;当两个堆的数据个数是偶数(各占一半),新数据插入最小堆,否则插入最大堆(这样最小堆的数据个数总是比最大堆多1或相等);保证最大堆中的数都不大于最小堆中的数。
https://leetcode.com/problems/find-median-from-data-stream/
\(\color{darkgreen}{Code}\)
1class MedianFinder 2{ 3public: 4 MedianFinder() {} 5 6 void addNum(int num) 7 { 8 if((max_heap.size() + min_heap.size()) & 1) 9 { 10 if(!min_heap.empty() && num > min_heap.top()) 11 { 12 // 新数插入最小堆,最小堆中最小的数(堆顶)插入最大堆 13 min_heap.push(num); 14 num = min_heap.top(); 15 min_heap.pop(); 16 } 17 max_heap.push(num); 18 } 19 else 20 { 21 if(!max_heap.empty() && num < max_heap.top()) 22 { 23 // 新数插入最大堆,最大堆中最大的数(堆顶)插入最小堆 24 max_heap.push(num); 25 num = max_heap.top(); 26 max_heap.pop(); 27 } 28 min_heap.push(num); 29 } 30 } 31 32 double findMedian() 33 { 34 if((max_heap.size() + min_heap.size()) & 1) return min_heap.top(); 35 else return (max_heap.top() + min_heap.top()) / 2.0; 36 } 37private: 38 priority_queue<int, vector<int>, less<int>> max_heap; 39 priority_queue<int, vector<int>, greater<int>> min_heap; 40};
4.51. 逆波兰式:转换与求值
https://leetcode.com/problems/evaluate-reverse-polish-notation/
\(\color{darkgreen}{Code}\)
1/* 2中缀表达式转后缀表达式(逆波兰式) 3遍历中缀表达式: 41)如果遇到操作数,则直接输出。 52)如果遇到左括号,则直接压入栈中。 63)如果遇到右括号,则将栈中元素弹出,直到遇到左括号为止;左括号只弹出栈而不输出。 74)如果遇到操作符,则检查栈顶元素优先级,如果其优先级**不低于**当前操作符(左括号除外),则弹出栈顶元素并输出; 8重复此过程直到:栈顶元素优先级小于当前操作符或者栈顶元素为左括号或者栈为空,然后将当前操作符压入栈中。 95)表达式处理完毕,将栈中元素依次弹出。 10注意:只有遇到右括号的情况下才会弹出左括号,其他情况都不会弹出。 11*/ 12 13 14class ReversePolishNotation 15{ 16public: 17 // 假设输入表达式一定是正确的,且只包含个位整数、+-*/、括号 18 vector<char> convertRPN(const string& expr) 19 { 20 vector<char> rpn; 21 if(expr.size() < 1) return rpn; 22 stack<char> op; 23 for(auto& e: expr) 24 { 25 if('0' <= e && e <= '9') rpn.push_back(e); 26 else if(e == '(') op.push(e); 27 else if(e == ')') 28 { 29 while(!op.empty() && op.top()!='(') 30 { 31 rpn.push_back(op.top()); 32 op.pop(); 33 } 34 op.pop(); // pop '(' 35 } 36 // + - * / 37 else 38 { 39 while(!op.empty() && op.top()!='(' && prior.at(op.top())>=prior.at(e)) 40 { 41 rpn.push_back(op.top()); 42 op.pop(); 43 } 44 op.push(e); 45 } 46 } 47 while(!op.empty()) 48 { 49 rpn.push_back(op.top()); 50 op.pop(); 51 } 52 return rpn; 53 } 54private: 55 static const unordered_map<char, int> prior; 56}; 57 58const unordered_map<char, int> ReversePolishNotation::prior = {{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};1/* 2逆波兰式求值 3遍历逆波兰式,遇到操作数就入栈;遇到操作符则出栈两个操作数执行运算,运算结果重新入栈; 4遍历结束时,栈内存放的是最终运算结果。 5*/ 6 7class Solution 8{ 9public: 10 int evalRPN(vector<string>& tokens) 11 { 12 int N = tokens.size(); 13 if(N == 0) return 0; 14 stack<int> stk; 15 int k = 0; 16 int res = 0; 17 while(k < N) 18 { 19 if(tokens[k] != "*" && tokens[k] != "-" && tokens[k] != "+" && tokens[k] != "/") 20 { 21 stk.push(atoi(tokens[k].c_str())); 22 } 23 else 24 { 25 int right_operand = stk.top(); 26 stk.pop(); 27 int left_operand = stk.top(); 28 stk.pop(); 29 switch(tokens[k][0]) 30 { 31 case '+': res = left_operand + right_operand; break; 32 case '-': res = left_operand - right_operand; break; 33 case '*': res = left_operand * right_operand; break; 34 case '/': res = left_operand / right_operand; break; 35 } 36 stk.push(res); 37 } 38 k++; 39 } 40 if(!stk.empty()) 41 { 42 res = stk.top(); 43 stk.pop(); 44 } 45 return res; 46 } 47};
https://leetcode.cn/problems/calculator-lcci
\(\color{darkgreen}{Code}\)
1class Solution: 2 def calculate(self, s: str) -> int: 3 def scan_num(s, i): 4 num = 0 5 while i < len(s) and ord('0') <= ord(s[i]) <= ord('9'): 6 num = num * 10 + int(s[i]) 7 i += 1 8 return i, num 9 def scan_op(s, i): 10 return i + 1, s[i] 11 def eval_op(num_stk, op): 12 rhs = num_stk.pop() 13 lhs = num_stk.pop() 14 if op == '+': 15 num_stk.append(lhs + rhs) 16 elif op == '-': 17 num_stk.append(lhs - rhs) 18 elif op == '*': 19 num_stk.append(lhs * rhs) 20 else: 21 num_stk.append(lhs // rhs) 22 s = s.replace(' ', '') 23 n = len(s) 24 op_stk, num_stk = [], [] 25 prior = {'+': 0, '-': 0, '*': 1, '/': 1} 26 i, num = scan_num(s, 0) 27 num_stk.append(num) 28 while i < n: 29 i, op = scan_op(s, i) 30 while op_stk and prior[op] <= prior[op_stk[-1]]: 31 eval_op(num_stk, op_stk[-1]) 32 op_stk.pop() 33 op_stk.append(op) 34 i, num = scan_num(s, i) 35 num_stk.append(num) 36 while op_stk: 37 eval_op(num_stk, op_stk[-1]) 38 op_stk.pop() 39 return num_stk[0]
4.52. 字典树/前缀树( Trie )
字典树/前缀树的查找时间复杂度是 \(\mathcal{O}(l)\) ,创建一棵树的时间复杂度是 \(\mathcal{O}(nl)\) , 其中 \(l\) 是单词长度, \(n\) 是字典大小。
https://leetcode.com/problems/implement-trie-prefix-tree
\(\color{darkgreen}{Code}\)
1class TrieNode 2{ 3public: 4 TrieNode* child[26]; 5 bool has_word; 6 TrieNode() 7 { 8 fill(child, child+26, nullptr); 9 has_word = false; 10 } 11}; 12 13class Trie 14{ 15public: 16 /** Initialize your data structure here. */ 17 Trie() 18 { 19 root = new TrieNode(); 20 } 21 22 /** Inserts a word into the trie. */ 23 void insert(string word) 24 { 25 TrieNode* p = root; 26 for(auto& c: word) 27 { 28 int branch = c - 'a'; 29 if(p->child[branch] == nullptr) p->child[branch] = new TrieNode(); 30 p = p->child[branch]; 31 } 32 p->has_word = true; 33 } 34 35 /** Returns if the word is in the trie. */ 36 bool search(string word) 37 { 38 TrieNode* p = root; 39 for(auto& c: word) 40 { 41 int branch = c - 'a'; 42 if(p->child[branch] == nullptr) return false; 43 p = p->child[branch]; 44 } 45 return p->has_word; 46 } 47 48 /** Returns if there is any word in the trie that starts with the given prefix. */ 49 bool startsWith(string prefix) 50 { 51 TrieNode* p = root; 52 for(auto& c: prefix) 53 { 54 int branch = c - 'a'; 55 if(p->child[branch] == nullptr) return false; 56 p = p->child[branch]; 57 } 58 return true; 59 } 60private: 61 TrieNode* root; 62};1from collections import defaultdict 2 3class TrieNode(object): 4 def __init__(self): 5 self.dict = defaultdict(TrieNode) 6 self.word = False 7 8class Trie(object): 9 10 def __init__(self): 11 """ 12 Initialize your data structure here. 13 """ 14 self.root = TrieNode() 15 16 def insert(self, word): 17 """ 18 Inserts a word into the trie. 19 :type word: str 20 :rtype: None 21 """ 22 child = self.root 23 for letter in word: 24 child = child.dict[letter] 25 child.word = True 26 27 def search(self, word): 28 """ 29 Returns if the word is in the trie. 30 :type word: str 31 :rtype: bool 32 """ 33 child = self.root 34 for letter in word: 35 child = child.dict.get(letter) 36 if child is None: 37 return False 38 return child.word 39 40 def startsWith(self, prefix): 41 """ 42 Returns if there is any word in the trie that starts with the given prefix. 43 :type prefix: str 44 :rtype: bool 45 """ 46 child = self.root 47 for letter in prefix: 48 child = child.dict.get(letter) 49 if child is None: 50 return False 51 return True
4.53. LRU(Least Recently Used) 缓存机制
Hint:通过双向链表辅以哈希表实现;双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的;哈希表将缓存数据的 key 映射到其在双向链表中的位置;访问和插入数据的时间复杂度都是 \(\mathcal{O}(1)\) 。
https://leetcode.com/problems/lru-cache/
\(\color{darkgreen}{Code}\)
1class LRUCache 2{ 3public: 4 LRUCache(int capacity): cache_capacity(capacity){} 5 6 int get(int key) 7 { 8 if(cache_loc.find(key) == cache_loc.end()) return -1; 9 auto p = cache_loc[key]; 10 int value = p->second; 11 cache.erase(p); 12 cache.emplace_front(key, value); // 最新访问的数据需要移到链表头部 13 cache_loc[key] = cache.begin(); 14 return value; 15 } 16 17 void put(int key, int value) 18 { 19 if(cache_loc.find(key) != cache_loc.end()) 20 { 21 auto p = cache_loc[key]; 22 cache.erase(p); 23 cache.emplace_front(key, value); 24 cache_loc[key] = cache.begin(); 25 return; 26 27 } 28 if(cache.size() == cache_capacity) 29 { 30 auto tail = cache.back(); 31 cache.pop_back(); 32 cache_loc.erase(tail.first); 33 } 34 cache.emplace_front(key, value); 35 cache_loc[key] = cache.begin(); 36 } 37private: 38 int cache_capacity; 39 list<pair<int,int>> cache; 40 unordered_map<int, list<pair<int,int>>::iterator> cache_loc; 41};
4.54. [LeetCode] House Robber II 环形房屋偷盗
Hint:在区间 \([0, n-2]\) 和区间 \([1, n-1]\) 分别做动态规划。
https://leetcode-cn.com/problems/house-robber-ii/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int rob(vector<int>& nums) 5 { 6 int n = nums.size(); 7 if(n == 0) return 0; 8 if(n == 1) return nums[0]; 9 int res = 0; 10 _rob(nums, 0, n-2, res); 11 _rob(nums, 1, n-1, res); 12 return res; 13 } 14private: 15 void _rob(vector<int>& nums, int from, int to, int& res) 16 { 17 vector<int> dp(nums); 18 res = max(res, nums[from]); 19 for(int i = from+1; i <= to; ++i) 20 { 21 for(int j = from; j < i-1; ++j) dp[i] = max(dp[i], dp[j] + nums[i]); 22 res = max(res, dp[i]); 23 } 24 } 25};
4.55. [LeetCode] Serialize And Deserialize Binary Tree 序列化与反序列化二叉树
https://leetcode-cn.com/problems/h54YBf/
https://leetcode.com/problems/serialize-and-deserialize-binary-tree/
\(\color{darkgreen}{Code}\)
1class Codec { 2public: 3 // Encodes a tree to a single string. 4 string serialize(TreeNode* root) { 5 // # 作为空节点, $ 用于分割不同节点的 val 6 if(!root) return "#"; 7 return to_string(root->val) + 8 "$" + 9 serialize(root->left) + 10 serialize(root->right); 11 } 12 13 // Decodes your encoded data to tree. 14 TreeNode* deserialize(string data) { 15 if(data.empty()) return nullptr; 16 int i = 0; 17 TreeNode* root = deserialize(data, i); 18 return root; 19 } 20private: 21 TreeNode* deserialize(string& data, int& i) 22 { 23 if(data[i] == '#') 24 { 25 // 注意这里 i 一定要自增 26 ++i; 27 return nullptr; 28 } 29 int val = scanDigit(data, i); 30 TreeNode* node = new TreeNode(val); 31 node->left = deserialize(data, i); 32 node->right = deserialize(data, i); 33 return node; 34 } 35 int scanDigit(string& data, int& i) 36 { 37 if(i == data.size()) return -9999; 38 bool pos = true; 39 if(data[i] == '-') 40 { 41 pos = false; 42 i++; 43 } 44 int res = 0; 45 while(i < data.size() && '0' <= data[i] && data[i] <= '9') 46 { 47 res = 10 * res + data[i] - '0'; 48 i++; 49 } 50 ++i; // 略过 $ 51 return pos? res: -1*res; 52 } 53};
4.56. [LeetCode] 生存人数:统计生存人数最多的年份
Hint:用差分数组记录出生年份和死亡年份的人数变化,差分数组的前缀和表示该年份的生存人数。
https://leetcode.cn/problems/living-people-lcci/
\(\color{darkgreen}{Code}\)
1class Solution 2{ 3public: 4 int maxAliveYear(vector<int>& birth, vector<int>& death) 5 { 6 const int SY = 1900; 7 const int EY = 2000; 8 int count[EY - SY + 2] = {0}; 9 int N = birth.size(); 10 for(int i = 0; i < N; ++i) 11 { 12 count[birth[i] - SY] += 1; 13 count[death[i] + 1 - SY] -= 1; // 区间 [birth[i], death[i]] 内生存人数都+1 14 } 15 int most = 0, year = -1; 16 int sum = 0; 17 for(int k = 0; k <= EY - SY; ++k) 18 { 19 sum += count[k]; 20 if(sum > most) 21 { 22 most = sum; 23 year = k + SY; 24 } 25 } 26 return year; 27 } 28};
4.57. [LeetCode] 验证栈的压入、弹出序列
Hint:如果下一个弹出的数字正好是栈顶数字,那么直接弹出;如果下一个弹出的数字不在栈顶,就把压栈序列中还没入栈的数字 压入辅助栈,直到把下一个需要弹出的数字压入栈为止。如果所有的数字都压入栈了,仍然没找到下一个弹出的数字,那么该序列 不可能是一个弹出序列。
https://leetcode.com/problems/validate-stack-sequences
\(\color{darkgreen}{Code}\)
1class Solution: 2 def validateStackSequences(self, pushed: List[int], popped: List[int]) -> bool: 3 stk = [] 4 i, j = 0, 0 5 n = len(pushed) 6 while i < n and j < n: 7 if len(stk) > 0 and stk[-1] == popped[j]: 8 stk.pop() 9 j += 1 10 else: 11 stk.append(pushed[i]) 12 i += 1 13 if i == n: 14 while len(stk) > 0 and stk[-1] == popped[j]: 15 stk.pop() 16 j += 1 17 if j == n: return True 18 return False
4.58. [LeetCode] 和为 s 的连续正数序列
Hint:双指针。
https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/
\(\color{darkgreen}{Code}\)
1class Solution: 2 def findContinuousSequence(self, target: int) -> List[List[int]]: 3 res = [] 4 begin, end = 1, 2 5 s = begin + end 6 while begin < end: 7 if s == target: 8 res.append(list(range(begin, end+1))) 9 s -= begin 10 begin += 1 11 end += 1 12 s += end 13 elif s < target: 14 end += 1 15 s += end 16 else: 17 s -= begin 18 begin += 1 19 return res
延伸:统计和为 n 的序列的个数。
https://leetcode.com/problems/consecutive-numbers-sum
\[\begin{split}x + (x+1) + \cdots + (x+k-1) & = \frac{k(2x+k-1)}{2},\ x > 0,\ k > 0 \\ & = kx + \frac{k(k-1)}{2} \\ & \geq \frac{k(k+1)}{2}\end{split}\]\(\color{darkgreen}{Code}\)
1import math 2class Solution: 3 def consecutiveNumbersSum(self, n: int) -> int: 4 ans = 0 5 for k in range(1, int(math.sqrt(2*n))+1): 6 s = k*(k-1)//2 7 if (n - s) % k == 0: 8 ans += 1 9 return ans
4.59. [LeetCode] Rotting Oranges 腐败的橙子
Hint:多源 BFS,同时从各个源推进一步;双端队列。
https://leetcode.com/problems/rotting-oranges
\(\color{darkgreen}{Code}\)
1class Solution: 2 def orangesRotting(self, grid: List[List[int]]) -> int: 3 if not grid or not grid[0]: return 0 4 m, n = len(grid), len(grid[0]) 5 maxd = 100 6 dq = deque() 7 fresh = 0 8 for x in range(m): 9 for y in range(n): 10 if grid[x][y] == 2: 11 dq.append((x,y)) 12 elif grid[x][y] == 1: 13 fresh += 1 14 minutes = 0 15 ## fresh = 0 就不需要再遍历了 16 while len(dq) and fresh > 0: 17 ## sn 代表源的数量 18 sn = len(dq) 19 for i in range(sn): 20 x, y = dq.popleft() 21 for dx, dy in [[-1,0], [0,-1], [0,1], [1,0]]: 22 _x, _y = x + dx, y + dy 23 if 0 <= _x < m and 0 <= _y < n and grid[_x][_y] == 1: 24 grid[_x][_y] = 2 25 dq.append((_x, _y)) 26 fresh -= 1 27 minutes += 1 28 if fresh == 0: return minutes 29 else: return -1
4.60. [LeetCode] Maximum Width of Binary Tree 二叉树最大宽度
Hint:层序遍历,计算每层最左边和最右边节点的序号之差作为该层的宽度。
https://leetcode.com/problems/maximum-width-of-binary-tree
\(\color{darkgreen}{Code}\)
1# Definition for a binary tree node. 2# class TreeNode: 3# def __init__(self, val=0, left=None, right=None): 4# self.val = val 5# self.left = left 6# self.right = right 7class Solution: 8 def widthOfBinaryTree(self, root: Optional[TreeNode]) -> int: 9 if root is None: return 0 10 dq = deque([(root, 1)]) 11 r = [] 12 while len(dq): 13 n = len(dq) 14 ## 为了避免溢出,每层最左侧的节点编号都从 1 开始 15 ## r 保存每一层最右侧的节点的编号,其值就等于相应层的宽度 16 init = dq[0][1] 17 r.append(dq[-1][1] - init + 1) 18 for i in range(n): 19 p, idx = dq.popleft() 20 idx = idx - init + 1 21 if p.left: 22 ## 左节点编号 23 dq.append((p.left, idx*2)) 24 if p.right: 25 ## 右节点编号 26 dq.append((p.right, idx*2+1)) 27 return max(r)
4.61. [LeetCode] Restore The Array 从字符串恢复整数数组
Hint:动态规划;整数不能以 0 开头;注意跳过数值范围明显越界的分割方法。
https://leetcode.com/problems/restore-the-array
\(\color{darkgreen}{Code}\)
1class Solution: 2 def numberOfArrays(self, s: str, k: int) -> int: 3 mod = 1000000007 4 n = len(s) 5 nk = len(str(k)) 6 dp = [0] * n 7 dp[0] = 1 8 for i in range(1, n): 9 if s[i] != '0': 10 ## s[i] 单独分割 11 dp[i] = dp[i-1] 12 else: 13 dp[i] = 0 14 ## s[j:i+1] 单独分割 15 ## 不需要去遍历明显越界的 s[j:i+1] 16 for j in range(max(0, i-nk), i): 17 if s[j] != '0': 18 dji = int(s[j:i+1]) 19 if dji <= k: 20 dp[i] += dp[j-1] if j > 0 else 1 21 dp[i] %= mod 22 return dp[-1]
4.62. 重叠区间问题
一般使用贪心算法,先对序列排序;维护区间 start/end 的最小值。
[LeetCode] Non-overlapping Intervals 不重叠区间。Hint:贪心算法,总是选择结束最早的区间。
https://leetcode.com/problems/non-overlapping-intervals
\(\color{darkgreen}{Code}\)
1class Solution: 2 def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: 3 intervals = sorted(intervals, key=lambda x: x[1]) 4 pend = float("-inf") 5 cnt = 0 6 for start, end in intervals: 7 if start >= pend: 8 pend = end 9 else: 10 cnt += 1 11 return cnt
[LeetCode] Minimum Number of Arrows to Burst Balloons 用最少数量的箭引爆气球。Hint:贪心算法。
https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons
\(\color{darkgreen}{Code}\)
1class Solution: 2 def findMinArrowShots(self, points: List[List[int]]) -> int: 3 points = sorted(points, key = lambda x: (x[1], x[0])) 4 min_end = - 2**31 - 1 5 ans = 0 6 for start, end in points: 7 if start > min_end: 8 ## 在 x = min_end 处发射一支箭 9 min_end = end 10 ans += 1 11 return ans
[LeetCode] Maximum Beauty of an Array After Applying Operation 数组最大美感(最大重叠区间个数)。
https://leetcode.com/problems/maximum-beauty-of-an-array-after-applying-operation
\(\color{darkgreen}{Code}\)
1from collections import deque 2class Solution: 3 def maximumBeauty(self, nums: List[int], k: int) -> int: 4 intervals = [] 5 for num in nums: 6 intervals.append((num - k, num + k)) 7 intervals = sorted(intervals, key=lambda x: x[1]) 8 beauty = 1 9 n = len(nums) 10 dq = deque() 11 for i in range(n): 12 s, e = intervals[i] 13 while len(dq) and intervals[dq[0]][1] < s: 14 dq.popleft() 15 dq.append(i) 16 beauty = max(beauty, len(dq)) 17 return beauty
4.63. [LeetCode] Stone Game 石头游戏
Hint:记忆化递归;动态规划。
https://leetcode.com/problems/stone-game-ii
\(\color{darkgreen}{Code}\)
1class Solution: 2 def stoneGameII(self, piles: List[int]) -> int: 3 m = 1 4 k = len(piles) 5 ## 记录状态 (i, m) 的最大分 6 mem = [[-1]*k for _ in range(k)] 7 return self.maxScore(piles, 0, m, mem) 8 9 def maxScore(self, piles, i, m, mem): 10 if len(piles) - i <= 2*m: 11 return sum(piles[i:]) 12 if mem[i][m] >= 0: 13 return mem[i][m] 14 ## 拿完 piles[i:i+x] 之后,要等对方从 i+x 开始拿,之后能拿到的最大分是 sum(piles[i+x:]) - self.maxScore(piles, i+x, max(m, x), mem) 15 mem[i][m] = max([sum(piles[i:i+x]) + sum(piles[i+x:]) - self.maxScore(piles, i+x, max(m, x), mem) for x in range(1, 2*m+1)]) 16 return mem[i][m]
https://leetcode.com/problems/stone-game-iii
\(\color{darkgreen}{Code}\)
1INT_MIN = -0x80000000 2class Solution: 3 def stoneGameIII(self, stoneValue: List[int]) -> str: 4 n = len(stoneValue) 5 totalScore = sum(stoneValue) 6 ## 后 n 项和,避免后续重复计算 7 accuScore = [0] * (n+1) 8 for i in range(n-1, -1, -1): 9 accuScore[i] = accuScore[i+1] + stoneValue[i] 10 ## dp[i]:从第 i 堆石头开始拿能够得到的最大分 11 dp = [0] * (n+1) 12 for i in range(n-1, -1, -1): 13 takeOne = stoneValue[i] + accuScore[i+1] - dp[i+1] 14 takeTwo = INT_MIN 15 if i < n - 1: 16 takeTwo = sum(stoneValue[i:i+2]) + accuScore[i+2] - dp[i+2] 17 takeThree = INT_MIN 18 if i < n - 2: 19 takeThree = sum(stoneValue[i:i+3]) + accuScore[i+3] - dp[i+3] 20 dp[i] = max(takeOne, takeTwo, takeThree) 21 22 aliceScore = dp[0] << 1 23 if aliceScore == totalScore: 24 return "Tie" 25 if aliceScore > totalScore: 26 return "Alice" 27 return "Bob"
4.64. 栈和队列的实现
https://leetcode.com/problems/implement-queue-using-stacks
Hint:总在第一个栈压入最新元素,在第二个栈弹出最旧元素。
\(\color{darkgreen}{Code}\)
1class MyQueue 2{ 3public: 4 MyQueue() {} 5 6 void push(int x) 7 { 8 s1.push(x); 9 } 10 11 int pop() 12 { 13 int front = peek(); 14 s2.pop(); 15 return front; 16 } 17 18 int peek() 19 { 20 if(s2.empty()) 21 { 22 while(!s1.empty()) 23 { 24 s2.push(s1.top()); 25 s1.pop(); 26 } 27 } 28 return s2.top(); 29 } 30 31 bool empty() 32 { 33 return s1.empty() && s2.empty(); 34 } 35private: 36 stack<int> s1; 37 stack<int> s2; 38};
https://leetcode.com/problems/implement-stack-using-queues
Hint:总在第一个队列压入最新元素。
\(\color{darkgreen}{Code}\)
1class MyStack 2{ 3public: 4 MyStack() {} 5 6 void push(int x) 7 { 8 // 保证 q1 数据比 q2 新 9 q1.push(x); 10 } 11 12 int pop() 13 { 14 // 优先从 q1 弹出 15 if(!q1.empty()) 16 { 17 while(q1.size() > 1) 18 { 19 q2.push(q1.front()); 20 q1.pop(); 21 } 22 int t = q1.front(); 23 q1.pop(); 24 return t; 25 } 26 else 27 { 28 while(q2.size() > 1) 29 { 30 q1.push(q2.front()); 31 q2.pop(); 32 } 33 int t = q2.front(); 34 q2.pop(); 35 return t; 36 } 37 } 38 39 int top() 40 { 41 if(!q1.empty()) 42 { 43 while(q1.size() > 1) 44 { 45 q2.push(q1.front()); 46 q1.pop(); 47 } 48 return q1.front(); 49 } 50 else 51 { 52 while(q2.size() > 1) 53 { 54 q1.push(q2.front()); 55 q2.pop(); 56 } 57 int t = q2.front(); 58 // 下面这一步很重要,保证 q2 的数据总是比 q1 旧 59 q1.push(t); 60 q2.pop(); 61 return t; 62 } 63 } 64 65 bool empty() 66 { 67 return q1.empty() && q2.empty(); 68 } 69private: 70 queue<int> q1; 71 queue<int> q2; 72};
4.65. 单调栈
单调栈(Monotonic Stack)在栈的「先进后出」规则基础上,要求从栈顶到栈底的元素单调递增/递减。一般情况下,数组的每个元素都入栈一次,且至多出栈一次,因此时间复杂度是 \(\mathcal{O}(n)\) 。
[LeetCode] Daily Temperatures 每日温度。
https://leetcode.com/problems/daily-temperatures
\(\color{darkgreen}{Code}\)
1class Solution(object): 2 def dailyTemperatures(self, temperatures): 3 """ 4 :type temperatures: List[int] 5 :rtype: List[int] 6 """ 7 n = len(temperatures) 8 ans = [0] * n 9 stk = [] 10 ## 逆序遍历,栈顶元素最小 11 for i in range(n-1, -1, -1): 12 while stk and temperatures[i] >= temperatures[stk[-1]]: 13 stk.pop() 14 if stk: 15 ans[i] = stk[-1] - i 16 ## 入栈 17 stk.append(i) 18 return ans
[LeetCode] Next Greater Element 下一个更大的元素。延伸:数组为循环数组,Hint:把原数组拷贝一遍追加在尾部,就可以模拟循环数组。
https://leetcode.com/problems/next-greater-element-i
\(\color{darkgreen}{Code}\)
1class Solution: 2 def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]: 3 ## 用字典存储已经找到的结果 4 mp = {} 5 stk = [] 6 ## 逆序遍历,栈顶元素最小 7 for num in nums2[::-1]: 8 while stk and num >= stk[-1]: 9 stk.pop() 10 if stk: 11 mp[num] = stk[-1] 12 else: 13 mp[num] = -1 14 ## 入栈 15 stk.append(num) 16 ans = [] 17 for num in nums1: 18 ans.append(mp[num]) 19 return ans
https://leetcode.com/problems/next-greater-element-ii
\(\color{darkgreen}{Code}\)
1class Solution(object): 2 def nextGreaterElements(self, nums): 3 """ 4 :type nums: List[int] 5 :rtype: List[int] 6 """ 7 n = len(nums) 8 ## 把 nums 拷贝一遍就可以模拟循环数组 9 nums.extend(nums[:]) 10 stk = [] 11 ans = [] 12 for i in range(2*n-1, -1, -1): 13 while stk and nums[i] >= stk[-1]: 14 stk.pop() 15 if i < n: 16 if stk: 17 ans.append(stk[-1]) 18 else: 19 ans.append(-1) 20 stk.append(nums[i]) 21 return ans[::-1]
[LeetCode] Largest Rectangle in Histogram 直方图中的最大矩形面积。
https://leetcode.com/problems/largest-rectangle-in-histogram
\(\color{darkgreen}{Code}\)
1class Solution(object): 2 def largestRectangleArea(self, height): 3 """ 4 :type height: List[int] 5 :rtype: int 6 """ 7 if not height: 8 return 0 9 ## 尾部追加 -1,方便计算以最后一个 bar 结尾的矩形面积 10 height.append(-1) 11 ans = 0 12 stk = [] 13 ## 栈顶元素最大 14 for i in range(len(height)): 15 while stk and height[i] < height[stk[-1]]: 16 last = stk[-1] 17 stk.pop() 18 h = height[last] 19 pre = stk[-1] if stk else -1 20 ## 计算 [pre + 1, i - 1] 之间的 bar 构成的矩形面积 21 ## pre = -1 代表高度最低的矩形,面积为 h * i 22 ans = max(ans, h * (i - 1 - pre)) 23 ## 入栈 24 stk.append(i) 25 return ans
[LeetCode] Maximal Rectangle 矩阵中的最大矩形。Hint:以 \(0 \sim i-1\) 行为底,以第 \(i\) 行为顶,将问题转化为「直方图中的最大矩形面积」进行求解,其中直方图的高度为连续 1 的数量。
https://leetcode.com/problems/maximal-rectangle
\(\color{darkgreen}{Code}\)
1class Solution: 2 def maximalRectangle(self, matrix: List[List[str]]) -> int: 3 rows, cols = len(matrix), len(matrix[0]) 4 height = [0] * cols 5 ans = 0 6 for r in range(rows): 7 for c in range(cols): 8 if matrix[r][c] == '1': 9 height[c] += 1 10 else: 11 height[c] = 0 ## 注意,这里不是累加 12 ans = max(ans, self.largestRectangleArea(height)) 13 return ans 14 15 def largestRectangleArea(self, height): 16 """ 17 :type height: List[int] 18 :rtype: int 19 """ 20 if not height: 21 return 0 22 ## 尾部追加 -1,方便计算以最后一个 bar 结尾的矩形面积 23 height.append(-1) 24 ans = 0 25 stk = [] 26 ## 栈顶元素最大 27 for i in range(len(height)): 28 while stk and height[i] < height[stk[-1]]: 29 last = stk[-1] 30 stk.pop() 31 h = height[last] 32 pre = stk[-1] if stk else -1 33 ## 计算 [pre, i) 之间的 bar 构成的矩形面积 34 ## pre = -1 代表高度最低的矩形,面积为 h * i 35 ans = max(ans, h * (i - 1 - pre)) 36 ## 入栈 37 stk.append(i) 38 return ans
[LeetCode] Beautiful Towers I 美丽塔。Hint:两个单调栈(正向 + 反向),动态规划。
https://leetcode.com/problems/beautiful-towers-i
\(\color{darkgreen}{Code}\)
1class Solution: 2 def maximumSumOfHeights(self, maxHeights: List[int]) -> int: 3 n = len(maxHeights) 4 ## prefix[i] 表示区间 [0, i] 能构成的非递减数组的元素和最大值,且位置 i 是山顶,高度为 maxHeights[i] 5 ## suffix[i] 表示区间 [i, n) 能构成的非递增数组的元素和最大值,且位置 i 是山顶,高度为 maxHeights[i] 6 ## 以位置 i 为山顶的 mountain 数组的元素和为:prefix[i] + suffix[i] - maxHeights[i] 7 prefix = [0] * n 8 suffix = [0] * n 9 ## stk 保存下标 10 stk = [] 11 for i in range(n): 12 while stk and maxHeights[stk[-1]] > maxHeights[i]: 13 stk.pop() 14 if not stk: 15 ## 区间 [0, i] 的高度统一设为 maxHeights[i] 16 prefix[i] = (i + 1) * maxHeights[i] 17 else: 18 ## 动态规划 19 ## 区间 (stk[-1], i] 的高度统一设为 maxHeights[i] 20 prefix[i] = prefix[stk[-1]] + (i - stk[-1]) * maxHeights[i] 21 stk.append(i) 22 stk = [] 23 for i in range(n-1, -1, -1): 24 while stk and maxHeights[stk[-1]] > maxHeights[i]: 25 stk.pop() 26 if not stk: 27 suffix[i] = (n - i) * maxHeights[i] 28 else: 29 suffix[i] = suffix[stk[-1]] + (stk[-1] - i) * maxHeights[i] 30 stk.append(i) 31 ans = 0 32 for i in range(n): 33 ans = max(ans, prefix[i] + suffix[i] - maxHeights[i]) 34 return ans
4.66. 单词接龙
[LeetCode] Word Ladder 单词接龙(最短路径的长度)。Hint:BFS。
https://leetcode.com/problems/word-ladder
\(\color{darkgreen}{Code}\)
1## l 单词长度,n 单词数量 2## 预先计算好两两单词之间距离的时间复杂度是 O(ln^2),这还不包括 dfs/bfs 每次遍历距离矩阵的时间 3## 遍历替换单词每一个字母的时间复杂度是 O(26ln),基于哈希表的 set 查找时间是 O(1) 4 5from queue import Queue 6class Solution: 7 def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: 8 wordSet = set(wordList) 9 q = Queue() 10 q.put((beginWord, 1)) 11 usedSet = set([beginWord]) 12 while not q.empty(): 13 word, l = q.get() 14 if word == endWord: 15 return l 16 for i in range(len(word)): 17 ## 改变第 i 个字母 18 wordP1, wordP2 = word[:i], word[i+1:] 19 for c in "abcdefghijklmnopqrstuvwxyz": 20 nextWord = wordP1 + c + wordP2 21 if c != word[i] and nextWord not in usedSet and nextWord in wordSet: 22 usedSet.add(nextWord) 23 q.put((nextWord, l+1)) 24 return 0
[LeetCode] Word Ladder II 单词接龙(最短路径集合)。Hint:如果直接在 BFS 的时候存储路径,需要很大的空间。首先用 BFS 确定路径长度,再用 DFS 沿着路径长度递减的方向查找。
https://leetcode.com/problems/word-ladder-ii
\(\color{darkgreen}{Code}\)
1## bfs 从 endWord 出发,这样在 dfs 的时候从 beginWord 出发、沿着 dist 减小的方向一定是通向 endWord 的方向。 2## 如果 bfs 从 beginWord 出发,那么 dfs 沿着 dist 增大的方向是发散的、不一定是通向 endWord 的方向。 3 4from queue import Queue 5class Solution: 6 def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]: 7 self.wordSet = set(wordList) 8 self.wordSet.add(beginWord) ## beginWord 加入 wordSet,需要计算路径长度 9 self.distDict = {} ## 存储 endWord 到其他 word 的路径长度 10 self.bfs(endWord) ## 从 endWord 出发 11 res, seq = [], [] 12 self.dfs(beginWord, endWord, seq, res) 13 return res 14 15 def bfs(self, startWord): 16 q = Queue() 17 q.put((startWord, 0)) 18 self.distDict[startWord] = 0 19 while not q.empty(): 20 word, dist = q.get() 21 nextWordList = self.getNextWordList(word) 22 for nextWord in nextWordList: 23 if nextWord not in self.distDict: 24 q.put((nextWord, dist + 1)) 25 self.distDict[nextWord] = dist + 1 26 27 def dfs(self, beginWord, endWord, seq, res): 28 seq.append(beginWord) 29 if beginWord == endWord: 30 res.append(seq[:]) ## 注意:这里要对 seq 拷贝 31 else: 32 nextWordList = self.getNextWordList(beginWord) 33 for nextWord in nextWordList: 34 if nextWord in self.distDict and self.distDict[nextWord] == self.distDict[beginWord] - 1: 35 self.dfs(nextWord, endWord, seq, res) 36 seq.pop() 37 38 def getNextWordList(self, word): 39 nextWordList = [] 40 for i in range(len(word)): 41 wordP1, wordP2 = word[:i], word[i+1:] 42 for c in "abcdefghijklmnopqrstuvwxyz": 43 nextWord = wordP1 + c + wordP2 44 if c != word[i] and nextWord in self.wordSet: 45 nextWordList.append(nextWord) 46 return nextWordList