数据结构-基础算法

前言
理解算法思路,背代码模板,用相应的模板进行默写,加深记忆;
能够默写后,删除重写4-5遍将模板记熟

一、排序

1.1快速排序

算法思想:

分治;
步骤:
在这里插入图片描述
重点为第二步,调整区间。

算法模板:

暴力做法:
在这里插入图片描述
开辟两个额外空间,用于存放大于和小于x的值,然后迭代;
非暴力做法:
在这里插入图片描述
代码实现:

void quick_sort(int q[],int l, int r)//q[]传入某一个数组,不需要指定大小,传入时只要传入数组首地址就行(q或者&q[0])
{
   if(l >= r) return;//void-return

   int  x = q[l],i = l - 1, j = r +1;//算法在一次交换后会进行一次一位,然后再判断,思因此为了保证第一次移动后在边界上(不然考虑不到边界),需要留出一格的余量

   while(i < j )
   {
      do i++;
      while (q[i]<x);
      do j--;
      while (q[j]>x);
      if(i < j)
      swap(q[i],q[j]);
   }
   //对两段进行递归排序
   quick_sort(q,l,j);
   quick_sort(q,j+1,r);//如果写i的话,(q,l,i-1),(q,i,r) 那么x不能取到q[l](左边界),否则死循环,写j的话,右边界同理,x不能取到q[r]
}

1.2归并排序

算法思想:

分治;
步骤:
在这里插入图片描述
方法:
先进行l和r部分的递归,此时left和right部分以及是有序数组,然后通过如下方法进行两个有序序列的合二为一
在这里插入图片描述

代码实现:

void merge_sort(int q[],int l,int r)
{
   if ( l >= r) return;
   int mid = (l + r)/2,i = l,j = mid + 1;//确定中点和分界点

   merge_sort(q,l,mid),merge_sort(q,mid+1,r);//递归左右两边
  //进行归并
   int k = 0 ; 
   while(i <= mid && j <= r)
   {
      if(q[i] <= q[j]) tmp[k++] = q[i++];
      else if(q[i] > q[j]) tmp[k++] = q[j++];
   }//写法记住
   //把没有遍历到的部分直接接到后面
   while(i <= mid)
   tmp[k++] = q[i++];
   while(j <=r)
   tmp[k++] = q[j++];
   //把tmp里的数放回q里面
   for (i = l,j = 0;i <=r;i++,j++)
   q[i] = tmp[j];
}

时间复杂度

快排,归并 o(nlogn)

二、二分查找

将数组分为两部分每部分一种性质,二分是在查找两个性质的边界点

2.1 整数二分

mid包含在左边和包含在右边,处理是不同的

代码实现:

case1:mid在左边

//===二分查找======case1:mid在左边
int divide_two_1(int q[],int x,int l,int r)
{
   if(l >= r) return 0;
   while(l<r)
   {
      int mid = (r+l)/2;//向下取整
      if(q[mid] >= x)r = mid;//r = mid,则mid不需要+1
      else l = mid + 1;
   }
   return l;
}

case2:mid在右边

//===二分查找======case2:mid在右边
int divide_two_2(int q[],int x,int l,int r)
{
   if(l>=r) return 0;
   while(l < r)
   {
      int mid = (l + r +1)/2;
      if(mid <= x ) l = mid;//l = mid,则mid需要+1
      else r = mid-1;
   }
   return r;//写l和r一样,最终l = r
}

区别在于到底是l = mid(要+1)(若不+1会死循环)还是r = mid

2.2 浮点数二分

相比整数二分查询更加简单,不用判断边界

代码实现:

//用二分法求根号x的值
 int main()
 {
   double x;
   cin >> x;
   double l = 0,r = x;
   while(r - l >1e-6)
   {
      double mid = (r + l)/2;
      if(mid*mid >= x) r = mid;//mid和根号x进行比较
      else l = mid;
   }
   cout << l;
   return 0;
 }

三、高精度运算

c++中没有大整数的类型,一般不用整型来存储,用数组来存储,对数组进行运算成为高精度运算

3.1两个大整数相加:A + B

思路:
在这里插入图片描述
每一位对应相加,然后有一个t来表示进位

代码实现:

vector<int> add(vector<int> &A,vector<int> &B)
{
   vector<int> C;
   int t = 0;//用t来表示对应位数相加的和
   for(int i = 0; i < A.size() ||i <B.size(); i++ )
   {
      if(i < A.size())
      t += A[i];
      if(i < B.size())
      t += B[i];
      //if(t > 9)
      //k = 1
   //用取余来表示此位相加结果
      C.push_back(t % 10);
   //用整除来表示要进的位
      t /= 10;
   }
   if(t)
   C.push_back(t);
   return C;
}
   cin>>a>>b;//
//==将大整数存入vector里面,倒着存入,在string里低位在末尾,push_back到vector里面后低位就在开头了
int main()
{
	...
	vector<int> A,B;
   	for(int i = a.size() - 1;i>= 0; i--)
      A.push_back(a[i] - '0');//存的是字符,要减去‘0’得ascii码才是对应的数字
   	for(int i = b.size() - 1; i>= 0;i--)
      B.push_back(b[i] - '0');
	auto c = add(A,B);
	for(int i = c.size() - 1; i >= 0; i--)
	cout << c[i];
	...
}

3.2大整数减法

思路:
在这里插入图片描述
t为被借位,需要判断Ai和Bi的大小关系

代码实现:

大整数大小判断:

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> substr(vector<int> &a,vector<int> &b)
{
   int t;
   vector<int> c;
   for(int i = 0;i < a.size()|| i <b.size();i++ )//
   {
      //==我的写法=====(错误,已改正)
      if(i < a.size())
      t +=a[i];
      if(i < b.size())
      t -=b[i];
      //c.push_back(t);没有区分a[i]-b[i]结果的正负,负的结果不应该直接存入
      if(t>=0)
      c.push_back(t);
      else
      c.push_back(t+10);
      //==标准写法=====(正确)
      // t = a[i] + t;
      // if(i < b.size())
      // t -= b[i]; 
      // c.push_back((t+10)%10);//用(t+10)%10替代了两种情况的判断
      //==============
      if(t < 0)
      t = -1;
      else
      t = 0;
   }   
   while(c.size() >1 &&c.back() == 0)c.pop_back();//移除多余的0,尾部开始移除,因为倒着存的,尾部就是最高位的多余的0
   return c;
}
int main()
{
	...
   if(cmp(A,B))
   {
      auto c = substr(A,B);
      for(int i = c.size() - 1; i >= 0; i--)
      cout << c[i];
   }
   else
   {
      auto c = substr(B,A);
      cout<< "-";
      for(int i = c.size() - 1; i >= 0; i--)
      cout << c[i];
   }
   ...
} 

3.3高精度整数乘以低精度整数

思路:
在这里插入图片描述

代码实现:

//==乘法======高精度整数乘以低精度整数
vector<int> mul(vector<int> &A,int b)
{
   
   vector<int> c;
   int t;
   for(int j = 0;j<A.size()||t;j++)//进位t不为零的话仍会进行循环
   {
      if(j<A.size()) 
      t +=b*A[j];
      c.push_back(t%10);
      t/=10;
   }
   return c;
} 
int main()
{
   ...
   string a;
   int k;
   vector<int> A;
   cin>>a>>k;
   for(int i = a.size() - 1;i>= 0; i--)
      A.push_back(a[i] - '0');//存的是字符,要减去‘0’得ascii码才是对应的数字
   auto c = mul(A,k);
   for(int i = c.size() - 1; i >= 0; i--)
   cout << c[i];
   ...
}

3.4 高精度整数除以低精度整数

思路:
在这里插入图片描述

代码实现:

//==除法===高精度整数除以低精度整数
//除法从高位开始算,一般单独计算除法时可以首部存高位,但为了和+-*兼容,还是让低位存储在首部
//A[i]/b=商[i],t=A[i]%b为余数
vector<int> div(vector<int> &A,int b,int &k)//此处比较巧妙,除法需要返回商和余数,所以我们用int &k来表示余数,使用引用可以该改变外部k的值
{
   vector<int> c;
   k = 0;
   for( int i =A.size()-1;i >= 0;i--)
   {
      k = k*10 + A[i];
      c.push_back(k/b);
      k%=b;
   }
//c.back() 和 c.end() 的区别在于它们的返回值类型和语义不同。
//c.back() 返回一个元素的引用,表示容器中最后一个元素,
//而 c.end() 返回一个迭代器,指向容器中最后一个元素之后的位置
//c.begin()是第一个元素,c.end()-1=c.back();
   reverse(c.begin(),c.end());//需要传入末尾,即最后一位的下一位
   //把前面多余的0去掉,因为是倒着存的,所以弹出最后一位就是弹出最高位
   while(c.size()>=1&&c.back()==0)c.pop_back();
   return c;
}
int main()
{
   ...
   string a;
   int b,r;
   vector<int> A;
   cin>>a>>b;
   for(int i = a.size()-1;i >= 0; i--)
      A.push_back(a[i] - '0');
   auto c = div(A,b,r);

   for(int i = c.size()-1;i >= 0;i--)
      cout<<c[i];
   cout<<"余"<<r;
   return 0;
   ...
}

四、前缀和

4.1前缀和(前n项和)

代码实现:

#include<iostream>
using namespace std;
const int N = 1e6+10;
int n,m;
int a[N],s[N];
int main()
{
//==========求数组某一区间的和
    cin>>n;
    for(int i = 0; i < n; i++)
        cin>>a[i];
    s[0] = a[0];
//求前缀和
    for(int i = 1; i < n; i++)
        s[i] = s[i-1] + a[i];
//使用前缀和
    int r,l;
    cin>>r>>l;
    cout<<s[l] - s[r-1];
    return 0;
}

4.2求子矩阵的和

思路:
在这里插入图片描述

代码实现:

#include<iostream>
using namespace std;
const int N = 1010;
int n,m,q;
int a[N][N],s[N][N];
int main()
{
//========求子矩阵的和=====
    cout<<"test2";
    cin>>n>>m>>q;
    for(int i = 1;i <= n; i++)
        for(int j = 1;j <= m; j++)
            cin>>a[i][j];
    
    for(int i = 1;i <= n; i++)
        for(int j = 1;j <= m; 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;
        cout<<s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]<<endl;
    }
    
    return 0;
}

4.3差分

一维差分

(构造b不重要,只要满足ai=bi,那么ai就为前缀,bi就是差分)
在这里插入图片描述

应用:对一个数组的[l,r]部分,每个数加上C

题目描述:
输入一个长度为n的整数序列。
接下来输入m个操作,每个操作包含三个整数l, r, c,表示将序列中[l, r]之间的每个数加上c.请你输出进行完所有操作后的序列。

思路:
把b[l]+c,那么a[l]之后所有点都会+c,然后令b[r+1]-c,那么a[r+1]以及其后的数都会-c,这样就能够保证只有[l,r]+c;
若使用暴力解法需要遍历整个数组,时间复杂度o(n),使用差分操作时间复杂度o(1);

代码实现:

若假设任意a[i]=0,如果在[i,i]中加入a[i]可以实现数组中数插入

#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int a[N],b[N];
void insert(int l,int r ,int c)
{
    b[l]+=c;
    b[r + 1]-=c;
}
int main()
{
//===差分=== 
//本代码中写循环均为i = 1起始,实际上相当于没有使用a[0]而已,给出数组空间很大,够用;
//这里这样写是因为后面涉及b[i] +=b[i-1],需要留一位冗余项
    cin>>n>>m;
    for(int i = 1;i <= n;i++)
        cin>>a[i];
    //应用插入,将a[i]赋值给b[i]
    for(int i = 1;i <= n;i++)
        insert(i,i,a[i]);
    while(m--)
    {
        int l,r,c;
        cin>>l>>r>>c;
        insert(l,r,c);//唯一的重点操作
    }
    //对自己进行差分,应用差分思想
    //1.b[n] = 所有b[i]之和 (不针对n可行,针对任意i也b[i]=sum(k=0-i)b[k]),因此b[i]时自己的差分
    //可以这样理解,对数组b[n]求前缀和,但是为了减少数组的使用,直接把结果更新到b[n]上,
    //而在这之前已经执行了insert操作,那么更新后的结果就是+c的结果
    for(int i = 1; i<= n; i++)b[i] +=b[i-1]; 
    for(int i = 1; i <=n; i++)cout<<b[i];
    return 0;
}

2.二维差分

(同样不用考虑如何构造)在这里插入图片描述
(对于矩阵中的(xi,yi)点来说,这一点差分+c,则后续所有大于xi和yi的点的前缀和都+c,反之-c同理)

应用:

输入一个n行m列的整数矩阵,再输入q个操作,每个操作包含五个整数x1, y1, x2, y2,c,其中(x1,y1)和(x2, y2)表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上c。请你将进行完所有操作后的矩阵输出。

代码实现
#include<iostream>
using namespace std;
const int N = 1010;
int n,m,q;
int a[N][N],b[N][N];
void insert_mat(int x1,int y1,int x2,int y2,int c)
{
    b[x1][y1]+=c;
    b[x2+1][y1]-=c;
    b[x1][y2+1]-=c;
    b[x2+1][y2+1]+=c;
}
int main()
{
//====二维前缀和
    cin>>n>>m>>q;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin>>a[i][j];
    for(int i = 1;i <= n;i++)
        for(int j = 1;j <= m;j++)
            insert_mat(i,j,i,j,a[i][j]);
    while(q--)
    {
        int x1,x2,y1,y2,c;
        cin>>x1>>y1>>x2>>y2>>c;
        insert_mat(x1,y1,x2,y2,c);
    }
    for(int i =1;i <= n;i++)
        for(int j = 1;j <= m;j++)
            b[i][j] +=b[i-1][j] + b[i][j-1]-b[i-1][j-1];
    for(int i =1;i <= n;i++)
        {
            for(int j = 1;j <= m;j++)
            cout<<b[i][j];
            puts("");//输出换行符
        } 
    return 0;
}

五.双指针算法

1.类型

1.两个指针指向同一个序列
2.两个指针指向两个不同序列

在这里插入图片描述

2.基本结构

大部分双指针算法都是这样的结构:
先更新i,然后进行j的更新,while中先判断j的范围,然后检查是否某一种性质,都满足就j++。
在这里插入图片描述
核心用途:
降低时间复杂度,双指针虽然存在两层但每一层都执行次数都小于n,整体小于2n,因此可以把如下的双重循环的暴力做法,复杂度优化到o(n)
在这里插入图片描述

3.举例

思考方式:先想暴力做法,再优化

3.1输入字符串,把每个单词输出

#include <iostream>
#include <string.h>
using namespace std;
//输入一个字符串,把每个单词输出处理(单词以空格隔开)
int main()
{
    char str[1000];

    gets(str);

    int n = strlen(str);

    for(int i = 0;str[i];i++)
    {
        int j = i;
        while(j < n && (str[j]!=' '))j++;//使得i,j指向一段的两端
        //输出这段字符
        for(int k = i;k<j;k++)cout<<str[k];
        cout<<endl;
        i = j;
    }
    return 0;
}

3.2 给定一个长度为n整数序列,找到一个最长的不包含重复数字的连续子序列输出其长度

思路:红指针向右遍历,绿指针用来找举例红指针的最大距离,然后如果比之前的距离大就覆盖
暴力求解:
在这里插入图片描述

在这里插入图片描述
双指针算法:
在这里插入图片描述
在这里插入图片描述

j位置具有单调性,i往后走,j不能往左走(往左的话[j,i’]不成立那么[j’,i’]也不成立),因此只需要j++,j不用每次都遍历数组,这个问题由于这个单调性才能用双指针优化
代码实现:

int n;
const int N = 100000;
int a[N],s[N];//a[N]是总长度,S[N]是中间ij段长度
int main()
{
    cin>>n;
    for(int i = 0;i < n;i++)cin>>a[i];
    int res = 0;
    for(int i = 0,j = 0;i < n;i++)
    {
        s[a[i]]++;//记录每个数字出现的次数   
        while(j<=i&&s[a[i]] > 1)//s[a[i]] > 1表示这个数字出现过了,即出现重复,此时就要向右移动右指针
        {
            s[a[j]]--;//由于要准备向右移动j指针,则要把之前j指针指向的数排除,即使其对应的数出现次数减一
            j++;//右移j指针
        }
        res = max(res,i - j + 1);
    }
    cout<<res<<endl;
    return 0;
}

精髓点:用s[N]来计数数字出现次数

二.位运算

2.1 n的二进制表示中第k位是几(个位开始 )

核心思路:
n>>k&1
(n>>k表示将n表示的二进制数右移三位,&同1异0)
代码:

int main()
{
    int n = 10;
    for(int k =3;k >= 0;k--)cout<<(n>>k&1)<<endl;
    return 0;
}

返回每一位对应的二进制数是多少,即二进制转换

2.2 返回n的最后一位1

核心思路:
x&-x
-x=~x+1(补码)
在这里插入图片描述
代码:实现统计输入数字二进制中1的个数

int lowbit(int x)
{
    return x&(~x+1);
}
int main()
{
    int n ;
    cin >> n;//n表示可以多次输入,计算多个数
    while(n--)
    {
        int x;
        cin >> x;
        int res = 0;
        while(x)
        {
            x -= lowbit(x);//返回的只有最低为1的数,1的位置不变,而且因为只有最低位一样,其余位置为0.相减只有最低位消失
            res++;
        }
    cout << res;
    }
    return 0;
}

三.离散化

对于一组有序的数,值很大但是个数很小,开一个大数组很浪费,我们就需要把这些映射到从零开始的连续自然数,此过程被称为离散化,
在这里插入图片描述
存在注意问题:
1.a中可能有重复元素;
2.如何算出a里面每一个值映射后为多少,即找出x在a中的下标为多少,二分查找;
模板步骤:
在这里插入图片描述
应用:给定一个无限长的数轴,数值全为0,现在有n个操作,每次操作将对应的位置x上数加c,接下来,进行m次询问,每次询问包含两个整数的和,你需要求出区间[l,r]之间所有俩个数和的数组集合

代码实现:


typedef pair<int,int> PII;//pair 是一个标准库类型,表示两个值的有序组合,通常用于需要返回两个值的函数、数据结构和算法中

const int N = 300000;

int n,m;//
int a[N],s[N];//a数组是存的数,s数组是前缀和

vector<int> alls;//所有需要离散化的值
vector<PII> add,query;

int find(int x)
{
    int l = 0,r = alls.size() - 1;
    while(l < r)
    {
        int mid = l + r >> 1;//>>1相当于除以2
        if(alls[mid] >= x) r = mid;
        else l = mid + 1;
    } 
    return r + 1;
}

int main()
{
    cin>>n>>m;
    for(int i = 0; i < n;i++)
    {
        int x,c;
        cin>>x>>c;
        add.push_back({x,c});
        alls.push_back(x);
    }
    for(int i = 0; i < m;i++)
    {
        int l,r;
        cin>>l>>r;
        query.push_back({l,r});
        //????
        alls.push_back(l);
        alls.push_back(r);
    }
    //去重复
    sort(alls.begin(),alls.end());
    alls.erase(unique(alls.begin(),alls.end()),alls.end());
    //处理插入
    for(auto item :add)
    {
        int x = find(item.first);
        a[x] += item.second;
    }
    //预处理前缀和
    for(int i = 1; i <=alls.size();i++) 
    s[i] = s[i - 1] + a[i];
    //处理询问
    for(auto item :query)
    {
        int l = find(item.first),r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }
    return 0;
}

四.区间合并

将有交集的区间进行合并

举例:
在这里插入图片描述
将上述区间合并之后,如下图:
在这里插入图片描述

得到三个区间
步骤:
1.按照区间左端点把每个区间进行排序
2.扫描所有区间,扫描过程中将所有有交集的区间进行合并
代码实现:

using namespace std;

typedef pair<int,int> PII;

const int N = 100000;

int n;

vector<PII> segs;

void merge(vector<PII> &segs)//和全局的segs重名了会不会有影响
{
    vector<PII> res;
    sort(segs.begin(),segs.end());

    int st = -2e9,ed = -2e9;
    for(auto seg : segs)
        if(ed < seg.first)
        {
            if(st != -2e9)
            res.push_back({st,ed});
            st = seg.first,ed = seg.second;
        }
        else
        ed = max(ed,seg.second);
    if(st !=-2e9)res.push_back({st,ed}); 

    segs = res;
}
int main()
{
    cin >> n;
    for(int i = 0;i < n;i++)
    {
        int l,r;
        cin >>l>>r;
        segs.push_back({l,r});
    }
    merge(segs);

    cout << segs.size() << endl;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值