leetcode 100热题二分篇

leetcode 100热题二分篇

本篇还包含了其他关于二分的题目

4. 寻找两个正序数组的中位数

算法

本题其实和二分关系不大 核心是找出第k个数 其中k = (M + N) / 2 (此时是针对一个数组的情况 ) 对于两个数组就要对于各自k/2 的数进行讨论.以下为具体思路:

给定两个有序的数组,找中位数(n + m) / 2,等价于找第k小的元素,k = (n + m) / 2

  • 1、当一共有偶数个数时,找到第total / 2left和第total / 2 + 1right,结果是(left + right / 2.0)
  • 2、当一共有奇数个数时,找到第total / 2 + 1小,即为结果

如何找第k小?
2020-06-09_170256.jpg

  • 1、默认第一个数组比第二个数组的有效长度小
  • 2、第一个数组的有效长度从i开始,第二个数组的有效长度从j开始,其中[i,si - 1]是第一个数组的前k / 2个元素,[j, sj - 1]是第二个数组的前k - k / 2个元素
  • 3、当nums1[si - 1] > nums2[sj - 1]时,则表示第k小一定在[i,n][sj,m]
  • 4、当nums1[si - 1] <= nums2[sj - 1]时,则表示第k小一定在[si,n][j,m]

代码

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int tot = nums1.size() + nums2.size();
        //偶数 中位数为中间两个数的平均数
        if(tot % 2 == 0){
            int left = find(nums1,0,nums2,0,tot / 2);
            int right = find(nums1,0,nums2,0,tot / 2 + 1);
            return (left + right) / 2.0;//2.0 是为了保证输出值为浮点数
        }else return find(nums1,0,nums2,0,tot / 2 + 1);
    }

    int find(vector<int>& nums1,int i,vector<int>& nums2,int j,int k)
    {
        // 保证前一个数组大小比第二个数组小,[i, nums1.size() - 1] 表示数组 1 的长度,[j, nums2.size() - 1] 表示数组 2 的长度
        if(nums1.size() - i > nums2.size() - j) return find(nums2,j,nums1,i,k);
        // 如果第一个数组遍历结束,则返回第二个数组
        if(nums1.size() == i) return nums2[j + k - 1];//k从1开始
        // 当取出第 1 个元素
        if(k == 1) return min(nums1[i],nums2[j]);
        //第一个数组长度较小 si 取min(nums1.size(),i + k / 2) 防止越界
        int si = min((int)nums1.size(),i + k / 2),sj = j + k - k / 2;
        //当nums1[si - 1] > nums2[sj - 1]时,则表示第k小一定在[i,n]与[sj,m]中 
        //此时[j,sj] 是要删去的部分
        if(nums1[si - 1] > nums2[sj - 1]) return find(nums1,i,nums2,sj,k - (sj - j));
        else return find(nums1,si,nums2,j,k - (si - i));

    }
};

33.搜索旋转排序数组

这道题其实是要我们明确「二分」的本质是什么。

「二分」不是单纯指从有序数组中快速找某个数,这只是「二分」的一个应用。

「二分」的本质是两段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。

经过旋转的数组,显然前半段满足 >= nums[0],而后半段不满足 >= nums[0]。我们可以以此作为依据,通过「二分」找到旋转点。

0.png

找到旋转点之后,再通过比较 targetnums[0] 的大小,确定 target 落在旋转点的左边还是右边。

代码

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int l = 0,r = nums.size() - 1;
        //首先二分两端区间的旋转点
        while(l < r)
        {
            int mid = (l + r + 1) >> 1;
            if(nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }
        //判断target在旋转点的左边还是右边
        if(target >= nums[0]) l = 0;//在左边
        else l = r + 1,r = nums.size() - 1;//在右边

        // 在区间 [l, r] 内二分 target
        while(l < r)
        {
            int mid = (l + r) >> 1;
            if(nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        if(nums[r] == target) return r;
        return -1;

        


    }
};

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

解题思路

(二分) O(logn)

题目要求找两个位置,显然需要做两次二分
二分左端点的条件:找到第一个大于等于 target 的数
二分结束,如果左端点对应的值不是 target,说明 target不存在,直接返回 −1,−1,否则继续二分右端点
二分右端点的条件:找到最后一个小于等于 target 的数

时间复杂度

二分的时间复杂度为 O(logn)

代码

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.empty()) return {-1,-1};
        int l = 0,r = nums.size() - 1;
        while(l < r)
        {
            int mid = (l + r) >> 1;
            if(nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        if(nums[r] != target) return {-1,-1};
        int L = r;//记录左端点
        l = 0,r = nums.size() - 1;
        while(l < r)
        {
            int mid = l + r + 1>> 1;
            if(nums[mid] <= target) l = mid;
            else r = mid - 1;
        }
        return {L,r};

    }
};

2529.正整数和负整数的最大计数

二分查找

思路与算法

由于数组呈现非递减顺序,因此可通过二分查找定位第一个数值大于等于 0 的位置 pos1 及第一个数值大于等于 1 的下标 pos2。假定 n 表示数组长度,且数组下标从 0,则负数的个数为 pos1,正数的个数为 npos2,返回这两者的较大值即可。

代码

class Solution {
public:
    //返回>=val 的数的第一个下标
    int lowerBound(vector<int>& nums,int val)
    {
        int l = 0,r = nums.size();
        while(l < r)
        {
            int mid = (l + r) >> 1;
            if(nums[mid] >= val)  r = mid;
            else l = mid + 1;
        } 
        return l;
    }
    int maximumCount(vector<int>& nums) {
        int neg = lowerBound(nums,0);//负整数个数
        int pos = nums.size() - lowerBound(nums,1);//正整数个数
        return max(neg,pos);
    }
};

2300.咒语和药水的成功对数

为了方便,我们将 spells 记为 a,将 potions 记为 b,将 success 记为 t

对于每个 a[i],有多少个 b[j] 满足 a[ib[j]⩾t,等价于问数组 b 中值大于等于 a[i]t 的个数,这容易让我们想到先对数组 b 排升序,再通过二分找到满足该条件的最小下标,从该下标到数组结尾,均为满足条件的 b[j]。

代码

class Solution {
public:
    vector<int> successfulPairs(vector<int>& spells, vector<int>& potions, long long success) {
        int n = spells.size(),m = potions.size();
        vector<int> res(n);
        sort(potions.begin(),potions.end());
        for(int i = 0;i < n;i++)
        {
            //这里 *1.0 是为了转换为浮点数
            double target = success * 1.0 / spells[i];
            int l = 0,r = m - 1;
            while(l < r)
            {
                int mid = l + r >> 1;
                if(potions[mid] >= target) r = mid;
                else l = mid + 1;
            }
            // 最后特判一下 不满足 则res[i] = 0
            if((long long)spells[i] * potions[r]  >= success)  res[i] = m - r;
        }
        return res;
    }
};

275.H指数II

基本分析

为了方便,将 citations 记为 cs

所谓的 h 指数是指一个具体的数值,该数值为“最大”的满足「至少发表了 x 篇论文,且每篇论文至少被引用 x 次」定义的合法数,重点是“最大”

用题面的实例 1 来举个 🌰,给定所有论文的引用次数情况为 cs = [0,1,3,5,6],可统计满足定义的数值有哪些:

  • h=0,含义为「至少发表了 0 篇,且这 0 篇论文至少被引用 0 次」,空集即满足,恒成立;
  • h=1,含义为「至少发表了 1 篇,且这 1 篇论文至少被引用 1 次」,可以找到这样的组合,如 [1],成立;
  • h=2,含义为「至少发表了 2 篇,且这 2 篇论文至少被引用 2 次」,可以找到这样的组合,如 [3, 5],成立;
  • h=3,含义为「至少发表了 3 篇,且这 3 篇论文至少被引用 3 次」,可以找到这样的组合,如 [3, 5, 6],成立;
  • h=4,含义为「至少发表了 4 篇,且这 4 篇论文至少被引用 4 次」,找不到这样的组合,不成立;

实际上,当遇到第一个无法满足的数时,更大的数值就没必要找了。一个简单的推导:

至少出现 k 次的论文数不足 k 篇 => 至少出现 k+1 次的论文必然不足 k 篇 => 至少出现 k+1 次的论文必然不足 k+1 篇(即更大的 h 不满足)。

计数

首先,仍能使用「计数」的方式进行求解,该求解为线性复杂度,且不要求数组有序

根据分析,最大的 h 不超过 n

假设我们预处理出引用次数所对应的论文数量 cnt,其中 cnt[a] = b 含义为引用次数 恰好a 的论文数量有 b 篇。

那么再利用 h 是“最大”的满足定义的合法数,我们从 n 开始往前找,找到的第一个满足条件的数,即是答案。

具体的,创建 cnt 数组,对 cs 进行计数,由于最大 h 不超过 n,因此对于引用次数超过 n 的论文,可等价为引用次数为 n,即有计数逻辑 cnt[min(c, n)]++

再根据处理好的 cnt,从 n 开始倒序找 h

由于我们处理的 cnt[a] 含义为引用次数 恰好a,但题目定义则是 至少。同时「至少出现 k+1 次」的集合必然慢「至少出现 k 次」要求(子集关系),我们可以使用变量 tot,对处理过的 cnt[i] 进行累加,从而实现从 恰好至少 的转换。

代码

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int n = citations.size();
        vector<int> cnt(n + 1,0);
        for(int c : citations) cnt[min(c,n)]++;
        for(int i = n,tot = 0;i >= 0;i--)
        {
            //完成从恰好到至少的转化
            tot += cnt[i];
            //满足每篇论文至少被引用 x 次
            if(tot >= i) return i;
        }
        return -1;
    }
};

二分答案(线性 check

除了线性复杂度的「计数」做法,我们还容易想到「二分」。

其中最容易想到的是「二分答案」,该做法复杂度为 O(nlogn),同样并不要求数组有序。

我们发现对于任意的 cs(论文总数量为该数组长度 n),都必然对应了一个最大的 h 值,且小于等于该 h 值的情况均满足,大于该 h 值的均不满足。

那么,在以最大 h 值为分割点的数轴上具有「二段性」,可通过「二分」求解该分割点(答案)。

最后考虑在什么值域范围内进行二分?

一个合格的二分范围,仅需确保答案在此范围内即可。

再回看我们关于 h 的定义「至少发表了 x 篇论文,且每篇论文至少被引用 x 次」,满足条件除了引用次数,还有论文数量,而总的论文数量只有 n,因此最大的 h 只能是 n 本身,而不能是比 n 大的数,否则论文数量就不够了。

综上,我们只需要在 [0,n] 范围进行二分即可。对于任意二分值 mid,只需线性扫描 cs 即可知道其是否合法。

代码

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int n = citations.size();
        int l = 0,r = n;
        while(l < r)
        {
            int mid = (l + r + 1) >> 1;
            if(check(citations,mid)) l = mid;
            else r = mid - 1;
        }
        return r;

    }

    bool check(vector<int>& citations,int x)
    {
        int cnt = 0;
        for(int c : citations)
        {
            if(c >= x) cnt++;
        }
        return cnt >= x;
    }
};
  • 时间复杂度:对 [0,n] 做二分,复杂度为 O(logn);check 函数需要对数组进行线性遍历,复杂度为 O(n)。整体复杂度为 O(nlogn)
  • 空间复杂度:O(1)

二分下标(根据与 cs[i] 关系)

在上述二分中,我们没有利用本题的「数组有序」的特性。

根据对 h 定义,若 cs 升序,我们可推导出:

  • 在最大的符合条件的分割点 x 的右边(包含分割点),必然满足 cs[i]>=x
  • 在最大的符合条件的分割点 x 的左边,必然不满足 cs[i]>=x

因此,我们可以利用 分割点右边数的个数与分割点 cs[x]的大小关系进行二分

假设存在真实分割点下标 x,其值大小为 cs[x],分割点右边的数值个数为 nx,根据 H 指数 的定义,必然有 cs[x]>=nx 关系:

  • 在分割点 x 的右边:cs[i] 非严格单调递增,数的个数严格单调递减,仍然满足 cs[i]>=ni 关系;
  • 在分割点 x 的左边:cs[i] 非严格单调递减,数的个数严格单调递增,x 作为真实分割点,必然不满足 cs[i]>=ni 关系。

利用此「二段性」进行二分即可,二分出下标后,再计算出数的个数。

代码

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int n = citations.size();
        //二分下标
        int l = 0,r = n - 1;
        while(l < r)
        {
            int mid = (l + r) >> 1;
            //满足分割点右边数的个数与分割点 cs[x]
            if(citations[mid] >= n - mid) r = mid;
            else l = mid + 1;
        }
        // n - r(分割点右边数的个数) 就是 答案 特判一下 [0] 特例
        return citations[r] >= n - r ? n - r : 0;
    }
};

240.搜索矩阵IIlc240.png

代码

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int m = matrix.size(),n = matrix[0].size();
        int i = 0,j = n - 1;//从右上角开始
        while(i < m && j >= 0)//还有剩余元素
        {
            if(matrix[i][j] == target) return true;
            else if(matrix[i][j] > target) j--;//这一列元素大于target 排除
            else i++; //这一行元素小于target 排除
        }
        return false;
    }
};
1. Two Sum 2. Add Two Numbers 3. Longest Substring Without Repeating Characters 4. Median of Two Sorted Arrays 5. Longest Palindromic Substring 6. ZigZag Conversion 7. Reverse Integer 8. String to Integer (atoi) 9. Palindrome Number 10. Regular Expression Matching 11. Container With Most Water 12. Integer to Roman 13. Roman to Integer 14. Longest Common Prefix 15. 3Sum 16. 3Sum Closest 17. Letter Combinations of a Phone Number 18. 4Sum 19. Remove Nth Node From End of List 20. Valid Parentheses 21. Merge Two Sorted Lists 22. Generate Parentheses 23. Swap Nodes in Pairs 24. Reverse Nodes in k-Group 25. Remove Duplicates from Sorted Array 26. Remove Element 27. Implement strStr() 28. Divide Two Integers 29. Substring with Concatenation of All Words 30. Next Permutation 31. Longest Valid Parentheses 32. Search in Rotated Sorted Array 33. Search for a Range 34. Find First and Last Position of Element in Sorted Array 35. Valid Sudoku 36. Sudoku Solver 37. Count and Say 38. Combination Sum 39. Combination Sum II 40. First Missing Positive 41. Trapping Rain Water 42. Jump Game 43. Merge Intervals 44. Insert Interval 45. Unique Paths 46. Minimum Path Sum 47. Climbing Stairs 48. Permutations 49. Permutations II 50. Rotate Image 51. Group Anagrams 52. Pow(x, n) 53. Maximum Subarray 54. Spiral Matrix 55. Jump Game II 56. Merge k Sorted Lists 57. Insertion Sort List 58. Sort List 59. Largest Rectangle in Histogram 60. Valid Number 61. Word Search 62. Minimum Window Substring 63. Unique Binary Search Trees 64. Unique Binary Search Trees II 65. Interleaving String 66. Maximum Product Subarray 67. Binary Tree Inorder Traversal 68. Binary Tree Preorder Traversal 69. Binary Tree Postorder Traversal 70. Flatten Binary Tree to Linked List 71. Construct Binary Tree from Preorder and Inorder Traversal 72. Construct Binary Tree from Inorder and Postorder Traversal 73. Binary Tree Level Order Traversal 74. Binary Tree Zigzag Level Order Traversal 75. Convert Sorted Array to Binary Search Tree 76. Convert Sorted List to Binary Search Tree 77. Recover Binary Search Tree 78. Sum Root to Leaf Numbers 79. Path Sum 80. Path Sum II 81. Binary Tree Maximum Path Sum 82. Populating Next Right Pointers in Each Node 83. Populating Next Right Pointers in Each Node II 84. Reverse Linked List 85. Reverse Linked List II 86. Partition List 87. Rotate List 88. Remove Duplicates from Sorted List 89. Remove Duplicates from Sorted List II 90. Intersection of Two Linked Lists 91. Linked List Cycle 92. Linked List Cycle II 93. Reorder List 94. Binary Tree Upside Down 95. Binary Tree Right Side View 96. Palindrome Linked List 97. Convert Binary Search Tree to Sorted Doubly Linked List 98. Lowest Common Ancestor of a Binary Tree 99. Lowest Common Ancestor of a Binary Search Tree 100. Binary Tree Level Order Traversal II
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值