Poker time 2 (enhanced version)

声明:

        本文很长,并且最终不会有完整代码,仅提供算法;

        本文知识点中等偏难,但是看懂和学会之后有一定好处;

        本文绝对不是最优解,并且很多步骤待封装成函数,这里逐一分析;

        本文纯属个人刷题方法和总结,如有错误,欢迎指正;

        如果你做的时候有些吃力,真正想学点什么,请耐心看完,看懂。

题目如下:

        背景:两个人每人发3张牌(各从一副牌中),每张牌包括花色(红桃(Heart)>黑桃(Spade)>方块(Diamond)>梅花(Club))和大小(从小到大依次是:2-10、J、Q、K、A),胜负规则如下:同花顺(3张同花色的连牌,先比大小,再比花色后同)>炸弹(3张相同大小的牌)>连牌(3张不同花色的连牌)>对子(两张相同大小的牌)>单牌。例如,红桃QKA>黑桃QKA>梅花567>方块234>AAA(红桃、方块、梅花)>AAA(黑桃、方块、梅花)>JQK(红桃、红桃、方块)>JQK(黑桃、红桃、方块)>AA2(梅花黑桃梅花)>QQJ(红桃梅花方块)>JQA(红桃红桃红桃)。注:A23不算连牌。

输入:A的3张牌(未排序)和B的3张牌(未排序)。

输出:A的3张牌的排序后的输出和B的3张牌的排序后的输出,以及A和B谁获胜。

分析:

        这里继承了上一题,也就是Poker time的部分内容,这里不多赘述那题的做法,如果有需要的话可以私聊我,我视情况出第一题的算法。

        首先是排序,这里的最终排序取决于取决于牌的大小而不是花色(*);其次是胜利条件的判断,有明显的分级,对于每一级内部也有严格的先大小后花色的判断顺序(**);再者,顺子的QKA是否合理,这里没有给出解释,也是留了一手,不然题目的判断会更恶心(***);最后是让人最头疼的10,这个数字在第一题就作为最后一个隐藏示例存雷。

        不过有10必有‘1’,只需要找到这个1就行。

        这次编写,我不知道隐藏示例都是什么,主要是我一次就过了,除了一个本身就是错误的示例(凡尔赛)。

算法正文:

零、参数声明

        这里给出本文主要的参数

	char a[20];   // 这里给20,防一下 10 的炸弹
	char b[20];
	gets(a);
	gets(b);
    int i=0;
    int j=0;
	int flag=1;    // 循环进行指标 
	int flag1=1;   // 是否双赢指标 
	int flag2=0;	// 判断 A 赢 
	int flag3=0;	// 判断 B 赢 
	

一、建立在排序基础上的整体数组构造以及参数读入

        由于最终需要对牌进行先大小后花色的排序,我这里使用的是二位数组

        为什么定义成13*4呢?        例如:HA 就在数组 [0][0]的位置

        我的思路是:13行存储牌,4列存储花色,数字大、花色大的牌前,最后一次遍历输出牌序。

        这里需要将flag的指标置零,如果直接 return 0 也可以。

	int As[13][4]={0};
	int Bs[13][4]={0};

         于是,可以对数组内读入参数,这里只给出其中一个字符串的数据读入

	while(a[i]!='\0'&&flag)    // A 的数据填充 
	{
		if(a[i]==' ')   // 跳过空格
		{
			i++;
			continue;
		}
		if(a[i]=='H'||a[i]=='S'||a[i]=='D'||a[i]=='C')
		{
			int m=0;     // 列指标
			switch(a[i])   // 判断花色
			{
				case 'H':m=0;break;
				case 'S':m=1;break;
				case 'D':m=2;break;
				case 'C':m=3;break;
			}

			int n=0;   // 行指标
                
            //往后是 i+1 因为现在的 i 指向的是花色,往后以为才是牌的内容 
			
			// 向二维数组中填充数据 
			if(isdigit(a[i+1])||a[i+1]=='A'||a[i+1]=='J'||a[i+1]=='Q'||a[i+1]=='K') 
			{
				if(isdigit(a[i+1])&&a[i+1]!='1')  //判断2~9,小的在下往上填
				{
					n=12-(a[i+1]-'0'-2);     // 回忆一下 ascii码 就知道这步在干啥
				}
				else if(a[i+1]=='A')     // 其他牌的读入
				{
					n=0; 
				}
				else if(a[i+1]=='K')
				{
					n=1;
				}
				else if(a[i+1]=='Q')
				{
					n=2;
				}
				else if(a[i+1]=='J')
				{
					n=3;
				}	
				if(a[i+1]=='1'&&a[i+2]!='\0'&&a[i+2]=='0')    // 判断是不是 10 
				{
					n=4;
				}
				i+=n==4?3:2;     // 结合 i 的指向,如果不是10就跳过两位,是10就跳过三位
			}
			else    // 不符合的剔除 
			{
				printf("Input Error!\n");
				flag=0;
				flag1=0;
				break;	
			}
			if(As[n][m]==0)    // 如果没有重复的牌,那就可以在数组中标记这张牌
			{
				As[n][m]++;
			}
			else               // 否则报错
			{
				printf("Input Error!\n");
				flag=0;	
				flag1=0;
				break;
			}
		}
		else
		{
			printf("Input Error!\n");
			flag=0;
			flag1=0;
			break;
		}
	}

二、建立在先大小后花色基础上的排序

        是插入过程的逆,重新输出符合要求的字符串

	int k=0;
	for(i=0;i<13;i++)
	{
		for(j=0;j<4;j++)
		{
			if(As[i][j]==1)  //判断有没有这张牌
			{
				switch (j)   // 以下的选择与刚才插入的时候刚好相反,属于反操作
				{
					case 0:a[k++]='H';break;
					case 1:a[k++]='S';break;
					case 2:a[k++]='D';break;
					case 3:a[k++]='C';break;
				}	
				switch (i)
				{
					case 0:a[k++]='A';break;  // 根据牌所在的行数回推牌的内容
					case 1:a[k++]='K';break;
					case 2:a[k++]='Q';break;
					case 3:a[k++]='J';break;
					case 4:a[k++]='1';a[k++]='0';break;
					default:a[k++]=14-i+'0';     // 2~9 转回字符
				}
				a[k++]=' ';
			}
			else
			continue;
		}
	}
	a[--k]='\0';   // 数组结尾,最好加上,免得访问越界 

三、建立在规则上的初判断与初步输赢判定

        这里初判断 a 和 b 的牌型,如果牌型不同,直接就可以输出唯一胜者

        这里粗返回的 0~4 可以初步判断胜者,或者仍需比较

        这里返回的0~4可不是乱来的,后面有用到

        函数如下:

// 返回差,类似与冒泡排序,前一个与后一个比,根据差调整数组中元素的位置
// 打一下,试一试就会了,这个函数,很容易上手
int cmp(const void*e1,const void*e2)  // 比较方式
{
	return *(int*)e1-*(int*)e2;    
}


int search(int a[13][4])  // 初步判断牌的类型 
{
	int i=0;
	int j=0;
	int c[3]={0};    // 创建新的数组,存储牌的 行指标 !!!
	int current=0;        // 因为 行指标 标记了牌的大小

	for(i=0;i<13;i++) // 遍历查找牌
	{
		for(j=0;j<4;j++)
		{
			if(a[i][j]==1)
			{
				// 同花顺,占据连续的一列中的三个,最易判断
				if(a[i+1][j]==1&&a[i+2][j]==1&&i<=10)  // 这里i防止越界访问
				{
					return 4;
				}
				c[current++]=i;
			}
			else
			continue;
		}
	}
	qsort(c,3,sizeof(int),cmp);  // 对牌进行排序,这里使用了 qsort 函数
                                // qsort(数组名,元素个数,元素大小,比较函数)

	if(c[0]==c[1]&&c[1]==c[2])  // 炸弹(全部相等)
	{
		return 3;
	}
	else if(c[2]-c[1]==1&&c[1]-c[0]==1)  // 顺子(等差数列)
	{
		return 2;
	}
	else if(c[0]==c[1]||c[1]==c[2])   // 对子
	{
		return 1;
	}
	return 0;    // 啥也不是,只有单牌
}

 判断是否有胜者:

	if((flag2>flag3||flag2<flag3)&&flag1)   // 唯一胜者,且获胜方式单纯 
	{
		if(flag2>flag3)
		{
			printf("Winner is A!\n");
		}
		else if(flag2<flag3)
		{
			printf("Winner is B!\n");
		}
		printf("A: %s\n",a);   // 打印排序后的牌内容
		printf("B: %s\n",b);
	}
	else if(flag2==flag3&&flag1)   // 需要下一步比较 

 四、建立在再次判断上的函数指针数组及其调用

        函数指针数组:一个数组(定语),数组内元素是函数指针(状语)

        故名思意:根据牌型,选择不同的函数来判断最终的胜利,也称回调函数

int (*pfarr[4])(char c[15],char d[15])={nmsl,wdnm,cnmd,mdzz};

// *pfarr[4] 指针数组,运算符优先级决定它先与 [4] 结合成为数组
// (char c[15],char d[15]),参数类型,类似于函数的参数声明
// {nmsl,wdnm,cnmd,mdzz},函数名,我乱起的,没别的意思

// 总的意思就是,一个数组,里面四个元素,分别指向一个独立的函数
else if(flag2==flag3&&flag1)   // 需要下一步比较 
	{
		if(flag2)    // 非零 
		{
			int (*pfarr[4])(char c[15],char d[15])={nmsl,wdnm,cnmd,mdzz};
			switch(flag2)
			{
				case 4:                      // 这里的调用都是已经排序过的牌的字符串
					t=pfarr[0](a,b);break;   // 函数调用类似于访问数组的元素
				case 3:                      // 不过记得带上函数的参数调用,就是后面的 (a,b)
					t=pfarr[1](a,b);break;
				case 2:
					t=pfarr[2](a,b);break;
				case 1:
					t=pfarr[3](a,b);break;
			}
			if(t==0)
			{
				printf("Winner is A!\n");	
			}
			else if(t==1)
			{
				printf("Winner is B!\n");	
			}
			else
			{
				printf("Draw!\n");
			}
		}
		else         // 都是单牌,比较方法就是第一题的全部内容,不说了
		{
			int count=0;
			for(i=0;i<4&&flag;i++)   // 对比两个数组判断输赢 
			{
				for(j=0;j<13&&flag;j++)
				{
					if((As[i][j]==0&&Bs[i][j]==0))
					{
						continue;
					}
					else if(As[i][j]==1&&Bs[i][j]==1)
					{
						count++;
						if(count==3)
						{
							printf("Draw!\n");
							goto end;
						}
					}
					else if(As[i][j]>Bs[i][j])
					{
						printf("Winner is A!\n");
						flag=0;
						break;
					}
					else if(As[i][j]<Bs[i][j])
					{
						printf("Winner is B!\n");
						flag=0;
						break;
					}
				}
			}
		}

五、建立在近似判断机制上的四个函数

        刚才我们设定了四个函数,分别是{nmsl,wdnm,cnmd,mdzz},现在需要一一实现他们,达到我们预期的不同情况下的比较效果。

// 同花顺 判断
int nmsl(char a[15],char b[15])
{
	int i=0;
	if(strlen(a)==8)   // 判断有没有万恶的 10,这里显然没有
	{
		for(i=0;i<15;i++)
		{
			if(a[i]==b[i])  //相同跳过
			{
				continue;
			}
			else
			{
				if(a[i]=='H'&&b[i]!='H')  // 花色碾压
				{
					return 1;
				}
				else if(a[i]!='H'&&b[i]=='H')
				{
					return 0;
				}
				else if(a[i]=='A'&&b[i]!='A')   // 谨记,有A就没有10,这里是大小碾压
				{
					return 0;
				}
				else if(a[i]!='A'&&b[i]=='A')
				{
					return 1;
				}
				else                     // 其他牌的大小碾压
				{
					return b[i]-a[i]?1:0;
				}
			}
		}
	}
	else     // 如果有万恶的10
	{
		for(i=0;i<15;i++)
		{
			if(a[i]==b[i])
			{
				continue;
			}
			else
			{
				if(a[i]=='H'&&b[i]!='H')
				{
					return 1;
				}
				else if(a[i]!='H'&&b[i]=='H')
				{
					return 0;
				}
				else if(a[i]=='1'||b[i]=='1')  // 出现了 10 !
				{
					if(a[i]=='1')
					{
						return b[i]<=57?0:1;   // 10 跟另外一张牌比较,这里57,代表字符'9'
					}                          // 细想一下,是不是也合理?
					else
					{
						return a[i]<=57?1:0;
					}
				}
				else   // 其他牌的大小碾压
				{
					return b[i]-a[i]?1:0;
				}
			}
		}
	}
	return 2;
}

        其余的判断都是建立在刚才判断的基础上的,可以说比较部分几乎完全一致。

        这里多说一句,对子的判断,可以先找到是哪一对,创建一个两个元素的数组或者字符串,存下来这一个对子中的一张牌,然后跟另一个对子中的一张牌进行 皇城PK 即可。

        这里不给出余下的三个函数了,有心的同学看懂上面的思路一定信手拈来,不就是三个函数嘛?!何况这里已经给出了最核心的比较算法了。

六、写在最后的话

        我不否认出这道题的人有很多没有考虑到的地方,题目出的不是很严谨,但是在现有知识点的使用上,这绝对是综合的难题(就我的算法而言)

        如果你说你用一次遍历,哈希遍历,等等,那你可以完美的优化这道题的全部算法;

        但是就我而言,我希望写可读性更高的代码,不是说我不会写高效代码,hashmap我也可以写,但是这只是独善其身,不是每一个学C的人一开始就可以写0ms,2Mb的代码。

        如果你觉得这很难,那很抱歉,我已经尽我的一切可能,写最详细的注释,写最简单的代码了,谁又能刚开始一看就懂呢?

        如果你说,这博主怎么这么扣,这也不写,那也不赘述。

        不好意思,我都写了,是时候举一反三了。

        

        最后的最后,码字不易,要是本文多少学了点东西的友友们,点个赞让我看一下呗,至少我会觉得我的付出可以让大家获益

  • 15
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值