数组中的二分查找

先谈谈二分查找,二分查找是一种非常高效的查找算法,例题如下:
给定一个整形有序数组,如何找出某一整数是否在这个数组中,以及返回该整数所对应的下标?
此题最简单的方法就是遍历一遍原数组来判断是否存在,这也是顺序查找,时间复杂度为O(n)。还可以建立一个哈希表,但是这种方法的空间复杂度为O(n)。所以,此时最佳的算法便是二分查找,其时间复杂度为O(logn),空间复杂度为O(1)。
下面给出二分查找算法递归与非递归的两种形式:

private int binarySearch(int[] nums, int left, int right, int target) {
        if (left > right) {
            return -1;
        }
        int mid = left + (right - left) / 2;//防止数值溢出
        if (nums[mid] > target) {
            return binarySearch(nums, left, mid - 1, target);
        } else if (nums[mid] < target) {
            return binarySearch(nums,mid + 1,right,target);
        }
        return mid;
    }
private int binarySearch(int[] nums, int left, int right, int target) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

也许,这并不足以证明二分查找的威力,下面扩展一下原题目。
假设按照升序排序的数组在预先未知的某个点上进行了旋转
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。你可以假设数组中不存在重复的元素。
这个题我给出两种解题思路,而这两种方法都用到了二分查找。
第一种方法:二分查找加分治算法,第一次取得原数组的中间元素进行比较(中间元素恰好是目标值的情况单独拿出来即可),不难发现总有一侧的元素是有序,另外一侧的元素是无序的,有序的一侧直接复用二分查找即可,而无序的一侧仿照刚才思路再来一次,就这样通过二分来实现递归分治来判断目标值是否存在。
具体代码如下:

public int search1(int[] nums, int target) {
        return find(nums, 0, nums.length - 1, target);
    }

    private int find(int[] nums, int left, int right, int target) {
        //递归的终止条件
        if (left > right) {
            return -1;
        }
        //判断边界条件
        int mid = left + (right - left) / 2;
        if (target == nums[left]) {
            return left;
        }
        if (target == nums[right]) {
            return right;
        }
        if (target == nums[mid]) {
            return mid;
        }
        if (target > nums[right] && target < nums[left]) {
            return -1;
        }
        //判断的核心代码
        if (nums[mid] < nums[right]) {
            if (target > nums[mid] && target < nums[right]) {
                return binarySearch(nums, mid + 1, right - 1, target);
            } else {
                return find(nums, left, mid - 1, target);
            }
        } else {
            if (target > nums[left] && target < nums[mid]) {
                return binarySearch(nums, left + 1, mid - 1, target);
            } else {
                return find(nums, mid + 1, right, target);
            }
        }
    }

    private int binarySearch(int[] nums, int left, int right, int target) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

第二种方法:二分查找加二分查找,先通过整体的二分查找得到数组的旋转点,从而不难得出以旋转点为中心的左右两侧都是有序数组,进行再分别对其两侧进行二分查找来判断目标值是否存在。
具体代码如下:

public int search2(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int partition = findPartition(nums);
        if (partition != -1) {
            if (target == nums[partition]) {
                return partition;
            } else if (target > nums[nums.length - 1]) {
                return binarySearch(nums, 0, partition - 1, target);
            } else {
                return binarySearch(nums, partition + 1, nums.length - 1, target);
            }
        }
        return binarySearch(nums, 0, nums.length - 1, target);
    }

    private int findPartition(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            //此处是通过旋转点特殊的性质来得到旋转点
            if (mid != right && nums[mid] > nums[mid + 1]) {
                return mid;
            }
            if (mid != left && nums[mid] < nums[mid - 1]) {
                return mid - 1;
            }
            if (nums[mid] < nums[left]) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return -1;
    }

通过上面的题目就足以见得二分查找的威力不小。
其实就上面的题目还能进一步拓展,就是把上述数组中不存在重复的元素的条件去掉。
此时题目难度又增加了一些,因为有重复的元素必然会影响到二分查找的性能和查找的正确性,从而一个很直接的想法就是去重,即在上述代码的基础上加上一段可以去掉数组重复元素的代码即可。
具体代码如下:

public boolean search1(int[] nums, int target) {
        return find(nums, 0, nums.length - 1, target);
    }

    private boolean find(int[] nums, int left, int right, int target) {
        if (left > right) {
            return false;
        }
        while (left < right && nums[left] == nums[left + 1]) {
            left++;
        }
        while (left < right && nums[right] == nums[right - 1]) {
            right--;
        }
        int mid = left + (right - left) / 2;
        if (target == nums[left]) {
            return true;
        }
        if (target == nums[right]) {
            return true;
        }
        if (target == nums[mid]) {
            return true;
        }
        if (target > nums[right] && target < nums[left]) {
            return false;
        }
        if (nums[mid] < nums[right]) {
            if (target > nums[mid] && target < nums[right]) {
                return binarySearch(nums, mid + 1, right - 1, target);
            } else {
                return find(nums, left, mid - 1, target);
            }
        } else {
            if (target > nums[left] && target < nums[mid]) {
                return binarySearch(nums, left + 1, mid - 1, target);
            } else {
                return find(nums, mid + 1, right, target);
            }
        }
    }

    private boolean binarySearch(int[] nums, int left, int right, int target) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                return true;
            }
        }
        return false;
    }

以上就是我对这几个题目以及二分查找的理解,如若读者有更好的想法或者方法愿意分享,感谢留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章