笔试遇到过,不出意外的还是没写出来。代码很简单,但是逻辑不好想:
start
开始累积 gas[i] - cost[i]
,如果某一步累计和 curr
变成负值,就意味着从 start
到当前站点之间,任何中间点都不可能是有效的起点。gas[i] - cost[i]
总和(即 total
)大于等于 0,那么从一个合适的候选起点出发一定能走完整个环。证明的核心是:
通过不断更新 start
,当累积和 curr
一次次地重置时,算法实际上已经“跳过”了那些不可能作为起点的站点。所以最后的start
是唯一能保证一路累计不为负的起点。最终,代码如下:
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int total = 0, curr = 0, start = 0;
for (int i = 0; i < gas.size(); i++) {
int diff = gas[i] - cost[i];
curr += diff;
total += diff;
if (curr < 0) {
start = i + 1;
curr = 0;
}
}
return total < 0 ? -1 : start;
}
};
从左向右进行一遍贪心,从右向左再进行一遍。每个小孩最少获得一个糖果,所以数组内的值初始化为1
。
class Solution {
public:
int candy(vector<int>& ratings) {
int n = ratings.size();
vector<int> arr(n, 1);
arr[0] = 1;
//从左向右
for(int i = 1; i < n; i++){
if(ratings[i] > ratings[i-1]) arr[i] = arr[i - 1] + 1;
}
//从右向左
for(int i = n - 2; i >= 0; i--){
if(ratings[i] > ratings[i + 1]){
arr[i] = max(arr[i], arr[i + 1] + 1);
}
}
int ans = 0;
for(auto & i : arr) ans += i;
return ans;
}
};
这道题挺好想的,代码如下:
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
unordered_map<int, int> umap;
umap[5] = 0;
umap[10] = 0;
umap[20] = 0;
for(int i = 0; i < bills.size(); i++){
if(bills[i] == 10){
if(umap[5] > 0) umap[5]--;
else return false;
}else if(bills[i] == 20){
if(umap[10] > 0 && umap[5] > 0){
umap[10]--;
umap[5]--;
}else if(umap[10] == 0 && umap[5] >= 3){
umap[5] -= 3;
}else return false;
}
umap[bills[i]]++;
}
return true;
}
};
鉴于其实只有三种数额,用map
有点大材小用了。并且记录”20”这个数额没有意义,可以优化代码如下:
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0, ten = 0;
for(int i = 0; i < bills.size(); i++){
if(bills[i] == 10){
if(five > 0) five--;
else return false;
ten++;
}else if(bills[i] == 20){
if(ten > 0 && five > 0){
ten--;
five--;
}else if(ten == 0 && five >= 3) five -= 3;
else return false;
}else five++;
}
return true;
}
};
这道题也存在两个维度,因此需要进行两次贪心,第一次用比较函数,从大到小排列,第二次去考虑第二个维度将当前数值插入正确的位置。
class Solution {
public:
static bool cmp(vector<int> a, vector<int> b){
if (a[0] == b[0]) return a[1] < b[1]; //这一步很重要!!!
return a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
vector<vector<int>> que;
sort(people.begin(), people.end(), cmp);
for(int i = 0; i < people.size(); i++){
int pos = people[i][1];
que.insert(que.begin() + pos, people[i]);
}
return que;
}
};
鉴于vector
插入操作时间复杂度较高,这一步可以优化成链表,代码如下(cr. 代码随想录):
class Solution {
public:
// 身高从大到小排(身高相同k小的站前面)
static bool cmp(const vector<int>& a, const vector<int>& b) {
if (a[0] == b[0]) return a[1] < b[1];
return a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort (people.begin(), people.end(), cmp);c
list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
for (int i = 0; i < people.size(); i++) {
int position = people[i][1]; // 插入到下标为position的位置
std::list<vector<int>>::iterator it = que.begin();
while (position--) { // 寻找在插入位置
it++;
}
que.insert(it, people[i]);
}
return vector<vector<int>>(que.begin(), que.end());
}
};
C++中
vector
可以理解为一个动态数组,如果插入元素大于预先普通数组大小,vector
底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上(cr. 代码随想录)。
最后再复习一下c++中vector
和list
的特性:
特性 | vector | list |
---|---|---|
内存布局 | 连续内存,具有良好的缓存局部性 | 非连续内存,使用双向链表实现 |
随机访问 | O ( 1 ) O(1) O(1) 时间复杂度 | O ( n ) O(n) O(n) 时间复杂度,需要顺序遍历 |
插入/删除操作 | 尾部插入高效;中间或头部插入需要移动大量元素 | 任意位置插入/删除操作高效(只需调整指针) |
空间开销 | 内存连续,额外指针开销小,但可能预留额外空间 | 每个节点需要额外存储前后指针,内存开销较大 |
适用场景 | 需要频繁随机访问或尾部操作的场景 | 需要频繁在中间进行插入或删除操作的场景 |