算法 - 二分查找(可能我们理解错了应用场景)

目录

二分查找的使用场景

二分查找

"近似"二分查找

1、查找第一个X值出现的位置

2、查询最后一个X值出现的位置

3、查询第一个大于等于X值的位置

4、查询最后一个小于等于X值的位置


    二分查询也叫折半查找,即在一个有序的数组中,利用数组随机下标访问的时间复杂度是O(1)的特点,每轮查找可以折半计算数组中间的下标位置,每次将查找的范围缩小为原来的一半,这个比较好理解。只是对于二分查找的前提条件比较严苛,才能达到查询的时间复杂度为O(logN)。而红黑树、B+树、跳表都是可以执行快速读写的数据结构,并且性能也可以达到O(logN)的查询时间复杂度。

二分查找的使用场景

1、空间占用低的场景
        数组二分查找的应用场景是在Redis等对数据空间占用严苛的场景下,可以使用有序数组的二分查找。
2、二分查找处理"近似查找"的问题
       查找第一个X值出现的位置,最后一个X值出现的位置,最后一个小于等于X的值的位置,第一个大于等于X值出现的位置。这样的场景对于红黑树、跳表、B+树中实现起来就会比较的麻烦,并且需要考虑性能。
       真实的场景比如:
       1)、前分析了TimSort在python、java语言中作为默认的排序算法,而当数据量比较小时内部使用了插入排序算法,只是传统的插入排序本身没有利用前面待插入的子数组已经是有序的特点,而二分插入排序就是对其做了优化。插入排序本身是稳定的排序,所以优化之后的二分插头排序本身也应该需要是稳定排序。此时待排序的元素一定是在已拍好序的后面,即如果前面的有序子数组【1,2,4,4,5】那么当前如果要插入一个4,需求就是寻找最后一个4的下标(或者第一个大于4的下标)
       2)、再比如,每个城市都有自己的ip地址的区间 [202.102.133.0, 202.102.133.255] 为XX市,像这样的城市区间可能有几十万个,给一个随机的ip怎样才能快速定位到该ip所属的区间,使用上面的数据结构解决起来也是比较麻烦的。由于ip本身一般不会经常变动,比较符合静止数据的特点,这里解决的思路就是将ip转换为32位的整形数,存储在数组中,进行排序。查询ip区间就转换为“在有序数组中,查找最后一个小于等于某个给定值的元素”。

       String[] ip = strIp.split("\\."); 

       整形数为: (Long.parseLong(ip[0]) << 24) + (Long.parseLong(ip[1]) << 16) + (Long.parseLong(ip[2]) << 8) + Long.parseLong(ip[3]); 

 

二分查找

    二分查找每次将数据范围缩小为原理的一半,即每次查找的范围为 n,n/2,n/4,...,n/2^k, 查询范围极具的缩小。查询的时间复杂度为O(logN),在数据量比较小时与O(n)时间复杂度没有太大差别,但是当数据规模非常大时性能提升“恐怖”。下面是一个数据规模与两种复杂度的变化表:

    所以二分查找的特点或者应用场景也就出来了:

1、二分查询必须基于数组的结构

    上面分析过了,二分查询的二分是二分下标的意思,利用的是数组的下标访问是否复杂度为O(1)。基于有序链表等数据结构二分查找本身没有意义。

2、二分查询的前提是数组有序

    前面博客分析了那么多种数组排序算法,最好的时间复杂度也只能达到O(N*logN)。所以二分查询高性能是建立在该基础上,比较适合静态数据以及一次排序多次查询的场景。

3、二分查找不适合数据量小的场景

    因为数据量比较小的场景,数据规模在比较小的时候,O(logN)和O(n)时间复杂度的查询次数并没有明显的差别。

4、二分查询不适合数据量比较大的场景

    可能如上图,当数据规模达到42亿时,如果是遍历数组或链表时间复杂度是上亿级别,使用二分查询是32次。但是存储的数据结构是数组,必须要求开辟一片连续的内存空间,比如存储数据需要4个G的连续内存空间,如果内存是碎片化的,那么服务站内存为16个G也不一定创建数组成功。

 

    二分查询的代码是面试的高频,二分查询非常好理解但是非常容易出错(尽管第一个二分查找算法于 1946 年出现,然而第一个完全正确的二分查找算法实现直到 1962 年才出现 - 出自......)。注意点:

1)、判断退出的条件是 low <= high 而不是 low < high

2)、middle值计算写法

   最简单的就是 int middle = (high + low) / 2; 当数据量比较大时计算可能溢出则可以优化为:int middle = low + (high - low) / 2; 考虑极致性能可以使用位运算:int middle = low + ((high - low) >> 1);不要忘了最外层的括号,我有一次就是忘记写了,结果死循环。

3)、高低位的值计算

    high = middle - 1;而 low = middle + 1;否则也是死循环。

 

下面基于非递归和递归两种方式进行实现:

1、非递归的二分查找【自己偏向于写非递归】

/**
 * 基本的二分查找
 * @param array 待查找的有序数组
 * @param value 待查找的值
 * @return 查找值对应的下标
 */
public static int binarySearch(int[] array, int value) {
    int low = 0;
    int high = array.length - 1;
    while (low <= high) {
        int middle = low + ((high - low) >> 1);
        if (array[middle] == value) {
            return middle;
            // 升序,当前中间值比待查询的值小,则在后半段,将最小值重置;
            // 降序数组则把判断是小于变成大于
        } else if (array[middle] < value) {
            low = middle + 1;
        } else {
            high = middle - 1;
        }
    }
    return -1;
}

 

2、递归的二分查找

/**
 * 递归实现基本的二分查找
 * @param array 待查找的有序数组
 * @param value 待查找的值
 * @return 查找值对应的下标
 */
public static int recursionBinarySearch(int[] array, int value) {
    return binarySearchInternally(array, 0, array.length - 1, value);
}

private static int binarySearchInternally(int[] array, int low, int high, int value) {
    if (low > high) {
        return -1;
    }

    int middle = low + ((high - low) >> 1);
    if (array[middle] == value) {
        return middle;
    } else if (array[middle] < value) {
        return binarySearchInternally(array, middle + 1, high, value);
    } else {
        return binarySearchInternally(array, low, middle - 1, value);
    }
}

 

"近似"二分查找

public static void main(String[] args) {
    /*
        第一个 5 出现的下标位置为:3
        最后一个 5 出现的下标位置为:6
        第一个大于等于5 出现的下标位置为:3
        最后一个小于等于5 出现的下标位置为:6
    */
    int[] arr = new int[]{1, 2, 3, 5, 5, 5, 5, 7, 8, 9};
    int firstIndex = firstValueIndex(arr, 5);
    System.out.println("第一个 5 出现的下标位置为:" + firstIndex);

    int lastIndex = lastValueIndex(arr, 5);
    System.out.println("最后一个 5 出现的下标位置为:" + lastIndex);

    int firstGtEIndex = firstGtOrEquals(arr, 5);
    System.out.println("第一个大于等于5 出现的下标位置为:" + firstGtEIndex);

    int lastLtEIndex = lastLtOrEquals(arr, 5);
    System.out.println("最后一个小于等于5 出现的下标位置为:" + lastLtEIndex);
}

1、查找第一个X值出现的位置

/**
 * 查询第一个X出现的位置
 * @param array 待查询的有序数组
 * @param value 需要查询的值
 * @return 第一个等值的下标
 */
public static int firstValueIndex(int[] array, int value) {
    int low = 0;
    int high = array.length - 1;

    while (low <= high) {
        int middle = low + ((high - low) >> 1);
        if (array[middle] == value) {
            // 第一个位置,那么当前下标是0 或者 前面一个值等于value就是第一个出现的位置
            if (middle == 0 || array[middle - 1] != value) {
                return middle;
            } else {
                // 否则需要去前面的空间判断
                high = middle - 1;
            }
        } else if (array[middle] < value) {
            low = middle + 1;
        } else {
            high = middle - 1;
        }
    }

    return -1;
}

2、查询最后一个X值出现的位置

/**
 *  查询最后一个X出现的位置下标
 * @param arr 待查询的有序数组
 * @param value 待查询的值
 * @return 等值的最后一个元素的下标
 */
public static int lastValueIndex(int[] arr, int value) {
    int low = 0;
    int high = arr.length - 1;

    while (low <= high) {
        int middle = low + ((high - low) >> 1);
        if (arr[middle] == value) {
            // 如果当前已经是数组的最后一位(也防止后面判断时+1的下标越界),或者下一个值不等于该值
            if (middle == arr.length - 1 || arr[middle + 1] != value) {
                return middle;
            } else {
                // 去后面找
                low = middle + 1;
            }
        } else if (arr[middle] < value) {
            low = middle + 1;
        } else {
            high = middle - 1;
        }
    }

    return -1;
}

3、查询第一个大于等于X值的位置

/**
 *  查询第一个大于等于X值的下标位置
 * @param arr 待查询的有序数组
 * @param value 待查询的值
 * @return 第一个大于等于下下标出现的位置
 */
public static int firstGtOrEquals(int[] arr, int value) {
    int low = 0;
    int high = arr.length - 1;

    while (low <= high) {
        int middle = low + ((high - low) >> 1);
        if (arr[middle] >= value) {
            // 当前以及是数组头部或者前一个不是该值,说明当前就是等于该值的第一个
            if (middle == 0 || arr[middle - 1] != value) {
                return middle;
            } else {
                // 否则前面还有等于该值的,去前面空间找
                high = middle - 1;
            }
        } else {
            // 否则中间值比要查找的值小,去更大的空间找
            low = middle + 1;
        }
    }
    return -1;
}

4、查询最后一个小于等于X值的位置

/**
 *  查询最后一个小于等于X的下标位置
 * @param arr 待查询的有序数组
 * @param value 待查询的值
 * @return 最后一个小于等于值的下标位置
 */
public static int lastLtOrEquals(int[] arr, int value) {
    int low = 0;
    int high = arr.length - 1;

    while (low <= high) {
        int middle = low + ((high - low) >> 1);
        if (arr[middle] <= value) {
            // 如果当前已经是最好一个元素,或者后面一个下标位的值不能于该值,那么现在就是最后一个等值位置
            if (middle == arr.length - 1 || arr[middle + 1] != value) {
                return middle;
            } else {
                // 否则 arr[middle + 1] == value,后面还有等值, 调大最小值
                low = middle + 1;
            }
        } else {
            // 否则,中间值大于了待查询的值,需要去低空间查找
            high = middle - 1;
        }
    }
    return -1;
}

 

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
算法是解决特定问题或执行特定任务的一系列步骤或规则的有序集合。在计算机科学中,算法通常用来指导计算机执行特定的任务或解决问题。良好设计的算法能够有效地解决问题,并且在给定的输入下能够产生正确的输出。 常见的算法包括但不限于以下几种: 排序算法排序算法是将一组数据按照一定的顺序排列的算法。常见的排序算法包括冒泡排序、插入排序、选择排序、快速排序、归并排序等。 搜索算法:搜索算法用于在数据集中查找特定元素的算法。常见的搜索算法包括线性搜索、二分搜索等。 图算法:图算法用于处理图结构的数据,如最短路径算法(如Dijkstra算法、Floyd-Warshall算法)、最小生成树算法(如Prim算法、Kruskal算法)等。 动态规划:动态规划是一种通过将问题分解成更小的子问题来解决复杂问题的算法。常见的动态规划问题包括背包问题、最长递增子序列、编辑距离等。 贪心算法:贪心算法是一种在每一步选择中都采取当前状态下最优决策的算法。常见的贪心算法包括最小生成树算法中的Prim算法、Dijkstra算法等。 字符串匹配算法:字符串匹配算法用于在一个字符串(文本)中查找一个子串(模式)的出现位置。常见的字符串匹配算法包括暴力匹配、KMP算法、Boyer-Moore算法等。 这些是计算机科学中常见的算法类型,每种算法都有不同的应用场景和解决问题的方法。在实际编程中,选择合适的算法对于提高程序效率和性能至关重要。
### 回答1: 好的,这里给您提供一份基于二分查找算法实现根据员工号查找员工信息的代码,您可以参考一下: ```python class Employee: def __init__(self, emp_id, name, age): self.emp_id = emp_id self.name = name self.age = age class EmployeeDB: def __init__(self): self.employee_list = [] def add_employee(self, emp_id, name, age): emp = Employee(emp_id, name, age) self.employee_list.append(emp) def binary_search(self, emp_id): left = 0 right = len(self.employee_list) - 1 while left <= right: mid = (left + right) // 2 if self.employee_list[mid].emp_id == emp_id: return self.employee_list[mid] elif self.employee_list[mid].emp_id < emp_id: left = mid + 1 else: right = mid - 1 return None ``` 这里我们定义了一个`Employee`类,表示员工信息,其中包含员工号、姓名和年龄等信息。然后定义了一个`EmployeeDB`类,表示员工信息数据库,其中包含添加员工信息和根据员工号查找员工信息的方法。在`binary_search`方法中,我们采用二分查找算法来查找员工信息,如果找到了对应的员工信息,则返回该员工信息,否则返回`None`。 ### 回答2: 二分查找算法是一种在有序数组中快速定位目标值的方法,适用于数据量较大且有序的情况。根据题目要求,我们需要根据员工号来查找员工信息,因此可以使用二分查找算法来实现。 首先,我们需要将员工号按照升序排序,这样才能使用二分查找算法。然后,我们可以采取以下步骤来实现根据员工号查找员工信息的功能: 1. 定义一个包含员工号和员工信息的结构体,例如Employee。 2. 声明一个员工数组,并按照员工号的升序进行排序。 3. 定义一个二分查找的函数,传入员工号、员工数组数组长度作为参数。 4. 在二分查找函数中,设定左边界left为数组起始位置,右边界right为数组结束位置。 5. 进入循环,在循环中判断左边界是否小于等于右边界,如果是,则执行以下步骤: a. 计算中间位置middle,可以通过取(left + right) / 2来实现。 b. 判断员工号是否等于数组中间位置的员工号,如果是,则说明找到了对应的员工信息,直接返回该员工信息。 c. 如果员工号小于数组中间位置的员工号,说明需要在左半部分继续查找,此时将右边界right更新为middle-1。 d. 如果员工号大于数组中间位置的员工号,说明需要在右半部分继续查找,此时将左边界left更新为middle+1。 6. 如果循环结束后仍未找到对应员工信息,说明该员工号不存在,返回相应提示信息。 通过以上步骤,我们可以使用二分查找算法实现根据员工号查找员工信息的功能。该算法具有较高的效率和准确性,适用于大规模员工数据的查找场景。 ### 回答3: 根据题目要求,我们可以使用二分查找算法来实现根据员工号查找员工信息。具体步骤如下: 1. 首先,我们需要准备一个按照员工号有序排列的员工信息列表。 2. 确定二分查找算法的边界条件。在这个问题中,我们需要确定查找的范围是什么。假设员工号的最小值为1,最大值为N。那么边界条件可以设置为左边界为1,右边界为N。 3. 计算中间元素的索引。首先,找到左边界和右边界的中间值,可以使用公式 `(left + right) // 2` 来计算。 4. 检查中间元素是否是目标元素。比较中间元素的员工号与目标员工号是否相等。如果相等,则找到了目标员工信息,可以返回。 5. 如果中间元素的员工号小于目标员工号,说明目标员工号在右半部分,更新左边界为中间元素的索引 + 1。 6. 如果中间元素的员工号大于目标员工号,说明目标员工号在左半部分,更新右边界为中间元素的索引 - 1。 7. 重复步骤3到步骤6,直到左边界大于右边界,说明查找失败,员工信息不存在。 8. 返回查找结果。 通过以上步骤,我们可以使用二分查找算法实现根据员工号查找员工信息。由于二分查找算法的时间复杂度为O(logN),在大规模数据集下,效率较高。同时,由于员工号有序排列,可以使用二分查找算法进一步优化查找速度。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值