我们在了解计算机语言的时候,能够知道,当软件被使用时,会先调入内存,当调用完成,就释放空间。 那么。这些连续存储空间的分配和回收操作,存在一个问题:会产生“内存碎片” 为了避免上述情况,现代计算机通常采用“页式存储系统”来实现内存申请与回收操作
那么,为了将这个例子更加形象地展示出来,我们这里用一个算法来解释这种操作的原理——发牌算法
发牌算法:
顾名思义,要求是,发出去的牌不会再次被发出
现在看到这个要求,我们的第一想法可能是用一个数组(先命名为arr数组)来存储取出的值,每当规定范围随机数产生数的时候,我们遍历arr数组,来看看这个值存在不存在这个数组中,若不存在,我们就将这个数输出,并将这个数存在arr数组中,以防下次输出此数。
若是今天本篇博文按照上述的思想去编程的话,可能会让知情的编程老手笑掉大牙吧,因为我们数据结构与算法的主要思想是编写的程序的时间复杂度尽可能地低,而若是按照上面的思想,无疑每产生一个数就要去遍历数组,有新的数产生的话,我们还要为数组赋值,这样看来,时间复杂度很高。
那么,现在,本人来介绍本篇博文的主题算法——发牌算法。用这个算法来实现我们上述的要求,时间复杂度非常低,只需遍历一个长度尽可能低的数组(即时间复杂度为O(n))
那么,发牌算法大致思想: 我们按照数组的下标取数,将已经取出的“牌”和未取出数组内的最后一张“牌”交换下标,并使得下一次“取牌”时的未取出的范围比这次的范围少1
现在我们来编写一些程序,来使得我们上述的逻辑更加清晰化、形象化: 我们现在要取牌,那么就先编写两个全局数组——花色数组、牌值数组:
const char *pokerType[] = {"黑桃", "红桃", "方块", "梅花"}; //[]比*优先级高,所以这个定义的意义是字符串数组
//10是两个字符,所以类型是char *
const char *pokerValue[] =
{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
那么,现在我们来编写一个构建牌池函数:
#define POKERS_COUNT 52
void resetPoker(char *poker) {
int i;
for(i = 0; i < POKERS_COUNT; i++) {
poker[i] = i;
}
}
上面这个函数可能让好多人感觉一头雾水,为什么单纯的将相应下标存入数组相应单元的操作被我们称为取数函数呢? 因为一副扑克牌一共有52张,我们花色按照“黑桃、红桃、方块、梅花”,大小按照“A->K”的顺序,将所有牌从下标为0到51全部存入一个数组中,相当于将所有牌全部放在了poker数组中。
那么,有了牌池,我们现在就来为玩家发牌,即“取牌操作”了,现在我们来编写取牌函数:
#define POKERS_COUNT 52
#define POKER_TYPE_COUNT 13
void dealPoker(char (*pokerSet)[POKER_TYPE_COUNT]) {
char pokerSet[POKERS_COUNT];
int range;
int index = 0;
resetPoker(pokerSet);
srand(time(0));
for(range = POKERS_COUNT; range > 0; range--) {
int rnum = rand() % range; //这里要提醒的一点的是:所有的变量的声明都可以在使用该变量之前!
int tmp;
playerPokerSet[0][index] = pokerSet[rnum];
tmp = playerPokerSet[0][index];
pokerSet[rnum] = pokerSet[range - 1];
pokerSet[range - 1] = tmp;
}
}
这里本人再次对变量声明的位置做以下解释: 在我们的DEVC中,我们所运用C语言环境时,可以将变量声明放在使用它之前。但是在别的编译器中可能就会出现错误(例:单片机的KEIC编译器中,就会出现错误),所以,这里就建议大家养成良好习惯,在运用C语言环境时,我们最好在函数开头就对所有的变量进行声明!
我们在构建了牌池之后,就要用随机数函数按照我们上面说的发牌算法的思想来取牌:
用随机数取该随机数作为数组下标的数组内的值,并将该数组单元和未排序的部分的最后一个数组单元进行交换,这样就能保证下一轮开始取牌时,前几个数组单元全都是未取出牌的单元,这样就能在仅遍历数组一遍的前提下还能发放不同的牌
取牌操作我们实现了,那么,我们下面的操作是显示玩家取到的牌的函数——显示函数:
void showPlayPokerSet(char (*playerPokerSet)[POKER_TYPE_COUNT], int whichOne) {
int i;
int onePaper;
if(whichOne < 0 || whichOne > 3) {
return;
}
printf("第%d个玩家的牌:
", whichOne + 1);
for(i = 0; i <POKER_TYPE_COUNT; i++) {
onePaper = (int)playerPokerSet[whichOne][i];
printf("【%s:%s】 ", pokerType[onePaper / POKER_TYPE_COUNT], pokerValue[onePaper % POKER_TYPE_COUNT]);
}
printf("
");
}
关于显示函数,一直都是我们编写的函数中最容易实现的函数,这里仅对这个函数做如下解释: 1.我们传递的参数,是char [POKER_TYPE_COUNT] *类型的,这就意味着,这是个指针,它表示的是一个二维数组(即:playerPokerSet[哪一个玩家][第几张牌])。而至于另一个参数,则表示哪一个玩家。这样下来,某一玩家的牌就可以展示完毕了。 2.第十二行代码(倒数第二个printf())的参数:pokerType[onePaper / POKER_TYPE_COUNT], pokerValue[onePaper % POKER_TYPE_COUNT] 这个参数表示的是该玩家的该张牌的花色和数值
为了我们展示时能够清晰看到所发的牌有没有重复,我们来编写一个排序函数:
void sortPlayPokerSet(char *playerPoker) {
int i;
int j;
char tmp;
for(i = 0; i < POKERS_TYPE_COUNT; i++) {
for(j = i+1; j < POKERS_TYPE_COUNT; j++) {
if(playerPoker[i] > playerPoker[j]) {
tmp = playerPoker[i];
playerPoker[i] = playerPoker[j];
playerPoker[j] = tmp;
}
}
}
}
这样下来,我们基本上编写完成了发牌算法的所有要求,我们现在来总结下: poker.c:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define POKERS_COUNT 52
#define POKER_TYPE_COUNT 13
const char *pokerType[] = {"黑", "红", "方", "梅"}; //[]比*优先级高,所以这个定义的意义是字符串数组
//10是两个字符,所以类型是char *
const char *pokerValue[] =
{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
void resetPoker(char *poker);
void dealPoker(char (*pokerSet)[POKER_TYPE_COUNT]);
void showPlayPokerSet(char (*playerPokerSet)[POKER_TYPE_COUNT], int whichOne);
void sortPlayPokerSet(char *playerPoker);
void sortPlayPokerSet(char *playerPoker) {
int i;
int j;
char tmp;
for(i = 0; i < POKER_TYPE_COUNT; i++) {
for(j = i+1; j < POKER_TYPE_COUNT; j++) {
if(playerPoker[i] > playerPoker[j]) {
tmp = playerPoker[i];
playerPoker[i] = playerPoker[j];
playerPoker[j] = tmp;
}
}
}
}
void showPlayPokerSet(char (*playerPokerSet)[POKER_TYPE_COUNT], int whichOne) {
int i;
int onePaper;
if(whichOne < 0 || whichOne > 3) {
return;
}
printf("第%d个玩家的牌:
", whichOne + 1);
for(i = 0; i < POKER_TYPE_COUNT; i++) {
onePaper = (int)playerPokerSet[whichOne][i];
printf("【%s:%s】 ", pokerType[onePaper / POKER_TYPE_COUNT], pokerValue[onePaper % POKER_TYPE_COUNT]);
}
printf("
");
}
void dealPoker(char (*playerPokerSet)[POKER_TYPE_COUNT]) {
char pokerSet[POKERS_COUNT];
int range;
int index = 0;
resetPoker(pokerSet);
srand(time(NULL));
for(range = POKERS_COUNT; range > 0; range--) {
int rnum = rand() % range; //这里要提醒的一点是:所有的变量的声明都可以在使用该变量即可!
int tmp;
playerPokerSet[0][index++] = pokerSet[rnum];
tmp = playerPokerSet[0][index];
pokerSet[rnum] = pokerSet[range - 1];
pokerSet[range - 1] = tmp;
}
}
void resetPoker(char *poker) {
int i;
for(i = 0; i < POKERS_COUNT; i++) {
poker[i] = i;
}
}
int main() {
int i;
char playerPokerSet[4][POKER_TYPE_COUNT] = {0};
dealPoker(playerPokerSet);
printf("发牌结束!
每个人的牌是:
");
for(i = 0; i < 4; i++) {
sortPlayPokerSet(playerPokerSet[i]);
}
for(i = 0; i < 4; i++) {
showPlayPokerSet(playerPokerSet, i);
}
return 0;
}
那么,我们这里来运行一下,看一下结果: 我们能够发现:发的所有牌没有一张是重复的。 这说明我们编写的代码是正确的,而且时间复杂度明显比我们从前的那种每产生一个随机数就遍历数组的想法相比,时间复杂度和空间复杂度都小了很多。
这个算法不是我们凭空制造的算法,这是计算机处理“内存分配”时所用的方法——页式存储结构 这个存储结构意思是计算机所给予我们的存储空间,并不一定是连续的(在物理方面)。它会用“逻辑连续空间”为我们提供存储空间,顾名思义,逻辑连续的意思是在物理意义上,并不一定是连续的,它的大致处理思想和我们本篇博文的发牌算法是一致的。