二分查找

关键词: 排序数组

var binarySearch = (nums, target) => {
  let left = 0;
  let right = nums.length -1

  while(left <= right) {
    const mid = left + Math.floor((right - left)/2)
    
    if(nums[mid] == traget) {
      return mid
    }

    if (nums[mid] > target) {
      right = mid - 1 
    } else {
      left = mid + 1
    }
  }
  return -1
}

二分查找算法每次将查找范围减少一半,因此对于一个长度为n的数组可能需要O(logn)次查找,每次查找只需要比较当前查找范围的中间数字和目标数字,在O(1)的时间可以完成,因此二分查找算法的时间复杂度是O(logn)

数组既可能是整体排序的,也可能是分段排序的,但一旦题目是关于排序数组并且还有查找操作,那么二分查找算法总是值得尝试的。

在排序数组中二分查找

  1. 查找插入位置
输入一个排序的整数数组nums和一个目标值t,如果数组nums中包含t,则返回t在数组中的下标;如果数组nums中不包含t,则返回将t按顺序插入数组nums中的下标。假设数组中没有相同的数字。例如,输入数组nums为[1,3,6,8],如果目标值t为3,则输出1;如果t为5,则返回2。
var searchInsert = function(nums, target) {
  let left = 0
  let right = nums.length - 1
  while(left <= right) {
    let mid = left + Math.floor((right - left) / 2)

    if(nums[mid] >= target) {
      if(mid == 0 || nums[mid - 1] < target) {
        return mid
      }
      right = mid  - 1
    } else {
      left = mid + 1
    }
  }
  return nums.length
}
  1. 山峰数组的顶部
在一个长度大于或等于3的数组中,任意相邻的两个数字都不相等。该数组的前若干数字是递增的,之后的数字是递减的,因此它的值看起来像一座山峰。请找出山峰顶部,即数组中最大值的位置。例如,在数组[1,3,5,4,2]中,最大值是5,输出它在数组中的下标2。

不难想到直观的解法来解决这个题目,即逐一扫描整个数组,通过比较就能找出数组中的最大值。显然,这种解法的时间复杂度是O(n)。


/**
 * @param {number[]} arr
 * @return {number}
 */
var peakIndexInMountainArray = function (arr) {
    let left = 0
    let right = arr.length - 1
    let mid
    while (left <= right) {
        mid = left + Math.floor((right - left) / 2)

        if (arr[mid] > arr[mid - 1] && arr[mid] > arr[mid + 1]) {
            return mid
        } else if (arr[mid] > arr[mid + 1]) {
            right = mid;
        } else {
            left = mid + 1;
        }
    };
    return mid
}
  1. 排序数组中只出现一次的数字
在一个排序的数组中,除一个数字只出现一次之外,其他数字都出现了两次,请找出这个唯一只出现一次的数字。例如,在数组[1,1,2,2,3,4,4,5,5]中,数字3只出现了一次。
/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNonDuplicate = function(nums) {
    let low = 0, high = nums.length - 1;
    while (low < high) {
        const mid = Math.floor((high - low) / 2) + low;
        if (nums[mid] === nums[mid ^ 1]) {
            low = mid + 1;
        } else {
            high = mid;
        }
    }
    return nums[low];
};
  1. 按权重生成随机数
输入一个正整数数组w,数组中的每个数字w[i]表示下标i的权重,请实现一个函数pickIndex根据权重比例随机选择一个下标。例如,如果权重数组w为[1,2,3,4],那么函数pickIndex将有10%的概率选择0、20%的概率选择1、30%的概率选择2、40%的概率选择3。

可以创建另一个和权重数组的长度一样的数组sums,新数组的第i个数值sums[i]是权重数组中前i个数字之和。有了这个数组sums就能很方便地根据等概率随机生成的数字p按照权重比例选择下标。

var Solution = function(w) {
    pre = new Array(w.length).fill(0);
    pre[0] = w[0];
    for (let i = 1; i < w.length; ++i) {
        pre[i] = pre[i - 1] + w[i];
    }
    this.total = _.sum(w);
};

Solution.prototype.pickIndex = function() {
    // 生成随机数
    const x = Math.floor((Math.random() * this.total)) + 1;
    const binarySearch = (x) => {
        let low = 0, high = pre.length - 1;
        while (low < high) {
            const mid = Math.floor((high - low) / 2) + low;
            // 不断逼近
            if (pre[mid] < x) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }
    return binarySearch(x);
};

在数值范围内二分查找

  1. 求平方根

    题目:输入一个非负整数,请计算它的平方根。正数的平方根有两个,只输出其中的正数平方根。如果平方根不是整数,那么只需要输出它的整数部分。例如,如果输入4则输出2;如果输入18则输出4。

由数学常识可知,整数n的平方根一定小于或等于n。同时,除0之外的所有整数的平方根都大于或等于1。因此,整数n的平方根一定在从1到n的范围内,取这个范围内的中间数字m,并判断m2是否小于或等于n。如果m2≤n,那么接着判断(m+1)2是否大于n。如果满足(m+1)2>n,那么m就是n的平方根。如果m2≤n并且(m+1)2≤n,则n的平方根比m大,接下来搜索从m+1到n的范围。如果m2>n,则n的平方根小于m,接下来搜索从1到m-1的范围。然后在相应的范围内重复这个过程,总是取出位于范围中间的m,计算m2和(m+1)2并与n比较,直到找到一个满足m2≤n并且(m+1)2>n的m。


/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    let left= 1
    let right = x
    while(left <= right) {
        const mid = left + Math.floor((right -left) / 2)
        if(mid*mid <= x) {
            if((mid+1)*(mid+1) > x) {
                return mid
            } else {
                left = mid +1
            }
        } else  {
            right = mid - 1
        }
    }
    return 0
};

还不如用原生API

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    return Math.floor(Math.sqrt(x))
};
  1. 狒狒吃香蕉
题目:狒狒很喜欢吃香蕉。一天它发现了n堆香蕉,第i堆有piles[i]根香蕉。门卫刚好走开,H小时后才会回来。狒狒吃香蕉喜欢细嚼慢咽,但又想在门卫回来之前吃完所有的香蕉。请问狒狒每小时至少吃多少根香蕉?如果狒狒决定每小时吃k根香蕉,而它在吃的某一堆剩余的香蕉的数目少于k,那么它只会将这一堆的香蕉吃完,下一个小时才会开始吃另一堆的香蕉。
/**
 * @param {number[]} piles
 * @param {number} h
 * @return {number}
 */
var minEatingSpeed = function(piles, h) {

    var getHours = function(piles, speed) {
        let hours = 0
        for(let pile of piles) {
            hours += Math.ceil(pile / speed) 
        }
        return hours
    }
    let max = 0;
    for(let pile of piles) {
        max = Math.max(max, pile)
    }

    let left = 1;
    let right = max
    while(left <= right) {
        let mid = left + Math.floor((right - left) / 2)
        let hour = getHours(piles, mid)
        if(hour <= h) {
            if(mid == 1 || getHours(piles, mid-1)>h) {
                return mid
            }
            right = mid - 1
        } else {
            left = mid + 1
        }

    }
};  

总结

介绍了二分查找算法。如果要求在一个排序数组中查找一个数字,那么可以用二分查找算法优化查找的效率。二分查找算法的基本思路是在查找范围内选取位于中间的数字。如果中间数字刚好符合要求,那么就找到了目标数字。如果中间数字不符合要求,则比较中间数字和目标数字的大小并相应地确定下一轮查找的范围是当前查找范围的前半部分还是后半部分。由于每轮查找都将查找范围缩小一半,如果排序数组的长度为n,那么二分查找算法的时间复杂度是O(logn)。二分查找除了可以在排序数组中查找某个数字,还可以在数值范围内实现快速查找。可以先根据数值的最小值和最大值确定查找范围,然后按照二分查找的思路尝试数值范围的中间值。如果这个中间值不符合要求,则尝试数值范围的前半部分或后半部分。


看见了
876 声望16 粉丝

前端开发,略懂后台;