C语言-算法初步

在这里插入图片描述

1.排 序

1.1 选择排序 O(n2)

进行n趟操作,每一趟从待排序里,令第一个为最小,依次比较大小,找到最小的,再交换.

错误版本
int a[n]; 
for (int i=0;i<n-1;i++){
	int min=a[i];
	for(int j=i+1;j<n;j++){
		if (min>a[j]){
			min=a[j];//还没完,你只是赋值了,还要交换
			//但是交换写在这里,很不好,不如记下坐标,出了循环再交换
		}
	}
}
正确版本
int a[n]; 
for (int i=0;i<n-1;i++){
	int min=i;
	for(int j=i+1;j<n;j++){
		if (a[min]>a[j]){
			min=j;  //找到谁最小,记录下标
		}
	}
	int temp=a[min];  //这时候再交换!!!
	a[min]=a[i];
	a[i]=temp;
}

1.2 插入排序 O(n2)

进行n-1趟,让i从1始遍历到n-1.让a[i]插入到a[0]~a[i-1]中.使得a[0] ~ a[i]有序.再进行下一趟遍历. 具体插入过程是,在a[0] ~ a[i-1]找到位置j ,使a[i]插到a[j],使得原来的a[j] ~ a[i-1] 依次往后顺移一位.移动到a[j+1] , a[i]

int a[n];
for (int i=1;i<n;i++){
	temp=a[i];j=i;
	while(j>0&&temp<a[j-1]){
		a[j]=a[j-1];
		j--;
	}
	a[j]=temp;
}

1.3 sort函数

须加上

#include <algorithm> 
using namespace std;

sort(首元素地址(必填),尾元素地址的下一个地址(必填),比较函数(非必填))
eg:

int a[6]={9,4,2,5,6,-1};
sort(a,a+4) //对a[0]和a[3]进行排序
sort(a,a+6)//对a[0]和a[5]进行排序.全排序

写比较函数cmp

 //从大到小排序 :
 bool cmp(int a,int b){ return a>b;}
//结构体数组排序
//将x值从大到小对结构体数组node排序
 bool cmp (node a,node b){ return a.x>b.x;} 
 //如果想先按ⅹ从大到小排序,但当x相等的情况下,
 //按照y的大小从小到大来排序(即进行二级排序),那么cmp的写法是:
 bool cmp (node a,node b){
 if (a.x!=b.x) return a.x>b.x;
 else return a.y>b.y;} 
 //容器的排序
 //在STL标准容器中,只有 vector、 string、 deque是可以使用sort的。
 //这是因为像set、 map这种容器是用红黑树实现的(了解即可)
 //元素本身有序,故不允许使用sort排序
 //下面这个是按字符串长度从小到大排序的
 bool cmp(string strl, string str2){
 return strl.length()< str2.length()}

例子:

struct Student {
char name[10];
char id[10];
int score;
int r;
}
// 分数从高到低,若一样,则按名字字典序排,strcmp函数是string.h头文件下的,当str1<str2,返回负数.等于返回0
bool cmp(student a, student b){
 if (a.score!=b.score) return a.score>b.score;
 else return strcpm(a.name,b.name)<0;
}
//若要排名
std[0].r=1;
for (int i=1;i<n;i++){
if(stu[i].score==stu[i-1].score){
stu[i]r=stu[i-1].r;
}
else{
stu[i].r=i+1;
}
}

2.散 列(哈希)

2.1 直接把输入的数作为数组的下标来对这个数的性质进行统计(这种做法非常实用,请务必掌握)。这是一个很好的用空间换时间的策略.

2.2 散列(hash) 一般来说,散列可以浓缩成一句话“将元素通过一个函数转换为整数,使得该整数可以尽量唯一地代表这个元素”。其中把这个转换函数称为散列函数H,也就是说,如果元素在转换前为key,那么转换后就是一个整数H(key).
那么对key是整数的情况来说,有哪些常用的散列函数呢?一般来说,常用的有
直接定址法
平方取中法
除留余数法等,
其中直接定址法是指恒等变换(即H(key)=key,本节开始的问题就是直接把key作为数组下标,是最常见最实用的散列应用)或是线性变换(即H(key)=a*key+b);而平方取中法是指取key的平方的中间若干位作为hash值(很少用)。
一般来说比较实用的还有除留余数法.H(key)=key %mod
mod尽量为素数,不会越界,但是可能会发生冲突,常见的解决冲突的方法有:
2.2.1.线性探查法( Linear Probing)
当得到key的hash值H(key),但是表中下标为H(key)的位置已经被某个其他元素使用了,那么就检查下一个位置H(key)+1是否被占,如果没有,就使用这个位置;否则就继续检查下一个位置(也就是将hash值不断加1)。如果检查过程中超过了表长,那么就回到表的首位继续循环,直到找到一个可以使用的位置,或者是发现表中所有位置都已被使用。显然,这个做法容易导致扎堆,即表中连续若干个位置都被使用,这在一定程度上会降低效率。
2.2.2.平方探查法
在平方探查法中,为了尽可能避免扎堆现象,当表中下标为H(key)的位置被占时,将按下面的顺序检查表中的位置:H(key)+12、H(key)-12、H(key)+22、Hkey)-22、Hkey)+32,如果检查过程中H(key)+k2超过了表长 TSize,那么就把H(key)+k2对表长 TSize取模;如果检查过程中出现H(key)-k2<0的情况(假设表的首位为0),那么将(H(key)-k2)% TSize+TSize)%TSie作为结果(等价于将H(key)-k2不断加上 TSize直到出现第一个非负数)。
如果想避免负数的麻烦,可以只进行正向的平方探查。可以证明,如果k在(0, TSize)范围内都无法找到位置,那么当k≥ TSize时,也一定无法找到位置。

2.2.3.链地址法(拉链法)
和上面两种方法不同,链地址法不计算新的hash值,而是把所有H(key)相同的key连接成一条单链表。这样可以设定一个数组Link,范围是Link[0]~ Link[mod],其中Link[h]存放H(key)=h的一条单链表,于是当多个关键字key的hash值都是h时,就可以直接把这些冲突的key直接用单链表连接起来,此时就可以遍历这条单链表来寻找所有H(key)=h的key当然,一般来说,可以使用标准库模板库中的map(见6.4节)来直接使用hash的功能.

2.3 字符串hash起步
字符串hash是指将一个字符串S映射为一个整数,

先假设字符串均由大写字母A ~ Z构成。在这个基础上,不妨把A~
Z视为0~25,这样就把26个大写字母对应到了二十六进制中。接着,按照将二十六进制转换为十进制的思路,由进制转换的结论可知,在进制转换过程中,得到的十进制肯定是唯一的,由此便可实现将字符串映射为整数的需求
范围 26len-1

纯大写字母

int hashFunc(char S[],int len){
	int id=0;
	for (int i=0;i<len;i++){
		id=id*26+(s[i]-'A');
		//id=id+(s[i]-'A')*(int)pow(26.0,(double)len-1-i)
	}
	return id;
}

大小写字母

int hashFunc(char S[],int len){
	int id=0;
	for (int i=0;i<len;i++){
		 if(s[i]>='A'&&s[i]<='Z'){
			 id=id*52+(s[i]-'A');
		 }
		 else if(s[i]>='a'&&s[i]<='z'){
			 id=id*52+(s[i]-'A')+26;
		 }
		
	}
	return id;
}

大小写字母+数字

int hashFunc(char S[],int len){
	int id=0;
	for (int i=0;i<len;i++){
		 if(s[i]>='A'&&s[i]<='Z'){
			 id=id*62+(s[i]-'A');
		 }
		 else if(s[i]>='a'&&s[i]<='z'){
			 id=id*62+(s[i]-'A')+26;
		 }
		 else (s[i]>='0'&&s[i]<='9'){
			  id=id*62+(s[i]-'A')+52;
		 }
	}
	return id;
}

给出N个字符串(由恰好三位大写字母组成),再给出M个查询字符串,问每个查询字符串在N个字符串中出现的次数。

#include <stdio.h>
const int maxn=100;
char s[maxn][5],temp[5]; //虽然是3,但是设大一点没关系啦😊
int hashtable[26*26*26+10];//虽然范围是26*26*26-1,但是大一点没关系
int hash(char s[],int len){	
	int id=0;
	for (int i=0;i<len;i++){
		id=id*26+(s[i]-'A');
	}
	return id;
}
int main(){
	int n,m;
	scanf("%d %d",&n,&m);
	for (int i=0;i<n;i++){
		scanf("%s",s[i]);
		int id=hash(s[i],3);//转化为整数 
		hashtable[id]++; //记录出现次数 
	}
	for(int i=0;i<m;i++){
		scanf("%s",temp);
	 	int id=hash(temp,3);  //将temp也转换为整数 
		printf("%d\n",hashtable[id]); //统计出现次数 
		
	}
	return 0;
} 

3.递 归 ✍

3.1 分 治

分治( divide and conquer)的全称为“分而治之”,也就是说,分治法将原问题划分成若干个规模较小而结构与原问题相同或相似的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到为原问题的解。上面的定义体现出分治法的三个步骤:

①分解:将原问题分解为若干和原问题拥有相同或相似结构的子问题。
②解决:递归求解所有子问题。如果存在子问题的规模小到可以直接解决,就直接解决它。
③合并:将子问题的解合并为原问题的解。

3.2 递 归

很适合实现分治的思想。有两个重要概念,**递归边界和递归式 **

阶乘问题和Fibonacci问题都是典型递归问题。Fibonacci还体现出了分治的思想。

3.2.1 全排列问题:
#include<stdio.h>
const int maxn=11;
int n,p[maxn],hashtable[maxn]={false};
void pailie(int index){
	if(index == n+1){  //1~n已经填好了,递归边界 
	 	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;
			pailie(index+1);
			hashtable[x]=false;
		}
	}	
}
int main(){
	n=3;
	pailie(1);
	return 0;
} 

​ 从递归的角度去考虑,如果把问题描述成“输出1~n这n个整数的全排列”,那么它就可以被分为若干个子问题:“输出以1开头的全排列”“输出以2开头的全排列”…“输出以n开头的全排列”。于是不妨设定一个数组P,用来存放当前的排列;再设定一个散列数组hashTable,其中 hashTable【x】当整数x已经在数组P中时为true现在按顺序往P的第1位到第n位中填入数字。不妨假设当前已经填好了P【1】~ P【index-1】,正准备填 P【index】。显然需要枚举1~n,如果当前枚举的数字x还没有在p[1]- P[index-1]中(即 hashTable【x】=false,那么就把它填入 P[index],同时将 hashTable【x】置为tue,然后去处理P的第 Index+1位(即进行递归);而当递归完成时,再将 hashTable[x]还原为false,以便让 P[index]填下一个数字。

3.2.2 n皇后问题:
  • 解法1: 用全排列枚举出所有摆放情况,再判断每一种是否满足情况.

    #include<stdio.h>
    #include<math.h>
    const int maxn=11;
    int n,p[maxn],hashtable[maxn]={false};
    int flag=1,count;
    void getpl(int index){
    	if(index == n+1){  
    		flag=1; //一定记得重置!多少次犯错了 
    	 	for(int i=1;i<=n-1;i++){
    	 		for(int j=i+1;j<=n;j++){
    	 			if(abs(j-i)==abs(p[j]-p[i])){
    	 				flag=0;
    				 }
    	 			
    			 }
    		 } 
    		 if(flag==1)
    		 {
    			for(int i=1;i<=n;i++){
    	 			printf("%d ",p[i]); 
    		 	} 
    		 	count++;
    			printf("\n");	
    		 }
    		    return ;
    	}
    	for(int x=1;x<=n;x++){
    		if(hashtable[x]==false){//未出现 
    			p[index]=x;
    			hashtable[x]=true;
    			getpl(index+1);
    			hashtable[x]=0;
    		}
    	}	
    }
    int main(){
    	n=5;
    	getpl(1);
    	printf("%d",count);
    	return 0;
    } 
    
    
  • 解法2: 回溯法.因为有些情况摆了前几个,无论后面怎么摆都不可能满足,所以没必要去枚举 递归

    如果在到达递归边界前的某层,由于一些事实导致已经不需要往任何一个子问题递归,就可以直接返回上一层。一般把这种做法称为回溯法。

    这里没搞明白 😭

    #include<stdio.h>
    #include<math.h>
    const int maxn=11;
    int n,p[maxn],hashtable[maxn]={false};
    int flag=1,count;
    void getpl(int index){
    
if(index == n+1){  
	for(int i=1;i<=n;i++){
 		printf("%d ",p[i]); 
	} 
	printf("\n");
	count++;
	return ;
}
for(int x=1;x<=n;x++){ //第x行
	if(hashtable[x]==false){//第x行还没有皇后
		flag=1;
        //遍历之前的queen
		for(int pre=1;pre<index;pre++){   
       //第index列皇后的行号为x,第pre列的为p[pre]
			if(abs(index-pre)==abs(x-p[pre])){
				flag=0;
				break;
			}
		}
	
		if(flag){ //如果前面不冲突 
			p[index]=x;
			hashtable[x]=true;
			getpl(index+1);
			hashtable[x]=0;
		}
	}
}	
}
int main(){
	n=5;
	getpl(1);
	printf("%d",count);
	return 0;
} 

递推与递归的区别

递归:从已知问题的结果出发,用迭代表达式逐步推算出问题的开始的条件,即顺推法的逆过程,称为递归。
递推:递推算法是一种用若干步可重复运算来描述复杂问题的方法。递推是序列计算中的一种常用算法。通常是通过计算机前面的一些项来得出序列中的指定象的值。
递归与递推区别:相对于递归算法,递推算法免除了数据进出栈的过程,也就是说,不需要函数不断的向边界值靠拢,而直接从边界出发,直到求出函数值。

算法举例2
使用递归计算1+2+…+100 ;
分析:递归关系为f(n) = f(n-1) + n ,递归出口为f(1) = 1 ;
编写代码(递归):

public class Sum {
    static int fun(int n){
    if( n == 1){
        return 1 ;
    }else{
        return fun(n-1) + n ;
    }
}
public static void main(String[] args) {
    // TODO Auto-generated method stub
    System.out.println(fun(100));
}

编写代码(递推)

static int fun2(int n){
        int a[] = new int [200] ;
        a[1] = 1 ;
        for(int i=2 ; i<=n ; i++){
            a[i] = a[i-1] + i ;
        }
        return a[n] ;
    }

4.贪心

4.1简单贪心

当前状态达到局部最优,从而达到全局最优。

4.1.1月饼问题

题意:

​ 现有月饼需求量为D,已知n种月饼各自的库存量和总售价,问如何销售这些月饼,使得可以获得的收益最大。求最大收益。

思路:

​ 步骤1:这里采用“总是选择单价最高的月饼出售,可以获得最大的利润”的策略因此,对每种月饼,都根据其库存量和总售价来计算出该种月饼的单价。
之后,将所有月饼按单价从高到低排序

​ 步骤2:从单价高的月饼开始枚举。
​ ①如果该种月饼的库存量不足以填补所有需求量,则将该种月饼全部卖出,此时需求量减少该种月饼的库存量大小,收益值增加该种月饼的总售价大小。
​ ②如果该种月饼的库存量足够供应需求量,则只提供需求量大小的月饼,此时收益值增加当前需求量乘以该种月饼的单价,而需求量减为0这样,最后得到的收益值即为所求的最大收益值

注意点:
①月饼库存量和总售价可以是浮点数(题目中只说是正数,没说是正整数),需要用double型存储。对于,总需求量D虽然题目说是正整数,但是为了后面计算方便,也需要定义为浮点型。很多得到“答案错误”的代码都错在这里。
②当月饼库存量高于需求量时,不能先令需求量为0,然后再计算收益,这会导致该步收益为0。要先计算,再置0。
③当月饼库存量高于需求量时,要记得将循环中断,否则会出错。

#include<stdio.h>
#include <algorithm>  
#include<math.h> 
using namespace std;
struct moon{
	double store;
	double sale;
	double price;
}mc[1010]; //结构体
bool cmp(moon a,moon b){
 	return a.price>b.price ;} //从大到小排序
int main(){
	int n;//n种月饼 
	double D;//需求量 
	double ret;
	scanf("%d %lf",&n,&D) ;
	for(int i=0;i<n;i++){
		scanf("%lf",&mc[i].store);	//输入库存
	} 
	for(int i=0;i<n;i++){
		scanf("%lf",&mc[i].sale);//输入总价
		mc[i].price=mc[i].sale/mc[i].store;	//计算单价
	} 
	sort (mc,mc+n,cmp);
	
	for(int i=0;i<n;i++){ //遍历
		if(mc[i].store>=D){  //当库存大于需求
			ret=ret+mc[i].price*D;  //库存里需求的量全部卖出
			D=0; //需求清零
			break; //记得跳出循环 结束	
		}
		else{ 
			ret=ret+mc[i].sale; //库存小于需求,这种库存卖完,即sale
			D=D-mc[i].store;//需求相应减少
			}
	}
	printf("%.2lf",ret);
	return 0;
} 
4.1.2 最小数问题

题目描述:

给定数字0~9各若干个。可以任意顺序排列这些数字,但必须全部使用。目标是使得最后得到的数尽可能小(注意:0不能做首位)。例如,给定两个0、两个1、三个5和一个8得到的最小的数就是1001558现给定数字,请编写程序输出能够组成的最小的数。

思路:

先从1~ 9中选择个数不为0的最小的数输出,然后从0~9输出数字,每个数字输出次数为其剩余个数。

由于第一位不能是0,因此第一个数字必须从1~9中选择最小的存在的数字,且找到这样的数字之后要及时中断循环。

#include<stdio.h>
int main(){
	int a[10];
	for(int i=0;i<10;i++){
		scanf("%d",&a[i]); //空格能分割 ,一定别忘了&!!!!!
	}
	for(int i=1;i<10;i++){
		if(a[i]>0){
			printf("%d",i);   //输出的i,下标,不是a[i],a[i]是个数
			a[i]--;
			break;
		}
	}
	for(int j=0;j<10;j++){
		while(a[j]){
			printf("%d",j);//输出的j,是index
			a[j]--;
		} }		
	return 0;
}

4.2区间贪心

区间不相交问题:给出N个开区间(x,y),从中选择尽可能多的开区间,使得这些开区间两两没有交集。例如对开区间(1,3)、(2,4)、(3,5)、(6,7)来说,可以选出最多三个区间(1,3)、(3,5)、(6,7),它们互相没有交集。

策略:总是选择左端点最大的区间。所以要先进行排序,把每一个区间当成一个结构体,按照左端点从大到小进行排序。定义最大的左端点为lastX,从下一个区间依次遍历。比较lastX与I[i].y(右端点)进行比较。若I[i].y<=lastX,则记作互不相交的区间.并且令新的lastX为I[i].x.

int ans =1, last =I[0].x;
     for(int i=1;i<n;i++){
         if(I[i].y<=lastX){
             lastX=I[i].x;
             ans++;
         }
     }

与这个问题类似的是区间选点问题:给出N个闭区间[x,y],求最少需要确定多少个点,才能使每个闭区间中都至少存在一个点。例如对闭区间[1,4]、[2,6]、[5,7]来说,需要两个点例如(3、5)才能保证每个闭区间内都有至少有一个点.

int ans =1, last =I[0].x;
     for(int i=1;i<n;i++){
         if(I[i].y<lastX){ //取消等于的情况.因为是闭区间
             lastX=I[i].x;
             ans++;
         }
     }

5.二分

5.1 二分查找

严格递增的序列.

int binarySearch(int A[],int left,int right,int x){
    int mid;
    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;//失败
}

有相同元素的非严格递增.查询元素x,求出序列中第一个大于等于x的元素的位置L以及第一个大于x的元素的位置R,这样元素x在序列中的存在区间就是左闭右开区间(L,R)

1.求L的算法

int lower_bound(int A[],int left,int right,int x){
    int mid;
    while(left<right){ //< 不是<=
        mid=(left+right)/2; 
        if(A[mid]>=x){ // 是>= 不是>
            right=mid; //没有-1,在[left,mid]找
        }
        else{ //A[mid]<x
            left=mid+1; //往右区间[mid+1,right]查找
        }
    }
    return left;
}

2.求R的算法(第一个大于x的index)

int upper_bound(int A[],int left,int right,int x){
    int mid;
    while(left<right){ //
        mid=(left+right)/2; 
        if(A[mid]>x){ // 
            right=mid; //在[left,mid]找
        }
        else{ //A[mid]<=x
            left=mid+1; //往右区间[mid+1,right]查找
        }
    }
    return left;
} 

序列中是否存在满足条件的元素,

int solve(int A[],int left,int right,int x){
    int mid;
    while(left<=right){
        mid=(left+right)/2; //取中点,保留的是小的
        if(满足条件) return mid;
        else if(A[mid]>x){
            right=mid-1;
        }
        else{
            left=mid+1;
        }
    }
    return -1;//失败
}

5.2 二分法拓展

5.2.1 根号2的近似值

最好是float类型,不然运行超时

const double eps=1e-5;
double f(double x){
   return x*x;
}
double calSqrt{
   double left =1,right=2,mid;
   while(right-left>eps){
       mid=(left+right)/2;
       if(f(mid)>2){
           right=mid;
       }
       else{
           left=mid;
       }
   }
   return mid;
}

装水问题和木棒切割问题,

5.3 快速幂

typedef long long LL;
LL binaryPow(LL a,LL b,LL m){
    if(b==0) return 1;
    if(b%2==1)
        return a*binaryPow(a,b-1,m)%m;
    else {
        LL mul=binaryPow(a,b/2,m);
        return mul*mul%m;
    }
}

迭代写法:(计算a的b次方)

typedef long long LL;
LL binaryPow(LL a,LL b){
    LL ans=1;
    while(b>0){
        if(b%2==1){ //if(b&1) 二进制末尾为1
            ans=ans*a;
        }
        a=a*a;//平方
        b=b/2;
    }
    return ans;
}

6.two pointers

6.1 简单介绍

以一个例子引入:给定一个递增的正整数序列和一个正整数M,求序列中的两个不同位置的数a和b,使得它们的和恰好为M,输出所有满足条件的方案。

传统的枚举方法,复杂度是O(n2).这里用i,和j两个指针,复杂度为O(n)

i=0;j=n-1;
while(i<j){
	if(a[i]+a[j]==m){
        printf("&d %d",i,j);
        i++;
        j--;
    }
    else if(a[i]+a[j]<m){
        i++;
    }
    else {
        j--;
    }
}

假设有两个递增序列A与B,要求将它们合并为一个递增序列C

inr merge(int a[],int b[],int c[],int n,int m){
    int i=0,j=0,index=0;
    n=a.length;m=b.length;
    while(i<n&&j<m){
        if(a[i]<=b[j]){
            c[index]=a[i];
            index++;
            i++
        }
        else{
            c[index]=b[j];
            index++;
            j++;
        }
    }
    while(i<n){//包含了i<n但是j>m
        c[index]=a[i];
        index++;i++;
    }    
    while(j<m){ //包含了j<m打算i>n
        c[index]=b[j];
        index++;j++;
    }
    return index;//返回长度
}

使用两个下标i、j对序列进行扫描(可以同向扫描,也可以反向扫描),以较低的复杂度(一般是o(n)的复杂度)解决问题。

6.2 归并排序O(nlogn).

归并排序是一种基于“归并”思想的排序方法,本节主要介绍其中最基本的2路归并排序。2路归并排序的原理是,将序列两两分组,将序列归并为(2/n)个组,组内单独排序;然后将这些组再两两归并,生成个(4/n)组,组内再单独排序;以此类推,直到只剩下一个组为止。归并排序的时间复杂度为 O(nlogn).

其核心就在于如何将两个有序序列转化为一个有序序列.

6.2.1 递归版本
const int maxn=100;
//将数组a的[l1,r1]与[l2,r2]区间合并为有序区间.l2=r1+1;
void merge(int a[],int l1,int r1,int l2,int r2){
    int i=li,j=l2;
    int temp[maxn],index=0;
    while(i<=r1&&j<=r2){ //是<=,有等于符号,因为要考虑0,0,1,1这种两个元素一组的情况.
        if(a[i]<=a[j]){
            temp[index]=a[i];
            index++;
            i++
        }
        else {
            temp[index]=a[j];
            index++;
            j++
        }
    }
    while(i<r1){
        temp[index]=a[i];
        i++;index++;
    }
    while(j<r2){
        temp[index]=a[j];
        j++;index++;
    }
    for(i=0;i<index;i++){
        a[l1+i]=temp[i]; //合并后赋值给a
	}
}
最先排的是[0,1]
void mergesort(int a[],int left,int right){
    if(left<right){
        int mid=(left+right)/2;
        mergesort(a,left,mid);//递归,将左子区间[left,mid]归并排序
        mergesort(a,mid+1,right);//递归将右子区间[mid+1,right]归并排序
        merge(a,left,mid,mid+1,right); //合并
    }
}

6.2.2 非递归版本(不用掌握)

void mergesort(int a[]){
    for(int step=2;step/2<=n;step*=2){
        for(int i=1;i<=n;i+=step){
            int mid=i+step/2-1;
            if(mid+1<=n){
                merge(a,i,mid,mid+1,min(i+step-1),n);
            }
        }
    }
}

6.3 快速排序O(nlogn).

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

①选定主元素,让其左侧都小于它,右侧都大于它.

②对该元素的左侧和右侧分别递归进行①的调整,直到当前调整区间的长度不超过1。

思路

​ ①先将A[1]存至某个临时变量temp,并令两个下标left、 right分别指向序列首尾(如令left=1、 right=n)
​ ②只要 right指向的元素A[right]大于temp,就将right不断左移;当某个时候A[right]≤temp时,将元素 A[right]挪到left指向的元素A[left]处。
​ ③只要left指向的元素A[left]不超过temp,就将left不断右移;当某个时候A[left]>temp时,将元素A[left]挪到right指向的元素A[ right]处。
​ ④重复②③,直到left与 right相遇,把temp(也即原A[1])放到相遇的地方。

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];//不满足后,将a[right]挪到[left]
        while(left<right&&a[left]<temp){
            left++; //右移
        }
        a[right]=a[left];
    }
    a[left]=temp; //将temp放在相遇的位置,此时left=right
    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);
    }
}

跟归并排序的结构很像,

7. 其他技巧

7.1 1045 快速排序解法

硬编码超时了,所以只能用这种方法.

核心在于,快速排序,每一轮排完后.主元素不再变动(位置跟排序完毕后一样)

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int main(){
	int s[100010];
	int a[100010],b[100010];
	int n,index;
	index=0;
	scanf("%d",&n);
	getchar();
	for(int i=0;i<n;i++){
		scanf("%d",&s[i]);
		b[i]=s[i];
	} 
    sort(s,s+n); //把s排序 
	int max=0;
	for(int i=0;i<n;i++){
		if(s[i]==b[i]&&b[i]>max){ //如果是主元,一定序号不会变,并且大于前面的最大值, 
			a[index++]=s[i];
		}
		if(b[i]>max){//找最大值 
			max=b[i];
		}
	}
	printf("%d\n",index);
	for(int i=0;i<index;i++){
		if(i) printf(" ");
		printf("%d",a[i]);
	}	
	return 0;
} 

8.数学问题

8.1 1019数字黑洞

#include <stdio.h>
#include <algorithm>
using namespace std;
bool camp(int a,int b){ //从大到小 
	return a>b;
} 
void to_array(int n,int num[]){  //两个函数挺重要的
	 for(int i=0;i<4;i++){
	 	num[i]=n%10;
	 	n=n/10;
	 }
} 
int to_number(int num[]){
	int sum=0;
	 for(int i=0;i<4;i++){
	 	sum=sum*10+num[i];
	 }
	 return sum;
} 
int main(){
	int n,min,max;
	scanf("%d",&n);
	int num[5];
	while(1){
		to_array(n,num);
		sort(num,num+4);//默认从小到大
		min=to_number(num);
		sort(num,num+4,camp);
		max=to_number(num);
		n=max-min;
		printf("%04d - %04d = %04d\n",max,min,n);
		if(n==0||n==6174) break;
		
	}
	return 0;
}

8.2 最大公约数与最小公倍数

辗转相除法

递归式: gcd(a,b)=gcd(b,a%b)

递归边界: gcd(a,0)=a

int gcd(int a, int b){
    if(b==0) return a;
    else return gcd(b,a%b);
}

最小公倍数

在最大公约数基础上进行,ab乘积除以最大公倍数

int lcm(int a,int b){
    return a/gcd(a,b)*b
}

8.3 分数的四则运算

struct Fraction{
    int up,down;
};

8.4 素数

1.判断

bool isprime(int n){
    if(n<=1) return false;
    int sqr=(int)sqrt(1.0*n);//参数为浮点数
    for(int i=2;i<=sqr;i++){  //复杂度为O(sqrt(n))
        if(n%i==0) return false;
    }
    return true;
}

2.素数表获取

O(n*sqrt(n))

const int max=101;
int prime[max],pnum=0;
bool p[max]={0};
void find(){
    for(int i=1;i<max;i++){
        if(isprime(i)==true){
            prime[pnum++]=i;
            p[i]=true;
        }
    }
}

O(n*loglogn)

筛法,把i的倍数去掉,是个很好的办法

const int max=101;
int prime[max],pnum=0;
bool p[max]={0};//只能先假设所有的都是素数,为0表示是素数,1为非素数
void find(){
    for(int i=2;i<max;i++){
        if(p[i]==false){
            prime[pnum++]=i;
            for(int j=i+i;j<max;j=j+i){
                p[j]=true;
            }
        }
    }
}
10个换行,末尾不许有空格
    for(int i=m;i<=n;i++){
	printf("%d",prime[i-1]);
	count++;
	if(count%10!=0&&i<n) printf(" ");
	else printf("\n"); //会多一行
	}

8.5 扩展欧几里得算法

8.6 组合数

1.求n!里有多少个p质因子

等于1~n中p的倍数的个数(n/p)加上(n/p)!中质因子p的个数

int cal(int n,int p){
    if(n<p) return 0;
    return  n/p+cal(n/p,p);
}
8.6.1 组合数的计算

递归解法

long long res[67][67]={0};
long long  C(long long  n,long long m){
    if(m==0||m==n) return 1;
    if(res[n][m]!=0) return res[n][m];
    return res[n][m]=C(n-1,m)+C(n-1,m-1);
}
    

通过定义式求解

long long C(long long n,long long m){
    long long ans=1;
    for(long long i=1;i<=m;i++){
        ans=ans*(n-m+1)/i;
    }
    return ans;
}

9. 大整数运算ʕ•ᴥ•ʔ

9.1 大整数的存储及比较

让数组高位存储数字的高位,即反着存储

struct bign{
    int d[1000];
    int len;
    bign(){
        memset(d,0,sizeof(d));
        len=0;
    }
};

而输入大整数时,一般都是先用字符串读入,然后再把字符串另存为至bign结构体。由于使用char数组进行读入时,整数的高位会变成数组的低位,而整数的低位会变成数组的高位,因此为了让整数在bign中是顺位存储,需要让字符串倒着赋给数组

bign change(char str[]){
    bign a;
    a.len=strlen(str);
    for (int i=0;i<a.len;i++){
        a.d[i]=str[a.len-1-i]-'0';
    }
    return a;
}

判断大小

int compare(bign a,bign b){
	if(a.len>b.len) return 1;//a大
    else if(a.len<b.len) return -1;
    else {
        for(int i=a.len-1;i>=0;i--){  //一定从高位进行比较!
            if(a.d[i]>b.d[i]) return 1;
            else if(a.d[i]<b.d[i]) return -1;  //相等进入下一次循环
        }
        return 0;
    }
}

9.2 大整数的四则运算

9.2.1 高精度加法

归纳出对其中一位进行加法的步骤:将该位上的两个数字和进位相加,得到的结果取个位数作为该位结果,取十位数作为新的进位。

bign add(bign a,bign b){
    bign c;
    int carry=0;//进位
    for(int i=0;i<a.len||i<b.len;i++){
        int temp=a.[i]+b.[i]+carry;
        c.d[c.len++]=temp%10;//把个位数存在低位的
        carry=temp/10;//十位数给下一位
    }
    //最后遍历完了,还有进位
    if(carry!=0){
        c.d[c.len++]=carry;
    }
    return c;
}
9.2.2 高精度减法

对某一步,比较被减位和减位,如果不够减,则令被减位的高位减1、被减位加10再进行减法;如果够减,则直接减。最后一步要注意减法后高位可能有多余的0,要忽视它们,但也要保证结果至少有一位数。

bign sub(bign a,bign b){
    bign c;
    for(int i=0;i<a.len||i<b.len;i++){
        if(a.d[i]<b.d[i]){
            a.d[i+1]--;
            a.d[i]=a.d[i]+10; //借位
        }
        c.d[c.len++]=a.[i]-b.d[i];
    }
    while(c.len-1>=1&&c.d[c.len-1]==0){
        c.len--;//去除最高位的0,但是要保证最低位有0;
    }
    return c;
}

最后需要指出,使用sub函数前要比较两个数的大小,如果被减数小于减数,需要交换两个变量,然后输出负号,再使用sub函数。

9.2.3 高精度与低精度的乘法

对某一步来说是这么一个步骤:取bign的某位与int型整体相乘,再与进位相加,所得结果的个位数作为该位结果,高位部分作为新的进位

bign multi(bign a,int b){
    bign c;
    int carry=0;
    for(int i=0;i<a.len;i++){
        int temp=a.d[i]*b+carry;
        c.d[c.len++]=temp%10;
        carry=temp/10;
    }
    while(carry!=0){
        c.d[c.len++]=carry%10; //因为进位不止只有10,不止只有一位
        carry=carry/10;
    }
    return c;
}

另外,如果a和b中存在负数,需要先记录下其负号,然后取它们的绝对值代入函数。

9.2.4 高精度与低精度的除法

归纳其中某一步的步骤:上一步的余数乘以10加上该步的位,得到该步临时的被除数,将其与除数比较:如果不够除,则该位的商为0:如果够除,则商即为对应的商,余数即为对应的余数。最后一步要注意减法后高位可能有多余的0,要忽视它们,但也要保证结果至少有一位数

bign divide(bign a,int b,int& r){ //r是余数
    bign c;
    c.len=a.len;//被除数的每一位和商是对应的
    for(int i=a.len-1;i>=0;i--){
        r=r*10+a.d[i];
        if(r<b) c.d[i]=0;
        else{
            c.d[i]=r/b; //商
            r=r%b;//新余数
        }
    }
    while(c.len-1>=1&&c.d[c.len-1]==0){
        c.len--;//去除最高位的0
    }
    return c;
}

在上述代码中,考虑到函数每次只能返回一个数据,而很多题目里面会经常要求得到余数,因此把余数写成“引用”的形式直接作为参数传入,或是把r设成全局变量。其作用是在函数中可以视作直接对原变量进行修改,而不像普通函数参数那样,在函数中的修改不影响原变量的值。这样当函数结束时,r的值就是最终的余数。

总结:

乘法和加法,有carry,从低位开始,最后不需要去除重复的0但是要考虑最后一个进位的问题。

减法和除法需要去除多余的0

只有除法是从高位开始的

若有错,请多多指正

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值