题目:
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。
题目解析:
碰到这类题要考虑是否有序,无序的时候一种情况,有序的时候一种情况。
思路一(排序查找):
通常碰到无序的数组,会通过排序然后进行查找,这时通过快速排序O(nlogn)来实现。然后我们再遍历整个数组,统计每个数字出现的次数,找到出现次数超过一半的数字。总的时间复杂度为O(nlogn + n)。
思路二:
如果数组有序了,我们还有必要再进行遍历么?题目中说超过一半的数字,那么我们应该看arr[N/2]这个位置的数据即可。但时间复杂度仍然为O(nlogn)。
思路三(空间换时间):
除了将数组由无序变成有序是常识,通过空间换取时间解决复杂度问题也是常识。那么我们设法建立一个hash表,键值为数组中的数字,表值为出现的次数。然后遍历整个哈希表,输出超过一半对应的数字即可。
思路四(直接查找第k个数据):
毕竟我们用了空间去优化,也是不得已的事情。但还应该考虑还有没有更好的方法。既然我们知道要查找N/2位置的数据,也就转化成了查找第k个数据。对这个问题,有一套解决办法。利用快速选择来解决。
随机选取枢纽元,一趟partition后,如果枢纽元位置为k则退出,找到;如果小于k,就在有半部分继续找;如果大于k,就在左半部分继续找。
// Random Partition
int RandomInRange(int min, int max)
{
int random = rand() % (max - min + 1) + min;
return random;
}
void Swap(int* num1, int* num2)
{
int temp = *num1;
*num1 = *num2;
*num2 = temp;
}
int Partition(int data[], int length, int start, int end)
{
if(data == NULL || length <= 0 || start < 0 || end >= length)
throw new std::exception("Invalid Parameters");
int index = RandomInRange(start, end);
Swap(&data[index], &data[end]);
int small = start - 1;
for(index = start; index < end; ++ index)
{
if(data[index] < data[end])
{
++ small;
if(small != index)
Swap(&data[index], &data[small]);
}
}
++ small;
Swap(&data[small], &data[end]);
return small;
}
int MoreThanHalfNum_Solution1(int* numbers, int length)
{
if(CheckInvalidArray(numbers, length))
return 0;
int middle = length >> 1;
int start = 0;
int end = length - 1;
int index = Partition(numbers, length, start, end);
while(index != middle)
{
if(index > middle)
{
end = index - 1;
index = Partition(numbers, length, start, end);
}
else
{
start = index + 1;
index = Partition(numbers, length, start, end);
}
}
int result = numbers[middle];
if(!CheckMoreThanHalf(numbers, length, result))
result = 0;
return result;
}
思路五:
利用题目的特点,我们可以试着这么考虑,如果每次删除两个不同的数(不管是不是我们要查找的那个出现次数超过一半的数字),那么,在剩下的数中,我们要查找的数(出现次数超过一半)出现的次数仍然超过总数的一半。通过不断重复这个过程,不断排除掉其它的数,最终找到那个出现次数超过一半的数字。这个方法,免去了排序,也避免了空间O(n)的开销,总得说来,时间复杂度只有O(n),空间复杂度为O(1),貌似不失为最佳方法。
举个简单的例子,如数组a[5] = {0, 1, 2, 1, 1};
很显然,若我们要找出数组a中出现次数超过一半的数字,这个数字便是1,若根据上述思路4所述的方法来查找,我们应该怎么做呢?通过一次性遍历整个数组,然后每次删除不相同的两个数字,过程如下简单表示:
0 1 2 1 1 =>2 1 1=>1
最终1即为所找。
但是数组如果是{5, 5, 5, 5, 1},还能运用上述思路么?很明显不能,咱们得另寻良策。
改进优化:
咱们根据数组的特性考虑到:
下面,举两个例子:
第一个例子,假定数组为{5, 5, 5, 5, 1}
不同的相消,相同的累积。遍历到第四个数字时,candidate 是5, nTimes 是4;遍历到第五个数字时,candidate 是5, nTimes 是3;nTimes不为0,那么candidate就是超过半数的。
第二个例子,假定数组为{0, 1, 2, 1, 1}
开始时,保存candidate是数字0,nTimes为1;遍历到数字1后,与数字0不同,则nTimes减1变为零;接下来,遍历到数字2,2与1不同,candidate保存数字2,且nTimes重新设为1;继续遍历到第4个数字1时,与2不同,nTimes减1为零,同时candidate保存为1;最终遍历到最后一个数字还是1,与我们之前candidate保存的数字1相同,nTimes加1为1。最后返回的是之前保存的candidate为1。
//改自编程之美 2010
Type Find(Type* a, int N) //a代表数组,N代表数组长度
{
Type candidate;
int nTimes, i;
for(i = nTimes = 0; i < N; i++)
{
if(nTimes == 0)
{
candidate = a[i], nTimes = 1;
}
else
{
if(candidate == a[i])
nTimes++;
else
nTimes--;
}
}
return candidate;
}
#include
using namespace std;
bool g_Input = false;
int Num(int* numbers, unsigned int length)
{
if(numbers == NULL && length == 0)
{
g_Input = true;
return 0;
}
g_Input = false;
int result = numbers[0];
int times = 1;
for(int i = 1; i < length; ++i)
{
if(numbers[i] == result)
times++;
else
times--;
if(times == 0)
{
result = numbers[i];
times = 1;
}
}
//检测输入是否有效。
times = 0;
for(i = 0; i < length; ++i)
{
if(numbers[i] == result)
times++;
}
if(times * 2 <= length)
//检测的标准是:如果数组中并不包含这么一个数字,那么输入将是无效的。
{
g_Input = true;
result = 0;
}
return result;
}
int main()
{
int a[10]={1,2,3,4,6,6,6,6,6};
int* n=a;
cout<
题目扩展:加强版水王:找出出现次数刚好是一半的数字
我们知道,水王问题:有N个数,其中有一个数出现超过一半,要求在线性时间求出这个数。那么,我的问题是,加强版水王:有N个数,其中有一个数刚好出现一半次数,要求在线性时间内求出这个数。
因为,很明显,如果是刚好出现一半的话,如此例: 0,1,2,1 :
遍历到0时,candidate为0,times为1
遍历到1时,与candidate不同,times减为0
遍历到2时,times为0,则candidate更新为2,times加1
遍历到1时,与candidate不同,则times减为0;我们需要返回所保存candidate(数字2)的下一个数字,即数字1。
题目分析:
方案一:
正好一半的话,我们也可以利用类似减去两个不同的数来实现,这里我们减去的是三个数,因为至少有三个数是不相同的(当然数组中只有两个数a和b各占一半)。那么接下来的问题,就转化成了找到超过一半的数字,又变成第一类问题了。
方案二:
根据上面的例子,最后我们可能会输出不是符合条件的数字,那么仔细分析的话,占一半的数字,只能在两个变量中出现:candidate和arr[n-1]。如果arr[n-1]不是占一半的数据key,那么candidate最后保持着key,另一种情况,就是arr[n-1]为key。我们遍历到最后,再遍历一趟判断一下是否arr[n-1]占据一半即可。
int Find(int* a, int N) //a代表数组,N代表数组长度
{
int candidate;
int nTimes, i;
for(i = nTimes = 0; i < N; i++)
{
if(nTimes == 0)
{
candidate = a[i], nTimes = 1;
}
else
{
if(candidate == a[i])
nTimes++;
else
nTimes--;
}
}
int cTimes = 0;
int candidate2 = a[N-1];
for(i = 0; i < N; i ++)
{
if(a[i] == candidate)
{
cTimes++;
}
}
return cTimes == N/2 ? candidate : candidate2;
}
我们再遍历的过程中,让每一个数据与arr[n-1]比较,统计和arr[n-1]相同的数据,那么到最后就不用再遍历了,代码如下:
int MoreThanHalf(int a[], int N)
{
int sum1 = 0;//最后一个元素的个数
int sum2 = 0;
int candidate;
int i;
for(i=0;i