记录开始时间:2021,4,2
刷题拟定路线:1.数据结构 + 2.常考算法(参考报考院校学长经验)
刷题路线参考:
1.https://www.bilibili.com/read/cv9904414
2.https://blog.csdn.net/qq_38633884/article/details/102322912
此贴目的:方便复习,分享记录
使用语言:c++
刷题平台:leetcode
一点希望:希望最后能去西交、武大、中大、厦大
目录
- 一、 数据结构部分
- 1.树
- 1) 回溯
- 2) 遍历
- 2.图
- 1) 图的基本概念
- 2) 图的遍历(DFS&BFS)
- 3) 最小生成树(prim&kruskal算法)
- 4)拓扑排序与关键路径
- 5)最短路径(Dijkstra和Floyd)
- 6)欧拉图
- 3.查找
- 1)二分查找
- 2)二叉搜索树
- 4.排序
- 1)插入排序(直接排序+希尔排序)
- 2)快速排序
- 二、算法进阶
- 1.常见算法
- 1) 对有序数组的一些操作
- 2) 字符串处理(KMP算法等)
- 3) 高精度算法
- 2.并查集
- 3.贪心算法
- 1)贪心算法定义
- 2)贪心算法适用场景
- 3)使用贪心思想的算法
- 4)练习题
- 4.动态规划
- 1)定义和适用场景
- 2)解题关键4步
- 3)与贪心算法的关系
- 4)习题练习
- 5.数论基础
- 1)模运算
- 1.基本运算性质
- 2.快速幂取余
- 3.组合数取余
一、 数据结构部分
1.树
1) 回溯
- 90.子集Ⅱ
time: 4.2
main idea:经典回溯求子集+去重;
hard place:去重
class Solution {
public:
void powget(vector<vector<int>> &powsub, vector<int> nums, int start, vector<int> &sub){
powsub.push_back(sub); //实际上在记录路径;
for(int i=start; i<nums.size();i++){
// 同一层再向下时,使用过的元素就不要再用了;
// *******这个地方理解还是不是很清楚;
if(i>start && nums[i]==nums[i-1]) continue;
int temp = nums[i];
// 取;
sub.push_back(temp);
powget(powsub, nums, i+1, sub);
// 舍;
sub.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<vector<int>> res;
// 一种朴素的想法,先得到所有幂集,然后去重;
// 获得后去重非常麻烦,考虑在得到幂集时去重;
vector<int> sub;
// 先对nums进行排序;
sort(nums.begin(), nums.end());
powget(res, nums, 0, sub);
return res;
}
};
- 51.N皇后
time: 4.7
idea: 经典回溯题!!!!
hard: 判断对角线是否已有皇后
class Solution {
private:
vector<vector<string>> res;
public:
vector<vector<string>> solveNQueens(int n) {
// 超级经典的回溯需剪枝问题;
int count=1; // 用于记录已经放置皇后的行数;
vector<int> oneSlove; // 先用数组记录每行的列下标,之后再转换为期盼;
MySolve(count, n, oneSlove);
return res;
}
// 判断合法性;
bool judge(const vector<int> oneSlove,int count){
if(count==1) return true;
else{
for(int i=0;i<=count-2;i++){
// 列:最后一个元素与前面的元素不重合;
if(oneSlove[count-1]==oneSlove[i]) return false;
// 对角线方向1:从左上到右下->行下标与列下标之差相等;
if((count-oneSlove[count-1])==(i+1-oneSlove[i]))
return false;
// 对角线方向2:从左下到右上->行下表与列下标之和相等;
if((count+oneSlove[count-1])==(i+1+oneSlove[i]))
return false;
}
// 活了下来才能做皇后;
return true;
}
}
// 主函数部分;
void MySolve(int count, int n, vector<int> &oneSlove){
if(count>n){ // 得到一个解,加入;
// 先转换;
vector<string> one;
for(int row=0; row<n; row++){
string s=string(n,'.');
s[oneSlove[row]-1] = 'Q';
one.emplace_back(s);
}
res.emplace_back(one);
}
else{
for(int column=1; column<=n; column++){
// 第count行,第column列放置皇后Q;
oneSlove.emplace_back(column);
// 如果目前的sloveQ合法,再继续递归直至寻到解;
if(judge(oneSlove,count)) MySolve(count+1,n,oneSlove);
// 移走Q寻找下一个解(这一步将不合适剪枝也包括了进来);
oneSlove.pop_back();
}
}
}
};
2) 遍历
遍历常见知识点:
1. 前中后序遍历(递归及非递归形式)
2. 层次遍历(BFS以及一次遍历一层)
3. 推广到n叉树及其他演示问题
nice summary:
https://leetcode-cn.com/problems/convert-bst-to-greater-tree/solution/yi-tao-quan-fa-shua-diao-nge-bian-li-shu-de-wen-5/
- 100.相同的树
idea: 深度优先遍历-前序遍历
hard place: 想清楚遍历的时候要做什么很关键
time: 4.8
/**
* 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 {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
// 深度优先遍历-前序,递归;
if(p==nullptr&&q==nullptr) return true;
else if(p==nullptr||q==nullptr) return false;
else{
if(p->val != q->val) return false;
bool left,right;
left = isSameTree(p->left,q->left);
right = isSameTree(p->right,q->right);
return left&&right;
}
}
};
- 103.二叉树的锯齿形层序遍历
idea: 层序遍历的变形—一次遍历一层,同时奇数层正序,偶数层逆序
hard place: 奇数层与偶数层的正逆序高效处理(利用双端队列)
time: 4.12
// 简单处理方案,利用reverse()函数;
class Solution {
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
// 层次遍历的变形, 奇数层从左到右,偶数层从右到左;
vector<vector<int>> res;
if(root==nullptr) return res;
queue<TreeNode*> que;
que.push(root);
int count=1; // 指示从左遍历还是从右侧遍历;
while(!que.empty()){
// 一次遍历一层;
int n = que.size();
vector<int> tempVector;
for(int i=0; i<n; i++){
TreeNode* temp = que.front();
que.pop();
tempVector.emplace_back(temp->val);
if(temp->left) que.push(temp->left);
if(temp->right) que.push(temp->right);
}
if(count%2==0) // 偶数情形;
reverse(tempVector.begin(),tempVector.end());
res.emplace_back(tempVector);
count++;
}
return res;
}
};
该题也可以采用双端队列进行处理
class Solution {
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
// 层次遍历的变形, 奇数层从左到右,偶数层从右到左;
// 利用双向队列deque提高效率;
vector<vector<int>> res;
if(root==nullptr) return res;
deque<TreeNode*> que;
que.push_back(root);
int count=1; // 指示从左遍历还是从右侧遍历;
while(!que.empty()){
// 一次遍历一层;
int n = que.size();
vector<int> tempVector;
for(int i=0; i<n; i++){
if(count%2){
// 奇数情形,正常队列-队头读(出)队尾存(进);
TreeNode* temp = que.front();
que.pop_front();
tempVector.emplace_back(temp->val);
if(temp->left) que.push_back(temp->left);
if(temp->right) que.push_back(temp->right);
}
else{
// 偶数情形,队尾读(出)队头存(进);
TreeNode* temp = que.back();
que.pop_back();
tempVector.emplace_back(temp->val);
if(temp->right) que.push_front(temp->right);
if(temp->left) que.push_front(temp->left);
}
}
res.emplace_back(tempVector);
count++;
}
return res;
}
};
- 543.二叉树的直径
idea: 二叉树的深度计算变形
hard place: 计算每个结点的depth(left) + depth(right)的原因;
time: 4.29
private:
int res;
public:
int depth(TreeNode* root){
if(root==nullptr) return 0;
else{
int L = depth(root->left);
int R = depth(root->right);
res = max(res,L+R); // 左子树深度+右子树深度;
return max(L,R)+1;
}
}
int diameterOfBinaryTree(TreeNode* root) {
// 直径->左子树深度+右子树深度;
res = 0;
depth(root);
return res;
}
};
- 101.对称二叉树
idea: 双指针分别指向树的左右子树进行对比
hard: 双指针的想法以及最后判真的条件
time: 4.9
class Solution {
public:
bool isSymmetric(TreeNode* root) {
// 对称即左子树对称&&右子树对称,利用双指针遍历实现;
return check(root, root);
}
bool check(TreeNode* p, TreeNode* q){
if(p == nullptr && q==nullptr) return true;
else if(p==nullptr || q==nullptr) return false;
// 这个遍历条件不好想呀;
else return (p->val == q->val) && (check(p->left,q->right))
&& check(p->right,q->left);
}
};
2.图
1) 图的基本概念
- 997.找到小镇法官
time: 4.5(啊,过个周末,做题感觉自己跟个沙雕似的T_T)
idea: 图的基本应用,邻接矩阵,出度为0,入度为n-1
no hard
class Solution {
public:
int findJudge(int N, vector<vector<int>>& trust) {
//idea1: 如果定义一个邻接矩阵,则行和为0,列和为N-1就是法官;
//idea2: 如果尝试用邻接表存储似乎更容易实现存储,但是找到所有人都信任的, // 也要进行遍历;
//idea1;
// 数组大小为参数需要动态申请内存,此处需要优化,太耗内存;
int** Alltrust = new int* [N]; // aij = 1 代表i信任j,0则不信任;
for (int i = 0; i < N; i++) {
Alltrust[i] = new int[N];
}
for(int i=0; i<N; i++){
for(int j=0; j<N; j++) Alltrust[i][j] = 0; // 初始化;
}
for(int i=0; i<trust.size(); i++){
int a=trust[i][0]-1;
int b=trust[i][1]-1;
Alltrust[a][b]=1;
}
int flag=-1; // 用来标记法官位置;
for(int i=0; i<N; i++){// 先找行和为0;
int rowSum=0;
for(int j=0; j<N; j++){
rowSum+=Alltrust[i][j];
}
if(!rowSum){
int columnSum=0;
for(int k=0; k<N; k++){// 查看列;
columnSum+=Alltrust[k][i];
}
if(columnSum == N-1) {
flag = i+1; // 注意与原序号差1;
break;
}
}
}
// 释放内存;
for(int i=0; i<N; i++)
delete [] Alltrust[i];
delete []Alltrust;
return flag;
}// 时间复杂度O(N^2);
};
稍微优化一下; 还可以巧用出入度差为 (N-1)- 0 = N - 1再次进行优化;
class Solution {
public:
int findJudge(int N, vector<vector<int>>& trust) {
// idea1是典型的c语言思路,这里用C++容器进行优化,来优化内存;
// 定义一个信任容器一个被信任容器;
vector<vector<int>> g1(N+1); // trust;
vector<vector<int>> g2(N+1); // trusted;
for(const auto& x:trust){
g1[x[0]].push_back(x[1]);
g2[x[1]].push_back(x[0]);
}
int flag = -1;
for(int i=1; i<=N; i++){
if(g1[i].size()==0&&g2[i].size()==N-1){
flag = i;
break;
}
}
return flag;
}
};
2) 图的遍历(DFS&BFS)
- 54.螺旋矩阵
idea: 剥洋葱,收缩边界
time: 7.10
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
// 剥洋葱,边界缩小;
vector<int> res;
if(matrix.size()==0) return res;
// 上,下, 左,右 边界;
int u=0,b=matrix.size()-1;
int l=0,r=matrix[0].size()-1;
while(true){
for(int i=l;i<=r;i++) res.push_back(matrix[u][i]);
if(++u>b) break;
for(int i=u;i<=b;i++) res.push_back(matrix[i][r]);
if(--r<l) break;
for(int i=r;i>=l;i--) res.push_back(matrix[b][i]);
if(--b<u) break;
for(int i=b;i>=u;i--) res.push_back(matrix[i][l]);
if(++l>r) break;
}
return res;
}
};
- 59. 螺旋矩阵 II
idea: 同上嘞,剥洋葱,边界缩小; 注意一下二维数组的初始化;
time: 7.10
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
// 二维vector的初始化;
vector<vector<int>> res(n,vector<int>(n,0));
int u=0,b=n-1,l=0,r=n-1;
int count=1;
while(true){
for(int i=l;i<=r;i++) res[u][i]=count++;
if(++u>b) break;
for(int i=u;i<=b;i++) res[i][r]=count++;
if(--r<l) break;
for(int i=r;i>=l;i--) res[b][i]=count++;
if(--b<u) break;
for(int i=b;i>=u;i--) res[i][l]=count++;
if(++l>r) break;
}
return res;
}
};
- 133.克隆图
time: 4.6
idea: 图的遍历(深度优先DFS、广度优先BFS)+ 哈希表去重;
*这里要说一下hash表的美妙之处,通过函数关系 a d d r e s s = H a s h ( k e y ) address = Hash(key) address=Hash(key),知道关键字(key)=知道存储地址(address),这相比数组查找某值的位置确实方便了不少(这个hash表后面会专门学习的)
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> neighbors;
Node() {
val = 0;
neighbors = vector<Node*>();
}
Node(int _val) {
val = _val;
neighbors = vector<Node*>();
}
Node(int _val, vector<Node*> _neighbors) {
val = _val;
neighbors = _neighbors;
}
};
*/
class Solution {
private:
// 创建无序map容器(类似于hash表,value&key互相决定为pair);
unordered_map<Node*,Node*> visited;
public:
Node* cloneGraph(Node* node) {
// DFS;
// 空则直接返回;
if(node == nullptr) return node;
// 访问过直接作为邻接返回node,不再对其neighbor进行复制;
if(visited.find(node) != visited.end())
return visited[node];
// 深拷贝,只拷贝值,不拷贝邻居(需要改变地址);
Node* copynode = new Node(node->val);
// hash存储;
visited[node] = copynode;
// 每一个邻居都要加进来;
for(auto& neighbor:node->neighbors){
copynode->neighbors.emplace_back(cloneGraph(neighbor));
}
return copynode;
}
}; // 访问ALL节点仅一次,时间复杂度O(N),空间复杂度O(N);
class Solution {
private:
unordered_map<Node*,Node*> visited;
queue<Node*> que;
public:
Node* cloneGraph(Node* node) {
// BFS;
// 空则直接返回;
if(node == nullptr) return node;
// 深拷贝,只拷贝值,不拷贝邻居(需要改变地址);
Node* copynode = new Node(node->val);
// hash存储;
visited[node] = copynode;
que.push(node);
while(!que.empty()){
Node* cur = que.front();
que.pop();
for(auto& neighbor:cur->neighbors){
if(visited.find(neighbor)==visited.end()){
// 没被访问过则需要加入队列中继续遍历完成构建;
visited[neighbor] = new Node(neighbor->val);
que.push(neighbor);
}
// 克隆后加入visied[cur]的邻接表中;
visited[cur]->neighbors.emplace_back(visited[neighbor]);
}
}
return copynode;
}
};// 时间、空间复杂度均为O(N);
3) 最小生成树(prim&kruskal算法)
4)拓扑排序与关键路径
- 207.课程表
main idea: 拓扑排序,经典;
hard place: 建立邻接表控制时间复杂度在O(n+e),直接遍历将超时;
time: 4.15
相关试题210.课程表II
只需要在此题基础上加一个列表记录学习路径即可。
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
// 典型的拓扑排序问题,要判断有环;
// 首先建立一个入度列表,以及一个邻接表;
vector<int> indegree(numCourses,0);
vector<vector<int>> adjacent(numCourses);
for(auto related:prerequisites) {
indegree[related[0]]++;
adjacent[related[1]].emplace_back(related[0]); // 出度邻接表;
}
stack<int> S;
for(int i=0;i<numCourses;i++){ // 用栈格式记录可学习的课程;
if(indegree[i]==0) S.push(i);
}
int count=0;
while(!S.empty()){
int temp=S.top(); S.pop(); ++count;
// 更新以temp为尾的其他顶点入度;
for(int i=0;i<adjacent[temp].size();i++){
if(!(--indegree[adjacent[temp][i]]))
S.push(adjacent[temp][i]);
}
}
if(count==numCourses) return true;
else return false;
}
};
- 310.最小高度树
idea: 拓扑排序的变形, 逆向BFS;
hard place: 如何想到最小高度树与逆BFS联系;
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
// 类似拓扑排序,第一次做的时候完全没有思路;
// 逆BFS——剥洋葱,其实就是从度为1的叶子节点出发一次剥去一层
// 来求得树的最小deepth;
vector<int> res;
if(n==1){
res.emplace_back(0);
return res;
}
vector<int> degree(n,0);
vector<vector<int>> vlink(n); // 邻接表;
for(auto edge:edges){
degree[edge[0]]++;
degree[edge[1]]++;
vlink[edge[0]].emplace_back(edge[1]);
vlink[edge[1]].emplace_back(edge[0]);
}
queue<int> que;
// 度为1的入队;
for(int i=0; i<degree.size();i++){
if(degree[i]==1){
que.push(i);
}
}
while(!que.empty()){
res.clear(); // 每一次清空上一层的结果;
int queSize = que.size();
for(int i=0;i<queSize;i++){
int temp = que.front(); que.pop();
res.emplace_back(temp);
// 相邻点因为temp的删除而度--;
for(auto link:vlink[temp])
if((--degree[link])==1) que.push(link);
}
}
return res;
}
};
5)最短路径(Dijkstra和Floyd)
6)欧拉图
占坑,学完贪心算法可以看下面两个题,注意总结欧拉回路/通路定义、存在与否及应用。
- 332.重新安排行程
- 753.破解保密箱
3.查找
1)二分查找
对区间[left..mid+1]&[mid..right]的划分方法,以及while和if的使用条件说明这篇文章都解释的很棒
https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/
- 35.搜索插入位置
idea: 经典的二分查找问题
hard place: 主要是边界条件if…else…, 以及最后left与right的关系
time: 4.21
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
// 二分查找经典题;
int res=0;
int low=0,high=nums.size()-1;
// 特殊情况为target>nums的最后一个元素,是搜索区间[low..high]的盲区;
if(target>nums[high]) return high+1;
// 找到最后一个小于目标key的位置;
while(low<high){
int mid = low+(high-low)/2;
if(nums[mid]<target) low = mid + 1; // 在[mid+1..high]这个区间;
else high = mid; // nums[mid]>=traget则搜索区间[low..mid];
}
// 退出时一定有low = high;
res = low;
return res;
}
};
- 34.在排序数组中查找元素的边界
hard place: 使用划分区间的方法时,要注意当划分区间为mid = left时应向上取整,以避免陷入2个数的死循环(1=1+(2-1)/2);
time: 4.21
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
// 相比一般的二分查找,多了可重复的条件;
vector<int> res(2,-1);
if(nums.size()==0) return res;
int low = 0, high = nums.size()-1;
// 左侧逼近,得到第一个不小于目标值的位置low;
while(low<high){
int mid = low+(high-low)/2;
if(nums[mid]<target) // 搜索区间换为[mid+1..high];
low = mid + 1;
else high = mid; // 与之相对的搜索区间为[low..mid];
}
if(nums[low]!=target) return res;
else{
res[0] = low;
// 右侧逼近,得到第一个不大于目标值的位置high;
low=0,high=nums.size()-1;
while(low<high){
int mid = low+(high-low)/2 + 1; // 向上取整,2个数时避免死循环;
if(nums[mid]>target)
high = mid - 1;
else low = mid;
}
res[1] = low;
return res;
}
}
};
2)二叉搜索树
- 530.二叉搜索树的最小绝对差
time: 4.2
main idea: 二叉搜索树:左子树上所有节点值均小于根节点,而右子树上所有节点值均大于根节点值->中序遍历为升序序列;
no hard
/**
* 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:
vector<int> nodenum;
void inorder(TreeNode *root){
if(root->left) inorder(root->left);
nodenum.push_back(root->val);
if(root->right) inorder(root->right);
}
int getMinimumDifference(TreeNode* root) {
// 1.先中序遍历得到升序数组;
// 2.计算任意两个相邻的差的绝对值,取最小;
inorder(root);
int minAbs = abs(nodenum[1] - nodenum[0]);
for(int i=2; i<nodenum.size();i++)
minAbs = min(minAbs, abs(nodenum[i]-nodenum[i-1]));
return minAbs;
}
};
- 538.把二叉搜索树转换为累加树
main idea:
one: 一种最自然的想法:中序遍历->得到单调递增序列, 对应节点的和也能算出来(时O(n^2),空O(n));
two: ⬆这是笨比做法,直接反序中序遍历,顺便加和赋值即有O(n)的时复和O(logn)的空复(递归建栈所用);
time: 4.27
// idea one, 虽然笨但直接;
class Solution {
public:
void LDR1(TreeNode* root,vector<int>& rootval){
if(root==nullptr) return;
LDR1(root->left, rootval);
rootval.emplace_back(root->val);
LDR1(root->right, rootval);
}
void LDRSum(TreeNode* root, vector<int> rootval, int& count){
if(root==nullptr) return;
LDRSum(root->left, rootval, count);
root->val = rootval[count++];
LDRSum(root->right, rootval, count);
}
TreeNode* convertBST(TreeNode* root) {
// 一种最自然的想法:中序遍历->得到单调递增序列, 对应节点的和也能算出来;
// 这就需要两遍遍历和一个暂存空间。时间复杂度O(2n),空间复杂度O(n);
if(root==nullptr) return root;
vector<int> rootval;
TreeNode* tmep = root; // 指向根节点;
// 中序遍历时存下val;
LDR1(tmep, rootval);
// 计算每个位置的累加和;
for(int i=0; i<rootval.size();i++)
for(int j=i+1; j<rootval.size();j++)
rootval[i]=rootval[i]+rootval[j];
// 再次遍历改变root值并作为结果返回;
int count = 0;
LDRSum(root, rootval, count);
return root;
}
};
// idea2;
class Solution {
public:
int sum=0;
TreeNode* convertBST(TreeNode* root) {
if(root!=nullptr){
convertBST(root->right);
sum+=root->val;
root->val = sum;
convertBST(root->left);
}
return root;
}
};
4.排序
1)插入排序(直接排序+希尔排序)
2)快速排序
- 56.合并区间
idea: 排序哦~,这里救用最实用的快排吧,快排那个挖坑填数的思想好理解
time: 4.23
class Solution {
public:
int oneQ(vector<vector<int>>& intervals,int low, int high){
vector<int> mid = intervals[low];
int key = intervals[low][0];
while(low<high){
// 右侧找到小的放在左边;
while(low<high&&intervals[high][0]>=key) high--;
intervals[low]=intervals[high];
// 左侧找打大的放在右边;
while(low<high&&intervals[low][0]<=key) low++;
intervals[high]=intervals[low];
}
// 结束时一定有low==high;
intervals[low]=mid;
return low; // 返回枢轴的位置;
}
void Quicksort(vector<vector<int>>& intervals,int low, int high){
if(low<high){
int mid = oneQ(intervals,low,high);
Quicksort(intervals,low,mid-1);
Quicksort(intervals,mid+1,high);
}
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
// 就区间A、B而言:重合的条件即区间B左侧边界<=区间A右侧边界
// &B右侧边界>=A左侧边界;
// 如果能保证左侧区间为升序,则第二个条件将自动满足;
vector<vector<int>> res;
int n = intervals.size();
if(!n) return res;
// 用快排按左边界排序;
Quicksort(intervals,0,n-1);
res.emplace_back(intervals[0]);
for(int i=1; i<n; i++){
// 若res最后一个区间的右侧边界=>新的左侧边界则重合;
int end = res.size()-1;
if(res[end][1]>=intervals[i][0])
res[end][1] = max(res[end][1],intervals[i][1]);
else res.emplace_back(intervals[i]);
}
return res;
}
};
二、算法进阶
1.常见算法
1) 对有序数组的一些操作
- 88.合并两个有序数组
time: 4.5
idea1: 两个指针正序指向两个数组进行排序合并
hard: idea2: 逆向双指针
// idea1;
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
// 朴素的想法,用两个指针指向两个数组,每次取指针最小值;
int p1=0,p2=0;
vector<int> mergeNum;
while(p1<m || p2<n){
if(p1 == m) mergeNum.push_back(nums2[p2++]);
else if(p2 == n) mergeNum.push_back(nums1[p1++]);
else if(p1<m && p2 < n){
if(nums1[p1]<nums2[p2])
mergeNum.push_back(nums1[p1++]);
else
mergeNum.push_back(nums2[p2++]);
}
}
// 返回到nums1中;
for(int i=0;i<m+n;i++)
nums1[i] = mergeNum[i];
}
};// 时间复杂度O(m+n),空间复杂度O(m+n);
// idea2;
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
// 逆序双指针,直接利用nums1剩余内存;
// 要保证p1指针前面的元素不被覆盖;
// 即(m+n-1)-(m-p1-1)-(n-p2-1) >=p1 <-> p2>=-1;
int p1=m-1,p2=n-1;
int tail = m+n-1;
while(p1>=0 || p2>=0){
int cur;
if(p1 == -1) cur = nums2[p2--];
else if(p2 == -1) cur = nums1[p1--];
else if(p1>-1 && p2>-1){
if(nums1[p1]>nums2[p2])
cur = nums1[p1--];
else
cur = nums2[p2--];
}
nums1[tail--] = cur;
}
}
};
- 80.删除有序数组中的重复项 II
time: 4.6
idea: 双指针
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
// idea,双指针;
// 1.由于是保留 k 个相同数字,对于前 k 个数字,我们可以直接保留;
// 2.对于后面的任意数字,能够保留的前提是:与当前写入的位置前面的第 k 个元素进行比较,不相同则保留
int n = nums.size();
if(n<=2) return n;
int slow=2;int fast=2; // 双指针初始均指向第三个位置;
while(fast<n){
if(nums[slow-2]!=nums[fast]){
nums[slow++]=nums[fast];
}
fast++;
}
return slow;
}
};
// 解法2,借鉴,来源见下文;
class Solution {
public:
int work(vector<int>& nums, int k) {
int len = 0;
for(auto num : nums)
if(len < k || nums[len-k] != num)
nums[len++] = num;
return len;
}
int removeDuplicates(vector<int>& nums) {
return work(nums, 2);
}
};
//作者:AC_OIer
//链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii/solution/gong-shui-san-xie-guan-yu-shan-chu-you-x-glnq/
- 4.寻找两个正序数组的中位数
main idea: 要求log(m+n)的时间复杂度,显然要使用二分法求解,但此题的关键在于2分k,k即要找的第k小的数。利用nums1[k/2-1]<nums2[k/2-1],则nums1[0…k/2-1]共k/2个数均不可能为第k小的数,不断缩小数组。
hard place:1.二分思想的巧妙;2. 边界条件的考虑。
time: 8.1
class Solution {
public:
double getFirstK(vector<int> nums1, vector<int> nums2, int k){
int m=nums1.size();
int n=nums2.size();
int index1=0;
int index2=0;
while(true){
// 退出条件;
if(index1==m) return nums2[index2+k-1];
if(index2==n) return nums1[index1+k-1];
if(k==1) return min(nums1[index1],nums2[index2]);
int newIndex1=min(m-1, index1+k/2-1);
int newIndex2=min(n-1, index2+k/2-1);
if(nums1[newIndex1]<nums2[newIndex2]){
k = k-(newIndex1-index1+1);
index1 = newIndex1+1;
}
else{
k = k-(newIndex2-index2+1);
index2=newIndex2+1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int numLength=nums1.size()+nums2.size();
if(numLength%2==1) return getFirstK(nums1,nums2,numLength/2+1);
else return (getFirstK(nums1,nums2,numLength/2)+getFirstK(nums1,nums2,numLength/2+1))/2;
}
};
2) 字符串处理(KMP算法等)
- 3. 无重复字符的最长子串
idea: 滑动窗口,利用无序集合按值随机访问的特性;
time: 07.09;
// 单纯的暴力解O(n^2)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
vector<char> norep_s;
int count=0,res=0;
for(auto one:s){
bool flag = 0;
int n = norep_s.size();
int i=0;
for(;i<n;i++){
if(one==norep_s[i]){
flag = 1;
break;
}
}
if(flag==1){
norep_s.erase(norep_s.begin(),norep_s.begin()+i+1);
norep_s.push_back(one);
count = count-i;
}
if(flag==0){
norep_s.push_back(one);
count += 1;
}
if(count>res) res = count;
}
return res;
}
};
// 滑动窗口;
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// unordered_set为无序集合容器,允许基于值的快速元素检索;
unordered_set<char> news;
int left=0,res=0;
for(int i=0;i<s.size();i++){
while(news.find(s[i])!=news.end()){
// 找到了重复值, 滑动窗口;
news.erase(s[left]);
left++;
}
news.insert(s[i]);
res = max(res, i-left+1);
}
return res;
}
};
3) 高精度算法
2.并查集
3.贪心算法
1)贪心算法定义
贪心算法正如他的名字一样,不从整体最优考虑,而是每次做出当前的局部最优决策,在一定适用场景下贪心算法可以得到全局最优解或者近似全局最优解,其关键在于贪心策略的制定。
2)贪心算法适用场景
许多可以用贪心算法求解的问题中看到这类问题一般具有2个重要的性质:贪心选择性质和最优子结构性质
- 贪心选择性质:整体最优解可以通过一系列局部最优的选择达到。贪心算法是自顶而下,以迭代的方式相继做出贪心选择,将整体问题一步步缩小为规模更小的子问题。(eg: dijkstra算法)
- 最优子结构性质:一个问题的全局最优解包含子问题的最优解。(eg: Huffman最优编码树)
3)使用贪心思想的算法
- Huffman最优编码
- Dijkstra算法(Floyd是动态规划的思想)
- 最小生成树算法(Prime和kruskal)
4)练习题
- 55.跳越游戏
idea: 贪心思想:维护一个最远可到达的位置maxfar,从起始位置开始缩小问题规模,相当于每次都“跨”最大步子去尽快结束搜索过程。最终得到maxfar与nums.size()比较即可。
hard place: 贪心决策的制定
time: 5.07
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxfar=0; // 最远能到达的位置;
for(int i=0;i<=maxfar&&i<nums.size();i++){
// 更新maxfar;
maxfar = max(maxfar,nums[i]+i);
}
if(maxfar>=nums.size()-1) return true;
else return false;
// 每个位置最多遍历一次,维护一个int maxfar;
// 时复O(n),空复O(1);
}
};
- 45.跳跃游戏Ⅱ
idea: 典型的贪心算法思想,维护可以达到的最大位置,跳到可以让最大位置最大的下一个位置
hard place:思想容易想到,简洁的代码实现还要加油;
time: 5.07
class Solution {
public:
int jump(vector<int>& nums) {
// 维护一个maxfar,我要跳到能让maxfar最大的地方
// 当然,最后一步,能跳倒最后一格绝不跳倒二;
if(nums.size()==1) return 0;
int maxfar=nums[0],point=0,count=0;
while(maxfar<nums.size()-1){
// 贪心选择;
int tempoint;
for(int i=1, newpoint=point+i; maxfar<nums.size()-1 && i<=nums[point] && newpoint<=nums.size()-1; i++,newpoint++){
if(newpoint+nums[newpoint]>maxfar){
maxfar=newpoint+nums[newpoint];
tempoint = newpoint;
}
}
point = tempoint; // 指向下一个位置;
count++;
}
if(point==nums.size()-1) return count;
else return count+1;
}
};
// 官方简洁的代码;
class Solution {
public:
int jump(vector<int>& nums) {
int maxfar = 0; // 目前能到达的最远处;
int end = 0; // 记录能到达的边界;
int step = 0; // 记录跳跃次数;
for(int i=0;i<nums.size()-1;i++){
// 注意最后一个格子不必考虑,end一定能到达最后,我们在i=0=end那里已经补偿过了;
// 贪心选择;
maxfar = max(maxfar,i+nums[i]);
// 到达有边界,更新;
if(i==end){
end = maxfar; // 更新边界,其实就是下次起跳位置;
step++;
}
}
return step;
}
};
- 134.加油站
idea: // 从start位置开始到nowleft<0的位置(记为pos),则start…pos均不会是可能起点
// 而pos…n-1可以走过去并且有剩余nowleft,那么n-1…pos-1这段亏本路如果能走过去即nowleft-kuiben>=0等价于totalLeft>=0;
time:5.8
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int start=0;
int n=gas.size();
int totalLeft=0,nowLeft=0;
// 缩小问题规模;
for(int i=0;i<n;i++){
totalLeft += gas[i]-cost[i];
nowLeft+=gas[i]-cost[i];
if(nowLeft<0){
start=i+1;
nowLeft = 0;
}
}
return totalLeft < 0? -1:start;
}
};
4.动态规划
参考资料:
1.https://blog.csdn.net/sinat_19594515/article/details/102738781
1)定义和适用场景
2)解题关键4步
Step1:
Step2:
Step3:
Step4:
3)与贪心算法的关系
走出迷宫的人们,有的是认识路;有的是莽撞碰巧出来的;有的则是一路做着标记出来的;也有的是走遍了整个迷宫。
——证明了的贪心算法、没有证明的贪心算法、动态规划、暴力搜索的区别。URL
所有的贪心可解问题都可以用动态规划去做
4)习题练习
- 5.最长回文数
idea: 解题4步,最关键在第一步,想到回文串可以由子串+边界来判定->化为子问题;
time: 5.13
class Solution {
public:
string longestPalindrome(string s) {
// Step1: 最后一步,化为子问题;
// 若回文串的最大长度>=3,则去掉边界后,仍为回文串;判断s回文串->判断子串;
// Step2: 状态转移方程: dp[i][j]=true代表s[i..j]为回文串,则
// dp[i][j]=dp[i+1][j-1]&&(s[i]==s[j])
// Step3: 处理边界: s长度为1,为回文串,s=2,相等为回文串;
// Step4: 更新规则:从长度L=1开始更新d[i][i],左边界+L-1为有边界;
int n=s.size();
if(n<=1) return s;
int maxlength=1;
int beign=0;
bool dp[n][n];
// 更新L=1;
for(int i=0;i<n;i++) dp[i][i]=true;
for(int L=2;L<=n;L++){
// 从L=2开始更新;
for(int i=0;i<n;i++){
// 从左边界开始更新;
int right = i+L-1;
// right越界退出;
if(right>=n) break;
if(s[i]!=s[right]) dp[i][right]=false;
else{
if(L==2) dp[i][right]=true;
else dp[i][right]=dp[i+1][right-1];
if(L>maxlength&&dp[i][right]){
maxlength=L;
beign=i;
}
}
}
}
return s.substr(beign,maxlength);
}
};
- 343.整数拆分&剑指offer14-I.剪绳子
idea: 一段长为i的绳子减去长为j的一段,剩余(i-j),则dp[i]=(i-j)*max(j,dp[j]),j从2开始(截取1无意义,但要注意边界3);
time: 5.25
class Solution {
public:
int cuttingRope(int n) {
// 动态规划;
vector<int> dp(n+1,1); // 记录从2~n的所有分解最大乘积;
dp[2] = 1;
for(int i=3;i<=n;i++){ // 从3到n进行更新;
// 分一段长度为2..i-1;
// 状态转移方程,注意边界3,dp[1]=1->dp[3]=2,可行;
for(int j=2;j<i;j++)
dp[i]=max(dp[i],(i-j)*max(j,dp[j]));
}
return dp[n];
}
};
- 474.一和零
main idea: 双背包;
hard place: 有时从前往后推也很重要,而非只看最后一步;
time: 6.6
class Solution {
public:
vector<int> getzerosOnes(string s){
vector<int> zerosOnes(2,0);
for(int i=0;i<s.size();i++){
if(s[i]=='0') zerosOnes[0]++;
if(s[i]=='1') zerosOnes[1]++;
}
return zerosOnes;
}
int findMaxForm(vector<string>& strs, int m, int n) {
// 2个背包;
// dp[i][j][k]->前i个字符串,最多有m个0,n个1的最大子集个数;
int dp[601][101][101]={0};
for(int i=1;i<=strs.size();i++){
vector<int>&& zerosOnes=getzerosOnes(strs[i-1]);
int zeros = zerosOnes[0],ones = zerosOnes[1];
for(int j=0;j<=m;j++){
for(int k=0;k<=n;k++){
dp[i][j][k]=dp[i-1][j][k];
if(j>=zeros && k>=ones)
dp[i][j][k] = max(dp[i][j][k],dp[i-1][j-zeros][k-ones]+1);
}
}
}
return dp[strs.size()][m][n];
}
};
- 10.正则表达式匹配
main idea: 动态规划,哨兵思想,牛蛙
hard palce: 如何搜小到子问题,难哇
time: 8.09
class Solution {
public:
bool isMatch(string s, string p) {
// 动态规划
// 为避免初始为空的情形,开头添加空格
s = ' ' + s;
p = ' ' + p;
int m = s.size();
int n = p.size();
bool dp[m+1][n+1];
memset(dp,false,(m+1)*(n+1));
dp[0][0] = true;
for(int i=1; i<=m; i++){
for(int j=1; j<=n; j++){
if(s[i-1]==p[j-1] || p[j-1]=='.') dp[i][j] = dp[i-1][j-1];
else if(p[j-1]=='*'){
if(s[i-1]!=p[j-2] && p[j-2]!='.') dp[i][j] = dp[i][j-2];
else{
// 判断前面是否匹配;
dp[i][j] = dp[i][j-1] || dp[i][j-2] || dp[i-1][j];
}
}
}
}
return dp[m][n];
}
};
5.数论基础
1)模运算
1.基本运算性质
2.快速幂取余
// 数论基础:(a*b)%p=(a%p * b%p)%p=a%p*b%p;
// 快速幂取余: a^n%p=a%p*a%p*..*a%p=(a%p)^n -> a^n%p=a%p*((a*a)%p)^(n/2);
// a^n-1
3.组合数取余
最后
以上就是鲜艳飞鸟最近收集整理的关于计算机刷题记录贴一、 数据结构部分二、算法进阶的全部内容,更多相关计算机刷题记录贴一、内容请搜索靠谱客的其他文章。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复