本周完成的题目有:
博客专栏地址:https://blog.csdn.net/feng964497595/category_9848847.html
github地址:https://github.com/mufeng964497595/leetcode
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
class Solution {
public:
vector twoSum(vector& nums, int target) {
// 用map来记录已搜索过的值&对应的下标
std::map mVal2Idx;
vector vRes;
// 提前分配空间,避免push_back触发空间重新分配,影响性能
vRes.reserve(2);
int size = nums.size();
for (int i= 0; i < size; ++i) {
int val = target - nums[i];
auto mit = mVal2Idx.find(val);
if (mVal2Idx.end() == mit) {
// 没找到,将数组当前值送入map
mVal2Idx.insert(std::make_pair(nums[i], i));
} else {
// 从map中找到另一个值,那就得到结果了。
// 对于数组中有两个值相同的情况也兼容了,
// 因为题目说只有一个答案,那么如果出现两个值相同且其中一个是答案的情况,
// 就只能是这两个值都是答案这一种可能。
// 而此时map里只有一个值,所以不存在下标被覆盖的情况
// mit->second比较小,放前面
vRes.push_back(mit->second);
vRes.push_back(i);
break;
}
}
return vRes;
}
};
在 N * N 的网格上,我们放置一些 1 * 1 * 1 的立方体。
每个值 v = grid[i][j] 表示 v 个正方体叠放在对应单元格 (i, j) 上。
请你返回最终形体的表面积。
class Solution {
public:
int surfaceArea(vector>& grid) {
int n = grid.size();
int dirx[] = {0, 1}, diry[] = {1, 0};
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
// 当前格子没有正方体,不用处理
if (0 == grid[i][j]) continue;
ans += grid[i][j] * 4 + 2; // 四个面+上下底面
// 查找当前格子右边和上面相邻的两个格子,减去重复的区域。
for (int k = 0; k < 2; ++k) {
int x = i + dirx[k], y = j + diry[k];
if (x >= n || y >= n) continue;
// 两列都被挡住,所以减去的表面积要*2
ans -= std::min(grid[i][j], grid[x][y]) * 2;
}
}
}
return ans;
}
};
给定一副牌,每张牌上都写着一个整数。
此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组:
仅当你可选的 X >= 2 时返回 true。
示例 1:
输入:[1,2,3,4,4,3,2,1]
输出:true
解释:可行的分组是 [1,1],[2,2],[3,3],[4,4]
class Solution {
public:
bool hasGroupsSizeX(vector& deck) {
// 用map统计每个数字有几张牌
std::map mCard2Num;
for (auto val : deck) {
++mCard2Num[val];
}
// 计算各个数的最大公约数,大于1就表示有超过一种分配方法
int gcd = 1;
bool first = true;
for (auto item : mCard2Num) {
if (first) {
// 第一个数是自己的最大公约数
gcd = item.second;
first = false;
} else {
// 计算最大公约数
gcd = CalcGCD(gcd, item.second);
}
if (1 == gcd) break;
}
return gcd > 1;
}
private:
int CalcGCD(int a, int b) {
return b == 0 ? a : CalcGCD(b, a % b);
}
};
题目太长了,就转述一下。
题目意思大概就是,R可以往上下左右这四个方向一直走,只要走到边界、遇到B、遇到p就停止。问有多少个p是R走一步就可以吃到的。
class Solution {
public:
int numRookCaptures(vector>& board) {
// 找车在哪
int n = board.size();
int x, y;
bool bFind = false;
for (int i = 0; !bFind && i < n; ++i) {
for (int j = 0; !bFind && j < n; ++j) {
if ('R' == board[i][j]) {
x = i; y = j;
bFind = true;
}
}
}
if (!bFind) return 0;
// 四个方向都走一走,撞到边界、自家人、敌军就可以停了
int ans = 0;
int dirx[] = {0, 1, 0, -1}, diry[] = {1, 0, -1, 0};
for (int i = 0; i < 4; ++i) {
int new_x = x + dirx[i];
int new_y = y + diry[i];
while (new_x >= 0 && new_x < n && new_y >= 0 && new_y < n) {
if ('B' == board[new_x][new_y]) break;
if ('p' == board[new_x][new_y]) {
++ans;
break;
}
new_x += dirx[i];
new_y += diry[i];
}
}
return ans;
}
};
这里只展示使用终结标记的代码
class Solution {
public:
int minimumLengthEncoding(vector& words) {
int ans = 0;
SuffixTree root;
for (auto word : words) {
int len = word.size();
SuffixTree* pNode = &root; // 用来遍历的指针
int layer = 0;
bool isSuffix = true; // 标记当前字符串是否是后缀串
for (int i = len - 1; i >= 0; --i) {
++layer;
auto& next = pNode->next;
if (next.find(word[i]) != next.end()) {
// 使用现有节点
pNode = next[word[i]];
} else {
// 创建新节点
SuffixTree* pNewNode = new SuffixTree();
next.insert(std::make_pair(word[i], pNewNode));
pNode = pNewNode;
isSuffix = false;
}
if (pNode->isEnd && i != 0) {
// 当前节点有是某个字符串的终结节点,减去这个字符串的长度
// 加上i!=0的判断,是因为这种情况下就意味着有两个字符串完全相同,
// 前面的处理逻辑本身就会将后加的字符串当成后缀给舍弃,就没必要减掉前面的字符串。
ans -= layer + 1;
pNode->isEnd = false;
}
}
if (!isSuffix) {
ans += layer + 1; // 加上#号的长度
pNode->isEnd = true; // 最后一个节点打上终结标记
}
}
return ans;
}
private:
// 后缀树节点
class SuffixTree {
public:
SuffixTree() : isEnd(false) {
}
~SuffixTree() {
for (auto item : next) {
Delete(item.second);
}
}
private:
// 释放内存
void Delete(SuffixTree* tree) {
if (!tree) return;
for (auto item : tree->next) {
Delete(item.second);
}
}
public:
std::map next; // 树的子节点
bool isEnd; // 字符串终结标记
};
};
题目看起来挺绕的,其实就是两个概念在绕:
class Solution {
public:
int maxDistance(vector>& grid) {
// 找出所有陆地的坐标,压入队列
int dwLandNum(0);
std::queue q;
// bfs visit标记,其实也可以用grid来打标就行了,但是破坏原始数据的这个习惯不太好
bool visit[105][105];
memset(visit, 0, sizeof(visit));
int n = grid.size();
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (1 == grid[i][j]) {
++dwLandNum;
q.push(Point(i, j, 0));
visit[i][j] = true;
}
}
}
// bfs搜索就行了,最后一个找到的海洋就是目标
int dirx[] = {0, 1, 0, -1}, diry[] = {1, 0, -1, 0};
int ans = -1;
while (!q.empty()) {
Point p = q.front();
q.pop();
for (int i = 0; i < 4; ++i) {
int x = p.x + dirx[i];
int y = p.y + diry[i];
if (x >= 0 && x < n && y >= 0 && y < n && !visit[x][y] && 0 == grid[x][y]) {
visit[x][y] = true;
ans = p.step + 1;
q.push(Point(x, y, ans)); // 压入队列
}
}
}
return ans;
}
private:
struct Point {
int x, y; // 坐标
int step; // 搜索到当前坐标所花费的步数
Point(int xx = 0, int yy = 0, int s = 0)
: x(xx), y(yy), step(s) {
}
};
};
题目讲的很直白了,就是给出两个数,已经按十进制按位拆出放到链表中,求这两个数相加得到的链表。
模拟题,直接按照加法模拟照做就行了。因为我不想养成破坏原始数据的习惯,所以我新开了一个链表来保存计算结果。
题目有几个注意事项:
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
// 任一链表为nullptr,直接返回另一个链表,不需要处理
if (!l1) {
return l2;
} else if (!l2) {
return l1;
}
ListNode* p1 = l1;
ListNode* p2 = l2;
// 为了不养成破坏原始数据的习惯,使用第三个链表来记录结果
ListNode* ans = nullptr;
ListNode* pNow = nullptr;
int add(0); // 上一步加法的增量
while (p1 && p2) {
int val = p1->val + p2->val + add;
DealVal(val, add);
if (!ans) {
ans = new ListNode(val);
pNow = ans;
} else {
ListNode* tmp = new ListNode(val);
pNow->next = tmp;
pNow = tmp;
}
p1 = p1->next;
p2 = p2->next;
}
// 走到这一步,已保证pNow、ans不是空指针
if (p1) {
AddList(p1, pNow, add);
} else {
AddList(p2, pNow, add);
}
// 最后可能还有加法的进位
if (0 != add) {
ListNode* tmp = new ListNode(add);
pNow->next = tmp;
}
return ans;
}
private:
void DealVal(int& val, int& add) {
if (val >= 10) {
// 向前进位
val -= 10;
add = 1;
} else {
add = 0;
}
}
void AddList(ListNode*& pList, ListNode*& pRes, int& add) {
while (pList) {
int val = pList->val + add;
DealVal(val, add);
ListNode* tmp = new ListNode(val);
pRes->next = tmp;
pRes = tmp;
pList = pList->next;
}
}
};
题目乍一看是字符串子串问题,但其实不是。这道题换个马甲,改成求数组中无重复数字的最长连续区间长度,也是一样的道理,别被骗了然后一直往字符串算法上面去套。
因为做过挺多算法的,所以看到这种最长连续区间类型的题目,很自然就想到尺取法。你要是问我怎么就能想到这个算法,我也解释不上来,可能是打ACM培养出来的敏感性吧。。。。下面以字符串"pwekwp"为例演示一下尺取法。
初始如上图所示。尺取法有left、right两个指针,用来像尺子一样丈量数组。下标记录数据是用来标记已访问过的字符对应的下标,有两个作用,一个是标记哪些字符已访问,另一个是方便快速找到字符对应的上一个下标。
初始时left、right指向同一个字符,长度就是1啦,更新下标记录。这时候right指针右移,得到下图:
此时right指针对应的下标记录为-1,表示未访问过,那此时长度就是right-left+1=2了。再更新一下w的下标,right指针右移。同理,省略掉right指针在e、k的情况,直接演示下一个w的情况。注意一点就是,right指针右移的时候更新当前的最新长度。移动到下一个w时,最新长度已经是4了。
w对应的下标记录是1,且在left ~ right之间,那就说明left ~ right已经遇到重复的字符了,这时候把left指针移动到w下标记录+1的位置,也就是第一个e的位置,并将w的下标记录改为right的位置。然后更新当前的长度。由于right - left + 1 = 3 < 4,所以不需要更新。之后再继续把right指针右移。
这时候发现right对应的字符p已经有了下标记录,但是下标记录不在left ~ right中间,依旧认为是没有重复字符,更新长度,right指针右移。此时right指针已经超出范围了,可以结束了。(为什么不需要再看left指针右移呢?因为left ~ right已经没有重复字符了,left指针右移的长度肯定比现在的要短,就没必要浪费时间去算了)
从上面介绍的步骤可以看出,使用尺取法,只需要遍历一次字符串即可,时间复杂度O(n),很快速,实现起来也很简单。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int pos[256+5]; // 保存字符与下标的映射,字符ASCII码就那几百个
memset(pos, -1, sizeof(pos));
int len = s.length();
int left = 0, right = 0;
int ans = 0;
while (right < len) {
if (left == right) {
// 指向同一个字符,记录下标,右指针右移
pos[s[left]] = left;
++right;
ans = std::max(ans, 1);
} else {
if (pos[s[right]] != -1 && pos[s[right]] >= left) {
// 发现left~right中间的重复字符,right - left就是不重复字符串的长度
ans = std::max(ans, right - left);
// 左指针移动到重复字符的右边一位,避开这个重复字符
left = pos[s[right]] + 1;
} else {
// 未发现重复字符,更新当前长度
ans = std::max(ans, right - left + 1);
}
// 更新下标,右指针右移
pos[s[right]] = right;
++right;
}
}
return ans;
}
};