查缺补漏-二分查找

二分查找也常被称为 二分法 或者 折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(logn)

实现的代码很简单,简单看一下实现

// 二分查找(折半查找):对于已排序,若无序,需要先排序

// 非递归
int BinarySearch(vector<int> v, int value , int low, int high) {
    if (v.size() <= 0) {
        return -1;
    }
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (v[mid] == value) {
            return mid;
        }
        else if (v[mid] > value) {
            high = mid - 1;
        }
        else {
            low = mid + 1;
        }
    }

    return -1;
}

// 递归
int BinarySearch2(vector<int> v, int value, int low, int high)
{
    if (low > high)
        return -1;
    int mid = low + (high - low) / 2;
    if (v[mid] == value)
        return mid;
    else if (v[mid] > value)
        return BinarySearch2(v, value, low, mid - 1);
    else
        return BinarySearch2(v, value, mid + 1, high);
}

还是做一做题详细体会一下~

69.x 的平方根

69

策略:很简单的二分查找,但是需要注意边界处理,比如 0整数部分

int mySqrt(int a) {
    if (a == 0) return a;
    int l = 1, r = a, mid, sqrt;
    while (l <= r) {
        mid = l + (r - l) / 2;
        sqrt = a / mid;
        if (sqrt == mid) {
            return mid;
        } else if (mid > sqrt) {
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    return r;
}

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

34

策略:二分查找一个重复元素的边界问题,需要额外注意

vector<int> searchRange(vector<int>& nums, int target) {
    if (nums.empty()) return vector<int>{-1, -1};
    int lower = lower_bound(nums, target);
    int upper = upper_bound(nums, target) - 1; 
    if (lower == nums.size() || nums[lower] != target) {
        return vector<int>{-1, -1};
    }
    return vector<int>{lower, upper};
}

int lower_bound(vector<int> &nums, int target) {
    int l = 0, r = nums.size(), mid;
    while (l < r) {
        mid = (l + r) / 2;
        if (nums[mid] >= target) {
            r = mid;
        } else {
            l = mid + 1;
        }
    }
    return l;
}

int upper_bound(vector<int> &nums, int target) {
    int l = 0, r = nums.size(), mid;
    while (l < r) {
        mid = (l + r) / 2;
        if (nums[mid] > target) {
            r = mid;
        } else {
            l = mid + 1;
        }
    }
    return l;
}

81.搜索旋转排序数组 II

81

策略:​题目中对旋转的解释其实和它题解中的解释是有出入的,实际这道题的意思是, 例如 nums=[0,1,4,4,5,6,7], 旋转第一个 4, 是将第一个数字 4 后面的有序数组挪到原数组前面,也就是变成了 [4,5,6,7,0,1,4]
按照这个思路,二分的思路就需要想办法逐步缩小最小值所在的范围,只要使用 mid 不停和最右边的值进行比较即可,毕竟右边是部分有序的

bool search(vector<int>& nums, int target) {
    int start = 0, end = nums.size() - 1;
    while (start <= end) {
        int mid = (start + end) / 2;
        if (nums[mid] == target) {
            return true;
        }
        if (nums[start] == nums[mid]) {
            // 无法判断哪个区间是增序的
            ++start;
        } else if (nums[mid] <= nums[end]) {
            // 右区间是增序的
            if (target > nums[mid] && target <= nums[end]) {
                start = mid + 1;
            } else {
                end = mid - 1;
            }
        } else {
            // 左区间是增序的
            if (target >= nums[start] && target < nums[mid]) {
                end = mid - 1;
            } else {
                start = mid + 1;
            }
        }
    }
    return false;
}

其实暴力的 for 循环最起码代码简单很多,但是时间复杂度就不能到 O(log(n))~

154.寻找旋转排序数组中的最小值 II

154

策略:还是想办法逐步搜小最小值所在的范围,只要使用 mid 不同和最右边的值进行比较即可

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0;
        int right = nums.size() - 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 --;
            }
        }
        return nums[left];
    }
};

540.有序数组中的单一元素

540

策略:根据独立数出现前后,奇偶位置比较的变化,决定 leftright 如何变化

int singleNonDuplicate(vector<int>& nums) {
    int left = 0, right = nums.size() - 1;
    while (left < right) {
        int mid = left + (right-left)/2;
        if ( mid%2 == 0) {
            if (nums[mid] == nums[mid+1]) {
                left = mid+1;
            } else {
                right = mid;
            }
        } else {
            if ( nums[mid] == nums[mid-1]) {
                left = mid+1;
            } else {
                right = mid;
            }
        }
    }
    return nums[left];
}

4.寻找两个正序数组的中位数

4

下意识的想到先把两个数组合并,然后根据总长度的奇偶,来决定返回的中位数值,但是要设计一个时间复杂度为 O(log(m+n)) 的算法解决此问题, 肯定要用二分查找的方法。

官方提供的二分思路很有意思,将原本找中位数,变成找第 k 小的数,每次二分之后,移除不可能是第 k 小的元素,通过修正 k 这个目标,直到出现 边界情况 或者 k=1 时, 找出剩余数组中的第 k 个元素

int getKth(const vector<int>& nums1, const vector<int>& nums2, int k) {
    /* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
     * 这里的 "/" 表示整除
     * nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
     * nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
     * 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
     * 这样 pivot 本身最大也只能是第 k-1 小的元素
     * 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
     * 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
     * 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
     */

    int m = nums1.size();
    int n = nums2.size();
    int index1 = 0, index2 = 0;

    while (true) {
        // 边界情况
        if (index1 == m) {
            return nums2[index2 + k - 1];
        }
        if (index2 == n) {
            return nums1[index1 + k - 1];
        }
        if (k == 1) {
            return min(nums1[index1], nums2[index2]);
        }

        // 正常情况
        int newIndex1 = min(index1 + k / 2 - 1, m - 1);
        int newIndex2 = min(index2 + k / 2 - 1, n - 1);
        int pivot1 = nums1[newIndex1];
        int pivot2 = nums2[newIndex2];
        if (pivot1 <= pivot2) {
            k -= newIndex1 - index1 + 1;
            index1 = newIndex1 + 1;
        }
        else {
            k -= newIndex2 - index2 + 1;
            index2 = newIndex2 + 1;
        }
    }
}

double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    int totalLength = nums1.size() + nums2.size();
    if (totalLength % 2 == 1) {
        return getKth(nums1, nums2, (totalLength + 1) / 2);
    }
    else {
        return (getKth(nums1, nums2, totalLength / 2) + getKth(nums1, nums2, totalLength / 2 + 1)) / 2.0;
    }
}

总结

二分查找有2个前提 有序 且是 顺序 数组,这个有序只是为了便于我们缩小区间,所以数组可以不是完全有序,只要能够在判断后删除不可能的一半区间即可,循环,直到得到结果~