从零开始学习二分算法

二分算法的定义:

二分算法(Binary Search Algorithm)是一种在有序数组中查找某一特定元素的搜索算法搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

二分算法的知识点:

有序性:二分算法要求搜索的数组必须是有序的

边界处理:在迭代或递归过程中,需要注意数组的左右边界。

返回值:根据实际需求,可能需要返回找到的元素索引或者一个标志值表示是否找到。

时间复杂度:二分算法的时间复杂度O(log n),其中n是数组的长度。

核心思想:通过每次比较中间元素与目标值的大小来减少搜索范围,每次可以排除一半的数据,从而快速定位目标值。

查找过程:定义最小、中间和最大索引(min, mid, max),计算中间索引并与目标值比较,根据比较结果调整搜索范围,重复此过程直到找到目标值或范围为空。

注意事项:

  1. 有序数组:必须在有序数组中应用二分算法,否则无法保证算法的正确性和效率。

  1. 边界条件:需要注意边界条件的处理,包括查找范围的起始和结束位置等。

  1. 循环终止条件:循环终止条件通常是查找范围缩小到起始位置大于结束位置时停止。
  2. 在实现时要注意边界条件,避免出现数组越界的错误。
  3. 如果数组中有多个相同的目标值,二分查找可能只能找到一个位置,需要额外处理以找到所有目标值的位置。
  4. 根据问题需求,可能需要处理重复元素的情况。
  5. 当找到目标值时,立即返回其索引;如果搜索区间为空(左边界大于右边界),则返回表示未找到目标值的标志(如-1)。

Java实现:

public class BinarySearch {  
    public static int binarySearch(int[] arr, int target) {  
        int left = 0;  
        int right = arr.length - 1;  
          
        while (left <= right) {  
            int mid = left + (right - left) / 2; // 防止left和right过大时直接相加导致溢出  
              
            if (arr[mid] == target) {  
                return mid; // 找到目标,返回索引  
            } else if (arr[mid] < target) {  
                left = mid + 1; // 目标在右半部分  
            } else {  
                right = mid - 1; // 目标在左半部分  
            }  
        }  
          
        return -1; // 未找到目标,返回-1  
    }  
      
    public static void main(String[] args) {  
        int[] arr = {2, 3, 4, 10, 40}; // 有序数组  
        int target = 10; // 要查找的目标值  
          
        int result = binarySearch(arr, target);  
          
        if (result != -1) {  
            System.out.println("找到目标值 " + target + ",其索引为 " + result);  
        } else {  
            System.out.println("未找到目标值 " + target);  
        }  
    }  
}

解释某些代码:

int mid = left + (right - left) / 2;

这行代码的目的是计算数组的中间索引,同时避免整数溢出的问题。让我们一步步分解它:

首先,假设我们有一个非常大的数组,left 和 right 分别是数组的开始和结束索引。如果 left 和 right 都很大,那么直接相加可能会导致整数溢出。溢出通常发生在整数类型(如 int)的大小超过其能表示的最大值时。

例如,假设 int 类型在Java中是一个32位整数,其最大值是 2^31 - 1(即 2,147,483,647)。如果 left 是 2,147,483,640,right 是 2,147,483,647,那么直接相加 left + right 会得到 4,294,967,287,这个值超出了 int 能表示的范围,因此会发生溢出,导致一个错误的结果。

为了避免这个问题,我们可以先计算 right 和 left 的差值 right - left,然后将这个差值除以 2。由于差值通常会比单独的 right 或 left 值小得多,因此不太可能导致溢出。然后,我们再将这个差值的一半加到 left 上,从而得到中间索引 mid。

分解后的步骤如下:

right - left:计算 right 和 left 的差值。

(right - left) / 2:将差值除以 2,得到中间位置相对于 left 的偏移量。

left + (right - left) / 2:将得到的偏移量加到 left 上,得到中间索引 mid。

这种方法确保了即使 left 和 right 很大,我们也能安全地计算出中间索引,而不会导致整数溢出。

在二分查找算法中,这个中间索引 mid 被用来检查目标值是否可能存在于数组的左半部分、右半部分,或者就位于 mid 所指向的位置。根据比较结果,算法会相应地更新 left 或 right 的值,以便在下一次迭代中继续缩小搜索范围。

公式int mid = left + (right - left) / 2;int mid=(left+right)/2;在功能上是等价的,它们都是用来计算数组的中间索引的。但是,在处理大整数时,它们的行为可能会有所不同。

公式 int mid = left + (right - left) / 2; 是为了防止整数溢出而设计的。在这个公式中,我们先计算 right 和 left 的差值,然后将这个差值除以 2,最后再加上 left。由于差值通常比 right 和 left 本身要小,因此不太可能导致整数溢出。

公式 int mid = (left + right) / 2; 在 left 和 right 都非常大且接近 int 类型的最大值时,直接相加可能会导致溢出。一旦溢出发生,后续的除法运算将基于一个错误的结果,从而导致计算出的 mid 值不正确。

因此,虽然两个公式在大多数情况下都能给出正确的中间索引,但在处理大整数时,int mid = left + (right - left) / 2; 是更安全和可靠的选择。它确保了即使 left 和 right 的值非常大,也能正确地计算出中间索引,而不会发生整数溢出。

在实际编程中,特别是处理大数组或需要考虑性能和安全性的场景中,推荐使用 int mid = left + (right - left) / 2; 这个公式来计算中间索引。

left <= right为什么是有等号,没有等号会怎么样

在二分查找算法中,条件 left <= right 用来确保循环继续进行,直到左指针 left 不再小于或等于右指针 right。如果在条件中去掉等号(即使用 left < right),可能会导致以下几种情况:

丢失可能的解:如果目标值正好位于数组的中间,并且数组中有多个相同的元素,那么使用 left < right 作为循环条件可能会导致算法错过这个解。因为在这种情况下,当 left 和 right 都指向目标值时,循环会提前结束。

额外的迭代:如果数组中没有目标值,使用 left < right 作为循环条件可能会导致额外的迭代。因为在最后一次迭代中,当 left 和 right 相邻时,它们仍然会进入循环体,尽管此时中间位置已经不可能包含目标值。

代码逻辑复杂性:去掉等号可能会使代码逻辑变得更加复杂。因为你需要额外处理当 left 和 right 相邻时的情况,确保它们不会进入循环体。

保留等号 left <= right 可以确保:

当目标值存在时,算法能够找到它,即使它位于数组的中间位置。

当目标值不存在时,算法能够正确地终止循环,避免不必要的迭代。

代码逻辑清晰简洁,不需要处理额外的边界情况。

因此,在二分查找算法中,使用 left <= right 作为循环条件是一种标准的做法,它确保了算法的正确性和效率。

丢失可能的解这个问题主要发生在当目标值在数组中有多份且位于数组的中间位置时。假设我们有一个有序数组,其中目标值在中间部分重复出现。如果我们使用 left < right 作为循环条件,那么在 left 和 right 都指向目标值时,循环会提前结束,从而可能错过找到其他相同目标值的机会。

举个例子来说明这个问题:

假设我们有一个有序数组 arr = [1, 2, 3, 3, 3, 4, 5],并且我们要查找的目标值是 3。如果我们使用 left < right 作为循环条件,并在每次迭代中更新 left 和 right 的值,那么在某个时刻,left 和 right 都可能指向中间的一个 3。此时,由于 left 不再小于 right,循环会结束,即使数组中还有其他 3 的实例。

然而,如果我们使用 left <= right 作为循环条件,那么当 left 和 right 都指向同一个 3 时,循环仍会继续,直到 left 和 right 交叉或者相遇,从而确保我们找到数组中所有的 3。

下面是一个简单的二分查找算法的示例代码,展示了使用 left <= right 的情况:

public static int binarySearch(int[] arr, int target) {  
    int left = 0;  
    int right = arr.length - 1;  
      
    while (left <= right) { // 使用 left <= right 作为循环条件  
        int mid = left + (right - left) / 2;  
          
        if (arr[mid] == target) {  
            // 找到目标值,返回其索引  
            return mid;  
        } else if (arr[mid] < target) {  
            // 目标值在右半部分  
            left = mid + 1;  
        } else {  
            // 目标值在左半部分或者不存在  
            right = mid - 1;  
        }  
    }  
      
    // 没有找到目标值  
    return -1;  
}

在这个示例中,我们使用 left <= right 来确保循环能够正确处理所有情况,包括当目标值位于数组中间且重复出现的情况。如果我们改用 left < right,则可能会过早地结束循环,导致错过一些目标值的实例。

当使用 left < right 作为二分查找的循环条件时,如果目标值不在数组中,那么在最后一次迭代中,即使 left 和 right 已经相邻(即 right = left + 1),循环体仍然会被执行一次。这会导致一次不必要的迭代,因为此时 mid 指向的位置已经不可能是目标值所在的位置了。

举个例子来说明这个问题:

假设我们有一个有序数组 arr = [1, 2, 4, 5, 6],并且我们要查找的目标值是 3。显然,3 不在这个数组中。如果我们使用 left < right 作为循环条件,那么在最后一次迭代中,left 会指向 4,right 会指向 5。此时,计算出的 mid 仍然是 4,然后我们会根据 arr[mid] 与目标值的比较结果来更新 left 或 right。

这个额外的迭代是不必要的,因为当 left 和 right 相邻时,它们之间的区间已经不可能包含目标值了。如果我们使用 left <= right 作为循环条件,那么在 left 和 right 相邻时,循环就会结束,避免了这次不必要的迭代。

下面是使用 left < right 可能导致额外迭代的二分查找算法示例代码:

public static int binarySearch(int[] arr, int target) {  
    int left = 0;  
    int right = arr.length - 1;  
      
    while (left < right) { // 使用 left < right 作为循环条件  
        int mid = left + (right - left) / 2;  
          
        if (arr[mid] == target) {  
            return mid;  
        } else if (arr[mid] < target) {  
            left = mid + 1;  
        } else {  
            right = mid - 1;  
        }  
    }  
      
    // 检查 left(或 right,因为它们现在相等)指向的元素是否是目标值  
    if (arr[left] == target) {  
        return left;  
    } else {  
        return -1; // 没有找到目标值  
    }  
}

在这个示例中,如果目标值不在数组中,那么在最后一次迭代时,即使 left 和 right 相邻,循环体仍然会执行。这会导致一次不必要的比较操作,并且最后还需要额外检查 arr[left] 是否等于目标值。

使用 left <= right 可以避免这种情况,因为在 left 和 right 相邻时,循环就会自然结束,我们只需要检查 arr[left](或 arr[right])是否等于目标值即可。这样可以简化代码逻辑并提高效率。

当在二分查找算法中去掉等号,即使用 left < right 作为循环条件时,确实会使代码逻辑变得更加复杂。这是因为你需要特别处理当 left 和 right 相邻时的情况,以确保它们不会再次进入循环体。这通常意味着在循环外部需要添加额外的检查来确认 left 或 right 指向的元素是否是目标值。

下面是一个使用 left < right 作为循环条件的二分查找算法示例,并展示了由此带来的逻辑复杂性:

public static int binarySearch(int[] arr, int target) {  
    int left = 0;  
    int right = arr.length - 1;  
      
    while (left < right) { // 使用 left < right 作为循环条件  
        int mid = left + (right - left) / 2;  
          
        if (arr[mid] == target) {  
            return mid; // 找到目标值,返回其索引  
        } else if (arr[mid] < target) {  
            left = mid + 1; // 目标值在右半部分  
        } else {  
            right = mid - 1; // 目标值在左半部分或者不存在  
        }  
    }  
      
    // 循环结束后,left 和 right 相邻或相等  
    // 需要额外检查 left(或 right)指向的元素是否是目标值  
    if (left == right && arr[left] == target) {  
        return left; // 找到目标值,返回其索引  
    }  
      
    return -1; // 没有找到目标值  
}

在这个示例中,循环会在 left 和 right 相邻时结束。但是,由于我们使用的是 left < right,所以在循环外部需要添加一个额外的检查来确定 arr[left] 是否等于目标值。这个额外的检查是因为在最后一次迭代中,即使 left 和 right 指向相同的元素,循环也不会执行。如果没有这个额外的检查,我们可能会错误地返回 -1,即使目标值实际上就位于 left(或 right)指向的位置。

使用 left <= right 可以避免这种逻辑复杂性,因为当 left 和 right 指向相同的元素时,循环体仍然会执行一次。这样,我们就可以在循环内部处理找到目标值的情况,而不需要在循环外部添加额外的检查。这简化了代码逻辑,并减少了出错的可能性。

##如果文章对你有帮助,那就给个关注吧!!!!##

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值