Leetcode hot100中的题目
大家可以用题号or名字检索 部分题目有C++代码~
哈希
两数之和-1
(1)暴力双循环 n^2复杂度
(2)哈希表 记录nums[i]和i
unordered_map<int, int> mp; // C++
mp[nums[i]]=i; //插入
auto it=mp.find(target-nums[i]);
if(it!=mp.end()); // 找到
return {it->second,i}; // 返回格式是[x,y]
这题注意先查hash表再放入 防止自己和自己匹配
字母异位词分组-49
unordered_map<string,vector<string>> mp; // hash表存排列前的string和原string们
// 遍历hash表:
for(auto it:mp)
{
// 通过it.first it.second拿取元素
}
// 或者:
for(auto it=mp.begin();it!=mp.end();++it)
{
}
最长连续序列-128
用一个unordered_set存这些数 然后遍历set 慢慢+1去找
这里注意剪枝 如果当前元素-1在set中 那么就跳过(从那个-1开始肯定比这个长)
双指针
移动零-283
双指针遍历 一开始left=right=0 while(right < size) 如果nums[right]是0 right右移 不然就交换 left++ right++
盛最多水的容器-11
左右两个指针放最边上 然后往中间移动 移矮的那个(因为移高的不可能有收益)
三数之和-15
先排序 第一个数从头遍历 二、三个数用双指针在后面找 注意第一个数需要去重
vector<vector<int>> ans;
int n=nums.size();
sort(nums.begin(),nums.end());
for(int i=0;i<n;i++)
{
if(i>0&&nums[i]==nums[i-1])continue;
int left=i+1;
int right=n-1;
while(left<right)
{
if(nums[i]+nums[left]+nums[right]==0)
{
ans.push_back({nums[i],nums[left],nums[right]});
while(left<right&&nums[left]==nums[left+1])left++;
while(left<right&&nums[right]==nums[right-1])right--;
left++;
right--;
}
else if(nums[i]+nums[left]+nums[right]<0)
{
left++;
}
else{
right--;
}
}
}
return ans;
接雨水-42
- 从左往右遍历 记录每个柱子左边最高的柱子 从右往左遍历 记录每个柱子右边最高的柱子 然后竖向来看 能存的就是min(左max,右max)-当前高度
这个方法是竖着算的 ans依次加每一列
- 单调栈方法 维护一个单调递减栈(存下标)每一个i,判断当前栈非空且如果当前元素高于栈顶元素(即当前柱子作为右柱子存雨水) 取出栈中元素 栈中元素的上一个栈中元素(左柱子 如果没有就跳出 存不了雨水) 然后ans+=这一段能存的雨水
这个方法是横着算的 一层一层加
int n=height.size();
stack<int> stk;
int ans=0;
for(int i=0;i<n;i++)
{
while(!stk.empty()&&height[i]>height[stk.top()])
{
int top=stk.top();
stk.pop();
if(stk.empty())break;
int left=stk.top();
int width=i-left-1;
int curheight=min(height[left],height[i])-height[top];
ans+=curheight*width;
}
stk.push(i);
}
return ans;
滑动窗口
无重复字符的最长子串-3
套双循环 用一个unordered_set当前有的字符
unordered_set<char> st;
int right=0;
int maxlen=0;
for(int i=0;i<s.size();i++)
{
while(right<s.size()&&st.count(s[right])==0)
{
st.insert(s[right]);
right++;
}
maxlen=max(maxlen,right-i);
st.erase(s[i]);
}
return maxlen;
找到字符串中所有字母异位词-438
用vector<int> sCount(26)
来存当前窗口的情况 在循环中,移出一个,加一个
判断两个vector是否相等可以直接用sCount==pCount
子串
和为k的子数组-560
算前缀和并且加入hash表中 使用unordered_map
存储每个前缀和及其出现的次数 遍历过程中直接去找想要的前缀和 总和数目直接加想要的前缀和的数量 O(n)复杂度一次遍历
滑动窗口最大值-239
一个优先队列存所有数和下标,遍历i同时i往后移动,取当前优先队列最大值,如果下标不在窗口范围内,移出,再取直到在范围内。
priority_queue<pair<int, int>> q; //C++
q.emplace(nums[i], i); //加入元素
q.top().first;
q.top().second; // 取元素
q.pop() //出队列
一个双向队列 里面的下标递增 数字大小递减(因为如果下标增,数字也增,小的下标不可能被用到,所以可以出队列) 遍历i同时i往后移动,取当前队列最大值,如果下标不在窗口范围内,移出,再取直到在范围内。
deque<int> q;
q.pop_back();
q.push_back();
q.pop_front();
最小覆盖子串-76
滑动窗口的思路 用hash表存储需要的字符 先滑右边界 满足后尝试缩小左边界 看能不能继续缩小子串
普通数组
最大子数组和-53
经典题目 遍历过程中维护一个当前和 如果当前和小于0 则将当前和置为0 否则当前和加当前值 O(n)复杂度
这个思想是动态规划的思想 在pre[i]+x和x中间选一个大的
分治算法也可以(找左边最大,右边最大,左边连右边最大)
合并区间-56
按照first元素对intervals排序 注意lambda表达式写法
sort(intervals.begin(),intervals.end(),[](vector<int> &a,vector<int> &b){
return a[0]<b[0];
});
判断待加入的左端点和当前右端点的大小,如果待加入左端点大,那就分开;如果当前右端点大,就合并与更新右端点
轮转数组-189
-
可以理解成先整个数组反转 然后前k%n个反转 和后一半反转
-
直接开额外空间存 然后放到对应位置也可以
除自身以外数组的乘积-238
维护一个前缀积 这样ans[0]是1 ans[n]是前n-1个元素的积
然后从后面维护一个后缀积 ans[n]*=后缀积 返回ans即可
缺失的第一个正数-41
原地hash 遍历一遍 让该去自己位置的元素都去自己的位置(不该去的不用管) 遍历到i这个位置的时候 只要让nums[i]这个元素去它对应的位置就好了(如果新来的元素也能去 那就必须也要去 所以要套while而不是if)
然后从头开始找一遍即可
注意交换条件
while(nums[i]>0&&nums[i]<=n&&nums[nums[i]-1]!=nums[i]) // 新下标在0-n之间 且新下标的数位置不对
// 如果最后一个判断条件是 nums[i]-1!=i 那么会死循环(eg:[1,1]情况 在1下标这个位置 1 1一直换)
矩阵
矩阵置零-73
先记录第一行和第一列是否有0 然后以第一行(列)为标志位记录这一列(行)是否有0
先把标志位所在行(列)置0 然后如果第一行(列)有0 就置0
螺旋矩阵-54
按层模拟
旋转图像-48
遍历i,j的时候只需要记录左上角的1/4就可以,因为四个换位置一圈就保证了所有
for(int i=0;i<(n+1)/2;i++)
{
for(int j=0;j<n/2;j++)
{}
}
搜索二维矩阵II-240
-
对每一行二分查找 O(mlogn)
-
Z型查找 左下角开始 如果比想要的数字大 就排除了一行(x--) 小则排除了一列(y++) O(m+n)
链表
相交链表-160
- 两个链表同时往后移 p1走到末尾就移到p2的头 p2走到末尾就移到p1的头 那么就会相遇(都走了自己的开始未相交部分 相交部分 对方的未相交部分) 如果不想交 那么就是走完自己 走完对方以后 同时是null 所以就可以跳出while
if(headA==NULL||headB==NULL)return NULL;
ListNode *pa=headA;
ListNode *pb=headB;
while(pa!=pb)
{
pa=pa==NULL?headB:pa->next;
pb=pb==NULL?headA:pb->next;
}
return pa;
- hash表存链表的节点
反转链表-206
当前元素是cur 上一个元素是pre while(cur!=nullptr) 记录tmp是下一个 cur->next=pre pre=cur cur=tmp 最后return的是pre 因为最后一步之后 cur已经是最后一个的next了 pre才是最后一个
递归也是一样的思路 返回值是ListNode*类型
回文链表-234
翻转以后 两个链表一起移动 或者翻转一半
环形链表-141
快慢指针 如果快指针到null就直接退出来(不存在环 不然已经回去和慢指针相遇了)
while(fast!=nullptr)
{
fast=fast->next;
if(fast!=nullptr)fast=fast->next;
if(fast==slow)return true;
slow=slow->next;
}
环形链表II-142
- 这题要找到入环的节点 可以先让快慢指针在环中相遇
然后让相遇指针和头指针一起后移 遇到的节点就是入环的节点
证明:可以设环外节点有a个 slow指针进入环后 走了b与fast相遇 此时fast指针走了n圈 总距离为a+n(b+c)+b
a+n(b+c)+b=2(a+b) 得到a=c+(n-1)(b+c)
从相遇点到入环点的距离+(n-1)环长=头到环距离 所以得证
- 直接hash表记录也可以
合并两个有序链表-21
可以创建一个dummy 然后两个链表在while中套
两数相加-2
保留一个进位 创建一个新链表存结果 就从个位(链表头)开始加 每加一位相当于尾插法
删除链表的倒数第N个节点-19
-
计算出长度后 从头遍历 数数来删
-
双指针 使用两个指针 first比second超前n个节点 first到末尾时 second就是倒数第n个
两两交换链表中的节点-24
标记first second third
ListNode *dummy=new ListNode();
dummy->next=head;
ListNode *cur=dummy;
while(cur->next&&cur->next->next){
ListNode *first = cur->next;
ListNode *second = first->next;
ListNode *third = second->next;
cur->next = second;
second->next = first;
first->next = third;
cur = first;
}
return dummy->next;
K个一组翻转链表-25
写一个翻转链表的函数
注意翻转完之后 头节点和尾节点的连接都要处理 比如0 1 2 3 翻转的是1 2 然后0要连2(这个通过head->next=prev;实现) 1要连3(这个通过一开始把prev置为tail而不是null 最开始递归的时候实现)
下面这个也要注意:先建一个dummy节点 然后走过k个之后 调用函数的tail是下一个 比如k=2 一开始从0开始走 走两步 到2 tail是下一个3 然后翻转链表函数的返回值是新链表的curr也就是1 这个节点是下一次调用的头节点 还有一个判断是如果没走完k步就到null了 直接返回dummy->next 不要翻转了 如果翻转完之后 tail为null 就返回
ListNode* reverse(ListNode *head, ListNode *tail) {
// 不包括head和tail
ListNode *prev=tail;
ListNode *curr=head->next;
ListNode *first=curr;
while(curr!=tail){
ListNode *next=curr->next;
curr->next=prev;
prev=curr;
curr=next;
}
head->next=prev;
return first;
}
ListNode* reverseKGroup(ListNode* head, int k) {
if(head==nullptr||k==1)return head;
ListNode *dummy=new ListNode();
dummy->next=head;
ListNode *pre=dummy;
while(1)
{
ListNode *check=pre;
for(int i=0;i<k;i++)
{
check=check->next;
if(check==nullptr)return dummy->next;
}
ListNode *newstart=check->next;
pre=reverse(pre,newstart);
if(newstart==nullptr)break;
}
return dummy->next;
}
随机链表的复制-138
先遍历一遍 得到一个next完成的链表 并用hash表存一个原节点到新节点的对应
再遍历一遍 把random指针指向新节点
这样虽然麻烦但思路很清晰
排序链表-148
可以用归并排序 采用递归 从上到下 注意最后merge的是两个子链表 所以要切断链表避免有环等情况
ListNode* sortList(ListNode* head) {
return sortList(head,nullptr);
}
ListNode *sortList(ListNode* head, ListNode *tail)
{
if(head==nullptr)return nullptr;
if(head->next==tail)
{
head->next=nullptr; // 注意这里切断链表
return head;
}
ListNode *slow=head,*fast=head;
while(fast!=tail)
{
slow=slow->next;
fast=fast->next;
if(fast!=tail)fast=fast->next;
}
ListNode *mid=slow;
return merge(sortList(head,mid),sortList(mid,tail));
}
ListNode *merge(ListNode *l1,ListNode *l2)
{
ListNode *dummy=new ListNode();
ListNode *temp=dummy,*temp1=l1,*temp2=l2;
while(temp1!=nullptr&&temp2!=nullptr)
{
if(temp1->val<temp2->val)
{
temp->next=temp1;
temp1=temp1->next;
}
else{
temp->next=temp2;
temp2=temp2->next;
}
temp=temp->next;
}
if(temp1!=nullptr)
{
temp->next=temp1;
}
else if(temp2!=nullptr)
{
temp->next=temp2;
}
return dummy->next;
}
合并K个升序链表-23
可以写成递归形式 两两合并 中间二分 不然单个链表会比较长
ListNode *Two(ListNode *a,ListNode *b){
if(a==nullptr||b==nullptr)return a?a:b;
ListNode head,*tail=&head,*aptr=a,*bptr=b;
while(aptr&&bptr){
if(aptr->val<bptr->val){
tail->next=aptr;
aptr=aptr->next;
}
else{
tail->next=bptr;
bptr=bptr->next;
}
tail=tail->next;
}
tail->next=(aptr?aptr:bptr);
return head.next;
}
ListNode* merge(vector<ListNode*>& lists,int l,int r){
if(l==r)return lists[l];
if(l>r)return nullptr;
int mid=(l+r)>>1;
return Two(merge(lists,l,mid),merge(lists,mid+1,r));
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
return merge(lists,0,lists.size()-1);
}
LRU缓存-146
要一个双向链表存信息,一个unordered_map<int,DLinkedNode*>
存cache
一个size 一个capacity来维护
每次get从map中找到节点 然后在链表中移到头部
每次put如果不存在 就创新节点 加进map和链表头部 如果超出capacity 删除双向链表尾部节点 并从map中删除 如果存在 通过hash表定位再修改value
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int size;
int capacity;
public:
LRUCache(int _capacity): capacity(_capacity), size(0) {
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (!cache.count(key)) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
DLinkedNode* node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
if (!cache.count(key)) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode* node = new DLinkedNode(key, value);
// 添加进哈希表
cache[key] = node;
// 添加至双向链表的头部
addToHead(node);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode* removed = removeTail();
// 删除哈希表中对应的项
cache.erase(removed->key);
// 防止内存泄漏
delete removed;
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
DLinkedNode* node = cache[key];
node->value = value;
moveToHead(node);
}
}
void addToHead(DLinkedNode* node) {
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(DLinkedNode* node) {
removeNode(node);
addToHead(node);
}
DLinkedNode* removeTail() {
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
};
如果直接用list造好的轮子和迭代器要简单一些:
class LRUCache {
private:
unordered_map<int, list<pair<int, int>>::iterator> keyToNode;
list<pair<int, int>> cache;
int capacity;
public:
LRUCache(int _capacity) : capacity(_capacity) {}
int get(int key) {
if (keyToNode.find(key) == keyToNode.end()) return -1;
auto it = keyToNode[key];
int value = it->second;
cache.erase(it);
cache.push_front({key, value});
keyToNode[key] = cache.begin();
return value;
}
void put(int key, int value) {
if (keyToNode.find(key) != keyToNode.end()) {
auto it = keyToNode[key];
cache.erase(it);
} else if (cache.size() >= capacity) {
int keyToRemove = cache.back().first;
keyToNode.erase(keyToRemove);
cache.pop_back();
}
cache.push_front({key, value});
keyToNode[key] = cache.begin();
}
};
二叉树
二叉树的中序遍历-94
递归 注意返回条件 函数开始写个如果nullptr就返回 后面就不用判断是否为null了 直接遍历
迭代 用栈
二叉树的最大深度-104
int maxDepth(TreeNode* root) {
if(root==nullptr)return 0;
return max(maxDepth(root->left),maxDepth(root->right))+1;
}
翻转二叉树-226
TreeNode* swap(TreeNode *root)
{
if(root!=nullptr)
{
TreeNode* temp;
temp=root->left;
root->left=root->right;
root->right=temp;
swap(root->left);
swap(root->right);
}
return root;
}
TreeNode* invertTree(TreeNode* root) {
swap(root);
return root;
}
对称二叉树-101
bool Judge(TreeNode* root1,TreeNode* root2)
{
if(root1==nullptr&&root2==nullptr)return true;
if(root1==nullptr||root2==nullptr)return false;
if(root1->val!=root2->val)return false;
return Judge(root1->left,root2->right)&&Judge(root1->right,root2->left);
}
bool isSymmetric(TreeNode* root) {
if(root==nullptr)return true;
return Judge(root->left,root->right);
}
二叉树的直径-543
dfs到每个节点的时候,记录每个节点的左右子树的直径,然后加起来和ans比较
int ans=0;
int getD(TreeNode* root){
if(root==nullptr){
return -1;
}
int left=getD(root->left)+1;
int right=getD(root->right)+1;
ans=max(ans,left+right);
return max(left,right);
}
int diameterOfBinaryTree(TreeNode* root) {
getD(root);
return ans;
}
二叉树的层序遍历-102
用队列广搜
if(root==nullptr)return {};
queue<TreeNode *> q;
vector<vector<int>> ans;
q.push(root);
while(!q.empty()){
int n=q.size();
vector<int> cur;
while(n--)
{
TreeNode *node =q.front();
q.pop();
cur.push_back(node->val);
if(node->left)q.push(node->left);
if(node->right)q.push(node->right);
}
ans.push_back(cur);
}
return ans;
用递归深搜 注意需要判断是给ans加一层ans.push_back({node->val})
还是某一层加元素ans[level].push_back(node->val)
vector<vector<int>> ans;
void LevelOrder(TreeNode *node,int level)
{
if(level>=ans.size())
{
ans.push_back({node->val});
}
else{
ans[level].push_back(node->val);
}
if(node->left)
{
LevelOrder(node->left,level+1);
}
if(node->right)
{
LevelOrder(node->right,level+1);
}
}
vector<vector<int>> levelOrder(TreeNode* root) {
if(root)LevelOrder(root,0);
return ans;
}
将有序数组转换为二叉搜索树-108
先把中间节点建好 向两边递归
验证二叉搜索树-98
可以中序遍历然后判断是否递增
或者递归判断(注意最大值是INT_MAX,然后有等号是否 所以最大要开到比INT_MAX大,可以来LONG_MAX)
二叉搜索树种第K小的元素-230
中序遍历 找第k个元素
二叉树的右视图-199
递归的时候记录层数 把每层最后一个节点存到ans就行
二叉树展开为链表-114
前序遍历 把所有遍历到的节点存到vector
从前序与中序遍历序列构造二叉树-105
前序的第一个就是根 然后在中序中找到根 左边的都是左子树 右边的都是右子树 递归
unordered_map<int,int> mp;
TreeNode *dfs(vector<int>& preorder, vector<int>& inorder,int prel,int prer,int inl,int inr)
{
if(prel>=prer)return nullptr;
int val=preorder[prel];
int location=mp[val];
int leftsize=location-inl;
TreeNode *left=dfs(preorder,inorder,prel+1,prel+1+leftsize,inl,location);
TreeNode *right=dfs(preorder,inorder,prel+1+leftsize,prer,location+1,inr);
return new TreeNode(val,left,right);
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
for(int i=0;i<inorder.size();i++)
{
mp[inorder[i]]=i;
}
return dfs(preorder,inorder,0,preorder.size(),0,inorder.size());
}
路径综合III-437
维护一个前缀和的map 注意map要随着遍历去更新 dfs搜下去 这个点搜完了 就把这个点的前缀和从map中-1
unordered_map<long long,int> prefix;
int ret=0;
void dfs(TreeNode *root,long long cur,int targetSum)
{
if(root==nullptr)return;
cur+=root->val;
if(prefix.count(cur-targetSum))
{
ret+=prefix[cur-targetSum];
}
prefix[cur]++;
dfs(root->left,cur,targetSum);
dfs(root->right,cur,targetSum);
prefix[cur]--;
}
int pathSum(TreeNode* root, int targetSum) {
prefix[0]=1;
dfs(root,0,targetSum);
return ret;
}
二叉树的最近公共祖先-236
判断:p是root或者q是root或者root是空 返回root 递归左边 递归右边 如果都有 就返回root 如果左边有返回值 就返回左边 右边有 就返回右边
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root==nullptr||root==p||root==q)return root;
TreeNode *left=lowestCommonAncestor(root->left,p,q);
TreeNode *right=lowestCommonAncestor(root->right,p,q);
if(left!=nullptr&&right!=nullptr)return root;
if(left!=nullptr)return left;
if(right!=nullptr)return right;
return nullptr;
}
如果是二叉搜索树 判断p,q节点值和当前节点大小 如果都大于root 就在root的右子树 如果都小于root 就在root的左子树 如果一大一小 就是root 不需要判断是不是空 因为不可能会到空节点
二叉树中的最大路径和-124
先求每个节点的最大贡献值(node->val+max(左节点的max(最大贡献值,0)+右节点的max(最大贡献值,0)))
作为返回值(只能算上一边 因为这个节点不作为根节点 就不能两边的路都走)
同时更新最大值(左右两边都算上)
图论
岛屿数量-200
dfs遍历 记得标记已经遍历过的格子
void dfs(vector<vector<char>>& grid,int x,int y)
{
if(x<0||x>=grid.size()||y<0||y>=grid[0].size())
{
return;
}
if(grid[x][y]!='1')
{
return;
}
grid[x][y] = '2';
dfs(grid,x-1,y);
dfs(grid,x+1,y);
dfs(grid,x,y-1);
dfs(grid,x,y+1);
}
int numIslands(vector<vector<char>>& grid) {
int cnt=0;
for(int i=0;i<grid.size();i++)
{
for(int j=0;j<grid[0].size();j++)
{
if(grid[i][j]=='1')
{
cnt++;
dfs(grid,i,j);
}
}
}
return cnt;
}
腐烂的橘子-994
bfs遍历 用一个队列存当前腐烂的和时间
while(!Q.empty()) 要记录还有没有新鲜橘子 没有就直接break
二维数组也可以memset(dis, -1, sizeof(dis)) / memset(dis, -1, sizeof dis)
课程表-207
拓扑排序 不存在环就ok
可以用 unordered_map<int, vector<int>>
存邻接表 用vector<int>
存入度
bfs更好理解 用一个队列存可以的节点 慢慢出 时间复杂度O(n+m) (时间复杂度深搜广搜都一样)
实现Trie(前缀树)-208
需要用一个标记来表示是否走到底 不然没法确定是前缀还是真的有这个字符串
class Trie {
private:
vector<Trie*> children;
bool isEnd;
Trie* searchPrefix(string prefix)
{
Trie *node=this;
for(char ch:prefix)
{
ch-='a';
if(node->children[ch]==nullptr)return nullptr;
node=node->children[ch];
}
return node;
}
public:
Trie(): children(26),isEnd(false){
}
void insert(string word) {
Trie *node=this;
for(char ch:word)
{
ch-='a';
if(node->children[ch]==nullptr)
{
node->children[ch]=new Trie();
}
node=node->children[ch];
}
node->isEnd=true;
}
bool search(string word) {
Trie *node=this->searchPrefix(word);
return node!=nullptr &&node->isEnd;
}
bool startsWith(string prefix) {
return this->searchPrefix(prefix)!=nullptr;
}
};
回溯
全排列-46
和组合的差距:拿了后面的元素 还要回去拿前面的元素 所以套一个循环在Back函数内部 然后用used记录是否拿过这个元素 而不是下标递增的方式 也不需要尝试放该元素or不放 因为所有的都会被放 只是顺序问题
vector<vector<int>> keep;
void Back(vector<int> &nums,vector<int> &help,vector<bool> &used)
{
if(help.size()==nums.size())
{
keep.push_back(help);
return;
}
for(int i=0;i<nums.size();i++)
{
if(used[i])continue;
used[i]=true;
help.push_back(nums[i]);
Back(nums,help,used);
used[i]=false;
help.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(),false);
vector<int> help;
Back(nums,help,used);
return keep;
}
子集-78
标准的回溯
vector<int> t;
vector<vector<int>> ans;
void dfs(int cur,vector<int> &nums){
if(cur==nums.size()){
ans.push_back(t);
return;
}
t.push_back(nums[cur]);
dfs(cur+1,nums);
t.pop_back();
dfs(cur+1,nums);
}
vector<vector<int>> subsets(vector<int>& nums) {
dfs(0,nums);
return ans;
}
电话号码的字母组合-17
和上一题很类似
组合总和-39
注意每个数可以被选取多次 所以dfs调用的时候 下标不能后移 而是根据和来判断
括号生成-22
dfs的时候记录左括号和右括号数量
单词搜索-79
遍历每个点作为起点 dfs中记录已经走了几步 走完了就true
分割回文串-131
回溯+dp 先用dp数组保存哪些子串是回文串
然后dfs保存一个当前起点 套一个i循环作为长度回溯
vector<vector<string>> ret;
vector<string> ans;
int n;
void dfs(string &s,int i,vector<vector<bool>> f)
{
if(i==n)
{
ret.push_back(ans);
return;
}
for(int j=i;j<n;j++)
{
if(f[i][j])
{
ans.push_back(s.substr(i,j-i+1));
dfs(s,j+1,f);
ans.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
n=s.size();
vector<vector<bool>> f(n,vector<bool>(n));
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
f[i][j]=true;
}
}
for(int len=2;len<=n;len++)
{
for(int i=0;i+len<=n;i++)
{
int j=i+len-1;
f[i][j]=(s[i]==s[j])&&f[i+1][j-1];
}
}
dfs(s,0,f);
return ret;
}
N皇后-51
可以用一维数组来存每行放在第几列
bool JudgeOK(vector<int> &row,vector<int> &column,int l,int c)
{
for(int i=0;i<l;i++)
{
int c1=row[i];
if(c1-i==c-l||c1+i==c+l)return false;
}
return true;
}
void backtrace(vector<vector<string>> &solutions,vector<int> &row,vector<int> &column,int n)
{
if(n==row.size())
{
vector<string> help;
for(int i=0;i<row.size();i++)
{
string s="";
for(int j=0;j<column.size();j++)
{
if(j==row[i])s.push_back('Q');
else s.push_back('.');
}
help.push_back(s);
}
solutions.push_back(help);
}
for(int i=0;i<column.size();i++)
{
if(column[i]==-1&&JudgeOK(row,column,n,i))
{
column[i]=n;
row[n]=i;
backtrace(solutions,row,column,n+1);
row[n]=-1;
column[i]=-1;
}
}
}
vector<vector<string>> solveNQueens(int n) {
vector<int> row(n,-1);
vector<int> column(n,-1);
vector<vector<string>> solutions;
backtrace(solutions,row,column,0);
return solutions;
}
二分查找
搜索插入位置-35
经典二分 如果不存在就返回要插入的位置 最后返回left就行了 while里面是left<=right
比如1 3 5 6找2 mid=3 然后right=0 然后left=1之后break掉 return 1
搜索二维矩阵-74
-
把矩阵展成一维的然后二分
-
左下角or右上角开始 一次排除一行or一列
在排序数组中查找元素的第一个和最后一个位置-34
-
可以找第一个>=target的作为左边界 >target的-1作为右边界
-
找target-0.5和target+0.5
搜索旋转排序数组-33
先确认数组中最小的元素下标 再联合首元素和target判断是去前半部分找还是后半部分找
找最小值:需要时刻更新minn 注意>=minn 2 1 正确情况:mid=0 left=1 mid=1 可以更新minn 错误情况:mid=0 right=0 right=-1 跑出去了 mincur是0
int left=0,right=nums.size()-1;
int minn=nums[0],mincur=0;
while(left<=right)
{
int mid=left+(right-left)/2;
if(nums[mid]>=minn)
{
left=mid+1;
}
else if(nums[mid]<minn)
{
minn=nums[mid];
mincur=mid;
right=mid-1;
}
}
寻找旋转排序数组中的最小值-153
上一题代码
寻找两个正序数组的中位数
递归 找两个数组前k/2个元素(k要减小) 每次排除一半的元素 如果第一个数组的元素小于第二个数组的 说明第一个的前k/2个都不是中位数(第k个元素)
注意奇数个和偶数个的情况
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int n=nums1.size();
int m=nums2.size();
int left=(n+m+1)/2;
int right=(n+m+2)/2;
return (getKth(nums1,0,n-1,nums2,0,m-1,left)+getKth(nums1,0,n-1,nums2,0,m-1,right))/2.0;
}
int getKth(vector<int>& nums1,int start1,int end1,vector<int>& nums2,int start2,int end2,int k)
{
int len1=end1-start1+1;
int len2=end2-start2+1;
if(len1>len2)
{
return getKth(nums2,start2,end2,nums1,start1,end1,k);
}
if(len1==0)return nums2[start2+k-1];
if(k==1)return min(nums1[start1],nums2[start2]);
int i=start1+min(len1,k/2)-1;
int j=start2+min(len2,k/2)-1;
if(nums1[i]>nums2[j])
{
return getKth(nums1,start1,end1,nums2,j+1,end2,k-(j-start2+1));
}
else{
return getKth(nums1,i+1,end1,nums2,start2,end2,k-(i-start1+1));
}
}
栈
有效的括号-20
如果是([{就入栈 是)]}就出一个比较(注意栈非空 否则直接返回false) 注意最后栈要是空的
最小栈-155
关键是栈的性质 每个元素入栈时记录当前元素入栈后 栈内元素的最小值 求最小值的时候直接就可以拿出来 Keep.push_back(min(val,Keep[Keep.size()-1]))
如果不使用额外空间,在栈中保存当前值和最小值的差 注意如果当前元素是最小值 push的差值不要是0 而是那个负数 不然这个元素出栈以后 之前的信息恢复不了
元素出栈的时候 如果栈元素小于0 说明当前值是最小值 恢复最小值 如果栈元素大于0 直接出栈
注意需要开long long
字符串解码-394
开一个数字栈和字符串栈 如果遇到左括号 当前数字和字符串入栈 如果遇到右括号 栈顶字符串+当前字符串乘以数字以后成为新的当前字符串 栈顶字符串出栈
3[a2[c]] 3入栈 a入栈 2入栈 当前字符串变成acc a 2出栈 当前字符串变成accaccacc 3出栈
每日温度-739
单调栈做法:在O(n)时间复杂度下求数组中各元素右侧第一个更大的元素和其下标
性质:单调栈存储的下标中 对应的温度列表中的温度依次递减
遍历下标 如果栈空:入栈 如果栈非空:栈顶温度大于当前温度:入栈 栈顶温度小于当前温度:栈顶元素出栈 栈顶元素的下一个高温就是当前元素 然后继续看下一个栈顶元素
元素遍历完以后 如果栈中还有下标 那就是找不到下一个高温
柱状图中最大的矩形-84
利用单调栈存每个柱子当前左边比它矮的第一个 右边比它矮的第一个 一开始初始化left全是-1 right全是n 最后面积是right-left-1 这样不用特判最边上(比如最左边那个 左边没有比它矮的 left=-1 右边第一个就比它矮 right=1 他的宽只有它自身就是1 right-left-1也是1 正好对上)
堆
数组中的第K个最大元素-215
维护一个大根堆 全放进去 出k-1个
priority_queue<int, vector<int>> Heap;
priority_queue<int, vector<int>, greater<int>> Heap; // 小根堆
前K个高频元素-347
先用hash表记录每个元素出现次数 再维护一个小根堆 每次都移出最小的堆顶元素(保持堆尺寸<=k) 这样剩的就是最大的
数据流的中位数-295
两个堆 一个堆放小的一半 一个堆放大的一半 注意维护两个堆的大小(差距为1or相等)
贪心算法
买卖股票的最佳时机-121
遍历数组 在每一天考虑:如果我是在历史最低点买入的 那我当前卖出能赚多少 然后max(以前,当天) 所以遍历的同时用一个变量记录历史最低点
跳跃游戏-55
遍历数组每一个下标 更新能到达的最远距离 如果遍历过程中 有i>最远距离 说明到不了 false 否则就是true
跳跃游戏II-45
-
反向查找 从最后一个位置开始 选能到的最远的位置 O(n^2)复杂度
-
正向查找 维护当前能够到达的最大位置 如果到达该最大位置
例如 一开始只能到0 维护0能到的最远位置 为2 然后要走一步了 步数+1 最远位置为2 然后看1 2能到的最远位置 1能到4 然后要走一步了 步数+1 发现到4了 结束
这个过程一开始设置end=0 遍历i 从0到n-1 maxpos=max(maxpos,i+nums[i]) if(i==end) end=maxpos 更新最远位置 ans++
划分字母区间-763
用一个表格记录每个字母最远位置的元素
从头开始遍历 并用新字母的最远位置和原有的最远位置取个max 如果遍历到最远元素 就ok了 记录当前的长度 然后继续遍历直到结束
动态规划
爬楼梯-70
初始化dp[0]=dp[1]=1 dp[n]=dp[n-1]+dp[n-2] (或者不管0 直接初始化1和2 这样更有道理一点)
杨辉三角-118
dp[i][j]=dp[i-1][j-1]+dp[i-1][j]
打家劫舍-198
dp[i]=max(dp[i-1],dp[i-2]+nums[i])
完全平方数-279
for(int j=1;j*j<i;j++)
{
minn=min(minn,dp[i-j*j]+1);
}
零钱兑换-322
完全背包:每个元素无限多
i 金额(体积) j 硬币(物品)
一开始可以把dp数组都设成amount+1(因为最多就是amount个硬币,最后可以通过判断dp[amount]是否是amount+1判断是否有解)
外层枚举体积 内层枚举硬币 反过来也可以 但这样比较好理解
dp[0]=0
for(int i=1;i<=amount;i++)
{
for(int j=0;j<coins.size();j++)
{
if(i-coins[j]>=0)
dp[i]=min(dp[i],dp[i-coins[j]]+1);
}
}
关于时间复杂度 外层循环枚举物品的写法,只会遍历物品数组一次;而内层循环枚举物品的写法,会遍历物品数组多次。从 cache 的角度分析,多次遍历数组会导致额外的 cache miss,带来额外的开销。所以虽然这两种写法的时间空间复杂度是一样的,但外层循环枚举物品的写法常数更小。(来自0x3f)
单词拆分-139
把所有单词都存入一个unordered_set 字符串从短开始遍历
dp[0]=true;
for(int i=1;i<=s.size();i++)
{
for(int j=0;j<i;j++)
{
if(dp[j]&&sett.find(s.substr(j,i-j))!=sett.end())
{
dp[i]=true;
break;
}
}
}
最长递增子序列-300
dp[i]表示以s[i]结尾的最长递增子序列的长度(注意要s[i]结尾)
这样递推公式就很自然:每个i 遍历之前的j 如果s[j]<s[i] 那么dp[i]=max(dp[i],dp[j]+1)
int maxx=0;
for(int i=0;i<n;i++)
{
dp[i]=1;
for(int j=0;j<i;j++)
{
if(nums[i]>nums[j])
{
dp[i]=max(dp[i],dp[j]+1);
}
}
maxx=max(maxx,dp[i]);
}
乘积最大子数组-152
两个变量存到当前的最大乘积和到当前的最小乘积(包括当前的) 一个变量存整体的最大(不需要包含当前)
遍历数组 如果当前是正数 就更新当前的最大乘积maxx=max(nums[i],maxxnums[i]) 最小乘积minn=min(nums[i],minnnums[i]); 如果当前是负数 可以先把最大和最小换个位置 然后更新最大乘积和最小乘积 注意更新完之后都要更新整体的最大
分割等和子集-416
01背包问题 选or不选一个 最后总量是总和/2
可以把dp数组设成bool i表示数字 j表示和 dp[i][j]表示前i个数能否组成和为j 也可以把数组设成int 每个东西重量和价值都是nums[i] 最后判断dp[n-1][target]是否等于target
需要注意初始化
vector<vector<int>> dp(n,vector<int>(target+1));
for(int j=0;j<=target;j++)
{
dp[0][j]=0;
}
for(int i=1;i<n;i++)
{
dp[i][0]=0;
for(int j=1;j<=target;j++)
{
if(j-nums[i]>=0)
dp[i][j]=max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
else dp[i][j]=dp[i-1][j];
}
}
return dp[n-1][target]==target;
int target=summ/2;
vector<vector<bool>> dp(n,vector<bool>(target+1,false));
dp[0][0]=true;
for(int i=1;i<n;i++)
{
dp[i][0]=true;
for(int j=1;j<=target;j++)
{
if(j-nums[i]>=0)
dp[i][j]=dp[i-1][j]|dp[i-1][j-nums[i]];
else dp[i][j]=dp[i-1][j];
}
}
return dp[n-1][target];
最长有效括号-32
如果当前括号是右括号 说明最长有效括号长度可能增加 去看上一个 如果是左 那就是dp[i]=dp[i-2]+2(判断有没有i-2)
如果是右 去找i-dp[i-1]-1位置(因为dp[i-1]就代表带上它 它之前的这么多个元素组成了一组有效括号 去看这群的上一个是啥) 如果是左 就这一大段也是有效括号 加上 如果是右 无事发生
int longestValidParentheses(string s) {
int n=s.size();
vector<int> dp(n,0);
int maxx=0;
for(int i=1;i<n;i++)
{
if(s[i]==')')
{
if(s[i-1]=='(')
{
if(i>=2)
{
dp[i]=dp[i-2]+2;
}
else dp[i]=2;
}
else if(i-dp[i-1]>0&&s[i-dp[i-1]-1]=='(')
{
if(i-dp[i-1]>=2)
{
dp[i]=dp[i-1]+dp[i-dp[i-1]-2]+2;
}
else{
dp[i]=dp[i-1]+2;
}
}
maxx=max(maxx,dp[i]);
}
}
return maxx;
}
多维动态规划
不同路径-62
-
直接组合数计算 相当于共动m-1+n-1下 选m-1下往下 共有几种路径 C(m-1+n-1,m-1)
-
dp 先初始dp[i][0] dp[0][j]为1 然后dp[i][j]=dp[i-1][j]+dp[i][j-1]
最小路径和-64
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
最长回文子串-5
中心扩散法 有一个精妙的点是可以在外层i遍历中心的时候 内层j设置为0 1 然后l=i r=i+j
int n=s.size();
int maxlen=0;
int start=0;
for(int i=0;i<n;i++)
{
for(int j=0;j<=1;j++)
{
int l=i;
int r=i+j;
while(l>=0&&r<n&&s[l]==s[r])
{
l--;
r++;
}
int len=r-l-2+1;
if(maxlen<len)
{
maxlen=len;
start=l+1;
}
}
}
return s.substr(start,maxlen);
动态规划 dp[i][j]表示s[i..j]是不是回文串 注意先枚举长度为2的 长度依次递增
int n=s.size();
if(n<2)return s;
int maxLen=1;
int begin=0;
vector<vector<int>> dp(n,vector<int>(n));
for(int i=0;i<n;i++)
{
dp[i][i]=true;
}
for(int len=2;len<=n;len++)
{
for(int i=0;i<n;i++)
{
int j=i+len-1;
if(j>=n)break;
if(s[i]!=s[j])dp[i][j]=false;
else{
if(j-i<=2)dp[i][j]=true;
else dp[i][j]=dp[i+1][j-1];
}
if(dp[i][j]&&j-i+1>maxLen)
{
begin=i;
maxLen=j-i+1;
}
}
}
return s.substr(begin,maxLen);
最长公共子序列-1143
dp[i][j]表示s1[0,i]和s2[0,j]的最长公共子序列的长度
注意一开始初始化dp[0][x]和dp[x][0]全为0
for(int i=1;i<=r;i++)
{
for(int j=1;j<=c;j++)
{
if(text1[i-1]==text2[j-1])
{
dp[i][j]=dp[i-1][j-1]+1;
}
else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
编辑距离-72
// dp[i][j]:1的前i个和2的前j个的距离(注意要初始化的是前0个(就是有空串的情况))
int m=word1.size();
int n=word2.size();
if(m==0||n==0)return n+m;
vector<vector<int>> dp(m+1,vector<int>(n+1));
for(int i=0;i<m+1;i++)
{
dp[i][0]=i;
}
for(int j=0;j<n+1;j++)
{
dp[0][j]=j;
}
for(int i=1;i<m+1;i++)
{
for(int j=1;j<n+1;j++)
{
int left=dp[i-1][j]+1;
int up=dp[i][j-1]+1;
int leftup=dp[i-1][j-1];
if(word1[i-1]!=word2[j-1])leftup+=1;
dp[i][j]=min(left,min(up,leftup));
}
}
return dp[m][n];
技巧
只出现一次的数字-136
全部元素异或(两个相同元素异或为0)剩下的元素是什么 什么就只出现了一次
其他方法 如用集合存元素 没有就加 有就删; hash表存元素和次数 再遍历; 集合元素两倍-数组元素和 空间都是O(n)
多数元素-169
每次从数组中删除两个不同的元素 遍历完后 肯定剩下的就是众数
int x=0,vote=0;
for(int i=0;i<nums.size();i++)
{
if(vote==0)x=nums[i];
if(nums[i]==x)vote++;
else vote--;
}
return x;
颜色分类-75
-
直接统计0,1数量然后对数组赋值
-
开双指针 把0都挪动到前面 2都挪动到后面
下一个排列-31
从后往前找非递增的第一个 比如4 5 2 6 3 1这个 6 3 1已经没有操作空间了 最大了 找到2 然后从后面找第一个比2大的就是3 然后2 3换一下 4 5 3 6 2 1然后给3后面的逆序一下
如果整个是递减的 相当于把整个数组逆序一下
寻找重复数-287
快慢指针 相当于建立映射 就是判断是否有环
最后还要找到入环的点 相当于建一个从头跑的指针和slow指针一起再跑 类似于环形链表II-142