常见算法之二分查找

168 阅读4分钟

一、基础知识

在一个长度为n的数组中查找一个数字,如果逐一扫描数组中的每个数字,那么需要O(n)的时间。如果数组是排序的(通常按照递增的顺序排序),那么可以采用二分查找算法进行优化。可以取出位于数组中间的数字并和目标数字比较。如果中间数字正好等于目标数字,那么就找到了目标数字。如果中间数字大于目标数字,那么只需要查找数组的前半部分,这是因为数组是排序的,后半部分的数字都大于或等于中间数字,所以一定都大于目标数字,也就没有必要再在后半部分查找。如果中间数字小于目标数字,那么接下来只需要查找数组的后半部分,这是因为排序数组的前半部分的数字都小于或等于中间数字,所以一定都小于目标数字,也就没有必要再在前半部分查找。

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

二、常见算法

1. 山峰数组的顶部

题目:在一个长度大于或等于3的数组中,任意相邻的两个数字都不相等。该数组的前若干数字是递增的,之后的数字是递减的,因此它的值看起来像一座山峰。请找出山峰顶部,即数组中最大值的位置。例如,在数组[1,3,5,4,2]中,最大值是5,输出它在数组中的下标2。

解题思路:

  • 首先想找到数组的峰值,峰值必然大于左右两个数,而且左边的数组是递增的,右边的数组是递减的。
  • 用二分查找法,来每次看中间值落在递增数组中,还是在递减数组中。如果在递增数组中,那么峰值就在右边;如果在递减数组中,那么峰值就在左边。然后递归查询。

golang代码:

func peakIndexInMountainArray(arr []int) int {
   return callPeak(arr, 0)
}

func callPeak(arr []int, tag int) int {
   var (
      left, right, middle int
   )

   middle = len(arr)/2

   if middle - 1 >= 0 {
      left = arr[middle-1]
   }

   if middle + 1 < len(arr) {
      right = arr[middle+1]
   }
   
   if arr[middle] > left && arr[middle] > right {
      return middle+tag
   }

   if arr[middle] > left && arr[middle] < right {
      return callPeak(arr[middle:], tag+middle)
   }

   if arr[middle] < left && arr[middle] > right {
      return callPeak(arr[:middle+1], tag)
   }

   return 0
}

2. 排序数组中只出现一次的数字

题目:在一个排序的数组中,除一个数字只出现一次之外,其他数字都出现了两次,请找出这个唯一只出现一次的数字。例如,在数组[1,1,2,2,3,4,4,5,5]中,数字3只出现了一次。

解题思路:

  • 二分查找

golang代码:


func singleNonDuplicate(nums []int) int {
   if len(nums) == 1 {
      return nums[0]
   }

   if len(nums) == 3 {
      if nums[0] == nums[1] {
         return nums[2]
      } else {
         return nums[0]
      }
   }

   middle := len(nums)/2

   if nums[middle] != nums[middle-1] && nums[middle] != nums[middle+1] {
      return nums[middle]
   }

   if nums[middle] == nums[middle-1] {
      if middle%2 == 1 {
         return singleNonDuplicate(nums[middle-1:])
      } else {
         return singleNonDuplicate(nums[:middle+1])
      }

   }

   if nums[middle] == nums[middle+1] {
      if middle%2 == 1 {
         return singleNonDuplicate(nums[:middle+2])
      } else {
         return singleNonDuplicate(nums[middle:])
      }
   }

   return 0
}

3. 吃香蕉

题目:狒狒很喜欢吃香蕉。一天它发现了n堆香蕉,第i堆有piles[i]根香蕉。门卫刚好走开,H小时后才会回来。狒狒吃香蕉喜欢细嚼慢咽,但又想在门卫回来之前吃完所有的香蕉。请问狒狒每小时至少吃多少根香蕉?如果狒狒决定每小时吃k根香蕉,而它在吃的某一堆剩余的香蕉的数目少于k,那么它只会将这一堆的香蕉吃完,下一个小时才会开始吃另一堆的香蕉。

解题思路:

  • 碰到单调递增或递减的,就可以使用二分法来解题

golang代码:

func minEatingSpeed(piles []int, h int) int {
   max := getMax(piles...)
   min := 1
   var middle = (max+min)/2
   var count int
   for max > min {
      count = getHour(piles, middle)
      if count > h {
         min = middle
         middle = (max+min)/2
         if min+1 == max {
            //middle = max
            return max
         }
      } else if count == h {
         max = middle
         middle--
         //return middle
      } else {
         max = middle
         middle = (max+min)/2
      }
   }
   return middle
}

func getMax(a ...int) int {
   var res int = -1 << 31
   for _, item := range a {
      if item > res {
         res = item
      }
   }
   return res
}

func getHour(piles []int, middle int) int {
   var res int
   for _, item := range piles {
      if item%middle > 0 {
         res += item/middle + 1
      } else {
         res += item/middle
      }
   }
   return res
}