分治法求众数在C++中的实现
问题分析
该问题解决的是求解一串数组中的众数问题。
众数,即一串数组中出现次数最多的数,每个数字的出现次数为该数字的重数,所以重数最大的数即为众数。该问题要求能正确返回给定数组中的众数以及它的重数。
算法设计
该问题实际上已经将核心算法给限定了:分治法。
分治法将一个难以解决的大问题划分为一些规模较小的子问题,分别求解各个子问题,再合并子问题的解得到原问题的解。
实际上在我们学习分治法的例题的时候应该也发现,对于分治法的算法设计要尽量使子问题的规模完全相等,实现平衡子问题的启发式思想。举个例子:计算一个数n的8次方的时候,我们采用分治法将这个计算变为n4×n4,同理,n4还可以再分为n2×n2,n2分为n×n,虽然对于乘方问题,分治法并没有降低时间复杂度,仍然为O(n),但是这种平衡子问题的思路是可以学习引荐到其他问题中的。
那么对于这个求众数的问题,我们该如何以平衡子问题的启发式思想来进行算法设计呢?毕竟数组的长度是未知的,如果是个质数,比如说13个数,那么无论如何也无法实现子问题的规模完全相等了吧。
其实不然,对于求众数的问题其实并不是像刚刚的乘方问题一样,数字数量并不会影响求众数问题的子问题规模大小。说人话就是,即使每个子问题中数字数量不同,它们的规模也可以是相同的。
首先,我们需要对数组进行一次排序,不排序的话一堆数字很乱只能通过遍历去找一个数的重数,再迭代的时候每一遍都需要遍历整个数组,而且肯定不能通过条件判断直接减去不可能的情况了,算法效率会很低。排序的话C++里面插入#include<algorithm>
头文件后可以直接用sort函数:sort(数组名, 数组名 + 数组大小, greater<int>());
,这个排序会根据数据数量自动选择更优的排序方法,很好用。
接着对于整个数组分解成3个部分:中位数、中位数左边的数、中位数右边的数。这里的中位数指的并不是单单一个数,而是所有和它相等的,比如说在数组1 2 2 2 3 3中,这里分成的三个部分就是1 |2 2 2|3 3。
找到中位数后,我们对中位数求它的重数,这里可以一举两得,我们直接求出中位数的左右界,还是拿1 2 2 2 3 3这个例子来说,我们首先定位到第二个2,然后从这个位置往左往右分别寻找是否还有2,左右界一开始都设置为中位数的位置,左边每找到一个就left-1,右边每找到一个就right+1,因为是排过序的,只要发现一个不是2就可以停止这一边的搜素了。最后,中位数的重数就是right-left+1,同时还将数组成功按照我们刚刚的思路分成三部分了。
接下来就是印证我们每个部分数字数量不同,但是它们的规模却相同的时候了。
首先,得到了刚刚的中位数的重数后,我们用变量存储当前中位数即为众数,它的重数即为众数的重数,随后发现新的重数更大的数直接改变量的值就可以了。接着,对于中位数左边的部分进行一个判断,如果左边部分总数量还没有当前重数多,那直接不用判断了,因为即使整个部分全是同一个数,它也不可能是众数了。相反,如果存在可能性,就再将这个部分找中位数后分为3个部分。右边部分完全同理。
那么,现在来看这个问题是不是已经变得和刚刚的乘方问题完全一样,问题被一步步分到最小,用着同样的手段去解决每个部分的问题。
所以,最终我们设计一个递归即可实现整个代码。
源代码
#include<iostream>
#include<algorithm>
using namespace std;
void FindMiddle(int a[],int length,int &left,int &right,int &middle) {
int half = length / 2;//中位数的位置
middle = a[half];//中位数的值
left = half, right = half;//中位数的左右边界(同时也是分治法的分组边界)
for (int n = half - 1; n >= 0; n--) {
if (a[n] == middle) {
left--;//中位数的左边界
continue;
}
break;
}
for (int n = half + 1; n < length; n++) {
if (a[n] == middle) {
right++;//中位数的右边界
continue;
}
break;
}
}
void FindMax(int a[],int length,int &count,int &max) {
int left = 0, right = 0;
FindMiddle(a, length, left, right, max);
count = right - left + 1;
if (left - 1 >= count) {
int left1 = 0, right1 = 0, middle1 = 0;
FindMiddle(a, left, left1, right1, middle1);
int count1 = 0, max1 = 0;
FindMax(a, left, count1, max1);
if (count1 > count) { count = count1; max = max1; }
}
if (length - right - 1 >= count) {
int b[100];
for (int i = right + 1; i < length; i++) {
b[i - right - 1] = a[i];
}
int left1 = 0, right1 = 0, middle1 = 0;
FindMiddle(b, length - right - 1, left1, right1, middle1);
int count1 = 0, max1 = 0;
FindMax(b, length - right - 1, count1, max1);
if (count1 > count) { count = count1; max = max1; }
}
}
int main() {
int numbers[100], length = 0;
int n;
cout << "请输入数字个数:" << endl;
cin >> n;
cout << "请输入数字序列,每两个数字之间用空格隔开:" << endl;
for (int i = 0; i < n; i++) {
int num;
cin >> num;
numbers[i] = num;
length++;
}
sort(numbers, numbers + 100, greater<int>());
int max = 0;//众数
int count = 0;//众数的重数
FindMax(numbers, length, count, max);
cout << "这个序列的众数是:" << max << ",它的重数是:" << count << "。" << endl;
}
代码运行测试
首先用刚刚的例子打乱顺序测试一遍。
6
2 1 3 2 1 2
接着再测试一个复杂一点,需要多次递归的,首先定位中位数是3,众数也为3,随后从左部分找到重数更大的1,接着又在右部分找到重数更大的5。
15
6 8 5 2 1 3 7 5 1 2 1 5 5 3 4
总结
分治法虽然不能在每种情况都比蛮力法更高效,但是正确地使用分治法往往都比其他算法效率更高,它的逻辑性很强也更容易让人理解。进行分治法算法设计的时候,最重要的还是尽量保证子问题的规模相等,如果难以做到,那么对于当前问题可能不适用于分治法,可以尝试其他算法。