Leetcode总结,“公式化”二分法

一,学习别人的总结与讲解

本部分的参考见末尾,本部分文字是在其基础上的二度总结(节约时间和精力)。

1,典型的二分法

算法:当数据量很大适宜采用该方法。采用二分法查找时,数据需是排好序的。 

基本思想:假设数据是按升序排序的,对于给定值key,从序列的中间位置k开始比较,

如果当前位置arr[k]值等于key,则查找成功;

若key小于当前位置值arr[k],则在数列的前半段中查找;

若key大于当前位置值arr[k],则在数列的后半段中继续查找,直到找到为止。

时间复杂度:O(log(n))。


上面的思想就是最最简单的二分法,即从一个排好序的数组之查找一个key值。 如下面的程序:

  1. int search(int *arr, int n, int key)
  2. {
  3.     int left = 0, right = n-1;
  4.     while(left<=right) {//慎重截止条件,要仔细思考,待会再说
  5.         int mid = left + ((right - left) << 1);//防止溢出
  6.         if (arr[mid] == key) //找到了
  7.             return mid; 
  8.         else if(arr[mid] > key) 
  9.             right = mid - 1;//给定值key一定在左边,并且不包括当前这个中间值
  10.         else 
  11.             left = mid + 1;//给定值key一定在右边,并且不包括当前这个中间值
  12.     }
  13.     return -1;
  14. }

这个程序,相信只要是一个合格的程序员应该都会写。 稍微注意一点, 每次移动left和right指针的时候,根据实际的意义在mid的基础上+1或者-1(两个指针总是会移动到一个未被比较过得新位置), 防止出现死循环, 那么问题来了,如果找不到关键值key什么时候截止呢?当left小于right的时候肯定要继续判断,那么问题就在于left==right是否要继续判断?当然要!前面说了,两个指针移动情况可以看出他们所指向的位置总是没有与key比较过,所以必须再进行一次比较,程序也就能够正确的运行。

如果条件稍微变化一下, 还会写吗?其实,二分法真的不那么简单,尤其是二分法的各个变种。


2,二分法的变种1

数组之中的数据可能可以重复,要求返回匹配的数据的最小(或最大)的下标;更近一步, 需要找出数组中第一个大于key的元素(也就是最小的大于key的元素的)下标,等等。 这些,虽然只有一点点的变化,实现的时候确实要更加的细心。 下面列出了这些二分检索变种的实现。


a. 找出第一个与key相等的元素
  1. int searchFirstEqual(int *arr, int n, int key)
  2. {
  3.     int left = 0, right = n-1;
  4.     while(left<=right) //相等时必须再判断一次,
  5.     {
  6.         int mid = (left+right)/2;
  7.         if(arr[mid] >= key) 
  8.             right = mid - 1;//即使mid位置的值与key相等,我们也应该将其往左边移动,但是当前位置有可能是要找的位置
  9.         else if(arr[mid] < key) 
  10.             left = mid + 1;//一定在mid位置的右边,并且不包括当前mid位置
  11.     }
  12.     if( left < n && arr[left] == key) 
  13.             return left;
  14.     return -1;
  15. }

b. 找出最后一个与key相等的元素
  1. int searchLastEqual(int *arr, int n, int key)
  2. {
  3.     int left = 0, right = n-1;
  4.     while(left<=right) {//相等时进行一次误差判断
  5.         int mid = (left+right)/2;
  6.         if(arr[mid] > key) 
  7.             right = mid - 1;//key一定在mid位置的左边,并且不包括当前mid位置
  8.         else if(arr[mid] <= key) 
  9.             left = mid + 1; //不相等一定在mid位置的右边,相等时答案有可能是当前mid位置
  10.     }
  11.     if( right>=&& arr[right] == key) 
  12.             return right;
  13.     return -1;
  14. }


3,二分法的变种2

a. 查找第一个等于或者大于Key的元素
  1. int searchFirstEqualOrLarger(int *arr, int n, int key)
  2. {
  3.     int left=0, right=n-1;
  4.     while(left<=right) 
  5.     {
  6.         int mid = (left+right)/2;
  7.         if(arr[mid] >= key) 
  8.             right = mid-1;
  9.         else if (arr[mid] < key) 
  10.             left = mid+1;
  11.     }
  12.     return left;
  13. }

b. 查找第一个大于key的元素
  1. int searchFirstLarger(int *arr, int n, int key)
  2. {
  3.     int left=0, right=n-1;
  4.     while(left<=right)
  5.     {
  6.         int mid = (left+right)/2;
  7.         if(arr[mid] > key) 
  8.             right = mid-1;
  9.         else if (arr[mid] <= key) 
  10.             left = mid+1;
  11.     }
  12.     return left;
  13. }


4,二分法的变种3

a. 查找最后一个等于或者小于key的元素
  1. int searchLastEqualOrSmaller(int *arr, int n, int key)
  2. {
  3.     int left=0, right=n-1;
  4.     while(left<=right) 
  5.     {
  6.         int m = (left+right)/2;
  7.         if(arr[m] > key) 
  8.              right = m-1;
  9.         else if (arr[m] <= key) 
  10.              left = m+1;
  11.     }
  12.     return right;
  13. }

b. 查找最后一个小于key的元素

  1. int searchLastSmaller(int *arr, int n, int key)
  2. {
  3.     int left=0, right=n-1;
  4.     while(left<=right) {
  5.         int mid = (left+right)/2;
  6.         if(arr[mid] >= key) 
  7.              right = mid-1;
  8.         else if (arr[mid] < key) 
  9.              left = mid+1;
  10.     }
  11.     return right;
  12. }
下面是一个测试的例子:
  1. int main(void) 
  2. {
  3.     int arr[17] = {1, 
  4.                    2, 2, 5, 5, 5, 
  5.                    5, 5, 5, 5, 5, 
  6.                    5, 5, 6, 6, 7};
  7.     printf("First Equal           : %2d \n", searchFirstEqual(arr, 16, 5));
  8.     printf("Last Equal            : %2d \n", searchLastEqual(arr, 16, 5));
  9.     printf("First Equal or Larger : %2d \n", searchFirstEqualOrLarger(arr, 16, 5));
  10.     printf("First Larger          : %2d \n", searchFirstLarger(arr, 16, 5));
  11.     printf("Last Equal or Smaller : %2d \n", searchLastEqualOrSmaller(arr, 16, 5));
  12.     printf("Last Smaller          : %2d \n", searchLastSmaller(arr, 16, 5));
  13.     system("pause");
  14.     return 0;
  15. }
最后输出结果是:
  1. First Equal           :  3
  2. Last Equal            : 12
  3. First Equal or Larger :  3
  4. First Larger          : 13
  5. Last Equal or Smaller : 12
  6. Last Smaller          :  2

很多的时候,应用二分检索的地方都不是直接的查找和key相等的元素,而是使用上面提到的二分检索的各个变种,熟练掌握了这些变种,当你再次使用二分检索的检索的时候就会感觉的更加的得心应手了。


二,个人经验总结

首先一个基本的事实就是二分法一定有两个指针(low和high)在移动和一个中间位置mid(要是没有还能算二分法?),

二分法实际上就是在通过迭代这两个指针到指定的位置,只是迭代的条件可能式多样的(不一定像经典二分法那样与中间值比较),务必理清两个条件:

1,截止条件是什么?

截止条件的作用就是在截止后我们就可以判断出我们想要的答案了。

通常,如果以low 与 high的大小来决定是否终止的话,在low < high的情况下(如果此时没有找到解的话)是一定要进行搜索或者其他意义上的功能。那么迷茫的点就在于相等时怎么办?这取决于二分移动是如何进行的,但是无论移动与否,low和high中一定要有一个进行跨步移动,否则死循环。


2,二分移动条件是什么?

最重要的事情是理清移动的具体意义,到底该不该移动,即+1或者-1(我称之为跨步移动)?

1)如果当前mid位置确定不是解,那么直接跨步移动,即+1或者-1

2)但是如果这个位置有可能是解也有可能不是解怎么办?是具体情况而定,这时不要轻易+1或者-1!

那么就看跨步或者不跨步在low和high相等与否的两种情况时是否能得到预想结果,即是否应该截止?

所以分下面四种情况:

a)跨步,如果当low和high相等时已经可以得到结果,则用low<high这个条件即可。

b)不跨步,如果当low和high相等时已经可以得到结果,则用low<high这个条件即可。

c)跨步,如果当low和high相等时才可以得到结果,则用low<=high这个条件即可。

c)不跨步,如果当low和high相等时才可以得到结果,则用low<=high这个条件即可。

但是无论如何,最重要的就是弄清楚二分法中移动的意义,确定一定移动因素

如果全是确定移动因素二分算法就简单了,

如果具有不定的移动因素分上述四种情况讨论,再不然或者举特例来测试不定因素?

其实二分法难度还好,想想当年多么难的数学------《数学物理方程》《高等数学》都学了,这些与之相比就是“渣”


例子1

在一个有序数组中查找要插入的位置

原文地址,<LeetCode OJ> 35. Search Insert Position

用low来记录答案

class Solution {  
public:  
    int searchInsert(vector<int>& nums, int target) {  //数组不能空
        int low=0,high=nums.size()-1;  
        while(low<=high)  //相等时也需要判断一次
        {  
            int mid=(low+high)/2;  
            if(nums[mid]<target)  
                low=mid+1;//  确定移动因素,一定在右边
            if(nums[mid]>target)  
                high=mid-1;//  确定移动因素,一定在左边
            if(nums[mid]==target)  
                return mid;//确定因素,找到了  
        }  
        return low;  
    }  
}; 



例子2

任意相邻元素不相等的数组中,寻找峰位置(任意一个峰都行)

原文地址,<LeetCode OJ> 162. Find Peak Element

注意:题目说了相邻元素不会相等,这个条件很重要。

a)   nums[mid] < nums[mid + 1],

说明mid与后一个位置形成递增区间,则mid后面一定存在峰且当前mid一定不是峰,则low右移动mid一个位置(这个位置就有可能是峰了)

b)   nums[mid] > nums[mid + 1],

说明mid与后一个位置形成递减区间,则当前位置mid就有可能是峰(也可能在其前面),则high左移动到mid


当low和high相等时是否能得到结果了?即是否应该截止?

因为high的后面是下降的,而low的前面是上升的,所以相等时,已经可以得到结果。

用low来记录最终答案

class Solution {  
public:  
    int findPeakElement(vector<int>& nums) {  
        int low = 0,high = nums.size()-1;    
        while(low < high)  //根据移动情况,当两者相等时已经可以确定解  
        {    
            int mid = (low+high)/2;       
            if(nums[mid] < nums[mid+1])    
                low = mid+1;  //确定移动因素,因为mid位置一定不是峰,而low=mid+1才可能是峰  
            else   
                high = mid;      //不定移动因素  ,四种情况中寻找一种进行论证
        }    
            
        return low;   
    }  
}; 


例子3

在有序数组中,寻找第一个坏的版本

原文地址,<LeetCode OJ> 278. First Bad Version

用low来记录解

// Forward declaration of isBadVersion API.  
bool isBadVersion(int version);  
  
class Solution {  
public:  
    int firstBadVersion(int n) {  
        int low=1,high=n;  
        while(low<=high)  
        {  
            int mid=low+(high-low)/2;//测试案例有超大数,这样写更安全  
            if(isBadVersion(mid))//如果是坏的版本  
                high=mid-1; //不定移动因素,在四种情况中寻找解的情况
            else 
                low=mid+1;//确定移动因素,一定在mid右边
        }  
        return low;  
    }  
};  

也可写成如下版本

// Forward declaration of isBadVersion API.  
bool isBadVersion(int version);  
  
class Solution {  
public:  
    int firstBadVersion(int n) {  
        int low=1, high=n;    
        while(low<high) {    
            int mid=low + (high-low)/2;    
            if(isBadVersion(mid))    
                high = mid;  //不定移动因素, 
            else    
                low = mid + 1;  //确定移动因素  
        }    
        return low;   
    }  
}; 


例子4

在每一行有序的二维数组中寻找值

原文地址,<LeetCode OJ> 74. / 240. Search a 2D Matrix (I / II)

class Solution {  
public:  
    bool searchMatrix(vector<vector<int>>& matrix, int target) {  
        int row=matrix.size();//行    
        int col=matrix[0].size();    
    
        for(int i=0;i<row;i++)//对每一行进行二分查找    
        {    
            int low=0,high=col-1;    
            //不可能在此行找到,此处算是一个小小的优化条件  
            if(matrix[i][high]<target)  
                continue;  
            //在此行查找      
            while(low <= high)//注意此处条件是根据low和high的移动情况来定的    
            {    
                int mid=(low+high)/2;    
                if(matrix[i][mid] > target)//确定移动因素,说明在mid位置的右边    
                    high=mid-1;    
                else if(matrix[i][mid] < target)  //确定移动因素  
                    low=mid+1;    
                else    //确定因素,找到了
                    return true;    
            }    
        }    
        return false;   
    }  
};  



未完待续,持续学习二分法中........


注:本博文为EbowTang原创,后续可能继续更新本文。如果转载,请务必复制本条信息!

原文地址:http://blog.csdn.net/ebowtang/article/details/50770315

原作者博客:http://blog.csdn.net/ebowtang

本博客LeetCode题解索引:http://blog.csdn.net/ebowtang/article/details/50668895



参考资源:

【1】前半部分原作者,liubird,博文地址,http://blog.chinaunix.net/uid-1844931-id-3337784.html

你可能感兴趣的:(LeetCode,C++,算法,面试,二分法)