《算法笔记》第四章
排序
排序算法的思想都比较简单,而且c++可以直接用sort函数进行排序,一般不会直接写排序代码
归根到底就是每轮处理一个数据,n个数据只需处理n-1次即可变得有序
选择排序
最简单的排序,每轮找出一个最值移到相应位置即可,代码
void selectsort(){
for(int i=1;i<=n-1;i++){
int k=i;
for (int j=k;j<=n;j++){
if(a[j]>a[k]){
k=j;
}
}
if(i!=k){
int temp=a[i];
a[i]=a[k];
a[k]=temp;
}
}
插入排序
每轮将一个数据插入已排好的数据中,代码
void insertsort(){
for(int i=2;i<=n;i++){
int temp=a[i],j=i;
while(j>1&&temp<a[j-1]){
a[j]=a[j-1];
j--;
}
a[j]=temp;
}
}
sort函数
使用前需加上头文件
#include<algorithm>
using namespace std;
sort(首元素地址,尾元素地址的下一个地址,比较函数(非必填));
如不写比较函数,则默认升序
比较函数
bool cmp(int a,int b){
return a>b;//可以理解为当a>b时把a放在b前面,这样就实现了降序
}
对结构体排序
定义如下结构体
struct node{
int x,y;
}ssd[10];
则比较函数为
bool cmp(node a,node b){
return a.x>b.x;//按x值降序
}
如需二级排序,也可以
bool cmp(node a, node b){
if(a.x != b.x){return a.x>b.x;}
else{return a.y>b.y;}
}
散列
散列对于我来说是一个之前从来没有接触过的全新的算法思想
其核心思想是用空间换时间,用数组的下标来表示数据,代码如下
int hashtable[maxn]={0};
int main(){
int n,m,x;
scanf("%d%d",n,m);
for(int i=0;i<n;i++){
scanf("%d",&x);
hashtable[x]++;
}
for(int i=0,i<m,i++){
scanf("%d",&x);
printf("%d",hashtable[x]);
}
return 0;
}
这段代码能输出m个数据分别在n个数据中出现的次数
由此,只要将数据的值转换为数组的下标,即可省去将数组遍历一遍的繁琐
除了正常的十进制整数之外,字符也可以通过一定的转化方式转化为哈希数组的下标,这里的思路是,将字符转化为一个整数使得这个整数能唯一地表示这个字符,这里可以根据字符的种类数n定义一个n进制的坐标,比如26个字母就可以将a转化为0,z转化为25,用26进制表示一个字符串。输入时将这个字符串转化为26进制等价的十进制数,这个十进制数就是该字符串的下标,可见这个下标对于特定的字符串是独一无二的。代码如下
int hashfunc(char s[],int len){
int id=0;
for(int i=0;i<len;i++){
id=id*26+(s[i]-'a')//此处注意a必须是单引号,因为单引号才代表字符,双引号代表的是字符串数组
}
return 0;
}
其实不难发现,这个转化的函数就是把s[i]-'a’转化为26进制。
上面仅考虑了小写字母,如果是一段正常的字符串,那么还有大写字母和数字,只需如法炮制,增大进制数为62即可。
递归
递归的核心思想就是反复调用自身。
递归的一个重要作用是分治,也就是把一个问题划分为若干个小问题,分别解决这些小问题然后合并解就是原问题的解。
最经典的递归函数就是求斐波那契数列了,代码如下
int F(int n){
if(n==1||n==2){return 1;}
else return F(n-1)+F(n-2);
}
递归函数最重要的两件东西:递归边界和递归式。
递归边界就是最简单问题的解,递归式就是把问题分解为简单问题的方式。
在求斐波那契数列中,F(1)=F(2)=1就是递归边界,F(n)=F(n-1)+F(n-2)就是递归式
运用递归和哈希的思想,还能实现一些其他算法,比如输出n个数的全排列,代码如下
void generateP(int index){
if(index == n+1){
for(int i=1;i<=n;i++){
printf("%d",P[i]);
}
printf("\n");
return;
}
for(int x=1;x<=n;x++){
if(hashtable[x]==false){
P[index]=x;
hashtable[x]=true;
generateP(index+1);
hashtable[x]=false;
}
}
}
运用这个全排列思想,还可以优化解决n皇后问题(即把n个皇后放在n*n棋盘上,要求不同行列和对角线),代码如下
void generateP(int index){
if(index==n+1){
bool flag=true;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(abs(i-j)==abs(P[i]-P[j])){
flag=false;
}
}
}
if(flag){count++;}
return;
}
for(int x=1;x<=n;x++){
if(hashtable[x]==false){
P[index]=x;
hashtable[x]=true;
generateP(index+1);
hashtable[x]=false;
}
}
}
可以发现,这段代码其实和之前求全排列代码就是上半部分有改变,上半部分其实就是递归边界,全排列的边界是index=n+1就输出,而n皇后其实就是在index=n+1的基础上多判断了一步对角线是否相等。
这个算法还可以进行一点优化,不用把全排列都列出来再判断,可以回溯之前已经摆下的棋子判断是否冲突(即是否在同一条对角线),如果冲突那就下一个,这样可以省去一些无用的计算,代码如下
void generateP(int index){
if(index==n+1){
count++;
return;
}
for(int x=1;x<=n;x++){
if (hashtable[x]==false){
bool flag=true;
for(int pre=1;pre<index;pre++){
if(abs(index-pre)==abs(x-P[pre])){
flag=false;
break;
}
}
if(flag){
P[index]=x;
hashtable[x]=true;
generateP(index+1);
hashtable[x]=false;
}
}
}
}
递归算是我学习以来比较难理解的一块内容了,代码虽然勉强能看懂,其实中间有很多细节是要靠自己细细体会的,慢慢来吧。
贪心算法
贪心算法主要用来解决求最优的问题,而很多情况下我们无法一下子实现全局最优,于是就可以考虑在当前状态下实现局部最优,然后达到全局最优。
简单的贪心算法比如给一串数字输出最大(小)数等等,这些难度都不大。
还有一类区间不相交的问题,即从一堆给定区间中找出尽可能多的不相交区间。
解决这个问题的思路是首先考虑有无区间包含,若有区间包含了另一个区间则将其舍去,因为小区间可以腾出更多的空间。其次只需按照左端点从大到小依次选下去即可(按右端点从小到大也行),核心代码如下
struct Inteval{
int x,y;
}I[maxn];
bool bmp(Inteval a,Inteval b){
if(a.x!=b.x){
return a.x>b.x;
}else return a.y<b.y;
}
int main(){
int n;
while(scanf("%d",&n),n!=0){
for(int i=0;i<n;i++){
scanf("%d%d",&I[i].x,&I[i].y);
}
sort(I,I+n,cmp);
int count=1,last=I[0].x;
for(int i=1;i<=n;i++){
if(I[i].y<=last){
last=I[i].x;
count++;
}
}
printf("%d",count);
}
return 0;
}
二分
二分的算法其实不仅仅是用在计算机上,平时生活中也经常用到二分的思想。
在面对未知的情况时,折中似乎是一种最有效的方法。不停地折中,逼近正确答案的概率也就越大。尤其是在面对有序数列时,二分就能知道答案存在的大致区间然后缩小这个区间,不用一个个寻找。
代码如下
while(left<=right){
mid=(left+right)/2;
if(a[mid]==x){
return mid;
}else if(a[mid]>x){
right=mid-1;
}else{
left=mid+1;
}
}
return -1;//查找失败
这是最简单的二分查找,在实际的二分查找中经常会遇到输出第一个满足某条件的位置,通用代码如下
while(left<right){//如果left和right相等,意味着找到唯一位置
mid=(left+right)/2;
if(条件){
right=mid;
}else{
left=mid+1;
}
}
return left;
这段代码能输出第一个满足该条件的位置,并且该条件必须在序列中从左到右先不满足后满足如果不是的话取反即可。
除了查找之外,二分法还有其他的作用,比如估计无理数的近似值
以 2 \sqrt{2} 2为例,定义一个函数 f ( x ) = x 2 f(x)=x^2 f(x)=x2,而 2 \sqrt{2} 2的值又必定在1和2之间,所以只需找出一个逼近 2 \sqrt{2} 2的值(这里设精度为 1 0 − 5 10^{-5} 10−5),初始的二分区间为[1,2],只要将区间缩小到 1 0 − 5 10^{-5} 10−5即可,代码如下
double f(double x){
return x*x;
}
double calSqrt(){
double left=1,right=2,mid;
while(right-left>1e-5){
mid=(right+left)/2;
if(f(mid)>2){
right=mid;
}else{
left=mid;
}
}
return mid;
}
再推广一下,可以用该方法解决任何求f(x)=0方程解的代码
double f(double x){
return 函数;
}
double calSqrt(){
double left=1,right=2,mid;
while(right-left>精度){
mid=(right+left)/2;
if(f(mid)>0){//若f(x)递减,则改为<0
right=mid;
}else{
left=mid;
}
}
return mid;
}
可以得出二分可以用来解决一类答案在一个单调区间内的问题,设置好精度和关系,不停地二分尝试即可。
快速幂
求高次幂经常会溢出而且要进行多步循环,所以可以用二分的思想把幂运算拆分,即 a 2 n = a n ∗ a n a^{2n}=a^n*a^n a2n=an∗an, a 2 n + 1 = a 2 n ∗ a a^{2n+1}=a^{2n}*a a2n+1=a2n∗a,运用这个思路就可以用递归的思想求高次幂,代码如下(计算 a b a^b ab%m)
long long binarypow(long long a,long long b,long long m){
if(b==0)return-1;
if(b%2==1)return a*binarypow(a,b-1,m)%m;
else{
long long mul=binarypow(a,b/2,m);
return mul*mul%m;
}
}
除了递归的思路之外,还可以用迭代的思想计算。
具体思路是把b看成二进制数,然后就可以把 a b a^b ab拆分成若干 a 2 k a^{2k} a2k的乘积,具体代码如下
long long binarypow(long long a,long long b,long long m){
long long ans=1;
whlie(b>0){
if(b&1){//b的末尾是否为1,即b%2=1
ans=ans*a%m;
}
a=a*a%m;
b>>=1;//将b右移一位,即b=b/2
}
return ans;
}
two pointers
这个不是具体的算法,可以说是一种算法思想吧,当用一个循环变量枚举比较复杂时,改用两个循环变量,可以大大降低算法复杂度
具体的例子就是,从有序数列中找出两个数使其和为M,常规的逻辑当然是双层循环一个个试,但这样当数列很大时O(n2)的复杂度就不太方便,而运用two pointers的思想则能大大优化,代码如下
while(i<j){
if(a[i]+a[j]==M){
printf("%d%d",i,j);
i++;
j--;
}else if(a[i]+a[j]>M){
j--;
}else{
i++;
}
}
归并排序
归并排序的思想是先把无序序列拆成若干个2个数的小序列,然后在小序列里面各自排序,排好后两两合并小序列继续排序,直到合并成一个序列。
快速排序
运用two pointers方法,从序列的两头将大于某个值的数放到一边,小于的放到另一边,遍历整个序列即可,代码如下。
int partition(int a[],int left,int right){
int temp=a[left];
while(left<right){
while(left<right && a[right]>temp){right--;}
a[left]=a[right];
while(left<right && a[left]<=temp){left++;}
a[right]=a[left];
}
a[left]=temp;
return left;
}
void quicksort(int a[],int left,int right){
if(left<right){
int pos=partition(a,left,right);
quicksort(a,left,pos-1);//对左边区间排序
quicksort(a,pos+1,right);//对右边区间排序
}
}
这个算法还可以用于判断a[left]是数组中第几大的数
其他技巧
- 在一些算法中(尤其是一些递归算法中),前面计算的结果可能在后面也用的上,这时就可以先把这些数据输出或者存储起来,等到后面再需要用到的时候就不用重新计算了,以递归法求斐波那契数列为例,越到后面计算量会变得很大,这时候来一个数组存储之前的数就能大大降低计算量
- 如果算法比较复杂容易超时,可以在本地运行出结果然后直接在题目里面输出,也可以在本地先小规模地试一下找找规律。