查找

1.普通查找

剑指 Offer 03. 数组中重复的数字

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3

限制:

2 <= n <= 100000

class Solution {
    public int findRepeatNumber1(int[] nums) {
        // 暴力搜索
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] == nums[j]) {
                    return nums[i];
                }
            }
        }
        return -1;
    }

    public int findRepeatNumber(int[] nums) {
        // hashSet:存储无序的,不重复的元素
        Set<Integer> set = new HashSet<>();
        for (int num : nums) {
            if (set.contains(num)) {
                return num;
            }
            set.add(num);
        }
        return -1;
    }

    public int findRepeatNumber(int[] nums) {
        // 长度为n的数组中元素取值是0~n-1,则索引与值的对应关系是一对多的关系
        // 如果题目要求空间复杂度是O(1),则需要使用原地交换
        int i = 0;
        while (i < nums.length) {
            if (nums[i] == i) {// 只有当nums[i] == i时我们才i++,这样可以避免漏掉的元素
                i++;
                continue;
            }
            if (nums[i] == nums[nums[i]]) { // 找到相同的元素
                return nums[i];
            }
            // 否则交换nums[i]与nums[nums[i]]
            int tmp = nums[i];
            nums[i] = nums[tmp];
            nums[tmp] = tmp;
        }
        return -1;
    }
}

2.二分查找

2.1 什么时候用

排序数组的搜索问题,首先想到二分查找

  • 当题目数组是单调数组时,考虑用二分查找;
  • 当题目要求时间复杂度是logN时,考虑用二分查找;
  • 当题目求最小最大值时

2.2 模板

// 模板一:二分查找某个数,返回index
public int binarySearch(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            return mid;
        }
    }
    return -1;
}
// 模板二:二分查找左边界
public int searchLeft(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return left;
}
// 结果分析,对于nums = {5,7,7,8,8,10},target = 8,此时left = 3;如果target = 9,此时left = 5
// 即:如果nums包含target,则返回第一个target的下标,如果不包含target,则返回第一个大于target的下标
// 如果题目要求如果不包含target则返回-1,只需要添加如下判断就行
public int searchLeft(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    if (left >= nums.length || nums[left] != target) return -1;
    return left;
}
// 模板三:二分查找右边界
public int searchRight(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return right;
}
// 结果分析,对于nums = {5,7,7,8,8,10},target = 8,此时right = 4;如果target = 9,此时left = 4
// 即:如果nums包含target,则返回最后一个target的下标,如果不包含target,则返回第一个小于target的下标
// 如果题目要求如果不包含target则返回-1,只需要添加如下判断就行
public int searchRight(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    if (right < 0 || nums[right] != target) return -1;
    return right;
}

口诀:移右确左,移左确右

通用模板:

1.初始化:左边界i = 0,右边界j = nums.length - 1。

2.循环二分:当闭区间[i, j]无元素时跳出;

  • 计算中点m = (i + j) / 2(向下取整);
  • 若nums[m] < target,则target在闭区间[m+1, j]中,因此执行i = m + 1;
  • 若nums[m] > target,则target在闭区间[i, m - 1]中,因此执行j = m -1;
  • 若nums[m] = target,则右边界right在闭区间[m + 1, j],左边界在区间[i, m - 1]中,因此分为一下两种情况:
    1. 若查找右边界,则执行i = m + 1;(跳出时i指向右边界)
    2. 若查找左边界,则执行j = m - 1;(跳出时j指向左边界)

2.3 题目一

剑指 Offer 53 - I. 在排序数组中查找数字 I

统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 0

提示:

0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109

class Solution {
    public int search(int[] nums, int target) {
        // 解法一:暴力搜索
        if (nums.length == 0 || target < nums[0] || target > nums[nums.length - 1]) return 0;
        int res = 0;
        for (int num : nums) {
            if (num == target) {
                res++;
            }
        }
        return res;
    }

    public int search(int[] nums, int target) {
        // 解法一:二分查找,重复出现的次数 = right - left -1
        if (nums.length == 0 || target < nums[0] || target > nums[nums.length - 1]) return 0;
        // 1.查找右边界
        int i = 0;
        int j = nums.length - 1;
        while (i <= j) {
            int m = (i + j) / 2;
            if (nums[m] <= target) {
                i = m + 1;
            } else {
                j = m - 1;
            }
        }
        int right = i;
        if (j > 0 && nums[j] != target) return 0;
        // 2.查找左边界
        i = 0;
        j = nums.length - 1;
        while (i <= j) {
            int m = (i + j) / 2;
            if (nums[m] < target) {
                i = m + 1;
            } else {
                j = m - 1;
            }
        }
        int left = j;

        return right - left - 1;
    }

    public int search(int[] nums, int target) {
        // 解法三:查找左边界可以变换为查找target - 1的右边界
        return searchLeft(nums, target) - searchLeft(nums, target - 1);
    }

    private int searchLeft(int[] nums, int target) {
        int i = 0;
        int j = nums.length - 1;
        while (i <= j) {
            int m = (i + j) / 2;
            if (nums[m] <= target) {
                i = m + 1;
            } else {
                j = m - 1;
            }
        }
        return i;
    }
}

img

步骤:

  1. 确定x,f(x)和target分别是什么,并写出函数f的代码;
  2. 找到x的取值范围作为二分搜索的搜索区间,并初始化left和right变量;
  3. 根据题目的要求,确定是搜索左右区间。

2.4 题目二

875. 爱吃香蕉的珂珂

珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。

珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。

示例 1:

输入: piles = [3,6,7,11], H = 8
输出: 4
示例 2:

输入: piles = [30,11,23,4,20], H = 5
输出: 30
示例 3:

输入: piles = [30,11,23,4,20], H = 6
输出: 23

提示:

1 <= piles.length <= 10^4
piles.length <= H <= 10^9
1 <= piles[i] <= 10^9

class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        // x是吃香蕉的速度,f(x)是吃掉香蕉的时间,f(x)是单调递减的,target是时间H
        int left = 1, right = 1000000000;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (hours(piles, mid) <= h) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    private int hours(int[] piles, int speed) {
        int hour = 0;
        for (int i = 0; i < piles.length; i++) {
            hour += (piles[i] % speed) > 0 ? ((piles[i] / speed) + 1) : (piles[i] / speed);
        }
        return hour;
    }
}

2.5 题目三

1011. 在 D 天内送达包裹的能力

传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。

传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。

返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。

 

示例 1:

输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10

请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。 
示例 2:

输入:weights = [3,2,2,4,1,4], days = 3
输出:6
解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4
示例 3:

输入:weights = [1,2,3,1,1], D = 4
输出:3
解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1
 

提示:

1 <= days <= weights.length <= 5 * 104
1 <= weights[i] <= 500
class Solution {
    public int shipWithinDays(int[] weights, int days) {
        int left = Arrays.stream(weights).max().getAsInt();
        int right = Arrays.stream(weights).sum();
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (days(weights, mid) <= days) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    private int days(int[] weights, int capacity) {
        int day = 0;
        int curWeight = 0;
        for (int i = 0; i < weights.length; i++) {
            curWeight += weights[i];
            if (curWeight > capacity) {
                day += 1;
                curWeight = weights[i];
            }
        }
        return day + 1;// 遍历到最后curWeight <= capacity要单独算一天
    }
}
posted @ 2022-03-12 18:06  freryc  阅读(55)  评论(0编辑  收藏  举报