问题描述
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
思路
看到这个问题第一想法就是排序然后统计数字的出现次数就好了,题目给出的数组没有说是排序的,因此我们需要给他排序,时间复杂度为O(N*logN)。但是,最直观的算法通常是最让人不满意的算法,所以接下来我们讨论另外两种时间复杂度为O(N)的算法。
- 解法一:
基于快排函数的时间复杂度为O(N)的算法
可以发现,如果有一个数字的数量超过了数组元素的一半,那么,在该数组排好序的情况下,中间的那个数字一定就是那个出现次数超过一半的数字。也就是说,这个数字就是统计学上的中位数,及长度为n的数组中第n/2大的数字。我们可以套用现有的成熟的算法得出数组中任意第k 大的数字。
在随机快速排序算法中,我们先在数组中随机选择一个数字,然后调整数组中数字的顺序,使得比选中的数字小的数字都排在它的左边,比选中数字大的数字都排在它的右边。如果它的下标大于n/2,那么中位数应该位于它的左边,我们可以接着在他的左边部分继续查找;如果它的下标小于n/2,那么中位数应该位于它的右边,我们可以接着在他的右边部分继续查找。这是一个典型的递归过程。
Partition函数时完成快牌的基础,我们通过这个函数实现分组并找到其分组后的中间值。
在面试的时候,需要注意的地方还有以下两点:
- 检查输入的数组是否有效
如果函数的输入参数是一个空引用或者无元素的参数,那么需要通过一个函数用来专门检查数组是否无效。 - 检查得到的结果是否真的满足题目要求
题目中说数组中有一个数字出现的次数超过数组长度的一半,可是我们需要大胆的怀疑:如果出现最多的数字都没有超过该数组元素数量的一半呢?那不是该数组中其实不存在这样的一个数字!!!所以我们得再定义一个函数用来检查,我们得到的结果有没有满足题目的标准。
参考代码如下:
class Solution1
{
private bool isInvalid = false; //用于记录输入数组是否合法,不合法为false
/// <summary>
/// 找到数组中出现次数超过该数组元素数量一半的数字,如果没有返回0
/// </summary>
/// <param name="numbers">查找的数组</param>
/// <returns>返回查找到的结果</returns>
public int MoreThanHalfNum_Solution(int[] numbers)
{
if (!CheckInvalidArray(ref numbers))
return 0;
int middle = (numbers.Length >> 1);
int start = 0, end = numbers.Length - 1;
int index = Partition(ref numbers, start, end);
while (index != middle)
{
if (index > middle)
{
end = index - 1;
index = Partition(ref numbers, start, end);
}
else
{
start = index + 1;
index = Partition(ref numbers, start, end);
}
}
int result = numbers[index];
if (!CheckMoreThanHalf(ref numbers, result))
result = 0;
return result;
}
/// <summary>
/// 把在[left,right]区间的元素分组,并返回中间值
/// </summary>
/// <param name="numbers">传入的数组</param>
/// <param name="left">开始分组的左下标</param>
/// <param name="right">结束分组的右下标</param>
/// <returns>分组的中间值</returns>
private int Partition(ref int[] numbers, int left, int right)
{
int index = left + 1;
for (int i = index; i <= right; i++)
{
if (numbers[i] < numbers[left])
{
Swap(ref numbers, i, index);
index++;
}
}
Swap(ref numbers, index - 1, left);
return index - 1;
}
/// <summary>
/// 交换数组中的两个数字
/// </summary>
/// <param name="numbers">交换数字的数组</param>
/// <param name="index1">交换的数字1的下标</param>
/// <param name="index2">交换的数字2的下标</param>
private void Swap(ref int[] numbers, int index1, int index2)
{
int temp = numbers[index1];
numbers[index1] = numbers[index2];
numbers[index2] = temp;
}
/// <summary>
/// 检验输入的数组是否合法,不合法返回false
/// </summary>
/// <param name="numbers">检验的数组</param>
/// <returns>是否不合法</returns>
private bool CheckInvalidArray(ref int[] numbers)
{
isInvalid = true;
if (numbers == null || numbers.Length < 1)
{
isInvalid = false;
}
return isInvalid;
}
/// <summary>
/// 用于检测数组中出现次数最多的值是否占了整个数组的一半以上
/// </summary>
/// <param name="numbers">传入的数组</param>
/// <param name="number">数组中出现次数最多的数字</param>
/// <returns>结果是否合法</returns>
private bool CheckMoreThanHalf(ref int[] numbers, int number)
{
int times = 0; //记录出现的次数
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] == number)
times++;
}
bool isMoreThanHalf = true;
//如果中间值出现的次数小于数组元素数量的一半,那么改变本地布尔变量,返回假
if ((times << 1) <= numbers.Length)
{
isMoreThanHalf = false;
isInvalid = false;
}
return isMoreThanHalf;
}
}
- 解法二:
根据数组特点找出时间复杂度为O(N)的算法
数组中有一个珠子出现的次数超过数组元素数量的一半,也就是说他出现的次数比其他所有数字出现的次数的和还要多。
1、数组中的一个数字;
2、出现的次数。
当我们遍历到下一个数字的饿时候,如果下一个数字和我们之前保存的数字相同,次数+1;如果下一个数字和我们之前保存的数字不同,则次数减1 。如果次数为零,那么我们需要保存下一个数字,并把次数设为1 。由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。
参考的C#代码如下:
/// <summary>
/// 检索出现次数超过一半的字符
/// </summary>
/// <param name="numbers">检索的数组</param>
/// <returns>返回出现次数超过一半的字符</returns>
public int MoreThanHalfNum(int[] numbers)
{
int result = numbers[0];
int times = 0; //当前result出现的次数
//如果输入的数组不合法,直接返回0
if(!CheckInvalidArray(numbers))
return 0;
//遍历数组
for(int i = 0; i < numbers.Length; i++)
{
if (times == 0)
{
result = numbers[i];
times = 1;
}
else if (result==numbers[i])
{
times++;
}
else
{
times--;
}
}
//检查最后的结果是否满足条件
if (!CheckMoreThanHalf(numbers, result))
result = 0;
return result;
}
在第一种解法中,需要交换数组中数字的顺序,这就会修改输入的数组。是否可以修改输入的数组需要在面试的时候与面试官进行讨论,让他明确需求。如果不能修改,那么就只能采用第二种方法了。