演算法分析與設計 第二週
演算法分析與設計
尋找第K大數
題目描述
選題原因
本週學習了分治演算法,在學習中出現了例題
尋找第K大數字
,以往的做法通常是維護一個長度為k的陣列,儲存最大的k個數,掃描所有的值,不斷地加入陣列。而新的分治演算法則有著極低的複雜度。恰巧本題是中等難度,因此選擇使用兩種解法分別解題並做比較。
維護長度為K陣列
解題思路
維持長度為k的陣列,以及兩個變數
min
minpos
,記錄當前陣列中最小的數字及最小數所在的位置。當有數字大過最小數,則替換最小數,並且掃描整個陣列,更新最小數。
解題過程
int findKthLargest(vector <int>& nums, int k) {
int temp[k];
//初始化陣列
for (int i = 0; i < k; i++) {
temp[i] = -1;
}
int min = -1;
int minindex = 0;
vector<int>::iterator it = nums.begin();
//遍歷陣列
for ( ; it != nums.end(); it++) {
//如果有數字大於最小數,加入陣列
if (*it > min) {
temp[minindex] = *it;
min = *it;
//更新陣列中的最小數
for (int i = 0; i < k; i++) {
if (temp[i] < min) {
min = temp[i];
minindex = i;
}
}
}
}
return temp[minindex];
}
分治演算法
解題思路及過程
原始演算法
每次挑選一箇中間數
x
(首先選擇k - 1
),並維護三個陣列,left
right
v
,若數字大於x
,則放入right
;若數字小於x
,則放入left
;否則放入v
。再根據情況遞迴。遞迴規則如下:
圖中是找第k小數規則,只需要將他稍微顛倒一下即可。演算法如下:
int findKthLargest(vector<int>& nums, int k) {
int comp = nums[k - 1];
vector<int>::iterator it = nums.begin();
vector<int> left;
vector<int> right;
vector<int> v;
//為所有數字分組
for ( ; it != nums.end(); it++) {
if (*it > comp) {
right.push_back(*it);
} else if (*it < comp) {
left.push_back(*it);
} else {
v.push_back(*it);
}
}
//選擇需要遞迴的陣列
if (k <= right.size()) {
//在右邊
return findKthLargest(right, k);
} else if (k > right.size() && k <= right.size() + v.size()) {
//已選中
return v.front();
} else {
//在左邊
return findKthLargest(left, k - right.size() - v.size());
}
}
優化取中間值
結果發現超出了記憶體限制,演算法需要改進,首先修改中間數
x
的取值,當長度小於20
時,取nums
的中間數,為了保險,選擇兩個數取平均值(如果只取一個數可能取到最小數,因此取中間兩個數,作為調節)
int max = nums.size(); //總長
int comp = 0;
if (max > 20) {
comp = (nums[max/2] + nums[max/2 + 1]) / 2;
} else {
comp = nums[max/2];
}
通過測試,不妨修改一個簡單的引數,當長度小於
5
時,就用心的方法取中間值x
,再次測試。
發現演算法反而變差了,那我們不妨試著當長度大於
10
時則取值,再次測試。
發現演算法與
20
時差不多。
優化記憶體佔用
之前演算法失敗的原因是佔用太多記憶體,雖然在此基礎上使空間佔用稍小了一些,但實際上還有別的方法可以優化更多。
觀察後發現,實際上,每次分類之後我們只會用到一個分組,則另外一個分組實際上是不需要放入那麼多數字的。實際上,vector
是很佔用空間的。
我們發現,當右邊個數 >= k
時,則只會用到右邊,此時就不必向左邊陣列插入元素;同理,當leftnum >= max - k
,則只需要用到左邊。
因此,我們使用兩個變數標記useleft
useright
,每次插入前判斷。
int findKthLargest(vector<int>& nums, int k) {
int max = nums.size(); //總長
int comp = 0;
if (max > 20) {
comp = (nums[max/2] + nums[max/2 + 1]) / 2;
} else {
comp = nums[max/2];
}
vector<int>::iterator it = nums.begin();
bool useleft = false; //結果只需要left,不需要向right和v新增
bool useright = false; //結果只需要right,不需要向left和v新增
int leftnum = 0; //實際left應該有的數量,下同
int rightnum = 0;
int vnum = 0;
vector<int> left;
vector<int> right;
vector<int> v;
//為所有數字分組
for ( ; it != nums.end(); it++) {
if (*it > comp) {
//當只用到左邊的時候,則不需要實際插入
if (!useleft) {
right.push_back(*it);
}
//需要計數,之後用到
rightnum++;
//判斷此時是否只用得到
if (rightnum >= k) {
useright = true;
}
} else if (*it < comp) {
if (!useright) {
left.push_back(*it);
}
leftnum++;
if(leftnum >= max - k) {
useleft = true;
}
} else {
v.push_back(*it);
vnum++;
}
}
//選擇需要遞迴的陣列
if (k <= rightnum) {
//在右邊
return findKthLargest(right, k);
} else if (k > rightnum && k <= rightnum + vnum) {
//已選中
return v.front();
} else {
//在左邊
return findKthLargest(left, k - rightnum - vnum);
}
}
發現雖然速度上沒有什麼實際的變化,但是對於空間的損耗的確少了許多。
原始碼
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int max = nums.size(); //總長
int comp = 0;
if (max > 20) {
comp = (nums[max/2] + nums[max/2 + 1]) / 2;
} else {
comp = nums[max/2];
}
vector<int>::iterator it = nums.begin();
bool useleft = false; //結果只需要left,不需要向right和v新增
bool useright = false; //結果只需要right,不需要向left和v新增
int leftnum = 0; //實際left應該有的數量,下同
int rightnum = 0;
int vnum = 0;
vector<int> left;
vector<int> right;
vector<int> v;
//為所有數字分組
for ( ; it != nums.end(); it++) {
if (*it > comp) {
//當只用到左邊的時候,則不需要實際插入
if (!useleft) {
right.push_back(*it);
}
//需要計數,之後用到
rightnum++;
//判斷此時是否只用得到
if (rightnum >= k) {
useright = true;
}
} else if (*it < comp) {
if (!useright) {
left.push_back(*it);
}
leftnum++;
if(leftnum >= max - k) {
useleft = true;
}
} else {
v.push_back(*it);
vnum++;
}
}
//選擇需要遞迴的陣列
if (k <= rightnum) {
//在右邊
return findKthLargest(right, k);
} else if (k > rightnum && k <= rightnum + vnum) {
//已選中
return v.front();
} else {
//在左邊
return findKthLargest(left, k - rightnum - vnum);
}
}
};