”「致曾经的入门」ACM算法课二(经典数据结构与算法)“

记录跟着校队训练的一个月……

Posted by 许大仙 on July 27, 2017

一、本章内容常见的算法

  • 线性表(数组)
  • 队列
  • 排序
  • 查找
  • 堆栈
  • 二叉树

二、recaman问题(线性表—数组)

描述: recaman定义

(题目要求0<k<50000,耗时:1000ms) eg:0,1,3,6,2,7,13,20,12,21,11,22,10……

for(i=1;i<=k;++i){
	t=a[i-1]-i;
	if(t<=0){
		a[i]=a[i-1]+i;
		continue;
	}
	for(j=1;j<i;++j){//上步没有continue,那么这步t>0
		if(t==a[j])
			break;
	}
	if(j==i){
		a[i]=t;
	}
	else{
		a[i]=a[i+1]+i;
	}
}
  • 规模:500000
  • 基本操作:查找,比较(if(t==a[j]))
  • 最坏的情况:总是t>0
  • 复杂度:k*(k-1)/2∈O(n²)
    • i=1,查找=0
    • i=2,查找=1
    • ……
    • i=k,查找=k-1

所以基本上要运算:500000²。花费太长时间,必然超时。

解决办法:如果能只判断一次比较一次,那么复杂度就是一维线性:O(n)。 如何只要判断一次呢?定义两个线性表,一个线性表记录数据a[i],另一个线性表used[t](每计算一次a[i],就令used[a[i]]=1),记录t当前这个数是否使用过。那么只需要判断if(used[t]==0)就可以避免查找多次

int a[500001],b[3020000];//b数组的大小是测试出来的,一开始可以开的很大.
//计算a=500000时候,值最大大约是3019000,所以开了一个3020000的数组
//b[i]=1,表示已经存在过,b[i]=0表示首次出现 
int main(){
	a[0]=0;
	memset(b,0,sizeof(b));
	for(int i=1;i<500001;i++){
		a[i]=a[i-1]-1;
		if(a[i]<=0||b[a[i]]==1)
			a[i]=a[i-1]+i;
		b[a[i]]=1;//标记是否使用 
		
	}
		while(scanf("%d",&n),n+1)
			printf("%d\n",a[n]);
			 
	return 0;
} 

(PS:开的数组一般大小超过1万,2万,3万,4万,就不要开在函数内了。改在全局好。并且一般bool型的数组运算比int型的数组运算慢) 运用线性表–标记法!(牺牲了空间,换取时间)

三、队列,链表(类约瑟夫问题)

数据结构的循环队列(标记后继(指针)和数据)

定义队列为:

Struct queue{
	Data password;
	*queue next;
} 解决下列问题可以简化指针为:queue[0]=1,queue[i]=i+1,queue[n]=0;(标记后继的数组)

描述:人数是n,每个人的密码是m,n后数m达到的位置所在的人出列,最后未出列的人获胜(n,m<=100)

Joseph问题

joseph(int m,int n,int a[]){//a数组记录密码值,也就是第i位置出列后,要报多少数 
	for(i=1;i<n;i++)//标记各个元素的后继结点 
		que[i]=i+1;
	que[n]=1;//环形标记,尾部链接头部 
	i=n;//i表示当前位置,从n开始,因为到1号位置已经是报1了。 
	while(que[i]!=i){//如果自己的后继不是自己,也就是队列剩下不止一个人的时候循环继续 
		for(j=1;j<m;++j)//往后报数 
			i=que[i];//报数一次,当前位置就变成之前位置的后继,直到报到 
		printf("%d",que[i]);//输出出列序号 
		m=a[que[i]];	//取得新密码 
		que[i]=que[que[i]];//删除该元素---利用更改后继结点的方法 
	}
	printf("%d\n",i);
}  更改后继,第i个出列,那么i-1的后继改成i的后继,也就是i-1原来的后继的后继(Q[i]=Q[Q[i]])。

复杂度:O(m* n)(出列n个人,每次出列一个要数m下,复杂度就是m *n)

四、堆栈(单栈)

处理需要反复从头开始进行的操作。对于前面的匹配后面的,这样的前后匹配的操作,适用于堆栈(eg:({[……]})这样的左右括号匹配,适用) 堆栈特点:先入后出,后入先出。

例题,找BUG 描述:输入一堆字符串,包含多个BUG字符。出现BUG的就删掉,留下其他字符输出 例如:BBUGUGBUBUGGG。最后余下空,输出。

BUG栈堆示意图

if(栈顶的两个字符是U,B。当前需要push压入的就是G,那么就pop->B,U,G)

不需要一次循环找到bug就从头开始找bug,这样会超时的。

由于题目的时间限制,且不需要太多的栈功能可以考虑自己写栈,否则调stack头文件占用很大内存和时间。 如下:

#include<stdio.h>
char STACK[10000];
int SP,i;
void push(char c){//入栈
	STACK[SP++]=c;
}
char pop(){//弹出栈顶
	return STARK[--SP];//栈顶下降,返回弹出后的现栈顶元素
}
int main(void){
	char c;
	SP=0;
	while((c=getchar())!=EOF){
		if(c=='G'&&SP>1&&STARK[SP-2]=='B'&&STARK[SP-1]=='U'){
			pop();
			pop();
		}
		else
			push(c);
	}
	for(i=0;i<SP;++i)
		printf("%c",STARK[I]);
	return 0;
}

堆栈使用类型应用

1.对于括号匹配(前后匹配)的问题适合堆栈来解决————匹配问题

例题:判断只由*和+组成的表达式中,去掉对运算结果没有影响的括号

思路:利用堆栈

  • 去掉不含加号的括弧。‘(’入栈,之后遇到的第一个加号就入栈。如果在遇到)之前没有加号,那么就去掉这对括号。(如果没有加号有乘号,乘法已经是最高优先级了,这对括弧也可去掉)。总之‘(’之后,到‘)’之前没有+就弹栈
  • 第一步之后,剩下的括号中必有+号。要使得这个这对括弧有保留下来的意义,需要括号前有字母或‘)’,或者括号后有‘(’或者字母.继续使用堆栈。只需要入栈‘(’.这个‘(’这个入栈的括弧肯定和栈顶的’)’(也就是表达式的最后一个右括号)匹配

代码如下:s栈记录‘(’位置,p栈记录加号位置

int main(){ while(gets(s)!=NULL){ for(i=0;s[i];++i) switch(s[i]){ case ‘)’:spush(i);break; case ‘)’:j=spop();if(!pp||pstack[pp-1]<j) s[i]=s[j]=’‘;else ppop();break;//j是前括弧的位置 case ‘+’:if(sp&&(!pp||sstack[sp-1]>))

} //上述消除了中间没有加号的括弧
for(i=j=0;s[i];++i)if(s[i]!='')t[j++]=s[i];t[j]=0;
	for(i=0;t[i];++i)
		switch(t[i]){
			case ')':
			case '(':


		}

//j,i分别是一堆括号的前后标记.判断这对括弧的前后情况,看看要不要删除这对括号. }

2.Boolean Expressions

用堆栈来解决:这类表达式计算。(优先级低的放入堆栈,遇到更低的把之前的低的计算掉,再压入更低的那个操作符) 定义两个栈,oper栈是操作符栈,num是操作数栈

char oper[100],num[100],osp,nsp; void push(char c,short f){//f==0 操作符栈,f==1表示操作数栈

//从前往后进行入栈操作。规则如下:

}

int main(){ while(gets(s)!=NULL){ for(j=0;j<100;++j) oper[j]=num[j]=0; j=osp=nsp=0; }//清空栈,或者说初始化为空

}

操作如下: //遇到操作数V或者F,就入栈。 //遇到‘(’ 入栈, ‘(’不入栈[遇到‘)’后,在这对括弧之间的所有操作符都处理完] //(级别很高,)级别很小,‘)’比栈顶优先级小,所以遇到)必须要把前面运算符都操作掉 //遇到操作符,判断栈顶的操作符和接下来新的运算符的优先级。 //如果栈顶的优先级>=新来,那么进行栈顶操作符运算。弹出操作符栈顶,然后弹出操作数栈顶的两个操作数(如果原栈顶是一元操作符,就弹一个操作数),进行原栈顶操作运算,结果再压入操作数的栈顶。 //如果操作符栈顶(eg:|)比新来的运算符(eg:&),优先级低,那么新来的操作符入操作符栈 //’\0’的优先级最低,操作符栈中的操作符都运算完,操作数栈中的结果只剩下一个,也就是结果——输出

五、查找

二分法查找(转载):C基础算法之二分法查找利用循环、递归实现

  • 适用情况:当数据量很大适宜采用该方法。采用二分法查找时,数据需是排好序的。
  • 基本思想:假设数据是按升序排序的,对于给定值x,从序列的中间位置开始比较,如果当前位置值等于x,则查找成功;若x小于当前位置值,则在数列的前半段 中查找;若x大于当前位置值则在数列的后半段中继续查找,直到找到为止。
  • 算法优势:二分法查找在针对大量有序排列的情况下发挥出很优越的效率,这里以最具规律性的数组为例,代码如下:

    int binarysearch(int arr[],int len,int key);
      	int binarysearch2(int array[],int key,int low,int high);   
      	int main(int argc, char * argv[])
      	{
      		int array[] = {3,5,6,7,11,22,44,47 };
      		int i;
      		printf("请参照下列数组,输入你要查找的目标:\n {");
      		int len = sizeof(array)/sizeof(int);
      		for (i = 0; i < len; i++) {
      		printf("%d,",array[i]);
      	}
      		printf("}:\n");
      		int dest;
      		scanf("%d",&dest);
      		int res = binarysearch(array,len,dest);
      		printf("1. res = %d\n",res);
      		int res2 = binarysearch2(array,dest,0,len - 1);
      		printf("2. res = %d\n",res2);
      }
    

函数名称:用循环实现二分法查找过程,

返回值:返回目标值在数组中的下标

int binarysearch(int arr[],int len,int key)
{

	int high,low;
   		high = len - 1;//假设数组是从小到大排列的
	low = 0;
	int midle = len/2;

	while(high >= low)
   	 {
     	   midle = (high + low)/2;
     	   
     	  if(arr[midle] == key)
     	      return midle;
     	  if(arr[midle] > key)
      	     high = midle - 1; //前提是假设数组是从小到大排序,否则不确定是该加1还是减1
		  else if(arr[midle] < key )
           	 low = midle + 1;
   	 }
	return (-1);
}

函数名称:用递归实现二分法查找

int binarysearch2(int array[],int key,int low,int high)
{

	if (low >= high)
  	  return (-1);

	int midle = (low + high)/2;
	if(array[midle] == key)
 	   return midle;
   	    if(midle == high || midle == low) //此时,只剩下了下标为high和low的两个数,
	//确定另一个数不是key后,就确定查找完毕,并且未找到目标	
    	{
        	if(array[high] == key)
           		return high;
      		else if(array[low] == key)
            	return low;
        	else
            	return (-1);            
    	}
	else if(array[midle] > key)
    	return binarysearch2(array,key,low,midle);  //由于不确定排序方向,所以此处只能用midle值,而不能加1或减1
	else if(array[midle] < key)                    //当中间值小于目标值时,在high的一侧继续查找,low变到midle位置上
    	return binarysearch2(array,key,midle,high);
}