二分binarySearch_求简单有序集合交集

#include<cstdio>

int binSearch1(int a[], int e, int lo, int hi){
    while (lo < hi){
        int mi = (lo + hi) >> 1;
        if (e < a[mi])hi = mi;
        else if (a[mi] < e)lo = mi + 1;
        else return mi;
    }
    return -1;
}

斐波那契查找是利用斐波那契数列将有序数组进行分割查找。核心是通过斐波那契数确定每次查找的分割点,逐步缩小查找范围,直到找到目标元素或确定元素不存在。

int fib(int n){
    if (n == 0)return 0;
    if (n == 1)return 1;
    int a = 0, b = 1;
    for (int i = 2; i <= n; i++){
        int temp = b;
        b = a + b;
        a = temp;
    }
    return b;
}

• 初始化 a 为斐波那契数列的第0项(值为0),b 为第1项(值为1) 。

• while 循环目的是生成大于等于查找区间长度(hi - lo)的斐波那契数。每次循环中,temp 暂存当前的 b 值,然后让 b 变为下一个斐波那契数(即 a + b ),a 变为原来的 b 值(即 temp ) 。当 b 大于等于查找区间长度时,循环结束。此时 b 就是合适的斐波那契数,a 是它的前一项斐波那契数。

int fibSearch1(int* a, int e, int lo, int hi){
    int fib_num = 0, i = 0;
    while ( fib(i) < hi - lo )i++;
    fib_num = fib(i);
    while (lo < hi){
        int mi = lo + fib(i - 1) - 1;//按黄金比例分割
        if (e < a[mi]){
            hi = mi; i--;
        }
        else if (a[mi] < e){
            lo = mi + 1; i -= 2;
        }
        else return mi;
    }
    return -1;
}

int fibSearch2(int* A, int e, int lo, int hi){
    int a = 0, b = 1;
    while (b < hi - lo){
        int temp = b;
        b = a + b;
        a = temp;
    }
    while (lo < hi){
        int mid = lo + a - 1;
        if (e < A[mid]){
            lo = mid + 1;
            int temp = a;
            a = b - a;
            b = temp;
        }
        else if (A[mid] < e){
            hi = mid;
            b = a;
            a = b - a;
        }
        else return mid;
    }
    return -1;
}

• 确定分割点:每次进入循环,通过 int mid = lo + a - 1; 计算分割点索引 mid 。这里利用 a (当前合适的斐波那契数的前一项)来按斐波那契数列的比例确定分割位置。

• 元素比较与区间调整:

◦ 当 A[mid] < e 时,说明目标元素 e 在 mid 右侧,更新查找区间左边界 lo = mid + 1 。同时,为了下次查找能正确利用斐波那契数列特性,更新 a 和 b 。temp 暂存 a ,a 更新为 b - a (新的合适的斐波那契数的前一项),b 更新为原来的 a 值(即 temp ) 。

◦ 当 A[mid] > e 时,说明目标元素 e 在 mid 左侧,更新查找区间右边界 hi = mid 。然后更新 b 为当前的 a ,a 为 b - a ,同样是为了适应新查找区间的斐波那契数列特性。

◦ 当 A[mid] == e 时,说明找到了目标元素,直接返回 mid 。

int binSearch2(int* a, int e, int lo,int hi){
    while (1 < hi - lo){
        int mi = (lo + hi) >> 1;
        e < a[mi] ? hi = mi : lo = mi;
        return e == a[lo] ? lo : -1;//出口时hi=lo+1,查找·区间仅含一个元素a[lo]
    }
}

int binSearch3(int* a, int e, int lo, int hi){
    while (lo < hi){
        int mi = (lo + hi) >> 1;
        e < a[mi] ? hi = mi : lo = mi + 1;
    }
    return --lo;
}

double binarySearch(double L, double R,int a){
    while (R - L>1e-6){
        double mid = (L + R) / 2;
        if (mid*mid - a>0)R = mid;
        else L = mid;
    }
    return L;
}

DSACPP

练习:利用二分求有序集合的交集

法1

左闭右区间 [lo, hi) 进行二分查找。循环条件 lo < hi 意味着 hi 所指向的元素不在当前查找范围内。当 e < a[mid] 时,将 hi 更新为 mid,缩小查找范围到左半部分;当 e >= a[mid] 时,将 lo 更新为 mid + 1,缩小查找范围到右半部分。调用时传入的区间是 [0, m],保证了整个数组元素都能被包含在查找范围内。

#include <stdio.h>

int binSearch(int a[], int e, int lo, int hi) {
    while (lo < hi) {
        int mid = (lo + hi) >> 1;
        (e < a[mid]) ? (hi = mid) : (lo = mid + 1);
    }
    return --lo;
}

int main() {
    int n, m;
    scanf("%d", &n);
    int a[n];
    for (int i = 0; i < n; i++)scanf("%d", &a[i]);
    scanf("%d", &m);
    int b[m];
    for (int i = 0; i < m; i++) scanf("%d", &b[i]);
    for (int i = 0; i < n; i++) {
        if (b[binSearch(b, a[i], 0, m )] == a[i]) printf("%d ", a[i]);
    }
    return 0;
}  

Q:将 binSearch(b, a[i], 0, m ) 改成 binSearch(b, a[i], 0, m - 1) 会不会出错呢?

A:会,是边界没处理好。

当把 binSearch(b, a[i], 0, m ) 改成 binSearch(b, a[i], 0, m - 1) 时,传递给 binSearch 函数的查找区间变为 [0, m - 1) ,而实际上数组 b 的合法下标范围是 [0, m - 1] (这里是常规的左闭右闭区间理解)。这就导致当目标元素 a[i] 恰好是 b 数组的最后一个元素时,在 [0, m - 1) 这个区间内无法找到它,因为查找范围根本没有覆盖到最后一个元素所在的下标位置 ,从而导致错误的结果。

例如,假设 b 数组为 {1, 2, 3} ,如果要查找元素 3 ,按照修改后的调用 binSearch(b, 3, 0, 2) ,在 [0, 2) 这个区间内是找不到 3 的,尽管 3 是 b 数组中的元素。而原代码调用 binSearch(b, 3, 0, 3) ,在 [0, 3) 区间内是可以正确找到 3 这个元素的。

所以,为了保证二分查找函数能正确在整个 b 数组范围内查找元素,应该使用 binSearch(b, a[i], 0, m ) ,维持左闭右开区间 [0, m) 来匹配函数内部的查找逻辑。

当 b 数组为 {1, 2, 3} ,调用 binSearch(b, 3, 0, 2) 时二分查找的过程:

第一轮循环

  • 初始时 lo = 0hi = 2 。
  • 计算中间位置 mid = (0 + 2) >> 1 = 1 。
  • 此时 a[mid] 即 b[1] ,值为 2 ,要查找的元素 e = 3 ,因为 3 > 2 ,根据函数逻辑 (e < a[mid])? (hi = mid) : (lo = mid + 1) ,会执行 lo = mid + 1 ,此时 lo 更新为 2 。

第二轮循环

  • 此时 lo = 2 ,hi = 2 ,不满足循环条件 lo < hi ,循环结束。

最终返回结果

  • 循环结束后执行 return --lo ,lo 先自减为 1 然后返回。但实际上我们要查找的元素 3 在 b 数组中的下标应该是 2 ,由于查找区间设置为 [0, 2) ,根本没有覆盖到 b 数组最后一个元素的下标位置,导致无法正确找到元素 3 ,返回了错误的结果。

引入:那如果改成 lo <= hi ,是不是就可以解决这个问题了呢?

可以是可以,但是要改一下binSearch的写法,请看 法2

相似的法2

左闭右区间 [lo, hi] 进行二分查找。在循环条件 lo <= hi 下,lo 和 hi 都代表有效元素的索引。当 e < a[mi] 时,将 hi 更新为 mi - 1,缩小查找范围到左半部分;当 e >= a[mi] 时,将 lo 更新为 mi + 1,缩小查找范围到右半部分。调用时传入的区间是 [0, m - 1],这与左闭右闭区间的逻辑相符。

#include <stdio.h>

int binSearch(int a[], int e, int lo, int hi) {
    while (lo <= hi) {
        int mi = (lo + hi) >> 1;
        (e < a[mi]) ? (hi = mi-1) : (lo = mi + 1);
    }
    return --lo;
}

int main() {
    int n, m;
    scanf("%d", &n);
    int a[n];
    for (int i = 0; i < n; i++)scanf("%d", &a[i]);
    scanf("%d", &m);
    int b[m];
    for (int i = 0; i < m; i++) scanf("%d", &b[i]);
    for (int i = 0; i < n; i++) {
        if (b[binSearch(b, a[i], 0, m-1 )] == a[i]) printf("%d ", a[i]);
    }
    return 0;
}

演示:

假设有一个有序数组 b = {1, 3, 5, 7, 9},想要查找元素 7

左闭右闭区间 [lo, hi]

#include <stdio.h>

int binSearch(int a[], int e, int lo, int hi) {
    while (lo <= hi) {
        int mi = (lo + hi) >> 1;
        (e < a[mi]) ? (hi = mi - 1) : (lo = mi + 1);
    }
    return --lo;
}

int main() {
    int b[] = {1, 3, 5, 7, 9};
    int m = sizeof(b) / sizeof(b[0]);
    int e = 7;
    int index = binSearch(b, e, 0, m - 1);
    printf("找到元素 %d 的索引是 %d\n", e, index);
    return 0;
}
查找过程
  1. 初始状态
    • lo = 0hi = 4(因为数组长度 m = 5m - 1 = 4),要查找的元素 e = 7
  2. 第一次循环
    • 计算中间位置 mi = (0 + 4) >> 1 = 2
    • a[mi] = a[2] = 5,因为 7 > 5,所以执行 lo = mi + 1,即 lo = 3
  3. 第二次循环
    • 计算中间位置 mi = (3 + 4) >> 1 = 3
    • a[mi] = a[3] = 7,因为 7 >= 7,所以执行 lo = mi + 1,即 lo = 4
  4. 第三次循环
    • 计算中间位置 mi = (4 + 4) >> 1 = 4
    • a[mi] = a[4] = 9,因为 7 < 9,所以执行 hi = mi - 1,即 hi = 3
  5. 循环结束
    • 此时 lo = 4hi = 3,不满足 lo <= hi 的条件,循环结束。
    • 执行 return --lolo 先减 1 变为 3,然后返回 3,即找到了元素 7 的索引。

左闭右开区间 [lo, hi)

#include <stdio.h>

int binSearch(int a[], int e, int lo, int hi) {
    while (lo < hi) {
        int mid = (lo + hi) >> 1;
        (e < a[mid]) ? (hi = mid) : (lo = mid + 1);
    }
    return --lo;
}

int main() {
    int b[] = {1, 3, 5, 7, 9};
    int m = sizeof(b) / sizeof(b[0]);
    int e = 7;
    int index = binSearch(b, e, 0, m);
    printf("找到元素 %d 的索引是 %d\n", e, index);
    return 0;
}
查找过程
  1. 初始状态
    • lo = 0hi = 5(因为采用左闭右开区间,要包含整个数组元素),要查找的元素 e = 7
  2. 第一次循环
    • 计算中间位置 mid = (0 + 5) >> 1 = 2
    • a[mid] = a[2] = 5,因为 7 > 5,所以执行 lo = mid + 1,即 lo = 3
  3. 第二次循环
    • 计算中间位置 mid = (3 + 5) >> 1 = 4
    • a[mid] = a[4] = 9,因为 7 < 9,所以执行 hi = mid,即 hi = 4
  4. 第三次循环
    • 计算中间位置 mid = (3 + 4) >> 1 = 3
    • a[mid] = a[3] = 7,因为 7 >= 7,所以执行 lo = mid + 1,即 lo = 4
  5. 循环结束
    • 此时 lo = 4hi = 4,不满足 lo < hi 的条件,循环结束。
    • 执行 return --lolo 先减 1 变为 3,然后返回 3,即找到了元素 7 的索引。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值