演算法風暴第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];
}