二分查找变形记:从循环不变量说起

二分查找

  • 二分查找
    • 二分查找
    • 循环不变量

首选我们介绍标题中提到的两个名词:

二分查找

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

循环不变量

其主要用来帮助理解算法的正确性。形式上很类似与数学归纳法,它是一个需要保证正确断言。对于循环不变式,必须证明它的三个性质:

  • 初始化:它在循环的第一轮迭代开始之前,应该是正确的。
  • 保持:如果在循环的某一次迭代开始之前它是正确的,那么,在下一次迭代开始之前,它也应该保持正确。
  • 终止:循环能够终止,并且可以得到期望的结果,不变式给了我们一个有用的性质,那就是获得正确的结果。

那这两个东西又有什么关联呢?上面已经提到循环不变量可以证明算法的正确性!这样的话,我们下面就运用循环不变量证明一下二分查找各种应用算法的正确性。

  1. 给定一个有序(升序,我们后面所有提到的数据均为升序,后面不再赘述)数组A,求任意一个i使得A[i]等于target,不存在则返回-1
    这是最原始的二分查找,利用数组有序性进行折半查找,时间复杂度为O(nlogn)。设原始数据为A[0 ~ n-1]

    • 初始化:low = 0, high = n - 1,mid = low + (high - low)/2。如果targer存在原始数组中,其对应index一定处于[low, high], 即 A[low] <= target <= A[high]
    • 保持: 对于 A[mid] target, 则index只能存在[low, mid - 1], 且high = mid;对于A[mid] = target,因为 low <= mid <= high, index = mid,直接返回。
    • 终止:low >high(即high + 1)。待处理数组为空,表示tartget不存在此数组中。
    int search(int A[], int n, int target)  
    {  
        int low = 0, high = n-1;  
        while(low <= high)  
        {  
            // 注意:若使用(low+high)/2求中间位置容易溢出  
            int mid = low+((high-low)>>1);   
            if(A[mid] == target)  
                return mid;  
            else if(A[mid] < target)  
                low = mid+1;  
            else // A[mid] > target  
                high = mid-1;  
            // 循环不变式: A[low - 1] < target < A[high + 1]
        }  
        return -1;  
    }

    后面的案例,也都可以按照上述方法证明算法的正确性,但我们不一一赘述,但在描述算法的时候,我们会指出,循环过程中不变的性质(虽然这个性质可能在初始化的时候不一定成立),这对我们理解算法为何这么写有很大帮助!

  2. 给定一个有序数组A,可含有重复元素,求最小的i使得A[i]等于target,不存在则返回-1

    // 两种写法
    int lowerBound(int A[], int n, int target) {
        int low = 0, right = n - 1;
        while(low <= high) {
        /*
        为什么上面一定是 low <= high? low < high 行不行?
        */
            int mid = low + (high - low)/2;
            if(A[mid] < target) low = mid + 1;
            else high = mid - 1;
        }
        /*
        循环过程中,当low大于0时,因为A[mid] < target时,low=mid+1, 所以A[low-1] < target;
        当high小于n-1时,因为A[mid] >= target时,high = mid  - 1, A[high + 1 >= target;
        循环结束时,low 等于 high + 1,所以,如果A[low](A[high + 1])存在就等于target, 
        那么low(high)就是target出现的最小位置,否则target在数组中不存在。 
        综上所示,循环不变式为:  A[low - 1] < target <= A[high + 1]
        */   
        if(low < n && A[low] == target) return low;
        return -1;
    }
    
    int searchFirstPos(int A[], int n, int target)  
    {  
        if(n <= 0) return -1;  
        int low = 0, high = n-1;  
        while(low < high)  
        {  
            int mid = low+((high-low)>>1);  
            if(A[mid] < target)  
                low = mid+1;  
            else // A[mid] >= target  
                high = mid;  
        }  
        /*  
        循环过程中,当low大于0时,因为A[mid] < target时,low=mid+1, 所以A[low-1] < target;
        当high小于等于n-1时,因为A[mid] >= target时,high = mid, A[high] >= target;
        循环结束时,low 等于 high,所以,如果A[low](A[high])就等于target, 
        那么low(high)就是target出现的最小位置,否则target在数组中不存在。 
        综上所示,循环不变式为:  A[low - 1] < target <= A[high]
        */   
        if(A[low] != target)  
            return -1;  
        else  
            return low;  
    }
  1. 给定一个有序数组A,可含有重复元素,求最大的i使得A[i]等于target,不存在则返回-1

    // 两种写法
    int upperBound(int A[], int n, int target) {
        int low = 0, hihg = n - 1;
        while (low <= high) {
            /* 
            
            */  
            int mid = low + (high - low) / 2;
            if (A[mid] > target) high = mid - 1;
            else low = mid + 1;
        }
        /*  
        循环过程中,当high小于n-1时,因为A[mid] > target时,high=mid-1, A[high+1] > target的;
        当low大于0时,因为A[mid] <= target时,low = mid + 1, A[low - 1] <= target;
        循环结束时,low 等于 high + 1,所以,如果A[high](A[low - 1])等于target, 
        那么high(low - 1)就是target出现的最大位置,否则target在数组中不存在。 
        综上所示,循环不变式为:  A[low - 1] <= target < A[high + 1]
        */ 
        if (high >= 0 && A[high] == target) return high;
        return -1;
    }
    
    int searchLastPos(int A[], int n, int target)  
    {     
        if(n <= 0) return -1;  
        int low = 0, high = n-1;  
        while(low < high)  
        {  
            /* 
            这里中间位置的计算就不能用low+((high-low)>>1)了,因为当low+1等于high 
            且A[low] <= target时,会死循环;所以这里要使用low+((high-low+1)>>1), 
            这样能够保证循环会正常结束。
            ? 如果high-low+1溢出了怎么办
            */  
            int mid = low+((high-low+1)>>1);  
            if(A[mid] > target)  
                high = mid-1;  
            else // A[mid] <= target  
                low = mid;  
        }  
        /*  
        循环过程中,当high小于n-1时,因为A[mid] > target时,high=mid-1, A[high+1] > target的;
        当low大于0时,因为A[mid] <= target时,low = mid, A[low] <= target;
        循环结束时,low 等于 high,所以,如果A[high](A[low])等于target, 
        那么high(low)就是target出现的最大位置,否则target在数组中不存在。 
        综上所示,循环不变式为:  A[low] <= target < A[high + 1]
        */  
        if(A[high] != target)  
            return -1;  
        else  
            return high;  
    }
  2. 给定一个有序数组A,可含有重复元素,求最大的i使得A[i]小于target,不存在则返回-1

    int LastBoundLessThan(int A[], int n, int target) {
        int low = 0, high = n - 1;
        while (low <= high) {
            /* 
            
            */  
            int mid = low + (high - low) / 2;
            if (A[mid] >= target) high = mid - 1;
            else low = mid + 1;
        }
        /*  
        循环过程中,当high小于n-1时,因为A[mid] > target时,high=mid-1, A[high+1] > target的;
        当low大于0时,因为A[mid] <= target时,low = mid + 1, A[low - 1] <= target;
        循环结束时,low 等于 high + 1,所以,如果A[high](A[low - 1])等于target, 
        那么high(low - 1)就是target出现的最大位置,否则target在数组中不存在。 
        综上所示,循环不变式为:  A[low - 1] < target <= A[high + 1]
        */ 
        if (high >= 0 && A[high] < target) return high;
        return -1;
    }
    
    int searchLastPosLessThan(int A[], int n, int target)  
    {  
        if(n <= 0) return -1;  
        int low = 0, high = n-1;  
        while(low < high)  
        {  
            /* 
            这里中间位置的计算就不能用low+((high-low)>>1)了,因为当low+1等于high 
            且A[low] < target时,会死循环;所以这里要使用low+((high-low+1)>>1), 
            这样能够保证循环会正常结束。
            ? 如果high-low+1溢出了怎么办
            */  
            int mid = low+((high-low+1)>>1); // 注意,不要导致死循环  
            if(A[mid] < target)  
                low = mid;  
            else // A[mid] >= target  
                high = mid-1;  
        }  
        /*  
        循环过程中,当low大于0时,因为A[mid] < target时, low=mid,A[low] < target;
        当high小于n-1时,因为A[mid] >= target时, high = mid-1,A[high+1] >= target的;
        循环结束时,low 等于 high,所以,如果A[low](A[high])小于target, 
        那么low(high)就是要找的位置,否则不存在这样的位置(A[0] >= target时)。 
        循环不变式: A[low] < target <= A[high+1]
        */  
        return A[low] < target ? low : -1;  
    }
  3. 给定一个有序数组A,可含有重复元素,求最小的i使得A[i]大于target,不存在则返回-1

    int FiststBoundGreaterThan(int A[], int n, int target) {
        int low = 0, high = n - 1;
        while(low <= high) {
            int mid = low + (high - low)/2;
            if(A[mid] > target) high = mid - 1;
            else low = mid + 1;
        }
        /*  
        循环过程中,当low大于0时,因为A[mid] <= target时,low=mid+1, 所以A[low-1] <= target;
        当high小于等于n-1时,因为A[mid] > target时,high = mid - 1, A[high + 1] > target;
        循环结束时,low 等于 high + 1,所以,如果A[low](A[high + 1])大于target, 
        那么low(high)就是target出现的最小位置,否则target在数组中不存在。 
        综上所示,循环不变式为:  A[low - 1] <= target < A[high + 1]
        */ 
        if(low < n && A[low] > target) return low;
        return -1;
    }
    
    
    int searchFirstPosGreaterThan(int A[], int n, int target)  
    {  
        if(n <= 0) return -1;  
        int low = 0, high = n-1;  
        while(low < high)  
        {  
            int mid = low+((high-low)>>1);  
            if(A[mid] > target)  
                high = mid;  
            else // A[mid] <= target  
                low = mid+1;  
        }  
    
        /*  
        循环过程中,当low大于0时,因为A[mid] <= target时,low=mid+1, 所以A[low-1] <= target;
        当high小于等于n-1时,因为A[mid] > target时,high = mid, A[high] > target;
        循环结束时,low 等于 high,所以,如果A[low](A[high])大于target, 
        那么low(high)就是target出现的最小位置,否则target在数组中不存在。 
        综上所示,循环不变式为:  A[low - 1] <= target < A[high]
        */     
        return A[high] > target ? high : -1;  
    }

    下面的一些问题是二分查找拓展性的问题,均可重用或改进上述 代码解决:

  4. 给定一个有序数组A,可含有重复元素,求target在数组中出现的次数
    求出第一次出现位置和最后一次出现位置,相减即可获得

    int count(int A[], int n, int target)  
    {  
        int firstPos = searchFirstPos(A, n, target); // 第一次出现位置  
        if(firstPos == -1)  
            return 0;  
        int lastPos = searchLastPos(A, n, target);  // 最后一次出现位置  
        return lastPos-firstPos+1;  // 出现次数  
    }
  5. 给定一个有序数组A,若target在数组中出现,返回位置,若不存在,返回它应该插入的位置
    leetcode原题:Search Insert Position
    题目分析,简单来说就是寻找第一个大于等于target的位置,如果target大于所有数,则在vector最后插入该数。

    int searchInsert0(int A[], int n, int target) {
        if(target > A[n-1]) return n;
        int low = 0, high = n-1;
        while(low <= high) {
            int mid = low + (high - low)/2;
            if(A[mid] < target) low = mid + 1;
            else high = mid - 1;
        }
        return low;
    }
8. 给定一个有序数组A,可含有重复元素,求绝对值最小的元素的位置  

    找第一个大于等于0的位置,然后和前一个元素的绝对值比较,返回绝对值较小的元素的位置

    ```c++
    int searchMinAbs(int A[], int n)  
    {  
        int low = 0, high = n-1;  
        while(low < high)  
        {  
            int mid = low+((high-low)>>1);  
            if(A[mid] < 0)  
                low = mid+1;  
            else // A[mid] >= 0  
                high = mid;  
        }  
        /* 循环结束时,如果low != n-1,A[low] >= 0,如果low>0,A[low-1] < 0 */  
        if(low > 0 && abs(A[low-1]) < abs(A[low]))  
            return low-1;  
        else  
            return low;  
    }  
    ```

9. 给定一个有序数组A和一个有序(非降序)数组B,可含有重复元素,求两个数组合并结果中的第k(k>=0)个数字   
    这个题目出现了两个数组,有序的,不管怎样我们就应该首先考虑二分查找是否可行。若使用顺序查找,时间复杂度最低为O(k),就是类似归并排序中的归并过程。使用用二分查找时间复杂度为O(logM+logN) 

    ```c++
    int findKthIn2SortedArrays(int A[], int m, int B[], int n, int k)  
    {  
        if(m <= 0) // 数组A中没有元素,直接在B中找第k个元素  
            return B[k];  
        if(n <= 0) // 数组B中没有元素,直接在A中找第k个元素  
            return A[k];  
        int i = (m-1)>>1; // 数组A的中间位置  
        int j = (n-1)>>1; // 数组B的中间位置  
        if(A[i] <= B[j])  // 数组A的中间元素小于等于数组B的中间元素  
        {  
            /* 
            设x为数组A和数组B中小于B[j]的元素数目,则i+1+j+1小于等于x, 
            因为A[i+1]到A[m-1]中还可能存在小于等于B[j]的元素; 
            如果k小于i+1+j+1,那么要查找的第k个元素肯定小于等于B[j], 
            因为x大于等于i+1+j+1;既然第k个元素小于等于B[j],那么只 
            需要在A[0]~A[m-1]和B[0]~B[j]中查找第k个元素即可,递归调用下去。 
            */  
            if(k < i+1+j+1)  
            {  
                if(j > 0)  
                    return findKthIn2SortedArrays(A, m, B, j+1, k);  
                else // j == 0时特殊处理,防止死循环  
                {  
                    if(k == 0)  
                        return min(A[0], B[0]);  
                    if(k == m)  
                        return max(A[m-1], B[0]);  
                    return A[k] < B[0] ? A[k] : max(A[k-1], B[0]);  
                }  
            }  
            /* 
            设y为数组A和数组B中小于于等于A[i]的元素数目,则i+1+j+1大于等于y; 
            如果k大于等于i+1+j+1,那么要查找到第k个元素肯定大于A[i],因为 
            i+1+j+1大于等于y;既然第k个元素大于A[i],那么只需要在A[i+1]~A[m-1] 
            和B[0]~B[n-1]中查找第k-i-1个元素,递归调用下去。 
            */  
            else  
                return findKthIn2SortedArrays(A+i+1, m-i-1, B, n, k-i-1);  
        }   
        // 如果数组A的中间元素大于数组B的中间元素,那么交换数组A和B,重新调用即可  
        else  
            return findKthIn2SortedArrays(B, n, A, m, k);  
    }  
    ```

10. 一个有序数组,可以含有重复元素,在某一个位置发生了旋转后,求target在变化后的数组中出现的位置,不存在则返回-10 1 2 4 5 6 7 可能变成 4 5 6 7 0 1 2  
    我们先比较中间元素是否是目标值,如果是返回位置。如果不是,我们就应该想办法将搜索区间减少一半。因为存在旋转变化,所以我们要多做一些判断。我们知道因为只有一次旋转变化,所以中间元素两边的子数组肯定有一个是有序的,那么我们可以判断target是不是在这个有序的子数组中,从而决定是搜索这个子数组还是搜索另一个子数组。    

    ```c++
    int searchInRotatedArray(int A[], int n, int target)   
    {  
        int low = 0, high = n-1;  
        while(low <= high)  
        {  
            int mid = low+((high-low)>>1);  
            if(A[mid] == target)   
                return mid;  
            if(A[mid] > A[low])   
            {  
                // low ~ mid 是升序的  
                if(target >= A[low] && target < A[mid])  
                    high = mid-1;  
                else  
                    low = mid+1;  
            }  
            else  
            {  
                // mid ~ high 是升序的  
                if(target > A[mid] && target <= A[high])  
                    low = mid+1;  
                else  
                    high = mid-1;  
            }  
        }  
        return -1;  
    }  
    ```

11. 一个有序数组,可以含有重复元素,在某一个位置发生了旋转后,求target在变化后的数组中出现的位置,不存在则返回-1   
    直接上代码:  

    ```c++
    int searchInRotatedArray(int A[], int n, int target)   
    {  
        int low = 0, high = n-1;  
        while(low <= high)  
        {  
            int mid = low+((high-low)>>1);  
            if(A[mid] == target)   
                return mid;
            if(A[mid] == A[low] && A[mid] == A[high]) {
                for( int i = low + 1; i < high; i++) {
                    if (target == A[i]) return i;
                }
            }
            if(A[mid] >= A[low])   
            {  
                // low ~ mid 是升序的  
                if(target >= A[low] && target < A[mid])  
                    high = mid-1;  
                else  
                    low = mid+1;  
            }  
            else if(A[mid] <= A[high])
            {  
                // mid ~ high 是升序的  
                if(target > A[mid] && target <= A[high])  
                    low = mid+1;  
                else  
                    high = mid-1;  
            }  
        }  
        return -1;  
    }  
    ```

12. 一个有序数组,没有重复元素,在某一个位置发生了旋转后,求最小值所在位置   
    我们用两个指针分别指向数组第一个元素和最后一个元素,然后寻找中间元素。如果中间元素位于前面递增子数组汇总(A[mid] > A[low]), 则最小元素应该位于中间元素的后面。 我们可以把第一个指针指向中间元素位置,在缩小寻找空间的同时,第一个指针仍然位于前面的递增子数组中。  
    如果中间元素位于后面的递增子数组中,那么中间元素应该小于最后一个元素即A[low] < A[high]。则最小元素应该位于中间元素的前面(或者中间元素本身),我们可以把第二个指针指向中间元素。  
    这样在缩小寻找范围的同时,第一个指针一定指向前方的递增子数组,第二个指针一定指向后面的递增子数组。最终他们会指向相邻的两个元素,第二个指针指向的恰好是最小元素,循环终止。   

    ```c++
    int searchMinInRotatedArray(int A[], int n)
    {
        if(n == 1)
            return 0;
        int low = 0, high = n-1;
        int mid = low;
        while(A[low] > A[high])
        {
            cout << low << " " << high << endl;
            if(high - low == 1) {
                mid = high;
                break;
            }
            mid = low + (high - low)/2;
            if(A[mid] > A[low]) low = mid;
            else if(A[mid] < A[high]) high = mid;
        }
        return mid;
    } 
    ```

    我们用两个指针分别指向第一个和最后一个元素。然后寻找中间元素,如果A[mid] < A[low],则最小值在low ~ mid范围内,我们将第二个指针指向中间元素位置;否则最小元素在mid ~ high范围内,我们将第一个元素指向中间位置。当两个指针两个相邻位置时,则循环终止。 

    ```c++
    int searchMinInRotatedArray1(int A[], int n)   
    {  
        if(n == 1)  
            return 0;  
        int low = 0, high = n-1;  
        while(low < high - 1) // 保证mid != low且mid != high  
        {  
            int mid = low+((high-low)>>1);  
            if(A[mid] < A[low]) // 最小值在low~mid  
                high = mid;  
            else // A[mid] > A[low], // 最小值在mid和high之间  
                low = mid;  
        }  
        return A[low] < A[low+1] ? low : low+1;  
    } 
    ```


13.一个有序数组,可能含有重复元素,在某一个位置发生了旋转后,求最小值所在位置  

    [1, 2, 2, 2, 2], [2, 1, 2, 2, 2], [2, 2, 1, 2, 2], [2, 2, 2, 1, 2], [2, 2, 2, 2, 1]这些都是有第一个数组旋转一次变化来的,我们不能通过11提到的两种方法确定是否存在元素111中提到的第一个方法,我们进行改善:    

    ```c++
    int searchMinInRotatedArray_duplicate(int A[], int n)   
    {  
        if(n == 1)  
            return 0;  
        int low = 0, high = n-1; 
        int mid = low;
        while(A[low >= A[high]])  
        {  
            if(high - low == 1) {
                mid = high;
            }
            mid = low + (high - low)/2;
            // 如果下表 low, index, high 元素相等,此时只能顺序查找
            if(A[mid] == A[low]  && A[mid] == A[high]) 
                return minInorder(a, low, high);
            if(A[mid >= A[low]]) low = mid;
            else if(A[mid] <= A[high]) high = mid;
        }
        return mid;
    }   

    int minInorder(int A[], int low, int high) {
        int res = A[low];
        int index;
        for(int i = low + 1; i < high; i++) {
            if (res > A[i]) res = A[i], index = i;
        }
        return index;
    }   
    

    ```
    对第二个方法进行修改,我还没有想到,有想法的同学可以评论或联系我

14. 一个有序数组,可能含有重复元素,在某一个位置发生了旋转后,求第k(k > 0)小元素  
    利用上一题的解答,求出最小值所在位置后,便可以求出第k小元素      

    ```c++
    int searchKthInRotatedArray(int A[], int n, int k)   
    {  
        int posMin = searchMinInRotatedArray_duplicate(A, n);  
        return A[(posMin+k-1)%n];  
    }  
    ```

你可能感兴趣的:(Algorithms)