和你一起轻松愉快的刷题
一、前言
- 为了方便阅读,完整笔记分为两篇文章,第(1)篇题目为1-38题,第(2)篇题目为39-75题。
- 所有题目均来自 LeetCode《剑指 Offer(第 2 版)》。
- 截止到编写文章时,所有题解代码均可通过LeetCode在线评测,即AC。
- 笔记中一些题目给出了多种题解和思路,笔记中大多数题解都是较为完美的解法,时间复杂度和空间复杂度都较低的解法。
- 由于作者水平有限,欢迎大家指教,共同交流学习。
- 最后祝大家刷题愉快。
二、开始刷题
最小k个数
大根堆,优先队列(priority_queue)
首先将前 k 个数插入大根堆中,随后从第 k+1 个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后大根堆里的 k 个数就是要求的最小的 k 个数。
方法二:快排的思想
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
if(k <= 0) return vector<int>();
vector<int> ans(k, 0);
priority_queue<int> pq;
for(int i=0; i<k; ++i){
pq.push(arr[i]);
}
for(int i=k; i<arr.size(); ++i){
if(pq.top() > arr[i]){
pq.pop();
pq.push(arr[i]);
}
}
for(int i=0; i<k; ++i){
ans[i] = pq.top();
pq.pop();
}
return ans;
}
};
大堆小堆找中位数
使用一个最大堆一个最小堆来管理海量数据。
将数据分为[小][大]两部分,[左边最大堆][右边最小堆]。
维护两个平衡优先队列,保证左边数量>=右边数量 且 左边数量-右边数量<=1。
当累计添加的数的数量为奇数时,向左边插入,此时中位数为左边的队头。当累计添加的数的数量为偶数时,向右边插入,两个优先队列中的数的数量相同,此时中位数为它们的队头的平均值。
class MedianFinder {
public:
/** initialize your data structure here. */
MedianFinder() {
count = 0;
}
void addNum(int num) {
++count;
if(count%2 == 1){ //奇数时
right_small.push(num);
left_big.push(right_small.top());
right_small.pop();
}else{ //偶数时
left_big.push(num);
right_small.push(left_big.top());
left_big.pop();
}
}
double findMedian() {
if(count%2 == 1){
return (left_big.top()) / 1.0;
}
return (left_big.top() + right_small.top()) / 2.0;
}
private:
int count;
priority_queue<int, vector<int>, less<int>> left_big;
priority_queue<int, vector<int>, greater<int>> right_small;
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
动态规划
常规DP做法,可以直接在原数组上进行修改,节约一点空间。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
int maxSum = dp[0];
for(int i=1; i<nums.size(); ++i){
dp[i] = max(nums[i], nums[i] + dp[i-1]);
maxSum = max(maxSum, dp[i]);
}
return maxSum;
}
};
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int maxSum = nums[0];
for(int i=1; i<nums.size(); ++i){
nums[i] = max(nums[i], nums[i] + nums[i-1]);
maxSum = max(maxSum, nums[i]);
}
return maxSum;
}
};
思维 + 找规律
分两种情况,例如:1234和2234,high为最高位,pow为最高位权重,在每种情况下都将数分段处理,即0-999,1000-1999,...,剩余部分
case1:最高位是1,则最高位的1的次数为last+1(1000-1234),每阶段即0-999的1的个数1*countDigitOne(pow-1) ,除最高位剩余部分1的个数为countDigitOne(last)。
case2:最高位不是1,则最高位的1的次数为pow(1000-1999) 每阶段除去最高位即0-999,1000-1999中1的次数为high*countDigitOne(pow-1) 剩余部分1的个数为countDigitOne(last) 发现两种情况差别仅在最高位的1的个数,因此单独计算最高位的1的次数,合并处理两种情况。
class Solution {
public:
int countDigitOne(int n) {
if(n <= 0) return 0;
if(n < 10) return 1;
int high = n, pow = 1;
while(high >= 10){
high /= 10;
pow *= 10;
}
int last = n - high*pow;
int cnt = high==1? last+1: pow;
return cnt + countDigitOne(last) + high*countDigitOne(pow-1);
}
};
数学规律 + 迭代 + 求整 / 求余
class Solution {
public:
int findNthDigit(int n) {
int digit = 1;
long long start = 1;
long long count = 9;
// 第1步
while(n > count){
n -= count;
++digit;
start *= 10;
count = 9 * start * digit;
}
// 第2步
int num = start + (n-1)/digit;
// 第3步
string s = to_string(num);
return s[(n-1) % digit]-'0';
}
};
动态规划
经典青蛙跳台阶问题 f(n) = f(n-1) + f(n-2) 的变形。
数字转字母,只能从两个途径产生,一个是自己单独成字母,一个是和前一个组合成两位数成字母。
与青蛙跳台阶问题不同的是要注意边界条件:dp[1]既可能为1也可能为0,大于25就不是字母了,01, 02, ... 09 这种0开头的也不能转字母。
class Solution {
public:
int translateNum(int num) {
string str = to_string(num);
int n = str.size();
vector<int> dp(n + 1, 0);
dp[0] = 1;
for(int i=1; i<n; ++i){
if(i == 1){ //特判dp[1] = ?
if((str[0] == '1') || (str[0] == '2' && str[1] < '6')){
dp[1] = dp[0] + 1;
}else{
dp[1] = dp[0];
}
continue;
}
string s = str.substr(i-1, 2);
if(stoi(s) >= 10 && stoi(s) <= 25){
dp[i] = dp[i-1] + dp[i-2];
}else{
dp[i] = dp[i-1];
}
}
return dp[n-1];
}
};
动态规划
dp[i][j] = dp[i-1][j] + dp[i][j-1];
可以使用两个长度为 n 的一位数组代替 m×n 的二维数组,交替地进行状态转移,减少空间复杂度。
class Solution {
public:
int maxValue(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n));
dp[0][0] = grid[0][0];
// 处理第一行和第一列
for(int i=1; i<m; ++i) dp[i][0] = dp[i - 1][0] + grid[i][0];
for(int j=1; j<n; ++j) dp[0][j] = dp[0][j - 1] + grid[0][j];
// dp
for(int i=1; i<m; ++i){
for(int j=1; j<n; ++j){
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
};
动态规划 + 哈希表
查找上一次元素出现的位置可以用哈希表或线性遍历
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.size();
if(n == 0) return 0;
vector<int> dp(n, 0);
dp[0] = 1;
unordered_map<char, int> hash;
int maxLen = 1;
hash[s[0]] = 0;
for(int i=1; i<n; ++i){
if(hash.find(s[i]) == hash.end()){ //第一次出现,可以直接连接上dp[i-1]
dp[i] = dp[i-1] + 1;
}else{
if(hash[s[i]] < i-dp[i-1]){ //上一次出现不在dp[i-1]内,可以连接
dp[i] = dp[i-1] + 1;
}else{ //上一次出现在dp[i-1]内,长度 = 当前位置-上一次出现的位置
dp[i] = i - hash[s[i]];
}
}
hash[s[i]] = i; //加入到哈希表
maxLen = max(maxLen, dp[i]); //更新最大长度
}
return maxLen;
}
};
最小堆
要得到从小到大的第 n 个丑数,可以想到使用最小堆实现。
初始时堆为空。首先将最小的丑数 1 加入堆。每次取出堆顶元素 x,则 x 是堆中最小的丑数,由于 2x,3x,5x 也是丑数,因此将 2x,3x,5x 加入堆。
上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合去重,避免相同元素多次加入堆。
在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。
时间复杂度:O(nlogn)
空间复杂度:O(n)
动态规划 / 三指针
首先一定要知道,后面的丑数一定由前面的丑数乘以2,或者乘以3,或者乘以5得来。
下一次寻找丑数时,则对这三个位置分别尝试使用一次乘2机会,乘3机会,乘5机会,看看哪个最小,最小的那个就是下一个丑数。最后,那个得到下一个丑数的指针位置加一,因为它对应的那次乘法使用完了。
这里需要注意下去重的问题,如果某次寻找丑数,找到了下一个丑数10,则p2和p5都需要加一,因为5乘2等于10, 5乘2也等于10,这样可以确保10只被数一次。
时间复杂度:O(n)
空间复杂度:O(n)
class Solution {
public:
int nthUglyNumber(int n) {
vector<int> factors{2, 3, 5};
unordered_set<long> uset;
priority_queue<long, vector<long>, greater<long>> heap;
heap.push(1);
uset.insert(1);
int ugly = 0;
for(int i=0; i<n; ++i){
long cur = heap.top();
heap.pop();
ugly = cur;
for(int factor: factors){
long next = cur * factor;
if(!uset.count(next)){
uset.insert(next);
heap.push(next);
}
}
}
return ugly;
}
};
class Solution {
public:
int nthUglyNumber(int n) {
if(n > 0 && n < 7) return n;
vector<int> res(n, 0);
res[0] = 1;
int p2 = 0, p3 = 0, p5 = 0;
for(int i=1; i<n; ++i){
int curUgly = min(min(res[p2]*2, res[p3]*3), res[p5]*5);
if(curUgly == res[p2]*2) ++p2;
if(curUgly == res[p3]*3) ++p3;
if(curUgly == res[p5]*5) ++p5;
res[i] = curUgly;
}
return res[n-1];
}
};
哈希表
两次哈希操作,第一次从前到后记录字符出现的次数,第二次找最先出现一次的字符。
class Solution {
public:
char firstUniqChar(string s) {
unordered_map<char, int> hash;
for(int i=0; i<s.size(); ++i){
++hash[s[i]];
}
for(int i=0; i<s.size(); ++i){
if(hash[s[i]] == 1)
return s[i];
}
return ' ';
}
};
分治思想 / 归并排序
「归并排序」与「逆序对」是息息相关的。归并排序体现了 “分而治之” 的算法思想,具体为:
分: 不断将数组从中点位置划分开(即二分法),将整个数组的排序问题转化为子数组的排序问题;
治: 划分到子数组长度为 1 时,开始向上合并两个排序数组,不断将 较短排序数组 合并为 较长排序数组,直至合并至原数组时完成排序;每当遇到 左子数组当前元素 > 右子数组当前元素 时,意味着 「左子数组当前元素 至 末尾元素」 与 「右子数组当前元素」 构成了若干 「逆序对」 。
class Solution {
public:
int reversePairs(vector<int>& nums) {
int n = nums.size();
vector<int> tmp(n);
return mergeSort(nums, tmp, 0, n-1);
}
int mergeSort(vector<int>& nums, vector<int>&tmp, int l, int r){
// 终止条件
if(l >= r) return 0;
// 递归 分
int mid = l + (r-l)/2;
int res = mergeSort(nums, tmp, l, mid) + mergeSort(nums, tmp, mid+1, r);
// 合并 治
int i = l, j = mid+1;
for(int k=l; k<=r; ++k){
tmp[k] = nums[k];
}
for(int k=l; k<=r; ++k){
if(i == mid+1){ //左子数组已合并完
nums[k] = tmp[j++];
}else if(j == r+1 || tmp[i] <= tmp[j]){ //右子数组已合并完 或 左子元素<右子元素
nums[k] = tmp[i++];
}else{ // 右子元素<左子元素,产生了逆序对
nums[k] = tmp[j++];
res += mid-i+1;
}
}
return res;
}
};
相交链表
假设链表 A 的头节点到相交点的距离是 a ,链表 B 的头节点到相交点的距离是 b ,相交点到链表终点的距离为 c 。我们使用两个指针,分别指向两个链表的头节点,并以相同的速度前进,若到达链表结尾,则移动到另一条链表的头节点继续前进。按照这种前进方法,两个指针会在 a + b + c 次前进后同时到达相交节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* L1 = headA, *L2 = headB;
while(L1 != L2){
L1 = L1? L1->next: headB;
L2 = L2? L2->next: headA;
}
return L1;
}
};
STL中equal_range() 函数
函数equal_range()返回first和last之间等于val的元素区间,返回值是一对迭代器。
此函数假定first和last区间内的元素可以使用<操作符或者指定的comp执行比较操作。
equal_range()可以被认为是lower_bound()和upper_bound()的结合,pair中的第一个迭代器由lower_bound返回,第二个则由upper_bound返回。实现lower_bound()和upper_bound()函数
二分法
class Solution {
public:
int search(vector<int>& nums, int target) {
auto pos = equal_range(nums.begin(), nums.end(), target);
return pos.second - pos.first;
}
};
哈希集合
unordered_set
数学公式
位运算
a ^ b ^ a = b
class Solution {
public:
int missingNumber(vector<int>& nums) {
unordered_set<int> uset;
int n = nums.size() + 1;
for(int num: nums){
uset.insert(num);
}
for(int i=0; i<n; ++i){
if(!uset.count(i)){
return i;
}
}
return -1;
}
};
class Solution {
public:
int missingNumber(vector<int>& nums) {
unordered_set<int> uset;
int n = nums.size() + 1;
int total = n*(n-1) / 2;
int sum = 0;
for(int num: nums){
sum += num;
}
return total - sum;
}
};
class Solution {
public:
int missingNumber(vector<int>& nums) {
int n = nums.size() + 1;
int ans = 0;
for(int i=0; i<n; ++i){
ans ^= i;
}
for(int num: nums){
ans ^= num;
}
return ans;
}
};
中序遍历
二叉搜索树的中序遍历为递增序列,所以二叉搜索树的中序遍历倒序为递减序列。
求 “二叉搜索树第 k 大的节点” 可转化为求 “此树的中序遍历倒序的第 k 个节点”。
中序遍历的倒序为:“右、根、左”
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int kthLargest(TreeNode* root, int k) {
if(root == nullptr) return 0;
stack<TreeNode*> st;
while(!st.empty() || root != nullptr){
while(root){
st.push(root);
root = root->right;
}
root = st.top();
st.pop();
if(--k == 0) return root->val;
root = root->left;
}
return 0;
}
};
二叉树深度
递归 / 深度优先搜索
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root){
return 1 + max(maxDepth(root->left), maxDepth(root->right));
}else{
return 0;
}
}
};
树的深度
解法类似于求树的最大深度,但有两个不同的地方:一是我们需要先处理子树的深度再进行比较,二是如果我们在处理子树时发现其已经不平衡了,则可以返回一个-1,使得所有其长辈节点可以避免多余的判断(本题的判断比较简单,做差后取绝对值即可;但如果此处是一个开销较大的比较过程,则避免重复判断可以节省大量的计算时间)。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isBalanced(TreeNode* root) {
return helper(root) != -1;
}
int helper(TreeNode* root){
if(!root) return 0;
int left = helper(root->left), right = helper(root->right);
if(left == -1 || right == -1 || abs(left-right) == -1){
return -1;
}
return 1 + max(left, right);
}
};
位运算 ^
a ^ b ^ a = b1.全部异或 2.从右向左找非 0 位 3.根据 2 找到的位置 0/1 分两组 4.分别找各组只出现一次的元素。
class Solution {
public:
vector<int> singleNumbers(vector<int>& nums) {
int tmp = 0;
for(int num: nums){
tmp ^= num;
}
// 找到从右向左的首个非0位
int index = 1;
while((tmp & index) == 0){
index <<= 1;
}
// 分组异或
int first = 0, second = 0;
for(int num: nums){
if(num & index){
first ^= num;
}else{
second ^= num;
}
}
return vector<int> {first, second};
}
};
剑指 Offer 56 - II. 数组中数字出现的次数 II
哈希表
大家都会
位运算 + 遍历统计
使用与运算,可获取二进制数字 num 的最右一位 n1,配合右移操作,可获取 num 所有位的值(即 n1 ~ n32),将 counts 各元素对 3 求余,则结果为 “只出现一次的数字” 的各二进制位。
利用左移操作和或运算,可将 counts 数组中各二进位的值恢复到数字 res 上( i∈[0,31] )。
实际上,只需要修改求余数值 m ,即可实现解决 除了一个数字以外,其余数字都出现 m 次 的通用问题。
class Solution {
public:
int singleNumber(vector<int>& nums) {
unordered_map<int, int> hash;
for(int num: nums){
++hash[num];
}
for(int num: nums){
if(hash[num] == 1){
return num;
}
}
return -1;
}
};
class Solution {
public:
int singleNumber(vector<int>& nums) {
vector<int> count(32, 0);
for(int num: nums){
for(int i=0; i<32; ++i){
count[i] += num & 1;
num >>= 1;
}
}
int res = 0, m = 3;
for(int i=0; i<32; ++i){
res <<= 1;
res |= count[31-i] % m;
}
return res;
}
};
双指针 / 滑动窗口
哈希表一次遍历,时间空间复杂度均为 O(n)。
由于是已排序数组,想到用双指针,降低空间复杂度为 O(1)。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int l=0, r=nums.size()-1;
while(l < r){
int sum = nums[l] + nums[r];
if(sum == target){
return vector<int>{nums[l], nums[r]};
}else if(sum > target){
--r;
}else{
++l;
}
}
return vector<int>();
}
};
暴力枚举
枚举每个正整数为起点,判断以它为起点的序列和 sum 是否等于 target 即可,由于题目要求序列长度至少大于 2,所以枚举的上界为 target / 2。
双指针/滑动窗口 + 数学
由 x 累加到 y 的求和公式得:
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
vector<vector<int>> res;
vector<int> tmp;
int sum = 0, limit = target / 2;
for(int i=1; i<=limit; ++i){
for(int j=i; ; ++j){
sum += j;
if(sum > target){
sum = 0;
break;
}else if(sum == target){
tmp.clear();
for(int k=i; k<=j; ++k){
tmp.emplace_back(k);
}
res.emplace_back(tmp);
sum = 0;
break;
}
}
}
return res;
}
};
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
vector<vector<int>> res;
int l = 1, r = 2;
while(l < r){
int sum = (l + r) * (r - l + 1) / 2; //求和
vector<int> tmp;
if(sum == target){
for(int i=l; i<=r; ++i){
tmp.emplace_back(i);
}
res.emplace_back(tmp);
++l; //即使当前满足,依然要前进,这有点tcp滑动窗口的意思吧
}else if(sum < target){
++r;
}else{
++l;
}
}
return std::move(res); //借助C++11的move函数,总体时间会更短
}
};
倒叙拆分
class Solution {
public:
string reverseWords(string s) {
int l = 0, r= s.size()-1;
// 去除首位空格
while(l<=r && s[l]==' ') ++l;
while(l<=r && s[r]==' ') --r;
string res, tmp;
int cnt = 0; //记录单词中间空格个数
for(int i=r; i>=l; --i){
if(s[i] != ' '){
cnt = 0;
tmp = s[i] + tmp;
}else if(s[i] == ' '){
if(++cnt == 1){
res = res + tmp + " ";
tmp = "";
}
}
}
if(tmp.size() != 0)
res += tmp;
return res;
}
};
substr()函数
拆分再拼接
class Solution {
public:
string reverseLeftWords(string s, int n) {
int len = s.size();
// if(n > len) n %= len; //n可能大于len
s += s;
return s.substr(n, len);
}
};
class Solution {
public:
string reverseLeftWords(string s, int n) {
int len = s.size();
string str, ans;
for(int i=0; i<n; ++i){
str += s[i];
}
for(int i=n; i<len; ++i){
ans += s[i];
}
ans += str;
return ans;
}
};
优先队列 / 最大堆
对于「最大值」,一种非常合适的数据结构,优先队列(大根堆)。为了方便判断堆顶元素与滑动窗口的位置关系,可以在优先队列中存储二元组 (num, index)。
时间复杂度:O(nlogn)
空间复杂度:O(n)
双端队列
利用双端队列(单调队列)进行操作:每当向右移动时,把窗口左端的值从队列左端剔除,把队列右边小于窗口右端的值全部剔除。这样双端队列的最左端永远是当前窗口内的最大值。另外,这道题也是单调栈的一种延申:该双端队列利用从左到右递减来维持大小关系。
双端队列的作用是保证每次L边界右移时从队列头弹出的都是当前窗口的最大值,队列中存储元素下标即可。
时间复杂度:O(n)
空间复杂度:O(k)
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
vector<int> ans;
priority_queue<pair<int, int>> heap;
for(int i=0; i<k; ++i){
heap.emplace(nums[i], i);
}
ans.emplace_back(heap.top().first);
for(int i=k; i<n; ++i){
heap.emplace(nums[i], i);
while(heap.top().second <= i-k){
heap.pop();
}
ans.emplace_back(heap.top().first);
}
return ans;
}
};
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
vector<int> ans;
deque<int> dq;
for(int i=0; i<n; ++i){
// 如果首元素超过滑动窗口,弹出
while(!dq.empty() && dq.front() <= i-k){
dq.pop_front();
}
// 构造单调递减队列
while(!dq.empty() && nums[i] > nums[dq.back()]){
dq.pop_back();
}
dq.push_back(i);
// 在滑动窗口内每次都要添加最大值
if(i >= k-1){
ans.push_back(nums[dq.front()]);
}
}
return ans;
}
};
动态规划
给定 n 个骰子,可得:每个骰子摇到 1 至 6 的概率相等,都为 1/6 。将每个骰子的点数看作独立情况,共有 6^n种「点数组合」。
n 个骰子「点数和」的范围为 [n,6n] ,数量为 6n−n+1 = 5n+1 种。二维dp数组,dp[i][j]:i表示几个骰子,j表示数字之和,dp[i][j]表示概率。
递推公式为:n个骰子某个数字之和 sum 的概率为n-1个骰子中(sum - 1~6)数字之和的概率之和除以6。
注意不能超范围,比如两个骰子就不可能有数字之和为1,如果数字之和取2,第二个骰子也取不到3点。
class Solution {
public:
vector<double> dicesProbability(int n) {
vector<vector<double>> dp(n+1, vector<double>(6*n + 1, 0));
// 1个骰子全部数字的概率都是1/6
for(int i=1; i<=6; ++i){
dp[1][i] = 1.0 / 6;
}
for(int i=2; i<=n; ++i){ //骰子个数
for(int j=i; j<=6*i; ++j){ //当前骰子个数,能取到的数字和的概率
for(int k=1; k<=6; ++k){ //第n个骰子取的值
if(j-k >= i-1){ //除去第n个骰子,数字和必须大于骰子个数
dp[i][j] += dp[i-1][j-k] / 6;
}else{
break;
}
}
}
}
vector<double> res(5*n+1, 0);
for(int i=0; i<=5*n; ++i){
res[i] = dp[n][n+i];
}
return res;
}
};
队列(超时)
先把所有元素入队,然后取出对队,判断个数是否达到m,不到加入队尾,到了重新计数。
数学推导(最优解)
class Solution {
public:
int lastRemaining(int n, int m) {
queue<int> q;
for(int i=0; i<n; ++i){
q.push(i);
}
int cnt = 0;
while (q.size() > 1) {
int temp = q.front();
q.pop();
cnt++;
if (cnt == m) {
cnt = 0;
} else {
q.push(temp);
}
}
return q.front();
}
};
class Solution {
public:
int lastRemaining(int n, int m) {
int ans = 0;
// 最后一轮剩下2个人,所以从2开始反推
for(int i=2; i<=n; ++i){
ans = (ans + m) % i;
}
return ans;
}
};
买卖股票
遍历一遍数组,在每一个位置 i 时,记录 i 位置之前所有价格中的最低价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益即可。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int buy = INT_MAX, sell = 0;
for(int price: prices){
buy = min(buy, price);
sell = max(sell, price - buy);
}
return sell;
}
};
求和公式
(n * (n + 1)) / 2
递归
利用逻辑与的短路特性实现递归终止。
class Solution {
public:
int sumNums(int n) {
char arr[n][n+1];
return sizeof(arr) >> 1;
}
};
class Solution {
public:
int sumNums(int n) {
int sum = n;
n > 0 && (sum += sumNums(n - 1));
return sum;
}
};
位运算
&操作获取进位,^操作相当于是不计算进位的求和运算,将&操作的结果<<1,重复操作直到没有进位。
class Solution {
public:
int add(int a, int b) {
while( b != 0){
unsigned int carry = (unsigned int)(a & b) << 1;
a = a ^ b;
b = carry;
}
return a;
}
};
暴力(超时)
O(n^2)
左右乘积
O(n),妙
class Solution {
public:
vector<int> constructArr(vector<int>& a) {
vector<int> ans;
for(int i=0; i<a.size(); ++i){
int tmp = 1;
for(int j=0; j<a.size(); ++j){
if(i != j) tmp *= a[j];
}
ans.push_back(tmp);
}
return ans;
}
};
class Solution {
public:
vector<int> constructArr(vector<int>& a) {
int n = a.size();
vector<int> ans(n, 0);
int tmp = 1;
for(int i=0; i<n; ++i){
ans[i] = tmp;
tmp *= a[i];
}
tmp = 1;
for(int i=n-1; i>=0; --i){
ans[i] *= tmp;
tmp *= a[i];
}
return ans;
}
};
递归
根据二叉搜索树性质递归。
函数体内不需要判空节点,以为根据二叉搜索树的性质去寻找,一定不会走到空节点。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root->val > p->val && root->val > q->val){
return lowestCommonAncestor(root->left, p, q);
}else if(root->val < p->val && root->val < q->val){
return lowestCommonAncestor(root->right, p, q);
}else{
return root;
}
}
};
递归
若 root 是 p,q 的最近公共祖先 ,则只可能为以下情况之一:
(1)p 和 q 在 root 的子树中,且分列 root 的异侧(即分别在左、右子树中);
(2)p = root ,且 q 在 root 的左或右子树中;
(2)q = root ,且 p 在 root 的左或右子树中;
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(!root || root==p || root==q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if(left && right){
return root;
}else if(left){
return left;
}else{
return right;
}
}
};
DFS
经典迷宫问题
class Solution {
public:
int getSum(int row, int col){
int sum = 0;
while(row != 0){
sum += row % 10;
row /= 10;
}
while(col != 0){
sum += col % 10;
col /= 10;
}
return sum;
}
void movingCountCore(int m, int n, int k, vector<vector<bool>> &visit, int row, int col, int &cnt){
if(row<0 || col<0 || row>m-1 || col>n-1 || visit[row][col]==true )
return ;
if(getSum(row, col) > k){
visit[row][col] = false;
return ;
}
visit[row][col] = true;
++cnt;
movingCountCore(m, n, k ,visit, row+1, col, cnt);
movingCountCore(m, n, k ,visit, row-1, col, cnt);
movingCountCore(m, n, k ,visit, row, col+1, cnt);
movingCountCore(m, n, k ,visit, row, col-1, cnt);
}
int movingCount(int m, int n, int k) {
vector<vector<bool>> visit(m, vector<bool>(n, false));
int cnt = 0;
movingCountCore(m, n, k, visit, 0, 0, cnt);
return cnt;
}
};
排序
此题求拼接起来的最小数字,本质上是一个排序问题。设数组 nums 中任意两数字的字符串为 x 和 y ,则规定排序判断规则 为:
若拼接字符串 x+y > y+x ,则 x “大于” y ;反之,若 x+y < y+x ,则 x “小于” y ;
x “小于” y 代表:排序完成后,数组中 x 应在 y 左边;“大于” 则反之。
class Solution {
public:
string minNumber(vector<int>& nums) {
vector<string> strs;
for(int num: nums){
strs.push_back(to_string(num));
}
sort(strs.begin(), strs.end(), [](string& x, string& y){return x+y < y+x;});
string res;
for(string str: strs){
res.append(str);
}
return res;
}
};
维护一个单调的双端队列
插入操作虽然看起来有循环,做一个插入操作时最多可能会有 n 次出队操作。但要注意,由于每个数字只会出队一次,因此对于所有的 n 个数字的插入过程,对应的所有出队操作也不会大于 n 次。因此将出队的时间均摊到每个插入操作上,时间复杂度为 O(1)。
class MaxQueue {
public:
queue<int> q;
deque<int> dq;
MaxQueue() {
}
int max_value() {
if(dq.empty()) return -1;
return dq.front();
}
void push_back(int value) {
while(!dq.empty() && dq.back() < value){
dq.pop_back();
}
dq.push_back(value);
q.push(value);
}
int pop_front() {
if(q.empty()) return -1;
int ans = q.front();
if(ans == dq.front()){
dq.pop_front();
}
q.pop();
return ans;
}
};
/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue* obj = new MaxQueue();
* int param_1 = obj->max_value();
* obj->push_back(value);
* int param_3 = obj->pop_front();
*/
思维
根据题意,此 5 张牌是顺子的 充分条件如下:
除大小王外,所有牌无重复 ;
设此 5 张牌中最大的牌为 max ,最小的牌为 min (大小王除外),则需满足:max − min < 5。
class Solution {
public:
bool isStraight(vector<int>& nums) {
unordered_set<int> u_set;
int nmax = 0, nmin = 14;
for(int num: nums){
if(num == 0) continue;
nmax = max(nmax, num);
nmin = min(nmin, num);
if(u_set.find(num) != u_set.end()) return false;
u_set.insert(num);
}
return nmax-nmin < 5;
}
};
模拟
atoi() / stoi()
class Solution {
public:
int strToInt(string str) {
int n = str.size();
if(n == 0) return 0;
int flag = 1, signal = 0;
int i = 0;
while(i<n && str[i]==' '){ //去掉前面空格
++i;
}
if(i == n) return 0; //不能全为空格
while(i<n && (str[i]=='+' || str[i]=='-')){ //判断数字的+-号,且只能有一个
if(str[i] == '-') flag = -1;
++i;
++signal;
if(signal > 1) return 0;
}
long long res = 0;
for(i; i<n; ++i){
if(str[i]<'0' || str[i]>'9') break;
res = res*10 + str[i] - '0';
if(res > INT_MAX && flag == 1) return INT_MAX; //正数溢出
if(res-1 > INT_MAX && flag == -1) return INT_MIN; //负数溢出
}
return flag * res;
}
};
觉得文章还不错,可以点个小赞赞,支持一下哈!