Design a data structure that efficiently finds the majority element of a given subarray.
The majority element of a subarray is an element that occurs threshold
times or more in the subarray.
Implementing the MajorityChecker
class:
MajorityChecker(int[] arr)
Initializes the instance of the class with the given arrayarr
.int query(int left, int right, int threshold)
returns the element in the subarrayarr[left...right]
that occurs at leastthreshold
times, or-1
if no such element exists.
Example 1:
Input
["MajorityChecker", "query", "query", "query"]
[[[1, 1, 2, 2, 1, 1]], [0, 5, 4], [0, 3, 3], [2, 3, 2]]
Output
[null, 1, -1, 2]
Explanation
MajorityChecker majorityChecker = new MajorityChecker([1, 1, 2, 2, 1, 1]);
majorityChecker.query(0, 5, 4); // return 1
majorityChecker.query(0, 3, 3); // return -1
majorityChecker.query(2, 3, 2); // return 2
Constraints:
1 <= arr.length <= 2 * 104
1 <= arr[i] <= 2 * 104
0 <= left <= right < arr.length
threshold <= right - left + 1
2 * threshold > right - left + 1
- At most
104
calls will be made toquery
.
這道題讓設計一種數據結構可以有效的找出給定范圍內子數組的多數,這里還給了一個閾值 threshold,只要出現次數大於等於這個閾值就算是多數。可能有些人看到這里就說,那還不簡單么,遍歷這個子數組區間,累加每個數字出現的次數唄,大於 threshold 就返回唄。如果就這么簡單的話怎么對得起這道題 Hard 的身價,當然是不行的,得另尋更高效的解決方法。既然這里需要統計相同數字出現的次數,有一種以空間換時間的方法就是建立每個數字和其在原數組中出現的所有位置坐標組成的數組的映射,這樣做的好處是可以快速知道任意一個數字出現的所有位置,而且坐標還是從小到大有序的,為之后的二分搜索法提供了條件。查找的時候遍歷所有的數字,此時有了該數字在數組中出現的所有按順序排列的坐標,可以用二分法查找第一個不小於左邊界 left 的位置,然后再用二分法查找第一個大於右邊界 right 的位置,若二者的差值大於等於 threshold,則說明該數字在區間 [left, right] 內至少出現了 threshold 次,返回該數字即可,否則返回 -1,參見代碼如下:
解法一:
class MajorityChecker {
public:
MajorityChecker(vector<int>& arr) {
for (int i = 0; i < arr.size(); ++i) {
idxMap[arr[i]].push_back(i);
}
}
int query(int left, int right, int threshold) {
for (auto &a : idxMap) {
if (a.second.size() < threshold) continue;
auto it1 = lower_bound(begin(a.second), end(a.second), left);
auto it2 = upper_bound(begin(a.second), end(a.second), right);
if (it2 - it1 >= threshold) return a.first;
}
return -1;
}
private:
unordered_map<int, vector<int>> idxMap;
};
我們可以進行一些優化,比如先驗證出現次數最多的數字,因為其更有可能會符合要求,這樣的話可以給映射對兒進行排序,按照數字出現的次數從大到小排列,查找的方法還是不變的,參見代碼如下:
解法二:
class MajorityChecker {
public:
MajorityChecker(vector<int>& arr) {
unordered_map<int, vector<int>> idxMap;
for (int i = 0; i < arr.size(); ++i) {
idxMap[arr[i]].push_back(i);
}
for (auto &a : idxMap) idxVec.push_back({a.first, a.second});
sort(begin(idxVec), end(idxVec), [](auto &a, auto &b) {return a.second.size() > b.second.size();});
}
int query(int left, int right, int threshold) {
for (auto &a : idxVec) {
if (a.second.size() < threshold) continue;
auto it1 = lower_bound(begin(a.second), end(a.second), left);
auto it2 = upper_bound(begin(a.second), end(a.second), right);
if (it2 - it1 >= threshold) return a.first;
}
return -1;
}
private:
vector<pair<int, vector<int>>> idxVec;
};
再來看一種優化方法,這里不是按照次數由多到少來選,而是用一種隨機的取數方法,由於多數出現的個數是超過一半的,所以隨機抽取一個數字是多數的概率是超過 50% 的,那么連續抽多次,不中的概率就變得非常非常小,可以忽略不計,這里將次數設置為 10 就可以了,除了隨機選數這部分,剩余的跟之前的就沒什么區別了,參見代碼如下:
解法三:
class MajorityChecker {
public:
MajorityChecker(vector<int>& arr) {
for (int i = 0; i < arr.size(); ++i) {
idxMap[arr[i]].push_back(i);
}
nums = arr;
}
int query(int left, int right, int threshold) {
for (int i = 0; i < 10; ++i) {
auto a = idxMap.find(nums[left + rand() % (right - left + 1)]);
if (a->second.size() < threshold) continue;
auto it1 = lower_bound(begin(a->second), end(a->second), left);
auto it2 = upper_bound(begin(a->second), end(a->second), right);
if (it2 - it1 >= threshold) return a->first;
}
return -1;
}
private:
vector<int> nums;
unordered_map<int, vector<int>> idxMap;
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/1157
類似題目:
參考資料:
https://leetcode.com/problems/online-majority-element-in-subarray/