1. 程式人生 > >演算法風暴第1篇-陣列中出現次數超過一半的數字

演算法風暴第1篇-陣列中出現次數超過一半的數字

用頭腦風暴學演算法,對於一個問題,我們不只是要解決它,還要去思考有什麼好的方法,差的方法去解決,甚至是一些錯誤的但可以提供思想借鑑的方法。

此問題“陣列中出現次數超過一半的數字”是一道非常經典的演算法題,我把它放在演算法風暴系列第一篇來解析,探討學習一個演算法的過程,從慢到快,從最直觀的方法到腦洞大開的方法,由表面深入本質。

問題描述

給定一個數組,且已知陣列中有一個數出現次數超過一半(嚴格),請求出這個數。

問題很簡單,方法也多樣,但什麼方法是最好的呢?為什麼它最好?各種方法之間有什麼優缺點?下面我們一一展開。

方法一:給陣列排序

這大概是最直觀的方法了,最容易想到,也是最多人能夠想出來的。如果我們使用快排的話,只需要O(nlogn)的時間就可以找到這個數。

那麼思考這樣一個問題:給陣列排序了,然後怎麼找這個數呢?有兩種方法

1、從小到大遍歷已排序陣列,同時統計每個數出現的次數(某個數和上一個數不同則計數置為1),如果出現某個計數超過一半,那麼正在計數的數就是所求數。

PS:這種方法可行,相比於快排的時間複雜度是可以忽略的,但是我們還有更好的方法,直擊本質。

2、對一個已排好序的序列,出現次數超過一半的數必定是中位數。因此,我們只要輸出中位數即可。

複雜度分析:

時間複雜度 O(nlogn)
空間複雜度 O(n)

手寫快排程式碼:

#include <algorithm>
#include <iostream>
#include <cstdlib>
#include
<ctime>
#define RAND(start, end) start+(int)(end-start+1)*rand()/(RAND_MAX+1); using namespace std; const int maxn = 10005; int Partition(int *data, int length, int start, int end) { if (start == end) return start; srand((unsigned)time(NULL)); int index = RAND(start, end); swap(data[index],
data[end]); int one = start - 1; for (index = start; index < end; ++index) { if (data[index] < data[end]) { ++one; if (one != index) swap(data[one], data[index]); } } ++one; swap(data[one], data[end]); return one; } void QuickSort(int *data, int length, int start, int end) { if (length <= 1) return; int mid = Partition(data, length, start, end); if (mid > start) QuickSort(data, length, start, mid - 1); if (mid < end) QuickSort(data, length, mid + 1, end); } int main() { int n, data[maxn]; cin >> n; for (int i = 0; i < n; ++i) { cin >> data[i]; } QuickSort(data, n, 0, n - 1); cout << data[n >> 1]; }

方法二:桶排序計數

如果我們需要統計的陣列元素都是正整數呢?那麼我們就可以使用桶排序,給他們計數,然後超過陣列大小一半的就是結果了。

然而桶排序看上去很簡單,“複雜度也不高”,卻有很多的限制。

1、首先,陣列統計的數需得是可hash的,不然無法將他們在hash陣列上計數。但是某些情況,如元素有負值,可進行靈活轉化,使其可hash。 2、其次,桶排序方法空間換時間,需要消耗額外的空間,取決於資料的範圍。 3、桶排序並非真的那麼快。桶排序的時間複雜度並非是普通的O(n), 它的n指的是最大資料範圍,如果有這樣一組資料1 100 10000 1000000,那麼桶排序將會有至少1000000次迴圈,且開出1e6的空間,大大浪費資源。

桶排序方法適合資料範圍不大,且資料密度較大的資料。非也,則在此問題上算不上好方法。

程式碼

#include <iostream>
using namespace std;

int main()
{
	int n, max_size = 0, ans = 0;
	cin >> n;
	int *data = new int[n];
	for (int i = 0; i < n; ++i) {
		cin >> data[i];
		max_size = max(max_size, data[i]);
	}
	int *hash = new int[max_size + 1];
	for (int i = 0; i <= max_size; ++i)
		hash[i] = 0;
	for (int i = 0; i < n; ++i)
		hash[data[i]]++;
	for (int i = 0; i <= max_size; ++i)
		if (hash[i] > n >> 1) ans = i;
	cout << ans;
	delete [] data;
	delete [] hash;
}

方法三:巧用棧

其實我們可以發現,上面的方法一和方法二,固然是這道題的解法之一,但不是非常具有針對性。也就是說,那兩種方法是功能過剩的,而這所謂功能過剩,也正是導致它效能不是最佳的原因。

那麼,我們就應該思考某種演算法,只針對這個問題,完全的利用好效率。那麼就要從題目出發,找蘊含在問題中的本質規律了。

其實這個問題的核心就是:出現次數超過一半

我們做這樣的思考:

假設k就是我們要求的那個數,那麼對這個陣列,刪掉其中任意兩個數所剩下的陣列,其對應的k值會改變嗎?答案是會的。但是,如果刪掉任意兩個不相同的數呢?答案是不會! 為什麼不會?相信聰明的讀者瞬間就明白原因,只需進行簡單的推導就可以了。

具體的實現過程就是:每遍歷一個數,就將其入棧,同時查詢它和棧內前一個元素的大小,如果不同,就同時出棧,否則不變。

以上,就是用棧的方法解決這個問題的核心。

時間複雜度 O(n)
空間複雜度 O(n)

棧實現程式碼:

#include <iostream>
using namespace std;

int main()
{
	int n;
	cin >> n;
	int *data = new int[n];
	int *stack = new int[n];
	int top = 0;
	cin >> data[0];
	stack[++top] = data[0];
	for (int i = 1; i < n; ++i) {
		cin >> data[i];
		stack[++top] = data[i];
		if (top > 1 && stack[top] != stack[top - 1]) top -= 2;
	}
	cout << stack[top];
	delete [] data;
	delete [] stack;
}

方法四:找中位數(第n/2大數)

從方法一的分析中我們知道,這個陣列的中位數就是答案。方法一是通過給所有的數進行排序找出這個中位數,而我們思考,排序是否有些大材小用?找這個中位數的方法是否可以更簡單些?

答案是有的,而且這類問題被稱為找第k個數

思想是快排的思想。時間複雜度為O(n)

這個演算法如何實現我將在下次部落格中介紹,敬請期待。

#include <iostream>
#include <cstdlib>
#include <ctime>
#define RAND(start, end) start + (int)(end - start + 1)*(rand()/(RAND_MAX + 1))
using namespace std;

int Partition(int *data, int length, int start, int end)
{
	if (start == end) return start;
	srand((unsigned)time(NULL));
	int index = RAND(start, end);
	swap(data[index], data[end]);
	int one = start - 1;
	for (index = start; index < end; ++index) {
		if (data[index] < data[end]) {
			++one;
			if (one != index) swap(data[one], data[index]);
		}
	}
	++one;
	swap(data[one], data[end]);
	return one;
}

void FindIt(int *data, int length, int start, int end)
{
	int mid = Partition(data, length, start, end);
	if (mid == length >> 1) return;
	else if (mid > length >> 1) FindIt(data, length, start, mid - 1);
	else FindIt(data, length, mid + 1, end);
}

int main()
{
	int n;
	cin >> n;
	int *data = new int[n];
	for (int i = 0; i < n; ++i) {
		cin >> data[i];
	}
	FindIt(data, n, 0, n - 1);
	cout << data[n >> 1];
}