4. 编程算法

4.1. 找出数组中的特异数字(Single Number)

4.2. 均匀分布生成其他分布的方法

Hint:中心极限定理。

https://blog.csdn.net/haolexiao/article/details/60511164

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:方法一,先求两个链表的长度差,双指针法;方法二,分别用栈保存两个链表的节点的地址(指针),从后往前比较。如果只需要判断两个链表是否相交,只需判断两个链表最后一个节点是否相同。

    https://blog.csdn.net/jiary5201314/article/details/50990349

  • 单链表 \(\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};
    
  • 最多两次交易

    http://www.cnblogs.com/grandyang/p/4281975.html

  • 最多k次交易

    http://www.cnblogs.com/grandyang/p/4295761.html

    https://blog.csdn.net/linhuanmars/article/details/23236995

  • 交易冷却

    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\) 使得

\[(1+2+...+n) - x = \frac{n(n+1)}{2} - x\]

是非负偶数。跳到 \(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

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

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://leetcode.com/problems/count-different-palindromic-subsequences/discuss/272297/DP-C%2B%2B-Clear-solution-explained

    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
 5class Solution
 6{
 7public:
 8    bool wordBreak(string s, vector<string>& wordDict)
 9    {
10        if(s == "") return true;
11        if(wordDict.size() == 0) return false;
12        return wordFind(s, wordDict, 0);
13    }
14private:
15    bool wordFind(string& s, vector<string>& wordDict, int k)
16    {
17        if(k == s.size()) return true;
18        for(int w = 0; w < wordDict.size(); ++w)
19        {
20            if(k+wordDict[w].size()<=s.size() && s.substr(k, wordDict[w].size()) == wordDict[w])
21            {
22                if(wordFind(s, wordDict, k + wordDict[w].size())) return true;
23            }
24        }
25        return false;
26    }
27};
 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
 4class Solution {
 5public:
 6    bool wordBreak(string s, vector<string>& wordDict) {
 7        if(s.empty() || wordDict.empty()) return false;
 8
 9        int n = s.size();
10        vector<bool> dp(n+1, false);
11        dp[0] = true; // 初始化
12
13        for(unsigned int i = 1; i <= n; ++i)
14        {
15           for(unsigned int j = 0; j < i; ++j)
16           {
17               if(dp[j]) // 两段子串:[0, j-1], [j, i]
18               {
19                   string str = s.substr(j, i-j);
20                   for(string& word: wordDict)
21                   {
22                       if(str == word)
23                       {
24                           dp[i] = true;
25                           break;
26                       }
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};

4.42. [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.43. [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.44. [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.45. 丢弃的数

\(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.46. [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.47. [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.48. [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.49. 滑动窗口

  • [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.50. 逆波兰式:转换与求值

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

4.51. 字典树/前缀树( 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.52. 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.53. [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.54. [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.55. [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.56. [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.57. [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.58. [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.59. [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.60. [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.61. 重叠区间问题

一般使用贪心算法,先对序列排序;维护区间 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:贪心算法。

    \(\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
    

4.62. [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.63. 栈和队列的实现

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.64. 单调栈

单调栈(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, i) 之间的 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.65. 单词接龙

  • [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