1、leetcode215 数组中的第k个最大元素
快速选择算法模板题。
由于找的是第k大的元素,除了前k大元素以外的元素并不关心是否有序,因此排序或者基于排序的方法比如基于小顶堆的优先队列并不是最优解(Onlogn)。基于快速排序每次定位一个元素的正确位置的思想改进而来的快速选择算法适用于此场景。
核心思想:每次从当前快速选择区间随机选取一个数作为基准,遍历所有元素,将小于基准的都放到基准右侧,大于基准的都放到基准左侧。整理完毕后,若当前基准的位置小于k,则递归地选取右侧区间;若当前基准的位置大于k,则递归地选取左侧区间;若正好为k则直接返回。
代码实现当中,为了能够高效的原地排序,采取的策略是选取了基准后,将基准与当前区间的第一个数交换,设置index为起始位置。然后从第二个数的位置开始遍历,遇到大于基准的数则将该数与index+1位置的数交换并++index。最后将位于起始位置的基准与index位置的数交换,此时基准回归到了正确的位置。
以原始数组2 1 6 3 0 5举例,选取3为基准:
- 3 1 6 2 0 5,解释:将基准3与起始位置的2交换。此时index=0,从i = 1开始遍历。
- 3 6 1 2 0 5,解释:遍历至i = 2时6 > 3,交换6和nums[index + 1] = 1,此时index改为1
- 3 6 5 2 0 1,解释:遍历至i = 5时5 > 3,交换5和nums[index + 1] = 1,此时index改为2
- 5 6 3 2 0 1,解释:最后将起始位置的3与nums[2] = 5交换。
可以看到此时3的左侧都比3大,右侧都比3小,说明上述做法实现快速选择的原地排序是有效的。
代码:
void quickPartition(vector<int>& nums, int start, int end, int target){
srand(time(0));
int random = rand() % (end - start + 1) + start;
int base = nums[random];
//--------------------------------------------
swap(nums[start], nums[random]);
int index = start;
for(int i = start + 1; i <= end; ++i){
if(nums[i] > base){
swap(nums[i], nums[index + 1]);
++index;
}
}
swap(nums[start], nums[index]);
//----------------------------------------------
if(index < target) quickPartition(nums, index + 1, end, target);
else if(index > target) quickPartition(nums, start, index - 1, target);
}
int findKthLargest(vector<int>& nums, int k) {
quickPartition(nums, 0, nums.size() - 1, k - 1);
return nums[k - 1];
}
2、循环依赖(华为笔试第三题)
给定一组元素,及其依赖关系,一个元素可以依赖于多个元素不包括自己,被依赖元素不会重复),一个元素也可被多个元素依赖。 假定总是存在唯一的循环依赖,请输出该循环依赖。 输入
第一行是个正整数N(1<N<100),表示依赖关系的个数。
下面每行表示一个依赖关系,是由空格分割的多个正整数,第一个数 n 表示后面有n个元素,第二个数为元素编号 a ,后面多个数字为 a 依赖的元素编号。任意元素编号 i 满足(0<i<10000)
输出
一串数字,首位和末位相同,为循环依赖中最小元素编号。元素之间用—个空格表示依赖关系。
样例1
输入:
3
3 1 2 5
3 2 3 4
2 3 1
输出:
1 2 3 1
解释: 输入的含义是,有3个依赖关系:
1)元素1依赖于2、5
2)元素2依赖于3、4
3)元素3依赖于1
存在唯一的循环依赖,1->2>3>1,从最小的元素开始/结束输出。
思路
首先这道题应该分两部分考虑,第一部分是有向图找环,第二部分是将该环中的元素按照从最小的元素开始的要求输出。
第一部分有向图找环,思路就是在一个vst数组的辅助下dfs遍历图。vst中每个元素代表图中一个点的状态,0代表未被访问过,1代表被当前这次dfs访问过,-1代表被之前的某次dfs访问过。dfs搜索过程中如果遇到vst值为1的点则说明从本次dfs的起点开始的路径包含一个环。用一个双端队列存储环所在的路径。
第二部分是从成环路径中元素编号最小的点开始遍历。注意,环入口可能不在本次dfs的起点,也就是可能是“6”形或者“0”形。若是“6”形则要剔除从起点到环入口之间的点。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
vector<vector<int>> out_degree(10000);
vector<int> vst(10000);
deque<int> path;
bool dfs(int node){
if(vst[node] == -1) return false;
path.push_back(node);
if(vst[node] == 1) return true;
vst[node] = 1;
for(int nx : out_degree[node]){
if(dfs(nx)) return true;
}
vst[node] = -1; //注意要把点的vst值回溯到-1
return false;
}
vector<int> solution(vector<vector<int>>& edges){
int max_node = 0;
for(auto& edge : edges){
int node = edge[0];
max_node = max(max_node, node);
for(int i = 1; i < edge.size(); ++i) out_degree[node].push_back(edge[i]);
} //构建邻接表
for(int i = 0; i <= max_node; ++i){
if(dfs(i)){
int enter = path.back(); //成环路径的最后一个点必然也是环的入口
while(path.front() != enter) path.pop_front(); //将“6”形路径不在环中的点剔除
vector<int> res(path.size());
vector<int> ans(path.size());
int index = 0;
while(!path.empty()){
res[index] = path.front();
path.pop_front();
++index;
}
int minIdx = 10001, minNum = 10001;
for (int j = 0 ; j < res.size() ; ++j) { //找环中编号最小的点
if (res[j] < minNum) {
minNum = res[j];
minIdx = j;
}
}
for(int j = 0; j < res.size(); ++j){
ans[j] = res[(j + minIdx) % res.size()]; //从编号最小的点开始,并注意环形的要求
}
return ans;
}
}
return {};
}
int main() {
int n;
cin >> n;
vector<vector<int>> edges;
for(int i = 0; i < n; ++i){
int m;
cin >> m;
vector<int> edge(m);
for(int j = 0; j < m; ++j) cin >> edge[j];
edges.push_back(edge);
}
vector<int> ans = solution(edges);
for(int i : ans) cout << i << ' ';
cout << endl;
return 0;
}
作为华为笔试的最后一题,难度确实顶。但主要的核心在于有向图找环的方法,即vst数组辅助的dfs,注意vst三种取值的意义。别的没啥好总结的,随便换道题又不一样了。拓扑排序只能判定图中有没有环,要找环里所有的点还得是dfs