5. 最长回文子串
string f(string &s, int l, int r, int len) {
while(l >= 0 && r < len) {
if(s[l] == s[r]) {
--l;
++r;
} else break;
}
return s.substr(l + 1, r -l - 1);//这里下标要注意,不能包含l和r
}
string longestPalindrome(string s) {
int l = s.size();
if(l < 2) return s;//特判
int maxl = 1;
string ans = s.substr(0, 1);
for(int i = 0; i < l - 1; ++i) {//这里遍历范围的设置参考上面的图
//兼顾两种情况,就不需要再分情况讨论了
string s1 = f(s, i, i, l), s2 = f(s, i , i + 1, l);
string s3 = s1.size() > s2.size()?s1:s2;
if(s3.size() > maxl) {
maxl = s3.size();
ans = s3;
}
}
return ans;
}
387. 字符串中的第一个唯一字符
题目链接
1.注意事项里面提示了只限小写字母,所以就可以考虑用常数量级规模的数组去装统计结果。
5325. 包含所有三种字符的子字符串数目
int numberOfSubstrings(string s) {
int r = -1, ans = 0, c[3] = {0, 0, 0}, n = s.size();
/*这里非常需要学习的一点是,双指针写法中,左/右指针要写成外层循环的形式,
也就是不要把两个指针的移动都放在同一个while里,容易搞乱*/
for(int l = 0; l < n; ++l) {
while((!c[0] || !c[1]|| !c[2]) && r < n - 1) {//这里因为是先加再用(与之对应的是右指针从-1开始),所以要小1防止越界
r++;
c[s[r] - 'a']++;
}
if(c[0] && c[1] && c[2]) {
ans += n - r;
}
c[s[l] - 'a']--;
}
return ans;
}
501. 二叉搜索树中的众数
/*这里的pre是引用,这意味着函数内对pre的赋值操作会改变pre指向的节点*/
void inOrder(TreeNode* root, TreeNode*& pre, int& curTimes,
int& maxTimes, vector<int>& res){
if (!root) return;
/*这个地方一定要传pre,这样才是真正的前序遍历的前序节点,如果传root的话,就会在无形当中发生节点下移*/
inOrder(root->left, pre, curTimes, maxTimes, res);
if (pre)
curTimes = (root->val == pre->val) ? curTimes + 1 : 1;
/*这里无论pre是否为NULL都进行处理,是为了包含对中序遍历第一个节点的处理*/
if (curTimes == maxTimes)
res.push_back(root->val);
else if (curTimes > maxTimes){
res.clear();
res.push_back(root->val);
maxTimes = curTimes;
}
/*在对节点处理完之后才让pre发生移动*/
pre = root;
inOrder(root->right, pre, curTimes, maxTimes, res);
}
vector<int> findMode(TreeNode* root) {
vector<int> res;
if (!root) return res;
TreeNode* pre = NULL;
/*这里也是为了处理中序第一个节点,与pre的初值NULL对应*/
int curTimes = 1, maxTimes = 0;
inOrder(root, pre, curTimes, maxTimes, res);
return res;
}
2. 两数相加
题目链接
1.单链表的头不能丢,所以要新创建一个指针来进行移动,保留原来的首指针以找到这个链表。
2.移动用的指针不一定需要移动到末尾,有时只需要让next取得最后一个节点就可以了。
7. 整数反转
题目链接
1.int和int相乘还是int,这时候如果有可能超过int范围,就要考虑将其中一个变量的类型设置为long long,这样会自动进行类型转换。
2.在有可能越界之前判断临界条件,就不需要考虑数据类型不足以存储临时结果的问题了。
3.利用乘法/除法(或者对应的移位)运算来进行数位整体前/后移。
237. 删除链表中的节点
题目链接
1.不要思维定势了,有时“删除”未必意味着真的删除,而是复制一份已知的再删掉已知的。这题通过输入没给头结点也可以猜到,并非常规解法,因为常规来说直接通过当前结点并不可能找到它的前驱并且改变它的next指针,这时就需要在这一前提下进行思考与解决。
下面这道题与该题看似相同,实则不同:这题是真正删除某一节点,其中用到了快慢指针的思想。
19. 删除链表的倒数第N个节点
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *t = head;
for(int i = 0; i < n; ++i) {
t = t->next;
}
//这里特判也是由下面的t->next启迪得到,除非是删除头结点,否则头结点本身不需要变化
if(!t) return head->next;
ListNode *t1 = head;
while(t->next) {
t1 = t1->next;
t = t->next;
}
//这里是由于C++必须显式地进行内存管理,所以要手动释放节点占据的内存空间
ListNode *tmp = t1->next;
t1->next = t1->next->next;
delete tmp;
return head;
}
59. 螺旋矩阵 II
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> v(n, vector<int>(n));//这里的元素要是用了new,那就是指针了,在这里就错了,这种不带new的就是直接通过构造函数构造对象
int i = 0, j = 0, a[4][2] = {{0, 1}, {1, 0}, {0 , -1}, {-1, 0}}, x = 0;//有多种方向的一定要定义这种单位向量,以及记录当前方向与位置的变量
for(int t = 1; t <= n * n; ++t) {
v[i][j] = t;//若刚开始的是符合要求的,那么方向、位置变化就在后面;若刚开始的就未必符合要求,那么方向、位置变化可以放前面。有点类似于先处理后变化还是先变化后处理的感觉。
if(i == 0 && j == n - 1 || i == n - 1 && j == n - 1 || j == 0 && i == n - 1) x = (x + 1) % 4;
else if(v[i + a[x][0]][j + a[x][1]]) x = (x + 1) % 4;
//位置变化一般都放最后
i += a[x][0];
j += a[x][1];
}
return v;
}
202. 快乐数
int cal(int n) {
int ans = 0;
while(n) {
ans += (n % 10) * (n % 10);
n /= 10;
}
return ans;
}
/*只要涉及到循环或者环形的问题,就可以考虑用快慢指针的思想*/
bool isHappy(int n) {
int s = n, f = n;
do {
s = cal(s);
f = cal(f);
f = cal(f);
} while(s != f);
return s == 1;
}
101. 对称二叉树
bool isSymmetric(TreeNode* r) {
if(!r) return true;//一定要记得特判
return f(r->left, r->right);
}
//类似于BFS的思想
bool f(TreeNode *r1, TreeNode *r2) {
queue<TreeNode*> q1, q2;
//BFS要先入队一个,然后再走循环
q1.push(r1);
q2.push(r2);
while(!q1.empty()) {
TreeNode *t1 = q1.front();
TreeNode *t2 = q2.front();
q1.pop();
q2.pop();
if(t1 && !t2) return false;
if(!t1 && t2) return false;
if(!t1 && !t2) continue;//这里不是true,因为其他的节点不一定对称
if(t1->val != t2->val) return false;//在使用指针的对象时都要当心空指针
q1.push(t1->left);
q1.push(t1->right);
q2.push(t2->right);
q2.push(t2->left);
}
return true;
}
15. 三数之和
题目链接
1.但凡是双指针、二分,细节都非常重要,何时更新值,何处可以进行剪枝,都需要注意。
vector<vector<int>> threeSum(vector<int>& n) {
vector<vector<int>> ans;
int len = n.size();
//一定不符合条件的边界条件,要么是小,要么是大,这题是把小的过滤掉
if(len < 3) return ans;
//典型双指针,基本上都要求有序,这才有高位低位指针
sort(n.begin(), n.end());
for(int i = 0; i < len; ++i) {
//利用了数组升序的特点进行剪枝
if(n[i] > 0) break;
//这里用continue滑动寻找除第一次外的“固定值”,以达到不重复的目的,注意是i和i - 1
if(i > 0 && n[i] == n[i - 1]) continue;
int l = i + 1, r = len - 1;
while(l < r) {
int sum = n[i] + n[l] + n[r];
if(sum == 0) {
vector<int> tmp(3);
tmp[0] = n[i];
tmp[1] = n[l];
tmp[2] = n[r];
ans.push_back(tmp);
//注意是下标,都是先确保不会重复才进行值的更新
while(l < r && n[r - 1] == n[r]) --r;
while(l < r && n[l + 1] == n[l]) ++l;
--r;
++l;
} else if(sum < 0) {
++l;
} else {
--r;
}
}
}
return ans;
}
33. 搜索旋转排序数组
int search(vector<int>& nums, int target) {
int len = nums.size();
int l = 0, r = len- 1;
while(l <= r) {//1.这里等号
int mid = (l + r) >> 1;//2.这里直接右移
// cout << mid << endl;
if(nums[mid] == target)
return mid;
//两个边界变化时都不是等于mid
if(nums[mid] >= nums[l]) {//3.这里加等号只因为2中是向下取整
if(nums[mid] > target && target >= nums[l] ) r = mid - 1;//4.边界为闭区间
else l = mid + 1;
} else {
if(nums[mid] < target && target <= nums[r]) l = mid + 1;//5.边界为闭区间
else r = mid - 1;
}
}
return -1;
}
此题扩展81. 搜索旋转排序数组 II,不同点在于元素允许重复,但是时间复杂度就会上升至O(n)(因为需要移动左右指针以判断中间元素处于哪半部分)。
204. 计数质数
/*埃氏筛,应用场景是对一定范围内的本质就是剔除掉质数的整数倍,有两处小的优化*/
int countPrimes(int n) {
if(n <= 1) return 0;//第一步,永远是特判
//其实这里也提示了第一步需要特判,但凡这种涉及到数组容量是变量的,都需要保证容量是正整数
bool *p = new bool[n];
for(int i = 1; i < n; ++i) p[i] = 1;
p[1] = 0;
//第一处小优化,外层循环的作用是将已经确定是质数的数的倍数设置为不为质数,以n=100,i=11为例,11*2、11*3...11*9都会在前面的循环中被筛掉,所以不需要再扫描11以上的质数了
for(int i = 2; i * i < n; ++i) {
if(p[i]) {
//第二处小优化,和第一处优化类似,都是更小的在前面已经被筛掉了
for(int j = i * i; j < n; j += i) {
p[j] = 0;
}
}
}
int c = 0;
for(int i = 1; i < n; ++i) if(p[i]) ++c;
return c;
}
238. 除自身以外数组的乘积
题目链接
1.首先,看到不要用除法,那就肯定得用乘法,通过观察每位的结果可以发现,每位的结果可以拆成左边和右边分别的乘积之积。
2.要求在O(n)时间复杂度下完成本题,则考虑用扫描累乘的方法。
3.结合1和2,可以发现,从左扫的可以做成累乘的形式,从右扫的也可以,所以就有了下面的解决方案。
vector<int> productExceptSelf(vector<int>& v) {
vector<int> ans(v.size());
int k = 1;//巧妙地用1避开了分类讨论,这是一种很好的思路,和累乘法中的下标0有点类似,用冗余的一个边界来避免分类讨论
for(int i = 0; i < v.size(); ++i) {
ans[i] = k;
k *= v[i];
}
k = 1;
for(int i = v.size() - 1; i >= 0; --i) {
ans[i] *= k;
k *= v[i];
}
return ans;
}
874. 模拟行走机器人
int robotSim(vector<int>& c, vector<vector<int>>& o) {
int x = 0, y = 0, t = 0, ans = 0;//所有中间结果都用int,是因为返回值类型为int,所以中间运算结果都不会超出int范围
int d[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
unordered_set<int> s;
//此题重点,用unordered_set查询,有两点需要注意:第一,pair不能直接哈希,所以需要编码为数;第二,移位时必须保证是非负数,结合题目所限定的范围,需要+30000的偏移让坐标范围变为非负数
for(auto i:o) {
int a = ((i[0] + 30000) << 16) + (i[1] + 30000);
s.insert(a);
}
for(auto i:c) {
if(i == -2) t = (t + 3) % 4;
else if(i == -1) t = (t + 1) % 4;
else {
for(int j = 1; j <= i; ++j) {//对机器人经过的路径上的点一个一个进行遍历,unordered_set就相当于哈希,速度基本可以视为是O(1),非常快,如果每次都直接从头到尾遍历o数组,会超时
int dx = x + d[t][0];
int dy = y + d[t][1];
if(s.find(((dx + 30000) << 16) + dy + 30000) == s.end()) {
x = dx;
y = dy;
ans = max(ans, x * x + y * y);
} else break;//这里这个跳出不会在本质上影响时间复杂度
}
}
}
return ans;
}
1348. 推文计数
题目链接
示例输入:
输入:
["TweetCounts","recordTweet","recordTweet","recordTweet","getTweetCountsPerFrequency","getTweetCountsPerFrequency","recordTweet","getTweetCountsPerFrequency"]
[[],["tweet3",0],["tweet3",60],["tweet3",10],["minute","tweet3",0,59],["minute","tweet3",0,60],["tweet3",120],["hour","tweet3",0,210]]
输出:
[null,null,null,null,[2],[2,1],null,[4]]
解释:
TweetCounts tweetCounts = new TweetCounts();
tweetCounts.recordTweet("tweet3", 0);
tweetCounts.recordTweet("tweet3", 60);
tweetCounts.recordTweet("tweet3", 10); // "tweet3" 发布推文的时间分别是 0, 10 和 60 。
tweetCounts.getTweetCountsPerFrequency("minute", "tweet3", 0, 59); // 返回 [2]。统计频率是每分钟(60 秒),因此只有一个有效时间间隔 [0,60> - > 2 条推文。
tweetCounts.getTweetCountsPerFrequency("minute", "tweet3", 0, 60); // 返回 [2,1]。统计频率是每分钟(60 秒),因此有两个有效时间间隔 1) [0,60> - > 2 条推文,和 2) [60,61> - > 1 条推文。
tweetCounts.recordTweet("tweet3", 120); // "tweet3" 发布推文的时间分别是 0, 10, 60 和 120 。
tweetCounts.getTweetCountsPerFrequency("hour", "tweet3", 0, 210); // 返回 [4]。统计频率是每小时(3600 秒),因此只有一个有效时间间隔 [0,211> - > 4 条推文。
class TweetCounts {
private:
unordered_map<string, map<int, int>> m;//无序map
public:
TweetCounts() {
}
void recordTweet(string tn, int time) {
m[tn][time]++;
}
vector<int> getTweetCountsPerFrequency(string freq, string tn, int st, int et) {
vector<int> v;
int f = 1;
if(freq == "minute") {
f = 60;
} else if(freq == "hour") {
f = 60 * 60;
} else {
f = 60 * 60 * 24;
}
int t = st;//一个变量记录左边界即可
while(t <= et) {
//C++11 auto关键字自动推导数据类型,省事
auto a = m[tn].lower_bound(t);//这里注意,lowe_bound是大于等于,正好符合左边闭区间的要求
auto b = m[tn].upper_bound(min(t + f - 1, et));//这里和上面同理,符合右边开区间的要求,另外注意不要超了总的结束时间
int c = 0;
for(auto i = a; i != b; ++i) c += i->second;
t += f;
v.push_back(c);
}
return v;
}
};