【常见面试算法题】之二分搜索

算法概要

在计算机科学中,二分搜索(英语:binary search),也称折半搜索(英语:half-interval search)[1]、对数搜索(英语:logarithmic search)[2],是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

时间复杂度

折半搜索每次把搜索区域减少一半,时间复杂度为 O(log n)。(n代表集合中元素的个数)

空间复杂度

虽以递归形式定义,但是尾递归,可改写为循环。空间复杂度为O(1).

实现(java)

循环式二分搜索

public static int rank(int key, int[] a) {
        int low = 0;
        int high = a.length - 1;
//        int mid = (low + high) / 2;
//        int mid = low + (high - low) / 2;//这行代码必须放在循环内部
        while (low <= high) {
//            int mid = low + (high - low) / 2; 
            int mid = low + ((high - low) >> 1); //不要忘记加括号,还有>> 1不是2
            //最好最后检测等于这个情况
            if (key == a[mid]) {
                return mid;
            } else if (key < a[mid]) {
                high = mid - 1;
            }else {
                low = mid + 1;
            }
        }
        return -1;
    }

递归式二分搜索

  private static int binarySearch(int[] numbers, int start, int end, int key) {
        while (start <= end) {
            int mid = start + ((end - start) >> 1);
            if (key < numbers[mid]) {
                return binarySearch(numbers, start, mid - 1, key);
            } else if (key > numbers[mid]) {
                return binarySearch(numbers, mid + 1, end, key);
            } else {
                return mid;
            }
        }
        return -1;
    }

应用场景:

  1. 有序序列中查找一个数,lgN
  2. 并不一定要在有序序列中才能得到,只要二分之后,淘汰掉一半,只需要处理另外一半。

二分搜索考察点

边界条件的界限

题目变化

  1. 有没有重复元素
  2. 判断条件不同
  3. 要求返回的内容不同

常见题目

1. 求数组局部最小值位置

定义局部最小的概念。arr长度为1时,arr[0]是局部最小。arr的长度为N(N>1)时,如果arr[0]

public class Solution {
  public int getLessIndex(int[] arr) {
        if (arr == null || arr.length <= 0) {
            return -1;
        }
        //arr长度为1时,arr[0]是局部最小
        if (arr.length == 1) {
            return 0;
        }
        //arr的长度为N(N>1)时,如果arr[0]<arr[1],那么arr[0]是局部最小
        if (arr[0] < arr[1]) {
            return 0;
        }
        //arr的长度为N(N>1)时,如果arr[N-1]<arr[N-2],那么arr[N-1]是局部最小
        int N = arr.length;
        if (arr[N-1] < arr[N-2]) {
            return N-1;
        }

        int lo = 1, hi = N - 2;
        while (lo <= hi) {
            int mid = lo + ((hi - lo) >> 1);
            if (arr[mid] > arr[mid-1]) {
                hi = mid - 1;
            } else if (arr[mid] > arr[mid+1]) {
                lo = mid + 1;
            } else {
                return mid;
            }
        }
        return lo;
    }
}

2. 元素最左出现的位置

对于一个有序数组arr,再给定一个整数num,请在arr中找到num这个数出现的最左边的位置。
给定一个数组arr及它的大小n,同时给定num。请返回所求位置。若该元素在数组中未出现,请返回-1。

思路:
由于数组有序,考虑用二分搜索法快速定位到数组中值为num位置处,找到后num后,因为数组有序递增,在num左边继续遍历,找到最左边的num位置

public int findPos(int[] arr, int n, int num) {  
        if(arr == null || arr.length == 1){  
            return -1;  
        }  
        int res = -1;  
        int left  = 0;  
        int right = arr.length - 1;  
        int mid = 0;  
        while(left <= right){  
            mid = (left + right) / 2;  
            if(arr[mid] > num){  
                right = mid - 1;  
            }else if(arr[mid] < num){  
                left = mid + 1;  
            }else {//arr[mid] == num  
                res = mid;  
                //因为需要寻找最左边的,而数组也是有序的,因此,还需要往mid左边寻找最左的一个num值位置  
                right = mid -1;  
            }  
        }  
        return res;  
    }  

3. 循环有序数组求最小值

对于一个有序循环数组arr,返回arr中的最小值。有序循环数组是指,有序数组左边任意长度的部分放到右边去,右边的部分拿到左边来。比如数组[1,2,3,3,4],是有序循环数组,[4,1,2,3,3]也是。

给定数组arr及它的大小n,请返回最小值。

测试样例:

[4,1,2,3,3],5

返回:1

import java.util.ArrayList;
//已知数组的旋转 求最小值
//特殊用例: 数组无旋转 2 . 大小为0 3. 元素有小于0
//方法一: 取巧的方式:遍历数组
//方法二: 用二分查找
public class Solution {
    public int minNumberInRotateArray(int [] array) {
       if (array == null || array.length <= 0) {
            return 0;
        }

        int lo = 0, hi = array.length - 1;
        while (lo <= hi) {
            int mid = lo + ((hi - lo) >> 1);
            if (array[mid] > array[hi]) {
                lo = mid + 1;
            } else if (array[mid] < array[hi]) {
                hi = mid;
            } else {
                hi --;
            }
        }
        return array[lo];
    }
}

4. 最左原位

有一个有序数组arr,其中不含有重复元素,请找到满足arr[i]==i条件的最左的位置。如果所有位置上的数都不满足条件,返回-1。

给定有序数组arr及它的大小n,请返回所求值。

测试样例:

[-1,0,2,3],4

返回:2

 public int findPos(int[] arr, int n, int num) {
        if (arr == null || arr.length <= 0) {
            return -1;
        }
        int lo = 0, hi = n - 1;
        int res = -1;
        while (lo <= hi) {
            if (arr[lo] > lo || arr[hi] < hi) {
                break;
            }
            int mid = lo + ((hi - lo) >> 1);
            if (arr[mid] > mid) {
                hi = mid - 1;
            } else if (arr[mid] < mid) {
                lo = mid + 1;
            } else {
                res = mid;
                hi = mid - 1;
            }
        }
        return res;
    }

5. 完全二叉树计数

给定一棵完全二叉树的根节点root,返回这棵树的节点个数。如果完全二叉树的节点数为N,请实现时间复杂度低于O(N)的解法。

给定树的根结点root,请返回树的大小。

思路:

(1)首先一直遍历到根节点的左子树的最左边,得到树的高度high;

(2)再遍历根节点的右子树的最左边,得到右子树的高度rightHigh;

(3)如果high==rightHigh,表示根节点的左子树为满二叉树,高度为high,此时左子树可以直接根据满二叉树的性质求出节点数量,再对右子树递归遍历;

(4)如果high!=rightHigh,表示根节点的右子树为满二叉树,高度为high - 1,此时右子树可以直接根据满二叉树的性质求出节点数量,再对左子树递归遍历;

  public int treeCount(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int height = 0;
        TreeNode node = root;
        while (node != null) {
            height++;
            node = node.left;
        }
        int rightHeight = 0;
        node = root;
        while (node != null) {
            rightHeight++;
            node = node.right;
        }
        if (height == rightHeight) {
            return (int) (Math.pow(2, height - 1) + treeCount(root.right));
        } else {
            return (int) (Math.pow(2, rightHeight - 1) + treeCount(root.left));
        }
    }

6. 快速N次幂

关于矩阵快速幂的应用
请见 魔力手环–2017网易编程应用题

 public int pow(int a, int b) {
        int res = 1;
        while (b > 0) {
            if ((b & 1) == 1) {
                res *= a;
            }
            a *= a;
            b >>= 1;
        }
        return res;
    }

更多文章

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值