目录
- 一、二分查找
- 1、整数二分(重要)
- 1.常规思路
- 1.确定区间
- 2.中间值(+1 or 不 + 1)
- 2.浮点数二分
- 二、双指针(重要)
- 1.两个指针均从起点开始(一个数组)
- 2.两个指针一前一后(两个数组)
- 3、滑动窗口(双指针类型的)
- 三、模拟(考察代码能力)
- 四、哈希表(重要)
- 1.数组模拟实现实现(常用)
- 2.unordered_map容器实现(常用)
- 3.unordered_set集合实现
- 五、位运算。
- 1.位运算符&、|、^、~、x与 -x的基本概念
- 2.具体题目
- 解法1:x =x - (x & -x)
- 解法2:x&(x - 1)
- 六、排序
- 1.快速排序
- 2.归并排序
- 七、高精度加减乘除(适用于数据特别大)
- 1.高精度加法
- 2.高精度减法
- 3.高精度乘法
- 4.高精度除法
- 八、前缀和与差分
- 1.前缀和
- 1、一维数组
- 2.二维矩阵
- 2.差分
- 1.一维数组
- 2.二维矩阵
- 总结
一、二分查找
1、整数二分(重要)
1.常规思路
我们进行整数二分,经常需要确定边界问题,而很多时候会出错,超时。
因此,我们二分时,需要确定一段范围的数,他区间的开闭情况。
下面代码所示例子为leetcode上下面链接的题目
1.确定区间
一般有以下几种情况:
1.左闭右开
此时我们会发现,因为右边是开,所以每次判断时,右边坐标便无需减一。而左区间因为闭合,所以需要加一。因为闭表示能取到这种情况,但是这种情况本身就是小于mid的,所以加一。
同时while(判断条件) 中的判断条件是 < 而非 <=。 这是因为当left == right 时,在左闭右开区间中不成立,为空集。
int search(vector<int>& nums, int target) {
int left = 0; int right = nums.size() ;
while(left < right)
{
int mid = left + (right - left) / 2;
if(nums[mid] < target) left = mid + 1;
else if(nums[mid] > target) right = mid;
else return mid;
}
return -1;
2.左闭右闭(闭区间)
此时while()中的判断条件变成了left<=right,因为此时left == right 的情况成立,
同时left = mid + 1。right= mid - 1。这是因为取到这种情况的时候,这种情况本身就不成立,因此缩小范围。
int search(vector<int>& nums, int target) {
int left = 0; int right = nums.size() - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] < target) left = mid + 1;
else if(nums[mid] > target) right = mid - 1;
else return mid;
}
return -1;
2.中间值(+1 or 不 + 1)
在上述区间中,我们初步了解了区间的闭合问题,然后我们有时候也会面临mid的取值问题,有时候mid = (left + right) / 2;有时候mid = (left + right + 1) / 2; 这其实是向上取整或者向下取整的问题
例如 对 有8 个数的数组而言,3,4都是中间值的下标,left = 3,right = 4. 如果直接相加除2,那mid = (3 + 4) / 2 = 3,余数舍弃。如果mid = (3 + 4 + 1) / 2;mid = 4。因此,中间值 + 1涉及向下还是向上取整。
下面是这种类型的leetcode题目(文章直接给解题代码)
https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/
主函数
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.size() < 1) return {-1, -1};
int leftbound = get_left(nums,target,0,nums.size()-1);
int rightbound = get_right(nums,target,0,nums.size()-1);
vector<int> p;
if(nums[leftbound] != target || nums[rightbound] != target){
leftbound = -1; rightbound = -1;
}
p.push_back(leftbound);p.push_back(rightbound);
return p;
}
1.确定左边界
我们首先做两件事,确定区间闭合情况,判断mid向上取整还是向下取整。
此处使用左闭右开(全闭也可),这里就是套用左闭右开的模板,return返回l 或者r 都可以,
因为最后l == r。
int get_left(vector<int>& nums,int target,int l,int r)
{
while(l < r)
{
int mid = l + (r - l) / 2;
if(nums[mid] < target) l = mid + 1;
else if(nums[mid] >= target) r = mid;
}
return r;
}
2.确定右边界
区间情况为左开右闭合,此时我们右边界向上取整 mid = (r - l + 1) / 2。
套用左开右闭模板,此处return时,l == r。
int get_right(vector<int>& nums,int target,int l,int r)
{
while(l < r)
{
int mid = l + (r - l + 1) / 2;
if(nums[mid] <= target) l = mid;
else if(nums[mid] > target) r = mid - 1;
}
return l;
}
2.浮点数二分
浮点数比整数二分简单的多。
下面用的是Acwing中的算法基础课的浮点数二分的题目
求的是一个数的立方根,例如输入1000.00 输出是10.00。
https://www.acwing.com/problem/content/792/
本题,while中的判断条件只要符合r - l 的差值为特别小的数,使得近似相同,然后反回结果即可
#include<iostream>
using namespace std;
int main()
{
double x;scanf("%lf",&x);
double l = -10000,r = 10000;
while(r-l>1e-8)
{
double mid = (l+r)/2;
if(mid*mid*mid>=x) r = mid;
else l = mid;
}
printf("%lf",l);
return 0;
}
二、双指针(重要)
思路:双指针常规思路就是设定一个快指针,一个慢指针,然后根据题目的某种性质,进行指针的移动,从而达到解题目的。如下:
下面为力扣题目
https://leetcode.cn/problems/remove-element/
1.两个指针均从起点开始(一个数组)
int removeElement(vector<int>& nums, int val) {
if(nums.size() == 0) return 0;
int fast = 0; int slow = 0;
while(fast != nums.size())
{
if(nums[fast] != val){
nums[slow++] = nums[fast++];
}
else fast ++;
}
return slow;
2.两个指针一前一后(两个数组)
下面为Acwing算法基础课中双指针的题目
#include<iostream>
using namespace std;
const int N = 1e6+10;
int a[N],b[N];
int main()
{
int n,m,x;cin>>n>>m>>x;
for(int i = 0; i<n; i++) cin>>a[i];
for(int i = 0; i<m; i++) cin>>b[i];
int i = 0;int j = m-1;
while(i < n && j < m){
if(a[i]+b[j]<x) i++;
else if(a[i]+b[j]==x){
printf("%d %d",i,j);
break;
}
else j--;
}
return 0;
}
3、滑动窗口(双指针类型的)
下面是力扣的题目链接
本题的滑动窗口就是动态的截取数组中的一部分,然后判断是否满足某种性质。
int minSubArrayLen(int target, vector<int>& nums) {
int i = 0; int j = 0;
int res = 1e9 + 10;
int sum = 0;
for(j = 0; j<nums.size(); j++){
sum += nums[j];
while(sum >= target)
{
res = min(res, j - i + 1);
sum -= nums[i++];
}
}
if(res == 1e9 + 10) return 0;
return res;
三、模拟(考察代码能力)
该类型没有具体算法类型,根据题目类型具体分析,模拟代码运行逻辑从而解题。
下面是经典模拟题目
四、哈希表(重要)
1.数组模拟实现实现(常用)
建立数组,通常用来记录某个数字出现过几次。
题目如下: 有效的字母异位词
bool isAnagram(string s, string t) {
if(s.size() != t.size()) return false;
int hash1[27] = {0}; int hash2[27] = {0};
for(int i = 0; i<s.size(); i++){
hash1[s[i] - 'a']++;
hash2[t[i] - 'a']++;
}
for(int i = 0 ;i < 27; i++)
{
if(hash1[i] != hash2[i]) return false;
}
return true;
}
2.unordered_map容器实现(常用)
该容器通常是key与value有对应关系时使用。
如下面题目:
此处对应的是每个数对应一个数组中的下标。
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> res;
vector<int> tem;
for(int i = 0; i<nums.size(); i++)
{
unordered_map<int,int>:: iterator it = res.find(target - nums[i]);
if(it != res.end()){
return {i, it->second};
}
res.insert(pair<int,int>(nums[i], i));
}
return {};
}
3.unordered_set集合实现
下方题目链接:
此处set集合主要应用于判断是否有重复的数,例如本题,只要有重复数字,表示将会进入死循环。
int get_num(int n)
{
int tem = 0;
while(n)
{
tem += (n % 10) * (n % 10);
n /= 10;
}
return tem;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1)
{
int tem = get_num(n);
if(tem == 1) return true;
if(set.find(tem) != set.end()){
return false;
}
else set.insert(tem);
n = tem;
}
五、位运算。
1.位运算符&、|、^、~、x与 -x的基本概念
1.&的运算法则是有0为0,全1为1。
2.|的运算法则是有1为1,全0为0。
3.^的运算法则是相同为0,相异为1。
4、-x = ~x + 1;
5.~x = (x所有二进制位置取反)。
eg: 6(2进制) : 110
5(2进制) : 101
6 & 5 = 100(2进制表示)。6 | 5 = 111。6^5 = 011。
以下为代码运行结果:
a & b = 4
a | b = 7
a ^ b = 3
2.具体题目
题目链接:
解法1:x =x - (x & -x)
#include<iostream>
using namespace std;
const int N = 100010;
int arr[N];
int main()
{
int n;scanf("%d",&n);
while(n--)
{
int tem = 0,x;
scanf("%d",&x);
while(x)
{
x-=(x&-x);
tem++;
}
printf("%d ",tem);
}
}
解法2:x&(x - 1)
#include<iostream>
using namespace std;
const int N = 1e5+10;
int main()
{
int n;cin>>n;int x;
for(int i = 0;i<n;i++){
cin>>x;
int tem = 0;
while(x){
x=(x&(x-1));
tem++;
}
cout<<tem<<" ";
}
return 0;
}
六、排序
1.快速排序
废话不多说,直接上模板
下面是对一个乱序数组进行升序排序。
void quick_sort(int arr[], int l, int r)
{
if(l >= r) return;
int mid = l + (r - l) / 2;
int i = l -1; int j = r + 1;
while(i < j)
{
do i++; while(arr[i] < arr[mid]);
do j--; while(arr[j] > arr[mid]);
if(i < j) swap(arr[i], arr[j]);
}
quick_sort(arr, l, j); quick_sort(arr, j + 1, r);
}
2.归并排序
直接模板走起
void merge_sort(int* arr,int l,int r)
{
if(l>=r) return;
int mid = l+r>>1;
merge_sort(arr,l,mid);merge_sort(arr,mid+1,r);//此处利用递归将数组变为升序。
int i = l;int j = mid+1;int k = 0;
while(i<=mid&&j<=r)
{
if(arr[i]<arr[j]) tem[k++] = arr[i++];
else
tem[k++] = arr[j++];
}
while(i<=mid) tem[k++] = arr[i++];//扫尾工作
while(j<=r) tem[k++] = arr[j++];//扫尾工作
for(int i = 0,j=l;j<=r;j++,i++)//物归原主
{
arr[j] = tem[i];
}
}
七、高精度加减乘除(适用于数据特别大)
1.高精度加法
此处我们从各位开始相加,利用t来记录相加的值,然后根据t % 10确定当前位置的值。
根据t / 10是否为0 确定下位是否进1。
#include<iostream>
using namespace std;
#include<vector>
#include<string>
vector<int> add(vector<int>& A, vector<int>& B)
{
if(A.size() < B.size()) return add(B, A);//此处确保A表示的是最大数量的数组
vector<int> C;
int t = 0;
for(int i = 0; i<A.size(); i++){
t += A[i];
if(i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
}
if(t) C.push_back(1);
return C;
}
int main()
{
string a,b;
cin>>a>>b;
vector<int> A,B;
for(int i = a.size() - 1; i>=0; i--){
A.push_back(a[i] - '0');
}
for(int i = b.size() - 1; i>=0; i--){
B.push_back(b[i] - '0');
}
vector<int> C = add(A, B);
for(int i = C.size() - 1; i >= 0; i--)
{
cout<<C[i];
}
return 0;
}
2.高精度减法
首先减法我们首先要确定是大减小还是小减大,正负情况区分开来求解。
随后,同样用t 记录差,然后如果小于零,那么t = 1, 方便下次计算进行减去操作。
#include<iostream>
using namespace std;
#include<vector>
#include<string>
bool cmp(vector<int>& A,vector<int>& B)
{
if(A.size()!=B.size()) return A.size()>B.size();
else{
for(int i = A.size()-1;i>=0;i--){
if(A[i]!=B[i]) return A[i]>B[i];
}
}
return true;
}
vector<int> sub(vector<int>&A,vector<int>&B)
{
vector<int>C; int t = 0;
for(int i = 0; i<A.size(); i++)
{
t = A[i] - t;
if(i < B.size()) t = t - B[i];
C.push_back((t + 10) % 10);
if(t < 0) t = 1;
else t = 0;
}
while(C.back() == 0 && C.size() > 1){
C.pop_back();
}
return C;
}
int main()
{
string a,b;
cin>>a>>b;
vector<int> A,B;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}
for(int i = b.size()-1;i>=0;i--){
B.push_back(b[i]-'0');
}
if(cmp(A,B)){
vector<int> C = sub(A,B);
for(int i = C.size()-1;i>=0;i--){
cout<<C[i];
}
}
else{
cout<<"-";
vector<int> C = sub(B,A);
for(int i = C.size()-1;i>=0;i--){
cout<<C[i];
}
}
return 0;
}
3.高精度乘法
此处的乘法,我们对第一个数据采取同样处理,但是我们将第二个数看作一个整体,即b与A的个位开始乘,同时记录各个乘完的和,然后取个位,for循环判断时候,需要加t非0。因为很可能当i遍历完A,t还没有完全放进vector中。
#include<iostream>
using namespace std;
#include<vector>
#include<string>
vector<int> mul(vector<int>& A,int b)
{
int t = 0;vector<int> C;
for(int i = 0;i<A.size() || t;i++){
if(i<A.size()) t+=b*A[i];
C.push_back(t%10);
t/=10;
}
while(C.size()>1&&C.back()==0){
C.pop_back();
}
return C;
}
int main()
{
string a;int b;
cin>>a>>b;
vector<int>A;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}
vector<int> C = mul(A,b);
for(int i = C.size()-1;i>=0;i--){
cout<<C[i];
}
return 0;
}
4.高精度除法
余数用r来表示,r从高位开始相加,reverse函数是因为在进行操作的时候,我们最高位可能是0,reverse之后0尾部方便去除,打印继续反着打印即可。
#include<iostream>
using namespace std;
#include<string>
#include<vector>
#include<algorithm>
vector<int> div(vector<int>& A,int b,int& r)
{
vector<int> C;
r = 0;
for(int i = A.size() - 1; i >= 0; i--){
r = r*10 + A[i];
C.push_back(r / b);
r %= b;
}
reverse(C.begin(), C.end());
while(C.back() == 0 && C.size() > 1){
C.pop_back();
}
return C;
}
int main()
{
string a;int b;int r;
cin>>a>>b;
vector<int>A;
for(int i = a.size()-1;i>=0;i--)
{
A.push_back(a[i]-'0');
}
r = 0;
vector<int> C = div(A,b,r);
for(int i = C.size()-1;i>=0;i--){
cout<<C[i];
}
printf("\n%d",r);
return 0;
}
八、前缀和与差分
1.前缀和
1、一维数组
a[i] 表示每部分的单个数值,此处核心公式为s[i] = s[i - 1] + a[i]。转换一下就是,s[i] - s[i - 1] = a[i]。s[i]表示前缀和。
#include<iostream>
using namespace std;
const int N = 1e6+10;
int a[N],s[N];
int main()
{
int n,m;scanf("%d%d",&n,&m);
for(int i = 1;i<=n;i++)
{
cin>>a[i];
}
for(int i = 1; i<=n; i++){
s[i] = s[i - 1] + a[i];
}
while(m -- )
{
int l,r;cin>>l>>r;
printf("%d\n",s[r] - s[l-1]);
}
return 0;
}
2.二维矩阵
s表示矩阵左上角所有数的和,a表示每个部分的数字。
重要公式 :s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
下面这则是根据s[i][j]性质求{x1,y1} 和{x2, y2}之间所有数的和。
int x1,y1,x2,y2;cin>>x1>>y1>>x2>>y2;
printf("%d\n", s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]);
#include<iostream>
using namespace std;
const int N = 1010;
int a[N][N],s[N][N];
int main()
{
int n,m,q;cin>>n>>m>>q;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m ;j++){
cin>>a[i][j];
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
}
}
while(q -- )
{
int x1,y1,x2,y2;cin>>x1>>y1>>x2>>y2;
printf("%d\n", s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]);
}
return 0;
}
2.差分
1.一维数组
此处关键代码 b[i] = a[i] - a[i - 1]。
a[i] 其实就相当于 b[i] 的前缀和。
#include<iostream>
using namespace std;
const int N = 1e6+10;
int a[N],b[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i = 1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i = 1; i<=n; i++){
b[i] = a[i] - a[i - 1];
}
while(m -- )
{
int l,r,c;cin>>l>>r>>c;
b[l] += c;b[r + 1] -= c;
}
for(int i = 1; i <= n; i++) {
a[i] = b[i] + a[i - 1];
}
for(int i = 1; i<=n; i++) cout<<a[i]<<" ";
return 0;
}
2.二维矩阵
此处需要需注意,b[i][j] 表示的是a[i][j] 的差分矩阵:
关键公式
1、b[i][j] = a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1]; (此处根据公式赋予b[i][j]差分矩阵的性质)
2、b[x1][y1]+=c,b[x1][y2+1]-=c,b[x2+1][y1]-=c,b[x2+1][y2+1]+=c;则是表示了每个范围中的加减的情况,注意,b[i][j] 加减影响的是右下角。 这是因为其差分矩阵的性质。
3.处理结束后
a[i][j] = b[i][j]+a[i-1][j]+a[i][j-1]-a[i-1][j-1];将原数组重新加上b[i][j]。实现数据的回归。
最后a[i][j]就是最后的结果。
#include<iostream>
using namespace std;
const int N = 1010;
int a[N][N],b[N][N];
int i,j,n,m,q;
int main()
{
scanf("%d%d%d",&n,&m,&q);
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)scanf("%d",&a[i][j]);
//构建差分数组
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)b[i][j] = a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1];
while(q--)
{
int x1,y1,x2,y2,c;
scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&c);
b[x1][y1]+=c,b[x1][y2+1]-=c,b[x2+1][y1]-=c,b[x2+1][y2+1]+=c;
}
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)a[i][j] = b[i][j]+a[i-1][j]+a[i][j-1]-a[i-1][j-1];
for(i=1;i<=n;i++)
{
for(j=1;j<=m;j++)
{
printf("%d ",a[i][j]);
}
printf("\n");
}
return 0;
}
总结
以上为一部分学过的较基础和重要的算法的总结。(主要是建立一个体系)