最近在准备面试题,发现了一道关于二分法查询的算法题,于是想着边复习算法边研究下那道题的解法。
题目是这样的:给定一个有序的重复数组,在给一个指定值,使用二分法求出指定值在有序数组出现的次数?
首先第一眼看到这个题,脑子就有点转不过来,因为我一直认为二分法查询的优势应该是在有序且不重复的大量数据中查找指定数据,现在突然要求在重复的数组中找出数字出现的次数,就仿佛怕驴累着背着驴走路一样。
但是题目是这样的要求,我也就想着能解就解的思路,开启了求解之旅。要求出重复字符的数字出现的次数,只有两种办法,一种是顺序循环,一个一个比对,找到一个自增一下;另一种是先求出重复数字首次出现的下标,接着在求出重复数字最后出现的下标,最后下标减去首次下标再加1就能求出重复次数。因为二分法不符合第一种解题的原理,所以我只能往第二种思路上靠,结果效果不错,在网上找了一下其他人的解题思路,大多也是这种解法。
在进行解题之前,我先复习了一下二分法的实现,代码如下:
package dichotomy;
/**
* @Author: 黄文伟
* @description: 二分法查询例子,二分法适用于数据量大、有序、不重复的数组
* @Date:Created in 16:38 2019/7/8
*/
public class DichotomyDemo {
public static void main(String[] args) {
int[] arrs = {1, 2, 3, 4, 5, 6, 7, 8, 9};
System.out.println(dichotomySearch(arrs, 3));
}
/**
* 二分法查找实现逻辑
* @param arrs 有序、不重复数组
* @param param 查找数
* @return -1:查找失败,数组中没有查找数
* 非-1:返回查找数在数组中对应下标
*/
public static int dichotomySearch(int[] arrs, int param) {
// 数组下限
int min = 0;
// 数组上限
int max = arrs.length - 1;
// 循环查找,当数组下限不大于上限时
while (min <= max) {
// 取下限和上限的中间值,未除尽则向下取整
int mid = (min + max) / 2;
// 当中间值作为下标的数组元素大于查询数时,取数组下半段
if (arrs[mid] > param) {
max = mid - 1;
}
// 当中间值作为下标的数组元素小于查询数时,取数组上半段
else if (arrs[mid] < param) {
min = mid + 1;
}
// 否则中间值作为下标的数组元素等于查询数,返回下标
else {
return mid;
}
}
// 查找失败返回-1
return -1;
}
}
在简单实现二分法的逻辑之后,我已经想起了二分法的原理,在通过求出重复数字首次出现的下标和最后出现的下标来计算出现次数
package dichotomy;
/**
* @Author: 黄文伟
* @description: 通过二分法查找有序数组中指定数字的重复出现次数
* @Date:Created in 17:41 2019/7/8
*/
public class GetTimesByDichotomy {
public static void main(String[] args) {
int[] arrs = {1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 4, 4, 5, 6};
System.out.println(getTimes(arrs, 3));
}
/**
* 通过获得重复数字的首次出现的下标和最后出现的下标,计算出现次数
* @param arrs 有序数组
* @param param 指定数字
* @return -1:查找失败,数组中没有查找数
* 非-1:返回指定数出现次数
*/
public static int getTimes(int[] arrs, int param) {
int first = getFirst(arrs, param);
int last = getLast(arrs, param);
if (first>=0 && last >= 0) {
return last - first + 1;
}
return -1;
}
/**
* 获得指定数字首次出现的下标
* @param arrs 有序数组
* @param param 指定数字
* @return -1:查找失败,数组中没有查找数
* 非-1:返回指定数首次出现的下标
*/
public static int getFirst(int[] arrs, int param) {
int min = 0;
int max = arrs.length - 1;
while (min <= max) {
// 取下限和上限的中间值,未除尽则向下取整
int mid = (min + max) / 2;
if (arrs[mid] > param) {
max = mid - 1;
} else if (arrs[mid] < param) {
min = mid + 1;
}
// 当指定数与中间值作为下标的数组元素相等时
else {
// 判断中间值大于0且中间值作为下标的数组元素的上一个元素不等于指定数
if (mid > 0 && arrs[mid-1] != param) {
return mid;
}
// 否则判断中间等于0
else if (mid == 0) {
return mid;
}
// 否则中间值作为下标的数组元素不是首次出现的重复数字,中间值赋值给数组上限,继续循环比较
else {
max = mid;
}
}
}
return -1;
}
/**
* 获得指定数字最后出现的下标
* @param arrs 有序数组
* @param param 指定数字
* @return -1:查找失败,数组中没有查找数
* 非-1:返回指定数最后出现的下标
*/
public static int getLast(int[] arrs, int param) {
int min = 0;
int max = arrs.length - 1;
while (min <= max) {
// 取下限和上限的中间值,未除尽则向下取整
int mid = (min + max) / 2;
if (arrs[mid] > param) {
max = mid - 1;
} else if (arrs[mid] < param) {
min = mid + 1;
}
// 当指定数与中间值作为下标的数组元素相等时
else {
// 判断中间值作为下标的数组元素不是数组最后一个元素
// 且中间值作为下标的数组元素的下一个元素不等于指定数
if (mid < arrs.length - 1 && arrs[mid+1] != param) {
return mid;
}
// 否则判断中间等于数组最后一个元素的下标
else if (mid == arrs.length - 1) {
return mid;
}
// 否则中间值作为下标的数组元素不是最后出现的重复数字,中间值赋值给数组下限,继续循环比较
else {
min = mid;
}
}
}
return -1;
}
}
运行截图: