前言
万万没想到,一年过去了,我还在刷leetcode。。。
上一篇题解里面字太多,编辑器都卡住了,所以另开一篇文档
第一轮面试阴沟里翻车,祈祷我后天二轮能过155551
期末季面试真太顶了,一个礼拜之前接到邮件,因为中间有考试愣是最后只剩下两天准备面试,好家伙
220. 存在重复元素III
看到这题,第一眼:主席树!第二眼:离散化+树状数组!
然后看了一眼题解学了一下这个hash做法,还是很精妙的
这个题里面有两个距离:k和t。k可以用滑动窗口解决,而t就要把数字分组,比如t=3的时候就分成[0,2][3,5][6,9]....这样。
可以发现,当加入一个数字的时候,如果它那个组里已经有一个数,那就一定输出true。
另外答案还有可能出现在相邻的两个组里。
由于刚才说如果一个组里有两个数直接输出答案,那么可以知道在每个状态下一个组里最多只有一个数。记一下这个数的下标,每次拿出来比较就可以了。
但是这个题主要是,它细节是真的多。。。
最重要的问题就是数字范围问题。由于我这里想直接用数字整除t的结果来分组,必须要把t加一。而这就导致在t=2147483647的时候会爆int,所以我这个写法需要很多强转long long
另外,直接整除在处理负数的时候也会遇到问题。因为这个整除它严格来说是“向零取整”。还是以t=2为例,这会导致0所在的那个组范围实际上是[-2,2]而不是[0,2],就会出问题。
所以我需要让负数向下取整。需要一个特判,在getkey函数中。
另外就是,用map这类东西做哈希表的时候要区分“组为空”或“组里的数字是0”。因为map元素如果是空,访问出来的结果也是0。。。
这里直接就把记录在map里的下标都从1开始了。
写的贼丑。
class Solution {
private:
long long getkey(long long num, long long t) {
if (num >= 0) return num/t;
else return (num/t)-1;
}
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int _t) {
unordered_map<long long, int> table;
int n = nums.size();
if (n == 1) return false;
long long t = (long long)_t+1;
k = k + 1;
for (int i = 0; i < min(n, k); i ++) {
long long key = getkey(nums[i], t);
if (table[key] != 0) return true;
table[key] = i + 1;
if (table[key-1] != 0) {
int tar = table[key-1];
if (abs((long long)nums[i] - nums[tar-1]) < t)
return true;
}
if (table[key+1] != 0) {
int tar = table[key+1];
if (abs((long long)nums[i] - nums[tar-1]) < t)
return true;
}
}
for (int i = k; i < n; i ++) {
long long key = getkey(nums[i-k], t);
table[key] = 0;
key = getkey(nums[i], t);
if (table[key] != 0) return true;
table[key] = i + 1;
if (table[key-1] != 0) {
int tar = table[key-1];
if (abs((long long)nums[i] - nums[tar-1]) < t)
return true;
}
if (table[key+1] != 0) {
int tar = table[key+1];
if (abs((long long)nums[i] - nums[tar-1]) < t)
return true;
}
}
return false;
}
};
221. 最大正方形
二分。因为有边长为k的正方形就一定有边长比k小的正方形,反之亦然。
预处理一个二维前缀和,判定的时候枚举所有边长为mid的正方形即可。
注意处理全0矩阵的情况。
class Solution {
private:
int n, m;
vector<vector<int>> s;
void buildSum(vector<vector<char>> &matrix) {
for (int i = 0; i <= n; i ++) {
vector<int> tmp;
tmp.clear();
for (int j = 0; j <= m; j ++)
tmp.push_back(0);
s.push_back(tmp);
}
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
s[i][j] = (matrix[i-1][j-1]-'0') + s[i-1][j] + s[i][j-1] - s[i-1][j-1];
}
int calc(int x, int y, int xx, int yy) {
return s[xx+1][yy+1] - s[x][yy+1] - s[xx+1][y] + s[x][y];
}
bool check(int len) {
for (int i = 0; i < n-len+1; i ++)
for (int j = 0; j < m-len+1; j ++) {
int sum = calc(i, j, i+len-1, j+len-1);
if (sum == len * len) return true;
}
return false;
}
int divide(int l, int r) {
int mid, ans = l;
while (l <= r) {
mid = (l+r)>>1;
if (check(mid)) {
ans = max(ans, mid);
l = mid + 1;
} else r = mid - 1;
}
return ans*ans;
}
public:
int maximalSquare(vector<vector<char>>& matrix) {
n = matrix.size();
m = matrix[0].size();
buildSum(matrix);
if (s[n][m] == 0) return 0;
return divide(1, min(n, m));
}
};
222. 完全二叉树的节点个数
这个题O(n)的做法很简单,遍历就可以了。
要缩减复杂度的话,考虑它是一个完全二叉树。完全二叉树决定节点个数的就只有两个量:它的高度和它最底层的节点数目。
它的高度可以通过一直顺着左儿子走来求出,而最底层的节点数目可以发现满足二分单调性:从某一个点开始,左边全都有点,右边全都没有点。只要找到这个分界点就可以了。
那么就是每次二分一个mid,检查最底层的第mid个节点存不存在。
可以发现,如果把最底层的节点从左往右从0开始编号,那么它的编号的二进制就描述了从根节点走到它的路径。
举个栗子,就题目里样例的那棵二叉树。底层节点可以顺序编号为0 1 2 3(3号节点不存在)。那么如果要看2号节点存不存在,2号节点的二进制是10
(因为从顶到底最多只需要走两步,所以保留二进制的后两位即可。一般地,一个高度为h的二叉树保留h-1位即可)
2号节点的二进制是10,说明要先往右走一步(1)、再往左走一步(0)。
这样判定的复杂度是(O(h))也就是(O(logn)),然后二分的复杂度也是(O(logn)),总的复杂度是(O(log^2n))。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
int h;
int getdepth(TreeNode *root) {
int res = 0;
while (root != NULL) {
res ++;
root = root->left;
}
return res;
}
bool check(TreeNode *root, int id) {
for (int i = h-1; i >= 1; i --) {
if ((id >>(i-1)) & 1)
root = root->right;
else root = root->left;
if (root == NULL) return false;
}
return true;
}
int divide(TreeNode *root, int l, int r) {
int mid, ans = 1;
while (l <= r) {
mid = (l+r) >> 1;
if (check(root, mid-1)) {
ans = max(ans, mid);
l = mid + 1;
} else r = mid - 1;
}
return ans;
}
public:
int countNodes(TreeNode* root) {
if (root == NULL) return 0;
h = getdepth(root);
int lastdep = divide(root, 1, 1<<(h-1));
return (1<<(h-1))-1 + lastdep;
}
};
223. 矩形面积
这个题一眼就可以分类讨论,但情况特别多(完全包含的;相离的;左边覆盖的;右边覆盖的等等),如果要算覆盖面积的话非常不好做。
这里采用了离散化+填格子的方法。横纵坐标分别离散化,坐标范围缩小到4*4。然后枚举两个矩阵覆盖的每个格子,覆盖到的+1。最后计算每个被覆盖过的格子面积总和就可以了。
注意最后计算格子面积的时候坐标要用离散化之前的。
class Solution {
private:
int va[10], vb[10], ca, cb;
int mat[10][10];
void trans(int *v, int &cnt, int &n1, int &n2, int &n3, int &n4) {
v[1] = n1; v[2] = n2; v[3] = n3; v[4] = n4;
sort(v+1, v+4+1);
cnt = unique(v+1, v+4+1) - v - 1;
n1 = lower_bound(v+1, v+cnt+1, n1) - v;
n2 = lower_bound(v+1, v+cnt+1, n2) - v;
n3 = lower_bound(v+1, v+cnt+1, n3) - v;
n4 = lower_bound(v+1, v+cnt+1, n4) - v;
}
void add(int x, int y, int xx, int yy) {
for (int i = x; i < xx; i ++)
for (int j = y; j < yy; j ++)
mat[i][j] ++;
}
int getArea(int x, int y) {
int l1 = va[x+1] - va[x];
int l2 = vb[y+1] - vb[y];
return l1 * l2;
}
public:
int computeArea(int A, int B, int C, int D, int E, int F, int G, int H) {
trans(va, ca, A, C, E, G);
trans(vb, cb, B, D, F, H);
add(A, B, C, D);
add(E, F, G, H);
int ans = 0;
for (int i = 1; i <= ca; i ++)
for (int j = 1; j <= cb; j ++)
if (mat[i][j] != 0)
ans += getArea(i, j);
return ans;
}
};
227. 基本计算器
双栈法计算中缀表达式。
需要注意字符串末尾有空格的情况。
class Solution {
private:
int pri(char c) {
if (c == '+' || c == '-')
return 1;
if (c == '*' || c == '/')
return 2;
return 0;
}
int calc(int a, int b, char opt) {
switch (opt) {
case '+': return a+b;
case '-': return a-b;
case '*': return a*b;
case '/': return a/b;
}
return 0;
}
int getnum(int &ptr, string &s, int len) {
int x = 0;
while (ptr < len && (s[ptr] < '0' || s[ptr] > '9'))
++ptr;
while (ptr < len && s[ptr] <= '9' && s[ptr] >= '0') {
x = x * 10 + (s[ptr] - '0');
ptr ++;
}
return x;
}
char getopt(int &ptr, string &s, int len) {
while (ptr < len && pri(s[ptr]) == 0)
++ptr;
if (ptr >= len) return 0;
return s[ptr++];
}
public:
int calculate(string s) {
int ptr = 0, len = s.size();
int now, res;
char opt;
if (len == 0) return 0;
deque<int> nums;
deque<char> ops;
now = getnum(ptr, s, len);
nums.push_back(now);
while (ptr < len) {
opt = getopt(ptr, s, len);
if (ptr >= len) break;
now = getnum(ptr, s, len);
res = now;
if (ops.empty()) ops.push_back(opt);
else {
if (pri(ops.back()) < pri(opt)) {
res = calc(nums.back(), res, opt);
nums.pop_back();
} else ops.push_back(opt);
}
nums.push_back(res);
}
res = nums.front(); nums.pop_front();
while (!nums.empty()) {
opt = ops.front(); ops.pop_front();
now = nums.front(); nums.pop_front();
res = calc(res, now, opt);
}
return res;
}
};
229. 求众数
第一眼看到这个题就想起来那个求出现次数大于n/2的数字的题。那个题是让不同的数字相互抵消,最后剩下的那个就是大于n/2的数字。
然后就考虑把那个做法套到这个题里面。因为出现次数大于n/3的数字最多有两个,所以就维护两个数。
还是考虑相互抵消的思路。在求大于n/2的题目里,一个数字最多会被抵消n/2次。又保证众数存在,所以出现次数大于n/2的那个数最后一定能留下。
在这道题里也要保证一个数字最多会被抵消的次数不能超过n/3。否则众数就有可能被抵消掉。
但是也要保证最坏情况下(有数字恰好出现了n/3次,例如原题的第三个样例[1,1,1,2,2,3,3,3])抵消的次数一定要达到n/3,否则就消不掉“坏数”。
那么思路就是每次遇到不同的数字时,让维护的那两个数字同时被抵消一次。
这样,每次抵消都会少三个数(新来的和维护的两个),总共只有n个数,所以抵消次数不会超过n/3。
对于任意一个出现次数不大于n/3的数字,一定有超过2*(n/3)个数字和它不相等。即使每次抵消要消耗两个数字,也足够把它全部消掉。
class Solution {
public:
vector<int> majorityElement(vector<int>& nums) {
int ext[2], cnt[2];
int n = nums.size();
cnt[0] = cnt[1] = 0;
for (int i = 0; i < n; i ++) {
bool addin = false;
for (int j = 0; j < 2; j ++)
if (cnt[j] > 0 && ext[j] == nums[i]) {
++cnt[j]; addin = true; break;
}
if (addin) continue;
for (int j = 0; j < 2; j ++)
if (cnt[j] == 0) {
cnt[j] = 1; ext[j] = nums[i];
addin = true; break;
}
if (addin) continue;
cnt[0] --; cnt[1] --;
}
vector<int> ans; ans.clear();
for (int i = 0; i < 2; i ++)
if (cnt[i] > 0) {
int check = 0;
for (int j = 0; j < n; j ++)
if (nums[j] == ext[i]) ++check;
if (check > n/3) ans.push_back(ext[i]);
}
return ans;
}
};
230. 二叉搜索树中第K小的元素
就普通的平衡树Find操作,算个size然后从顶至底查就行了。时间复杂度(O(n))(因为要dfs一遍)
按理来说其实也可以不dfs做,就用普通的平衡树FindNext那种操作每次找下一个节点。
具体我记得是如果当前节点有右子树就找右子树最靠左的儿子,如果没有右子树就往上找父亲,找到最近的一个从左儿子上去的父亲就是。
这样做的话时间复杂度应该是(O(klogn))的。因为每次查找的最坏复杂度是(O(logn))。
k比较小的时候这样一个个找会比较好吧。。。k一大就退化成(O(nlogn)),还不如dfs一遍。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
unordered_map<TreeNode*, int> size;
void dfs(TreeNode *root) {
size[root] = 1;
if (root->left != NULL) {
dfs(root->left);
size[root] += size[root->left];
}
if (root->right != NULL) {
dfs(root->right);
size[root] += size[root->right];
}
}
TreeNode* Find(TreeNode* root, int k) {
int val;
if (root->left != NULL)
val = size[root->left];
else val = 0;
if (k == val + 1)
return root;
if (k <= val) return Find(root->left, k);
else return Find(root->right, k - val - 1);
}
public:
int kthSmallest(TreeNode* root, int k) {
size.clear();
if (root == NULL) return 0;
dfs(root);
return Find(root, k)->val;
}
};
232. 用栈实现队列
随便翻的时候翻到这个题,想起自己两年前计概考试的时候这题就没做出来,吓出一身冷汗,赶紧做一做。
进了A栈一堆元素以后,要“出队”出的是那个栈底的元素,那就必须要把栈底的元素翻到上面来,那就必定要一个一个往外弹,弹到另一个栈(B栈)里存起来。
一开始脑子有点叉劈,非得想维护栈里元素正确的顺序,就觉得应该把B栈里的元素再倒回A栈里。实际上没必要,因为B栈里就是按照正确的“出队”顺序存的,出的时候可以直接从B里面出。
而入栈的时候还是往A栈里面入,显然,B没空的时候不能从A栈往B栈倒元素,否则就破坏了B的正确顺序。但是当B出空了,就可以把A的元素倒过去。
这样每个元素一定会入A栈一次,入B栈一次,然后就被弹出去。均摊复杂度是O(n)的。
class MyQueue {
public:
/** Initialize your data structure here. */
stack<int> ins, outs;
MyQueue() {
while (!ins.empty()) ins.pop();
while (!outs.empty()) outs.pop();
}
/** Push element x to the back of queue. */
void push(int x) {
ins.push(x);
}
void trans() {
while (!ins.empty()) {
int tmp = ins.top();
ins.pop();
outs.push(tmp);
}
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
if (outs.empty()) trans();
int res = outs.top();
outs.pop();
return res;
}
/** Get the front element. */
int peek() {
if (outs.empty()) trans();
return outs.top();
}
/** Returns whether the queue is empty. */
bool empty() {
return ins.empty() && outs.empty();
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
238. 除自身以外数组的乘积
显然就是搞一个前缀和,一个后缀和,每次乘起来就行了。
不让开额外空间也比较简单,因为它说输出数组不算额外空间,所以就先把对应位置的前缀和存在输出数组里,然后从后往前遍历,一边维护后缀和一边往输出数组里面乘就可以了。
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
vector<int> ans; ans.clear();
int n = nums.size(), tmp;
if (n == 0) return ans;
tmp = 1; ans.push_back(1);
for (int i = 0; i < n-1; i ++) {
tmp = tmp * nums[i];
ans.push_back(tmp);
}
tmp = 1;
for (int i = n-1; i >= 1; i --) {
tmp = tmp * nums[i];
ans[i-1] = ans[i-1] * tmp;
}
return ans;
}
};
239. 滑动窗口最大值
惊了,真就年纪大了连个单调队列都写不对。。。
忘了单调队列要用双向的,整个单向的队列在那搞来搞去。。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> q;
int n = nums.size();
vector<int> ans;
for (int i = 0; i < n; i ++) {
while (!q.empty() && q.front() <= i-k) q.pop_front();
while (!q.empty() && nums[i] > nums[q.back()]) q.pop_back();
q.push_back(i);
if (i >= k-1) ans.push_back(nums[q.front()]);
}
return ans;
}
};