我这篇文章要讲的一道算法题,一点都不难,可以说是小白级别的,是关于一个数组中计算某个元素的重复个数的问题。
具体问题:看如下一个无序的数组,求其中数字2出现的次数。怎么样,是不是超级简单:)
[0,2,1,1,2,3,5,2,3,3,4,5,7,6,2]
能够想到的解法,直接一趟for循环,碰到元素等于2了就开始计数加一,2的个数就是计数值。
挺好。这肯定能解决问题。
下面给出上面思路的实现,使用c++代码。值得注意的是,这里应该使用c++中的模板,也就是通常说的泛型。因为,我们并不确定给定数组元素的具体类型,除了上面给出的数组元素是整数外,也可能是字符串,或者自定义对象。
template <typename T>
int countOccurrences(vector<T> arr, T target){
int result = 0;
for (int i = 0; i < arr.size(); i++) {
if (target == arr[i]) {
result++;
}
}
return result;
}
还有没有其他方法呢?这个时候就可以想一下,对了,我们也可以使用查找表来解决这个问题,以元素值为键,值为数组中相同元素的个数。需要一趟for 循环来统计相同元素的个数,最后返回这个查找表中对应元素的值就好了。
给出代码:
template <typename T>
int countOccurrences(vector<T> arr, T target){
unordered_map<T, int> map;
for (int i = 0; i < arr.size(); i++) {
map[arr[i]]++;
}
return map[target];
}
想一想,如果这个数组变成有序了?可以想到其他方法来解决这个问题吗?
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
此时就可以使用二分查找了,二分查找主要解决的就是有序集合的查找问题。可是问题来了,这里有4个2,二分查找只能找到其中的一个2,这....好像使用二分查找也不行吧。
如果有这个疑问,可以先按照二分查找的思路,停下来先思考下如何使用二分查找来解决这个问题。
继续。
其实,我们可以使用二次二分查找,第一次寻找的是查找值的左边界,第二次寻找的是查找值的右边界。
先给出代码,然后根据代码进行讲解。
template <typename T>
int countOccurrences(vector<T> arr, T target){
int low_left = 0;
int high_left = arr.size();
while (low_left < high_left) {
int middle = low_left + (high_left - low_left) / 2;
if (arr[middle] < target) {
low_left = middle + 1;
} else {
high_left = middle;
}
}
int low_right = 0;
int high_right = arr.size();
while (low_right < high_right) {
int middle = low_right + (high_right - low_right) / 2;
if (arr[middle] > target) {
high_right = middle;
} else {
low_right = middle + 1;
}
}
return low_right - low_left;
}
我准备根据程序运行的逻辑,一步步进行讲解。
首先,这是我们传入的数组,寻找2的重复元素
vector<int> vec = vector<int>{0,1,1,2,2,2,2,3,3,3,4,5,5,6,7};
int result = countOccurrences(vec, 2);
第一个循环的作用是寻找左边界,也就是2起始的索引,在第一个while循环前,设置两个变量,low_left,high_left,我对这两个值的定义是在区间 [low_left,high_left) 中寻找目标值,注意区间是前闭后开的,所以这两个值的初始值设置为 low_left = 0,high_left = arr.size()。
经过第一个循环,中间值middle = 7,arr[7] = 3, 3大于2,所以此时 high_right = 7,数组如下所示,high_left此时指向3。为了便于查看,#号表示low_left的位置,*号表示high_left的位置。
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
# *
继续第二次循环,中间值middle = 3,arr[3] = 2,2等于目标值,所以此时high_left = 3,所以在这里也可以看出这里用到的二分查找与一般的二分查找的区别了,一般的二分查找在找到目标值后就会结束循环,这里却不然,会继续向左边部分查找。因为我们要找的是2第一次出现的位置。
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
# *
继续第三次循环,中间值middle = 1,arr[1] = 1,1小于2,所以此时low_left = middle + 1,就应该等于2。
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
# *
继续第四次循环,middle = 2 + (3 - 2) / 2 = 2, arr[2] = 1, 1小于2,所以此时low_left继续加一,low_left = 3,结束循环。这个时候,我们就找到了2第一次出现的位置,如图。
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
#
*
寻找右边界与左边的逻辑类似,给出图示,就不一一讲解了,与寻找左边界不同的是,在找到2之后,会继续向右查找,2结束的位置。
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
# *
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
# *
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
# *
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
# *
[0,1,1,2,2,2,2,3,3,3,4,5,5,6,7]
#
*
查找到的结果右边界是索引在7的位置,所以2出现的次数是右边界减去左边界,也就是7减去3等于4,求出结果。
接下来讨论一种特殊情况。
当前数组中不包括需要查找的元素。那么此时low_right 和 low_left 会返回相同的数,两数相减等于零,是满足的,
总结一下,在这个方法中,查找到值后,并不会停止查找,而是继续向左或向右,找到目标值的起始位置和结束位置。可以看到,在掌握基本知识的基础上,能灵活运用是非常重要的。