找出数组中出现次数超过一半的数字

题目:数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。

解法一:

如果无序,那么我们是不是可以先把数组中所有这些数字先进行排序(至于排序方法可选取最常用的快速排序)。排完序后,直接遍历,在遍历整个数组的同时统计每个数字的出现次数,然后把那个出现次数超过一半的数字直接输出,题目便解答完成了。总的时间复杂度为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;
	}

猜你喜欢

转载自blog.csdn.net/u013230189/article/details/80894704