一文搞定搜索

搜索

算法入门

二分查找

左闭右开区间

二分查找插入点

无重复元素

存在重复元素 

 二分查找边界

查找左边界

 查找右边界

哈希优化策略 

线性查找

 哈希查找

相关例题

leetcode704.二分查找

法一:二分查找

leetcode278.第一个错误的版本

 法一:二分查找

leetcode724.寻找数组的中心下标

法一:前缀和

leetcode287.寻找重复数

 法一:快慢指针

leetcode154.寻找旋转排序数组中的最小值Ⅱ

法一:二分查找 


算法入门

二分查找

/**
 * 二分查找(双闭区间)
 * 在有序数组中查找指定目标值的索引。
 */
int binarySearch(int[] nums, int target) {
    // 初始化双闭区间 [0, n-1],即 i 和 j 分别指向数组的首元素、尾元素
    int i = 0, j = nums.length - 1;

    // 循环,当搜索区间为空时跳出(当 i > j 时为空)
    while (i <= j) {
        int m = i + (j - i) / 2; // 计算中点索引 m,避免溢出
        
        // 比较中间值与目标值
        if (nums[m] < target) { 
            // 此情况说明 target 在区间 [m+1, j] 中
            i = m + 1; 
        } else if (nums[m] > target) { 
            // 此情况说明 target 在区间 [i, m-1] 中
            j = m - 1; 
        } else { 
            // 找到目标元素,返回其索引
            return m; 
        }
    }

    // 未找到目标元素,返回 -1
    return -1; 
}

左闭右开区间

/**
 * 二分查找(左闭右开)
 * 在有序数组中查找指定目标值的索引。
 */
int binarySearch(int[] nums, int target) {
    // 初始化左指针为 0,右指针为数组长度
    int left = 0, right = nums.length; // 右指针为 nums.length,表示数组末尾的下一个位置

    // 循环,当搜索区间为空时跳出(当 left >= right 时为空)
    while (left < right) {
        int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出

        // 比较中间值与目标值
        if (nums[mid] < target) { 
            // 此情况说明 target 在区间 [mid + 1, right) 中
            left = mid + 1; 
        } else { 
            // 此情况说明 target 在区间 [left, mid) 中(包括 mid 本身)
            right = mid; 
        }
    }

    // 检查目标值是否存在于数组中
    if (left < nums.length && nums[left] == target) {
        return left; // 找到目标元素,返回其索引
    }

    // 未找到目标元素,返回 -1
    return -1; 
}
  1. 左闭右开:在这种搜索范围内,left 是闭合的(包括当前元素),而 right 是开口的(不包括 right 指向的元素)。
  2. 更新条件
    • 如果 nums[mid] < target,那么目标值在 mid + 1 到 right 范围内。
    • 其他情况下(即 nums[mid] >= target),目标值在 left 到 mid 范围内,包含 mid
  3. 查找结果:循环结束后,通过比较 left 指向的元素与目标值,判断目标值是否存在。
  4. 索引处理:注意到右指针的初始化设置为数组长度,以便能够正确处理所有元素,包括最后一个。

这种形式常用于需要搜索的范围不包含右端点的情况,可以更清晰地处理边界值和避免某些特殊边界条件的复杂性。

但是由于“双闭区间”表示中的左右边界都被定义为闭区间,因此通过指针 和指针 缩小区间的操作也是对称的。这样更不容易出错,因此一般建议采用“双闭区间”的写法

二分查找插入点

无重复元素

/**
 * 二分查找插入点(无重复元素)
 * 在有序数组中查找目标值的插入位置。
 * 如果目标值存在,则返回它的位置;如果不存在,则返回可以插入的位置。
 */
int binarySearchInsertionPoint(int[] nums, int target) {
    int left = 0, right = nums.length; // 初始化左指针为 0,右指针为数组长度

    // 循环,当搜索区间 [left, right) 不为空时继续
    while (left < right) {
        int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出

        // 比较中间值与目标值
        if (nums[mid] < target) {
            // target 在区间 [mid + 1, right) 中
            left = mid + 1; 
        } else { 
            // target 在区间 [left, mid) 中(包括 mid)
            right = mid; 
        }
    }

    // 循环结束时,left 即为目标值的插入位置
    return left; 
}

  1. 左闭右开:搜索区间 [left, right),包含 left,不包含 right,所以右指针设置为数组的长度。
  2. 插入点逻辑:如果 target 存在于数组中,通过返回索引可以获取其位置;如果不存在,则左指针left会指向可以插入的位置。
  3. 循环终止条件:循环持续到 left 小于 right,这表示区间未完全重合。
  4. 返回值:无论 target 是否存在,left 都是正确的插入位置。

存在重复元素 

/**
 * 二分查找插入点(存在重复元素)
 * 在有序数组中查找目标值的插入位置。
 * 如果目标值存在,则返回它的位置中最左侧的索引;
 * 如果不存在,则返回可以插入的位置。
 */
int binarySearchInsertionPointWithDuplicates(int[] nums, int target) {
    int left = 0, right = nums.length; // 初始化左指针为 0,右指针为数组长度

    // 循环,当搜索区间 [left, right) 不为空时继续
    while (left < right) {
        int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出

        // 比较中间值与目标值
        if (nums[mid] < target) {
            // target 在区间 [mid + 1, right) 中
            left = mid + 1; 
        } else { 
            // target 在区间 [left, mid] 中(包括 mid)
            right = mid; 
        }
    }

    // 在搜索完成后,left 会指向最左侧可以插入目标值的位置
    return left; 
}
  1. 左闭右开:与之前的方法相同,搜索区间设置为 [left, right),包含 left 但不包含 right,右指针为数组的长度。
  2. 处理重复元素:若 nums[mid] 等于 target,则将 right 更新为 mid,这会保证搜索的左半部分包含重叠值,并能返回最左侧位置。
  3. 插入逻辑:当循环结束时,left 指向的即为 target 的插入点,无论 target 是否存在于数组中。

 二分查找边界

查找左边界

/**
 * 二分查找最左一个 target
 * 在有序数组中查找目标值最左侧的位置。
 */
int binarySearchLeftEdge(int[] nums, int target) {
    int left = 0; // 初始化左指针
    int right = nums.length; // 初始化右指针,右指针为数组长度

    // 循环,当搜索区间 [left, right) 不为空时继续
    while (left < right) {
        int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出

        // 比较中间值与目标值
        if (nums[mid] < target) {
            left = mid + 1; // target 在右半部分,更新左指针
        } else {
            right = mid; // target 在左半部分或是 mid,自左相等,所以这部分包含 mid
        }
    }

    // 此时 left 指向可能的插入位置,需检查是否为目标值的索引
    if (left < nums.length && nums[left] == target) {
        return left; // 找到最左边的 target,返回索引
    }

    // 未找到 target,返回 -1
    return -1; 
}
  1. 循环条件:使用 while (left < right),确保左右指针交替。
  2. 中点计算int mid = left + (right - left) / 2; 计算中点,以避免大数组时可能发生的整数溢出。
  3. 定位左边界:根据目标值与中点值的比较,调整左右指针以找到最左侧的目标值。
  4. 最后检查:循环结束后,检查 left 指向的元素是否为目标值,若是,返回索引;若否,返回 -1。

 查找右边界

/**
 * 二分查找右边界
 * 在有序数组中查找目标值最右侧的位置。
 */
int binarySearchRightEdge(int[] nums, int target) {
    int left = 0; // 初始化左指针
    int right = nums.length; // 初始化右指针,右指针为数组长度

    // 循环,当搜索区间 [left, right) 不为空时继续
    while (left < right) {
        int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出

        // 比较中间值与目标值
        if (nums[mid] <= target) {
            left = mid + 1; // target 在右半部分,更新左指针
        } else {
            right = mid; // target 在左半部分,更新右指针
        }
    }

    // 此时 left 已经指向了目标值的右边界的下一个位置
    // 检查 left - 1 是否是目标值的索引
    if (left > 0 && nums[left - 1] == target) {
        return left - 1; // 找到最右边的 target,返回索引
    }

    // 未找到 target,返回 -1
    return -1; 
}
  1. 循环条件:使用 while (left < right),确保顺利搜索直到左右指针交替。
  2. 中点计算int mid = left + (right - left) / 2; 用于安全地避免整型溢出。
  3. 定位右边界:在比较中,如果 nums[mid] 小于或等于 target,则我们将左指针移动到 mid + 1,这样在目标值存在的情况下能够继续寻找右边界。
  4. 最后检查:当循环结束后,left 指向的元素是 target 的右边界的下一个位置。因此,检查 left - 1 是否等于目标值,用于返回最右侧目标值的索引。

哈希优化策略 

线性查找

以时间换空间
/**
 * 方法一:暴力枚举
 * 在给定数组中查找两个数的索引,使其和等于目标值。
 */
int[] twoSumBruteForce(int[] nums, int target) {
    int size = nums.length; // 获取数组的长度
    
    // 两层循环,时间复杂度为 O(n^2)
    for (int i = 0; i < size - 1; i++) {
        for (int j = i + 1; j < size; j++) {
            // 检查 nums[i] 和 nums[j] 的和是否等于目标值
            if (nums[i] + nums[j] == target) {
                // 找到符合条件的索引,返回结果
                return new int[] { i, j };
            }
        }
    }
    
    // 未找到符合条件的两个数,返回空数组
    return new int[0];
}
  1. 暴力枚举:使用两层嵌套循环遍历数组中的所有可能的数对。
  2. 时间复杂度:该算法有一个 O(n^2) 的时间复杂度,适合小型数组。这是因为每对元素之间都需要进行一次求和运算。
  3. 查找目标和:在内层循环中检查当前的两个数的和是否等于目标值 target
  4. 结果返回:一旦找到符合条件的索引,立即返回这两个数的索引。
  5. 未找到条件:如果没有找到符合条件的数对,返回一个空数组。

 哈希查找

以空间换时间

import java.util.HashMap;

/**
 * 方法二:哈希查找
 * 在给定数组中查找两个数的索引,使其和等于目标值。
 * 使用哈希表来提高查找效率。
 */
int[] twoSumHashMap(int[] nums, int target) {
    HashMap map = new HashMap<>(); // 创建哈希表
    
    // 遍历数组
    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i]; // 计算当前数的补数
        
        // 检查补数是否在哈希表中
        if (map.containsKey(complement)) {
            // 找到符合条件的索引,返回结果
            return new int[] { map.get(complement), i };
        }
        
        // 将当前数及其索引存入哈希表
        map.put(nums[i], i);
    }
    
    // 未找到符合条件的两个数,返回空数组
    return new int[0];
}
  1. 哈希表:使用 HashMap 存储数组中元素及其索引。键是元素的值,值是元素的索引。
  2. 查找补数:在遍历数组的同时,计算出当前元素的补数(即 target - nums[i]),并在哈希表中查看该补数是否已经存在。
  3. 查找效率:此方法的时间复杂度为 O(n),因为每个元素只需遍历一次,哈希表的查找、插入操作平均为 O(1)。
  4. 结果返回:一旦找到符合条件的补数及其索引,立即返回这两个数的索引。
  5. 未找到条件:如果没有找到符合条件的数对,返回一个空数组。

相关例题

leetcode704.二分查找

704. 二分查找icon-default.png?t=O83Ahttps://leetcode.cn/problems/binary-search/

法一:二分查找

public class Method01 {
    /**
     * 在有序数组中使用二分查找法查找目标值
     * @param nums 有序数组
     * @param target 需要查找的目标值
     * @return 目标值在数组中的索引,如果不存在则返回 -1
     */
    public int search(int[] nums, int target) {
        int len = nums.length; // 数组的长度
        int left = 0; // 左边界初始化为数组的起始索引
        int right = len - 1; // 右边界初始化为数组的末尾索引

        // 当左边界小于等于右边界时,继续进行查找
        while (left <= right) {
            int mid = left + (right - left) / 2; // 计算中间索引

            // 如果中间值大于目标值,说明目标值在左半部分
            if (nums[mid] > target) {
                right = mid - 1; // 更新右边界
            }
            // 如果中间值小于目标值,说明目标值在右半部分
            else if (nums[mid] < target) {
                left = mid + 1; // 更新左边界
            }
            // 如果中间值等于目标值,返回中间索引
            else {
                return mid; // 找到目标值,返回其索引
            }
        }

        // 如果目标值不存在于数组中,返回 -1
        return -1;
    }
}

leetcode278.第一个错误的版本

278. 第一个错误的版本icon-default.png?t=O83Ahttps://leetcode.cn/problems/first-bad-version/

 法一:二分查找

public class Method01 extends VersionControl {

    public int firstBadVersion(int n) {
        int left = 1; // 左边界初始化为 1,表示从第 1 个版本开始
        int right = n; // 右边界初始化为 n,表示到第 n 个版本为止

        // 二分查找,直到 left 和 right 相遇
        while (left < right) {
            int mid = left + (right - left) / 2; // 计算中间版本

            // 如果 mid 是坏的版本
            if (isBadVersion(mid)) {
                right = mid; // 则第一个坏版本在 mid 左侧(包括 mid)
            } else {
                left = mid + 1; // 否则第一个坏版本在 mid 的右侧
            }
        }

        // 当结束时,left 和 right 相等,指向第一个坏版本
        return left; // 返回第一个坏版本的版本号
    }
}

leetcode724.寻找数组的中心下标

724. 寻找数组的中心下标icon-default.png?t=O83Ahttps://leetcode.cn/problems/find-pivot-index/

法一:前缀和

public class Method01 {
    /**
     * 找到数组的枢轴索引
     * @param nums 输入的整数数组
     * @return 数组的枢轴索引,如果不存在则返回 -1
     */
    public int pivotIndex(int[] nums) {
        // 计算数组元素的总和
        int sum = Arrays.stream(nums).sum();
        int leftSum = 0; // 初始化左边的和为 0

        // 遍历数组
        for (int i = 0; i < nums.length; i++) {
            // 检查左边的和是否等于右边的和
            // 右边的和可以通过总和减去左边的和和当前元素得到
            if (leftSum == sum - leftSum - nums[i]) {
                return i; // 如果相等,返回当前索引 i 作为枢轴索引
            }
            // 更新左边的和,加入当前元素
            leftSum += nums[i];
        }

        // 如果没有找到枢轴索引,返回 -1
        return -1;
    }
}

leetcode287.寻找重复数

287. 寻找重复数icon-default.png?t=O83Ahttps://leetcode.cn/problems/find-the-duplicate-number/

 法一:快慢指针

public class Method01 {
    /**
     * 使用快慢指针方法查找数组中的重复数字。
     *
     * @param nums 输入的数组,其中的数字范围在 [1, n] 之间,并且数组长度为 n + 1,
     *             必定存在一个重复的数字。
     * @return 返回找到的重复数字。
     */
    public int findDuplicate(int[] nums) {
        // 快指针和慢指针初始化
        int slow = nums[0];              // 慢指针从数组的第一个元素开始
        int fast = nums[nums[0]]; // 快指针从数组的第一个元素的指向位置开始

        // 找到慢指针和快指针相遇的位置
        while (slow != fast) {
            slow = nums[slow];           // 慢指针每次移动一步
            fast = nums[nums[fast]]; // 快指针每次移动两步
        }

        // 重置快指针为 0
        fast = 0;

        // 找到重复数字所在的位置
        // 此时慢指针和快指针相遇处即为环的入口
        while (slow != fast) {
            slow = nums[slow];       // 慢指针也从相遇点开始,移动一步
            fast = nums[fast];       // 快指针从 0 开始,移动一步
        }

        // 返回重复的数字
        return slow;                 // slow 和 fast 在环的入口相遇,因此返回 slow
    }
}

leetcode154.寻找旋转排序数组中的最小值Ⅱ

154. 寻找旋转排序数组中的最小值 IIicon-default.png?t=O83Ahttps://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/

法一:二分查找 

public class Method01 {
    /**
     * 寻找旋转排序数组中的最小值。
     * 假设数组是通过某种方式旋转过的,可能包含重复元素。
     *
     * @param nums 输入的旋转排序数组
     * @return 数组中的最小值
     */
    public int findMin(int[] nums) {
        int left = 0;                          // 初始化左指针为数组的起始位置
        int right = nums.length - 1;          // 初始化右指针为数组的结束位置

        // 当左指针小于右指针时继续循环
        while (left < right) {
            int mid = left + (right - left) / 2; // 计算中间指针,避免可能的溢出
            if (nums[mid] < nums[right]) { // 判断中间值与右边值的大小关系
                right = mid;                  // 当中间值小于右边值,说明最小值在左半部分
            } else if (nums[mid] > nums[right]) {
                left = mid + 1;               // 当中间值大于右边值,说明最小值在右半部分
            } else { // 当中间值等于右边值时,可能会错过最小值,缩小右边界
                right--;                       // 缩小右边界,排除掉右边的重复值
            }
        }
        // 循环结束,left 指向最小值
        return nums[left];                    // 返回最小值
    }
}

文章记录了学习Krahets的《Hello 算法》的轨迹,代码均使用Java语言,原书支持 Python、C++、Java、C#、Go、Swift、JavaScript、TypeScript、Dart、 Rust、C 和 Zig 等语言。

教程链接:krahets/hello-algo: 《Hello 算法》:动画图解、一键运行的数据结构与算法教程。支持 Python, Java, C++, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 代码。简体版和繁体版同步更新,English version ongoing (github.com)​编辑https://github.com/krahets/hello-algoicon-default.png?t=O83Ahttps://github.com/krahets/hello-algo

你可能感兴趣的:(数据结构与算法,算法,java,数据结构)