题目:数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。
解法一:
如果无序,那么我们是不是可以先把数组中所有这些数字先进行排序(至于排序方法可选取最常用的快速排序)。排完序后,直接遍历,在遍历整个数组的同时统计每个数字的出现次数,然后把那个出现次数超过一半的数字直接输出,题目便解答完成了。总的时间复杂度为O(nlogn + n)。
但如果是有序的数组呢,或者经过排序把无序的数组变成有序后的数组呢?是否在排完序O(nlogn)后,还需要再遍历一次整个数组?
我们知道,既然是数组的话,那么我们可以根据数组索引支持直接定向到某一个数。我们发现,一个数字在数组中的出现次数超过了一半,那么在已排好序的数组索引的N/2处(从零开始编号),就一定是这个数字。自此,我们只需要对整个数组排完序之后,然后直接输出数组中的第N/2处的数字即可,这个数字即是整个数组中出现次数超过一半的数字,总的时间复杂度由于少了最后一次整个数组的遍历,缩小到O(n*logn)。
然时间复杂度并无本质性的改变,我们需要找到一种更为有效的思路或方法
/**
* 快速排序
*
* @param arr
* 带排序数组
* @param low
* @param high
*/
public static void quickSort(int[] arr, int low, int high) {
int i, j, temp, t;
if (low > high) {
return;
}
i = low;// i哨兵从左边开始
j = high;// j哨兵从右边开始
temp = arr[low];
while (i < j) {
// 从右边依次往左递减
while (temp <= arr[j] && i < j) {
j--;
}
// 从左边依次往右递增
while (temp >= arr[i] && i < j) {
i++;
}
// 如果满足条件则交换
if (i < j) {
t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
// 将基准位置与i和j相遇位置的数交换
arr[low] = arr[i];
arr[i] = temp;
// 递归调用左半数组
quickSort(arr, low, j - 1);
// 递归调用右半数组
quickSort(arr, j + 1, high);
}
/**
* 对数组进行排序,已排好序的数组索引的N/2处(从零开始编号),就一定是这个数字
*/
public static int findNumber(int[] numbers) {
quickSort(numbers, 0, numbers.length - 1);
return numbers[numbers.length / 2];
}
解法二:
既要缩小总的时间复杂度,那么可以用查找时间复杂度为O(1)的hash表,即以空间换时间。哈希表的键值(Key)为数组中的数字,值(Value)为该数字对应的次数。然后直接遍历整个hash表,找出每一个数字在对应的位置处出现的次数,输出那个出现次数超过一半的数字即可。
/**
* 用查找时间复杂度为O(1)的hash表,即以空间换时间。<br>
* 哈希表的键值(Key)为数组中的数字,值(Value)为该数字对应的次数。<br>
* 然后直接遍历整个hash表,找出每一个数字在对应的位置处出现的次数,输出那个出现次数超过一半的数字即可。
*
* @param numbers
* @return
*/
public static int findNumber1(int[] numbers) {
Map<Integer, Integer> intToInt = new HashMap<>();
for (int n : numbers) {
if (!intToInt.containsKey(n)) {
intToInt.put(n, 1);
} else {
int value = intToInt.get(n);
intToInt.put(n, ++value);
}
}
for (Integer i : intToInt.keySet()) {
if (intToInt.get(i) > numbers.length / 2) {
return i;
}
}
return -1;
}
解法三:
Hash表需要O(n)的空间开销,且要设计hash函数,还有没有更好的办法呢?我们可以试着这么考虑,如果每次删除两个不同的数(不管是不是我们要查找的那个出现次数超过一半的数字),那么,在剩下的数中,我们要查找的数(出现次数超过一半)出现的次数仍然超过总数的一半。通过不断重复这个过程,不断排除掉其它的数,最终找到那个出现次数超过一半的数字。这个方法,免去了排序,也避免了空间O(n)的开销,总得说来,时间复杂度只有O(n),空间复杂度为O(1),貌似不失为最佳方法。
/**
* 每次删除两个不同的数(不管是不是我们要查找的那个出现次数超过一半的数字),<br>
* 那么,在剩下的数中,我们要查找的数(出现次数超过一半)出现的次数仍然超过总数的一半。<br>
* 通过不断重复这个过程,不断排除掉其它的数,最终找到那个出现次数超过一半的数字。
*
* @param numbers
* @return
*/
public static int findNumber2(int[] numbers) {
int i = 0;
int j = numbers.length - 1;
while (i <= j) {
if (numbers[i] == numbers[j]) {
j--;
i++;
} else {
numbers[i] = Integer.MAX_VALUE;
numbers[j] = Integer.MAX_VALUE;
}
}
for (int n : numbers) {
if (n != Integer.MAX_VALUE) {
return n;
}
}
return -1;
}
解法四:
更进一步,考虑到这个问题本身的特殊性,我们可以在遍历数组的时候保存两个值:一个candidate,用来保存数组中遍历到的某个数字;一个nTimes,表示当前数字的出现次数,其中,nTimes初始化为1。当我们遍历到数组中下一个数字的时候:
- 如果下一个数字与之前candidate保存的数字相同,则nTimes加1;
- 如果下一个数字与之前candidate保存的数字不同,则nTimes减1;
- 每当出现次数nTimes变为0后,用candidate保存下一个数字,并把nTimes重新设为1。 直到遍历完数组中的所有数字为止。
/**
* 可以在遍历数组的时候保存两个值:一个candidate,用来保存数组中遍历到的某个数字<br>
* ;一个nTimes,表示当前数字的出现次数,其中,nTimes初始化为1。当我们遍历到数组中下一个数字的时候:<br>
* 如果下一个数字与之前candidate保存的数字相同,则nTimes加1;<br>
* 如果下一个数字与之前candidate保存的数字不同,则nTimes减1;<br>
* 每当出现次数nTimes变为0后,用candidate保存下一个数字,并把nTimes重新设为1。<br>
* 直到遍历完数组中的所有数字为止。
*
* @param number
* @return
*/
public static int findNumber3(int[] numbers) {
int nTimes = 1;
int candidate = numbers[0];
for (int i = 1; i <= numbers.length - 1; i++) {
if (nTimes == 0) {
candidate = numbers[i];
nTimes = 1;
} else {
if (candidate == numbers[i]) {
nTimes++;
} else {
nTimes--;
}
}
}
return candidate;
}