查找算法及应用

158 阅读3分钟

参考:

  1. 我写了首诗,把二分搜索算法变成了默写题
  2. 二分搜索怎么用?我又总结了套路
  3. 二分查找算法如何运用?我和快手面试官进行了深入探讨

问题列表

序号题目完成
704. 二分查找
35. 搜索插入位置
34. 在排序数组中查找元素的第一个和最后一个位置
1011. 在 D 天内送达包裹的能力
410. 分割数组的最大值
875. 爱吃香蕉的珂珂
287. 寻找重复数
658. 找到 K 个最接近的元素
793. 阶乘函数后 K 个零
剑指 Offer 53 - I. 在排序数组中查找数字 I
剑指 Offer II 073. 狒狒吃香蕉

题解

基础二分算法

704. 二分查找

二分查找算法的示例,只能处理无重复字段的集合的查找。

// 递归写法
class Solution {
    public int search(int[] nums, int target) {
        return search(nums, 0, nums.length - 1, target);
    }

    public int search(int[] nums, int start, int end, int target) {
        if (start > end) {
            return -1;
        }
        int mid = start + (end - start) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            return search(nums, mid + 1, end, target);
        } else {
            return search(nums, start, mid - 1, target);
        }
    }
}
// 迭代写法
class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1; // 注意
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1; // 注意
            }
        }
        return -1;
    }
}
class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return left;
    }
}

含有重复元素的左右边界

34. 在排序数组中查找元素的第一个和最后一个位置

相同问题:剑指 Offer 53 - I. 在排序数组中查找数字 I

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] ans = new int[2];
        int len = nums.length - 1;
        ans[0] = searchLeft(nums, 0, len, target);
        ans[1] = ans[0] == -1 ? -1 : searchRight(nums, 0, len, target);
        return ans;
    }

    public int searchLeft(int[] nums, int left, int right, int target) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                // 分两种情况,只有一个目标,有多个目标
                // 只有一个时,right = ans - 1, left 不断增大直到最后一个left = mid + 1,left就是最终的结果
                // 有多个时,每遇到一次相等,right就减1,最后变成只有一个的情况
                right = mid - 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (left == nums.length) {
            return -1;
        }
        return nums[left] == target ? left : -1;
    }

    public int searchRight(int[] nums, int left, int right, int target) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                // 分两种情况,只有一个目标,有多个目标
                // 只有一个时,left = ans + 1, right不断的缩小,知道最后一个right = mid -1,right就是最终的结果
                // 有多个时,每遇到一次相等,left就加1,最后变成只有一个的情况
                left = mid + 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (right < 0) {
            // 因为right一直在减少,所以判断right的下限
            return -1;
        }
        return nums[right] == target ? right : -1;
    }
}
// 左开右闭的写法,更简单些
class Solution {
    public int search(int[] nums, int target) {
        int leftIdx = findLeft(nums, target);
        if (leftIdx == -1) {
            return 0;
        }
        int rightIdx = findRight(nums, target);
        return rightIdx - leftIdx + 1;
    }

    public int findLeft(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target) {
                // 往左边收缩
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    public int findRight(int[] nums, int target) {
        int left = 0, right = nums.length;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] <= target) {
                // 当找到 target 时,收缩左侧边界
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left - 1;
    }
}

二分查找的应用

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

首先想到暴力解法,时间复杂度不是很理想。

执行耗时:284 ms,击败了5.86% 的Java用户
内存消耗:46.8 MB,击败了15.82% 的Java用户

于是想怎么优化,其实容量和天数是线性关系,确定了容量之后,所花费的天数其实是一个非递增数组,这种情况必然要想到用二分查找。

// 暴力解法
class Solution {
    public int shipWithinDays(int[] weights, int days) {
        int max = 0;
        int sum = 0;
        for (int num : weights) {
            max = Math.max(max, num);
            sum += num;
        }
        // 如果一天运完,直接返回总数
        if (days == 1) {
            return sum;
        }
        int len = weights.length;
        // 随着容量的增大,需要的天数会变小
        for (int capacity = max; capacity <= sum; capacity++) {
            int right = 0;
            int total = 0;
            int currentDays = 1;
            while (right < len && currentDays <= days) {
                total += weights[right];
                // 超过容量的时候,重新计数,day+1
                if (total > capacity) {
                    // 重新计数
                    total = weights[right];
                    right++;
                    currentDays++;
                    continue;
                }
                // 没超过容量,再计算下一个
                right++;
            }
            if (currentDays > days) {
                continue;
            }
            return capacity;
        }
        return sum;
    }
}
//二分查找
class Solution {
    public int shipWithinDays(int[] weights, int days) {
        int left = 0;
        int right = 0;
        for (int num : weights) {
            left = Math.max(left, num);
            right += num;
        }
        // 如果需要一天运完,直接返回总数,无需遍历
        if (days == 1) {
            return right;
        }
        // 随着容量的增大,需要的天数会变小
        // left = max, right = sum, currentDays会慢慢减少,currentDays是一个非递增数组,找左边界
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (getDays(weights, mid) <= days) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    // 根据容量得到运完需要的天数
    public int getDays(int[] weights, int capacity) {
        int right = 0;
        int total = 0;
        int days = 1;
        while (right < weights.length) {
            total += weights[right];
            // 超过容量的时候,重新计数,day+1
            if (total > capacity) {
                // 重新计数
                total = weights[right];
                days++;
            }
            // 没超过容量,再计算下一个
            right++;
        }
        return days;
    }
}
//leetcode submit region end(Prohibit modification and deletion)

287. 寻找重复数

//方法一:二分查找
class Solution {
    public int findDuplicate(int[] nums) {
        int n = nums.length;
        int l = 1, r = n - 1, ans = -1;
        // cnt[i] 表示nums数组中小于等于i的数有多少个
        // i的范围为[1,n]
        // 4, 3, 2, 7, 8, 6, 5, 3, 1
        // i   | 1 2 3 4 5 6 7 8 9
        // cnt | 1 2 4 5 6 7 8 9 9
        // 假设重复的数是target
        // [1, target-1]里所有的cnt[i]都小于等于i
        // [target, n]里所有的cnt[i]都大于i
        while (l <= r) {
            int mid = (l + r) >> 1;
            int cnt = 0;
            // 计算cnt
            for (int i = 0; i < n; ++i) {
                if (nums[i] <= mid) {
                    cnt++;
                }
            }
            // 找cnt,cnt是大于i的第一个数,可以理解为找右边界
            if (cnt <= mid) {
                l = mid + 1;
            } else {
                r = mid - 1;
                ans = mid;
            }
        }
        return ans;
    }
}

875. 爱吃香蕉的珂珂

相同问题:剑指 Offer II 073. 狒狒吃香蕉

class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        // 显然速度约大,h越小,那么h就是一个非递增数组,求最小的k,即求左边界
        // 最少要吃一个,否则无意义
        int left = 1;
        // 最多不超过max,否则无意义
        int right = 1;
        for (int pile : piles) {
            right = Math.max(right, pile);
        }
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (getHours(piles, mid) <= h) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    public int getHours(int[] piles, int k) {
        int hours = 0;
        for (int pile : piles) {
            hours += pile / k;
            if (pile % k > 0) {
                hours++;
            }
        }
        return hours;
    }
}

410. 分割数组的最大值

对于这种问题,我们首先应该想到对结果进行分析:

  1. 对各个子数组的和的范围:
  • 最小值:因为m<=nums.length,这就是说最极限的情况是每一个子数组里都只有一个元素,那么此时是元素中的最大值;
  • 最大值:m=1时,即是所有元素的和。
  1. 子数组的和(以下用target代替)与m的关系:
  • 随着target的增加,m会减少,因为target是最大子数组和,target越大说明元素越多,分的组自然就越小。
  • 由target得到的cnt(实时子数组数量)< m,说明target有点大了,可以减少
  • 由target得到的cnt(实时子数组数量)> m,说明target有点小了,可以增加
  • 由target得到的cnt(实时子数组数量)== m,说明正好,但是这个时候,我们需要求左边界,所以继续缩小。
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
    public int splitArray(int[] nums, int k) {
        int right = 0;
        int left = 0;
        for (int num : nums) {
            right += num;
            left = Math.max(left, num);
        }

        while (left < right) {
            int mid = left + (right - left) / 2;
            if (getSum(nums, mid) <= k) {
                // 小于说明还有优化的空间,target可以继续减少
                // 等于说明target是满足的,因为我们要寻找左边界,所以左移
                right = mid;
            } else {
                // 大于,说明target不够,需要增大
                left = mid + 1;
            }
        }
        return left;
    }

    public int getSum(int[] nums, int target) {
        int sum = 0;
        int cnt = 1;
        for (int i = 0; i < nums.length; i++) {
            if (sum + nums[i] > target) {
                cnt++;
                sum = nums[i];
            } else {
                sum += nums[i];
            }
        }
        return cnt;
    }
}
//leetcode submit region end(Prohibit modification and deletion)