经典算法_二分

二分查找算法

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,通常我们用该算法能够将我们O(n)的时间复杂度优化到 O( l o g 2 n {log_2{n}} log2n);

1、应用二分的场景和条件:可以二分并不是具有单调性, 真正来说能二分应该说具有二段性,即存在可以二分的某种性质,可以将左右两边都划为不同的两类。 二分本身并不难,但二分的条件,和边界条件的判断有时候很坑,这里二分建议直接背过y总的模板:

整数二分的模板

由于整数的二分并不能完全分为左右两边一样的情况,因为整数 / 2 会直接将小数部分去掉 , 在 C / C++ 中是直接下取整 。故其更新左右边界有如下两种情况 .

版本 1

bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;       // 等价于 (l + r) / 2 
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}

版本 2

// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;  //注意这里必须上取整 + 1, 不然l = mid, 可能在两个的情况下无限递归. 
        if (check(mid)) l = mid;   
        else r = mid - 1;         
    }
    return l;
}

分析一下为啥版本2是 mid = (l + r + 1) / 2

这里主要是分析当二分只剩下两个元素中选择的情况, 即此时存在 r = l + 1 这个关系
在这里插入图片描述

二分必定有解,即上面两个版本的 while(l < r) 必定可以退出, 退出的条件就是 l == r, 此时l / r 对应的位置的元素即为此次二分的结果, 但此时二分虽然必出结果, 但该结果未必正确. 具体可以看下面的例子.

举个例子

#include <iostream>
#include <vector>
using namespace std;

auto main() -> int
{
    vector<int> v = {1, 2, 3, 5, 7}; // 升序的序列 
    int l = 0, r = v.size() - 1, target;
    cin >> target; 
    
    while(r > l)
    {
        int mid = l + r >> 1; // 取中间的值 
		
		// 若此时中间的值 >= 目标值, 说明目标值在中间值的左边的位置,故此时将右边界更新为 mid
        if(v[mid] >= target) r = mid;    
        // 否则此时中间值小于目标值, 目标值若存在必定在中间值的右边,故更新左边界为 mid + 1 
        else l = mid + 1; 
    }
     
    if(v[l] == target) cout << "yes" << endl;
    else cout << "no" << " v[l] = " << v[l] << endl;
    
    return 0;
}
# case 1 
输入: 3 
输出:  yes

# case 2 
输入: 4
输出: no v[l] = 5

# case 3
输入: 6
输出: no v[l] = 7

对于case2 和 case3 的输出分析, 我们此时的if中的check条件为 : v[mid] >= target ,即我们想找到第一个大于等于 target的值, 并不是要找一个等于target的值, 通过这个check保证了我们的二分一定有解。 通过这个check将我们的有序序列分为了两部分, 一部分是 >= target , 另一部分 < target;

具体应用场景

题目链接:剑指offer 53.在排序数组中查找数字 I
这道题应该是很经典的二分题目,考虑了两种二分的边界情况. 统计一个数字在排序数组中出现的次数。如果按照平常的做法, 那肯定是从左到右遍历一遍, 记录下. 这样时间复杂度为 O ( n ) O(n) O(n), 但实际上这道题如果面试考你, 就是考你二分,因为题目已经说明了这是一个排序数组. 故这个数组本身就是具有二段性, 一段是不大于target目标值的区间, 一段是大于target目标值的区间.

class Solution 
{
public:
    int search(vector<int>& nums, int target) 
    {
        int l = 0, r = nums.size() - 1; 
        if(r - l + 1 == 0) return 0;

        while(r > l)
        {
            int mid = l + r >> 1; 
 // 如果存在多个target值, 我们先找到的是大于等于target的最小值, 即最左边的那个target,所以这里更新的是r,右边界向左逼近.
            if(nums[mid] >= target) r = mid;    
            else l = mid + 1;
        }
// 由于我们找的是大于等于target的最小值, 故有可能target值并不存在, 故此时还要进一步判断找到的左边界是否正确. 
        if(nums[l] != target)  return 0;  
        int l1 = l; 
        r = nums.size() - 1; 
        
        while(r > l)
        {
            int mid = l + r + 1 >> 1; 
// 接下来找target右边界, 即我们的左边界要向右逼近, 这个条件的更新即此时的mid对应的值小于等于target,我们才左边界才向右靠近.             
            if(nums[mid] <= target) l = mid; 
            else r = mid - 1;
        }

        return r - l1 + 1;    // 返回找到的个数. 
    }
};

题目链接 : 896. 最长上升子序列 II

这里不直接分析题目, 直接看二分的核心代码 
#include<iostream>
using namespace std;

constexpr int INF = 1e+9;
constexpr int N = 1e+5 + 10;

int n, a[N];       // 数组a[i]的含义为子序列长度为i, 且储存元素为长度为i的序列中结尾的最小值

auto main() -> int
{
    cin >> n;

    a[0] = -INF - 10;            // 子序列长度为0的结尾值直接设个最小值即可

    int t = 0;                   // 表示此时a数组所储存的最长子序列长度为t,初始为0
    for(int i = 1; i <= n; ++i)
    {
        int x; cin >> x;         // 依次输入待处理的数
        int l = 0, r = t;        // 这里用二分查找小于x的最大的那个数字
        while(r > l)
        {
            int mid = l + r + 1>> 1;
            if(a[mid] < x) l = mid;  // check条件和l的更新情况其是为了找到小于x的最大的那个数字
            else r = mid - 1;
        }
        t = max(t, r + 1);       // 更新t
        a[++r] = x;              // 更新长度为r + 1长度的序列的结尾的值
    }
    cout << t << endl;           
    return 0;
}

题目链接 : 153. 寻找旋转排序数组中的最小值

// 旋转数组即属于那种不具备单调性,但具备二段性, 故能用二分进行优化的情况.

class Solution 
{
public:
    int findMin(vector<int>& nums) 
    {
        int n = nums.size() - 1;
        if(nums[0] <= nums[n]) return nums[0];

        int l = 0, r = n;
        while(r > l)
        {
            int mid = l + r >> 1;
            if(nums[mid] < nums[0]) r = mid;  // 找到小于nums[0]的最小值. 
            else l = mid + 1;
        }
        
        return nums[l];
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
归并排序(Merge Sort)是一种稳定的、基于比较的排序算法,最坏时间复杂度为 O(nlogn)。其基本思想是将待排序序列分成若干个子序列,每个子序列都是有序的,然后将子序列合并成整体有序的序列。 归并排序的实现方法有两种:自顶向下和自底向上。 自顶向下的归并排序算法实现: 1. 将待排序序列分成两个子序列,分别对这两个子序列进行递归排序。 2. 将两个已经排好序的子序列合并为一个有序序列。 自底向上的归并排序算法实现: 1. 将待排序序列每个元素看成一个独立的有序序列,进行两两合并。 2. 得到 n/2 个长度为 2 的有序序列,再两两合并。 3. 重复步骤 2,直到得到一个长度为 n 的有序序列。 下面是自顶向下的归并排序算法的实现代码(使用了递归): ``` void MergeSort(int arr[], int left, int right) { if (left >= right) return; int mid = left + (right - left) / 2; MergeSort(arr, left, mid); MergeSort(arr, mid + 1, right); int* temp = new int[right - left + 1]; int i = left, j = mid + 1, k = 0; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) temp[k++] = arr[i++]; else temp[k++] = arr[j++]; } while (i <= mid) temp[k++] = arr[i++]; while (j <= right) temp[k++] = arr[j++]; for (int p = 0; p < k; p++) arr[left + p] = temp[p]; delete[] temp; } ``` 下面是自底向上的归并排序算法的实现代码(使用了迭代): ``` void MergeSort(int arr[], int n) { int* temp = new int[n]; for (int len = 1; len < n; len *= 2) { for (int left = 0; left < n - len; left += len * 2) { int mid = left + len - 1; int right = min(left + len * 2 - 1, n - 1); int i = left, j = mid + 1, k = 0; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) temp[k++] = arr[i++]; else temp[k++] = arr[j++]; } while (i <= mid) temp[k++] = arr[i++]; while (j <= right) temp[k++] = arr[j++]; for (int p = 0; p < k; p++) arr[left + p] = temp[p]; } } delete[] temp; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值