算法2-啊哈算法!

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_38446366/article/details/89522550

队列/栈/链表

队列

问题:小哈是小哼的新同桌,小哼向小哈询问 QQ 号,小哈给了小哼一串加密过的数字,同时小哈也告诉了小哼解密规则。规则是这样的:首先将第 1 个数删除,紧接着将第 2 个数放到这串数的末尾,再将第 3 个数删除并将第 4 个数放到这串数的末尾,再将第 5 个数删除…直到剩下最后一个数,将最后一个数也删除。按照刚才删除的顺序,把这些删除的数连在一起就是小哈的 QQ 啦。小哈给小哼加密过的一串数是“6 3 1 7 5 8 9 2 4”。

思路:

  • 解密的第一步是将第一个数删除,你可以想一下如何在数组中删除一个数呢。最简单的方法是将所有后面的数都往前面挪动一位,将前面的数覆盖。就好比我们在排队买票,最前面的人买好离开了,后面所有的人就需要全部向前面走一步,补上之前的空位,但是这样的做法很耗费时间。在这里,我将引入两个整型变量 head 和 tail。head 用来记录队列的队首(即第一位),tail 用来记录队列的队尾(即最后一位)的下一个位置。你可能会问:为什么 tail 不直接记录队尾,却要记录队尾的下一个位置呢?这是因为当队列中只剩下一个元素时,队首和队尾重合会带来一些麻烦。我们这里规定队首和队尾重合时,队列为空。
  • 现在有 9 个数,9 个数全部放入队列之后 head=1;tail=10;此时 head 和 tail 之间的数就是目前队列中“有效”的数。如果要删除一个数的话,就将 head++就 OK 了,这样仍然可以保持 head 和 tail 之间的数为目前队列中“有效”的数。这样做虽然浪费了一个空间,却节省了大量的时间,这是非常划算的。新增加一个数也很简单,把需要增加的数放到队尾即 q[tail]之后再 tail++就 OK啦。
  • 图解:
    在这里插入图片描述
    代码:
#include <stdio.h>
int main()
{
	int q[102]={0,6,3,1,7,5,8,9,2,4},head,tail;
	int i;
	//初始化队列
	head=1;
	tail=10; //队列中已经有9个元素了,tail指向队尾的后一个位置
	while(head<tail) //当队列不为空的时候执行循环
	{
		//打印队首并将队首出队
		printf("%d ",q[head]);
		head++;
		//先将新队首的数添加到队尾
		q[tail]=q[head];
		tail++;
		//再将队首出队
		head++;
	}
	getchar();getchar();
	return 0;
}

总结:

  • 队列是一种特殊的线性结构,它只允许在队列的首部(head)进行删除操作,这称为“出队”,而在队列的尾部(tail)进行插入操作,这称为“入队”。当队列中没有元素时(即 head==tail),称为空队列.
  • 队列将是我们今后学习广度优先搜索以及队列优化的 Bellman-Ford 最短路算法的核心数据结构。所以现在将队列的三个基本元素(一个数组,两个变量)封装为一个结构体类型,如下。
struct queue
{
	int data[100];//队列的主体,用来存储内容
	int head;//队首
	int tail;//队尾
};

上面的代码进行修改如下:

#include <stdio.h>
struct queue
{
	int data[100];//队列的主体,用来存储内容
	int head;//队首
	int tail;//队尾
};
int main()
{
	struct queue q;
	int i;
	//初始化队列
	q.head=1;
	q.tail=1;
	for(i=1;i<=9;i++)
	{
		//依次向队列插入9个数
		scanf("%d",&q.data[q.tail]);
		q.tail++;
	}
	while(q.head<q.tail) //当队列不为空的时候执行循环
	{
		//打印队首并将队首出队
		printf("%d ",q.data[q.head]);
		q.head++;
		//先将新队首的数添加到队尾
		q.data[q.tail]=q.data[q.head];
		q.tail++;
		//再将队首出队
		q.head++;
	}
	getchar();getchar();
	return 0;
}

还有一种是后进先出的数据结构,它叫做栈。栈限定为只能在一端进行插入和删除操作。

问题1:判断回文

问题:我们来看一个例子。“xyzyx”是一个回文字符串,所谓回文字符串就是指正读反读均相同的字符序列,如“席主席”、“记书记”、“aha”和“ahaha”均是回文,但“ahah”不是回文。通过栈这个数据结构我们将很容易判断一个字符串是否为回文。

思路:

  • 首先我们需要读取这行字符串,并求出这个字符串的长度。如果一个字符串是回文的话,那么它必须是中间对称的,我们需要求中点,即:mid=len/2-1;我们先将 mid 之前的字符全部入栈。因为这里的栈是用来存储字符的,所以这里用来实现栈的数组类型是字符数组即 char s[101];,初始化栈很简单,top=0;就可以了。入栈的操作是 top++; s[top]=x; (假设需要入栈的字符暂存在字符变量 x 中),其实可以简写为 s[++top]=x;
  • 接下来进入判断回文的关键步骤。将当前栈中的字符依次出栈,看看是否能与 mid 之后的字符一一匹配,如果都能匹配则说明这个字符串是回文字符串,否则这个字符串就不是回文字符串。

代码:

#include <stdio.h>
#include <string.h>
int main()
{
	char a[101],s[101];
	int i,len,mid,next,top;
	gets(a); //读入一行字符串
	len=strlen(a); //求字符串的长度
	mid=len/2-1; //求字符串的中点
	top=0;//栈的初始化
	//将mid前的字符依次入栈
	for(i=0;i<=mid;i++)
		s[++top]=a[i];
	//判断字符串的长度是奇数还是偶数,并找出需要进行字符匹配的起始下标
	if(len%2==0)
		next=mid+1;
	else
		next=mid+2;
	//开始匹配
	for(i=next;i<=len-1;i++)
	{
		if(a[i]!=s[top])
			break;
		top--;
	}
	//如果top的值为0,则说明栈内所有的字符都被一一匹配了
	if(top==0)
		printf("YES");
	else
		printf("NO");
	getchar();getchar();
	return 0;
}

栈还可以用来进行验证括号的匹配。比如输入一行只包含“()[]{}”的字符串,请判断
形如“([{}()])”或者“{()[]{}}”的是否可以正确匹配。

问题2:扑克游戏-小猫钓鱼

问题:游戏的规则是这样的:将一副扑克牌平均分成两份,每人拿一份。小哼先拿出手中的第一张扑克牌放在桌上,然后小哈也拿出手中的第一张扑克牌,并放在小哼刚打出的扑克牌的上面,就像这样两人交替出牌。出牌时,如果某人打出的牌与桌上某张牌的牌面相同,即可将两张相同的牌及其中间所夹的牌全部取走,并依次放到自己手中牌的末尾。当任意一人手中的牌全部出完时,游戏结束,对手获胜。
假如游戏开始时,小哼手中有 6 张牌,顺序为 2 4 1 2 5 6,小哈手中也有 6 张牌,顺序为 3 1 3 5 6 4,最终谁会获胜呢?现在你可以拿出纸牌来试一试。接下来请你写一个程序来自动判断谁将获胜。这里我们做一个约定,小哼和小哈手中牌的牌面只有 1~9。
思路:

  • 首先我们先来创建一个结构体用来实现队列,如下
struct queue
{
	int data[1000];
	int head;
	int tail;
}

再创建一个结构体用来实现栈,如下。

struct stack
{
	int data[10];
	int top;
};

其中 top 用来存储栈顶,数组 data 用来存储栈中的元素,大小设置为 10。因为只有 9种不同的牌面,所以桌上最多可能有 9 张牌,因此数组大小设置为 10 就够了。提示一下:为什么不设置为 9 呢?因为 C 语言数组下标是从 0 开始的。
接下来我们需要定义两个队列变量 q1 和 q2。 q1 用来模拟小哼手中的牌, q2 用来模拟小
哈手中的牌。定义一个栈变量 s 用来模拟桌上的牌。

struct queue
struct stack
q1,q2;
s;

接下来来初始化一下队列和栈。

//初始化队列q1和q2为空,此时两人手中都还没有牌
q1.head=1; q1.tail=1;
q2.head=1; q2.tail=1;
//初始化栈s为空,最开始的时候桌上也没有牌
s.top=0;

接下来需要读入小哼和小哈最初时手中的牌,分两次读入,每次读入 6 个数,分别插入q1 和 q2 中。

//先读入6张牌,放到小哼手上
for(i=1;i<=6;i++)
{
	scanf("%d",&q1.data[q1.tail]); //读入一个数到队尾
	q1.tail++;//队尾往后挪一位
}
//再读入6张牌,放到小哈手上
for(i=1;i<=6;i++)
{
	scanf("%d",&q2.data[q2.tail]); //读入一个数到队尾
	q2.tail++;//队尾往后挪一位
}

现在准备工作已经基本上做好了,游戏正式开始,小哼先出牌。t=q1.data[q1.head]; //小哼先亮出一张牌小哼打出第一张牌,也就是 q1 的队首,我们将这张牌存放在临时变量 t 中。接下来我们要判断小哼当前打出的牌是否能赢得桌上的牌。也就是判断桌上的牌与 t 有没有相同的,如何实现呢?我们需要枚举桌上的每一张牌与 t 进行比对,具体如下:

flag=0;
for(i=1;i<=top;i++)
{
	if(t==s[i]) { flag=1; break; }
}
如果 flag 的值为 0 就表明小哼没能赢得桌上的牌,将打出的牌留在桌上。
if(flag==0)
{
	//小哼此轮没有赢牌
	q1.head++; //小哼已经打出一张牌,所以要把打出的牌出队
	s.top++;
	s.data[s.top]=t; //再把打出的牌放到桌上,即入栈
}

如果 flag 的值为 1 就表明小哼可以赢得桌上的牌,
需要将赢得的牌依次放入小哼的手中。

f(flag==1)
{
	//小哼此轮可以赢牌
	q1.head++;//小哼已经打出一张牌,所以要把打出的牌出队
	q1.data[q1.tail]=t; //因为此轮可以赢牌,所以紧接着把刚才打出的牌又放到手中牌的末尾
	q1.tail++;
	while(s.data[s.top]!=t) //把桌上可以赢得的牌(从当前桌面最顶部一张牌开始取,直至取到与打出的牌相同为止)依次放到手中牌的末尾
	{
		q1.data[q1.tail]=s.data[s.top]; //依次放入队尾
		q1.tail++;
		s.top--; //栈中少了一张牌,所以栈顶要减1
	}
}

小哼出牌的所有阶段就模拟完了,小哈出牌和小哼出牌是一样的。接下来我们要判断游戏如何结束。即只要两人中有一个人的牌用完了游戏就结束了。因此需要在模拟两人出牌代码的外面加一个 while 循环来判断,如下。

while(q1.head<q1.tail && q2.head<q2.tail ) //当队列q1和q2都不为空的时候执行循环最后一步,输出谁最终赢得了游戏,以及游戏结束后获胜者手中的牌和桌上的牌。如果小哼获胜了那么小哈的手中一定没有牌了(队列 q2 为空),即 q2.head==q2.tail,具体输出如下。

if(q2.head==q2.tail)
{
	printf("小哼win\n");
	printf("小哼当前手中的牌是");
	for(i=q1.head;i<=q1.tail-1;i++)
	printf(" %d",q1.data[i]);
	if(s.top>0) //如果桌上有牌则依次输出桌上的牌
	{
		printf("\n桌上的牌是");
		for(i=1;i<=s.top;i++)
			printf(" %d",s.data[i]);
	}
	else
		printf("\n桌上已经没有牌了");
	}
}

反之,小哈获胜,代码的实现也是差不多的,就不再赘述了。到此,所有的代码实现就都讲完了,在上面我们讲解的所有实现中,每个人打出一张牌后,判断能否赢牌这一点可以优化。之前我们是通过枚举桌上的每一张牌来实现的,即用了一个 for 循环来依次判断桌上的每一张牌是否与打出的牌相等。其实有更好的办法来解决这个问题,就是用一个数组来记录桌上有哪些牌。因为牌面只有 1~9,因此只需开一个大小为 10 的数组来记录当前桌上已经有哪些牌面就可以了。

int book[10];

这里我再一次使用了 book 这个单词,因为这个单词有记录、登记的意思,而且单词拼写简洁。另外很多国外的算法书籍在处理需要标记问题的时候也都使用 book 这个单词,因此我这里就沿用了。当然你也可以使用 mark 等你自己觉得好理解的单词啦。下面需要将数组book[1]~book[9]初始化为 0,因为刚开始桌面上一张牌也没有。

for(i=1;i<=9;i++)
	book[i]=0;

接下来,如果桌面上增加了一张牌面为 2 的牌,那就需要将 book[2]设置为 1,表示牌面为 2 的牌桌上已经有了。当然如果这张牌面为 2 的牌被拿走后,需要及时将 book[2]重新设置为 0,表示桌面上已经没有牌面为 2 的牌了。这样一来,寻找桌上是否有与打出的牌牌面相同的牌,就不需要再循环枚举桌面上的每一张牌了,而只需用一个 if 判断即可。这一点是不是有点像第 1 章第 1 节的桶排序的方法呢?具体如下。

t=q1.data[q1.head]; //小哼先亮出一张牌
if(book[t]==0) // 表明桌上没有牌面为t的牌
{
	//小哼此轮没有赢牌
	q1.head++; //小哼已经打出一张牌,所以要把打出的牌出队
	s.top++;
	s.data[s.top]=t; //再把打出的牌放到桌上,即入栈
	book[t]=1; //标记桌上现在已经有牌面为t的牌
}

代码:
完整代码如下:

#include <stdio.h>
struct queue
{
	int data[1000];
	int head;
	int tail;
};
struct stack
{
	int data[10];
	int top;
};
int main()
{
	struct queue q1,q2;
	struct stack s;
	int book[10];
	int i,t;
	//初始化队列
	q1.head=1; q1.tail=1;
	q2.head=1; q2.tail=1;
	//初始化栈
	s.top=0;
	//初始化用来标记的数组,用来标记哪些牌已经在桌上
	for(i=1;i<=9;i++)
		book[i]=0;
	//依次向队列插入6个数
	//小哼手上的6张牌
	for(i=1;i<=6;i++)
	{
		scanf("%d",&q1.data[q1.tail]);
		q1.tail++;
	}
	//小哈手上的6张牌
	for(i=1;i<=6;i++)
	{
		scanf("%d",&q2.data[q2.tail]);
		q2.tail++;
	}
	while(q1.head<q1.tail && q2.head<q2.tail ) //当队列不为空的时候执行循环
	{
		t=q1.data[q1.head];//小哼出一张牌
		//判断小哼当前打出的牌是否能赢牌
		if(book[t]==0) //表明桌上没有牌面为t的牌
		{
			//小哼此轮没有赢牌
			q1.head++; //小哼已经打出一张牌,所以要把打出的牌出队
			s.top++;
			s.data[s.top]=t; //再把打出的牌放到桌上,即入栈
			book[t]=1; //标记桌上现在已经有牌面为t的牌
		}
		else
		{
			//小哼此轮可以赢牌
			q1.head++;//小哼已经打出一张牌,所以要把打出的牌出队
			q1.data[q1.tail]=t;//紧接着把打出的牌放到手中牌的末尾
			q1.tail++;
			while(s.data[s.top]!=t) //把桌上可以赢得的牌依次放到手中牌的末尾
			{
				book[s.data[s.top]]=0;//取消标记
				q1.data[q1.tail]=s.data[s.top];//依次放入队尾
				q1.tail++;
				s.top--; //栈中少了一张牌,所以栈顶要减1
			}
		}
		t=q2.data[q2.head]; //小哈出一张牌
		//判断小哈当前打出的牌是否能赢牌
		if(book[t]==0) //表明桌上没有牌面为t的牌
		{
			//小哈此轮没有赢牌
			q2.head++; //小哈已经打出一张牌,所以要把打出的牌出队
			s.top++;
			s.data[s.top]=t; //再把打出的牌放到桌上,即入栈
			book[t]=1; //标记桌上现在已经有牌面为t的牌
		}
		else
		{
			//小哈此轮可以赢牌
			q2.head++;//小哈已经打出一张牌,所以要把打出的牌出队
			q2.data[q2.tail]=t;//紧接着把打出的牌放到手中牌的末尾
			q2.tail++;
			while(s.data[s.top]!=t) //把桌上可以赢得的牌依次放到手中牌的末尾
			{
				book[s.data[s.top]]=0;//取消标记
				q2.data[q2.tail]=s.data[s.top];//依次放入队尾
				q2.tail++;
				s.top--;
			}
		}
	}
	if(q2.head==q2.tail)
	{
		printf("小哼win\n");
		printf("小哼当前手中的牌是");
		for(i=q1.head;i<=q1.tail-1;i++)
			printf(" %d",q1.data[i]);
		if(s.top>0) //如果桌上有牌则依次输出桌上的牌
		{
			printf("\n桌上的牌是");
			for(i=1;i<=s.top;i++)
			printf(" %d",s.data[i]);
		}
		else
			printf("\n桌上已经没有牌了");
	}
	else
	{
		printf("小哈win\n");
		printf("小哈当前手中的牌是");
		for(i=q2.head;i<=q2.tail-1;i++)
			printf(" %d",q2.data[i]);
		if(s.top>0) //如果桌上有牌则依次输出桌上的牌
		{
			printf("\n桌上的牌是");
			for(i=1;i<=s.top;i++)
				printf(" %d",s.data[i]);
		}
		else
			printf("\n桌上已经没有牌了");
	}
	getchar();getchar();
	return 0;
}

链表

在存储一大波数的时候,我们通常使用的是数组,但有时候数组显得不够灵活,比如下面这个例子。有一串已经从小到大排好序的数 2 3 5 8 9 10 18 26 32。现需要往这串数中插入 6 使其得到的新序列仍符合从小到大排列。如我们使用数组来实现这一操作,则需要将 8 和 8 后面的数都依次往后挪一位。
这样操作显然很耽误时间,如果使用链表则会快很多。那什么是链表呢?请看下图。
在这里插入图片描述
首先我们来看一下,链表中的每一个结点应该如何存储。
每一个结点都由两个部分组成。左边的部分用来存放具体的数值,那么用一个整型变量就可以;右边的部分需要存储下一个结点的地址,可以用指针来实现(也称为后继指针)。这里我们定义一个结构体类型来存储这个结点,如下。

struct node
{
	int data;
	struct node *next;
};

在这里插入图片描述
上面代码中,我们定义了一个叫做 node 的结构体类型,这个结构体类型有两个成员。第一个成员是整型 data,用来存储具体的数值;第二个成员是一个指针,用来存储下一个结点的地址。因为下一个结点的类型也是 struct node,所以这个指针的类型也必须是 struct node* 类型的指针。
如何建立链表呢?首先我们需要一个头指针 head 指向链表的最开始。当链表还没有建立的时候头指针 head 为空(也可以理解为指向空结点)。

struct node *head;
head = NULL;//头指针初始为空

现在我们来创建第一个结点,并用临时指针 p 指向这个结点。

struct node *p;
//动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
p=(struct node *)malloc(sizeof(struct node));

接下来分别设置新创建的这个结点的左半部分和右半部分。

scanf("%d",&a);
p->data=a;//将数据存储到当前结点的data域中
p->next=NULL;//设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空

在这里插入图片描述最后要将指针 q 也指向当前结点,因为待会儿临时指针 p 将会指向新创建的结点。
q=p;//指针q也指向当前结点

完整代码:

#include <stdio.h>
#include <stdlib.h>
//这里创建一个结构体用来表示链表的结点类型
struct node
{
	int data;
	struct node *next;
};
int main()
{
	struct node *head,*p,*q,*t;
	int i,n,a;
	scanf("%d",&n);
	head = NULL;//头指针初始为空
	for(i=1;i<=n;i++)//循环读入n个数
	{
		scanf("%d",&a);
		//动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
		p=(struct node *)malloc(sizeof(struct node));
		p->data=a;//将数据存储到当前结点的data域中
		p->next=NULL;//设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空
		if(head==NULL)
			head=p;//如果这是第一个创建的结点,则将头指针指向这个结点
		else
			q->next=p;//如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
		q=p;//指针q也指向当前结点
	}
	//输出链表中的所有数
	t=head;
	while(t!=NULL)
	{
		printf("%d ",t->data);
		t=t->next;//继续下一个结点
	}
	getchar();getchar();
	return 0;
}

需要说明的一点是:上面这段代码没有释放动态申请的空间,虽然没有错误,但是这样
会很不安全,有兴趣的朋友可以去了解一下 free 命令。

模拟链表

在这里插入图片描述代码如下:

#include <stdio.h>
int main()
{
	int data[101],right[101];
	int i,n,t,len;
	//读入已有的数
	scanf("%d",&n);
	for(i=1;i<=n;i++)
		scanf("%d",&data[i]);
	len=n;
	//初始化数组right
	for(i=1;i<=n;i++)
	{
		if(i!=n)
			right[i]=i+1;
		else
			right[i]=0;
	}
	//直接在数组data的末尾增加一个数
	len++;
	scanf("%d",&data[len]);
	//从链表的头部开始遍历
	t=1;
	while(t!=0)
	{
		if(data[right[t]]>data[len])//如果当前结点下一个结点的值大于待插入数,将数插入到中间
		{
			right[len]=right[t];//新插入数的下一个结点标号等于当前结点的下一个结点编号
			right[t]=len;//当前结点的下一个结点编号就是新插入数的编号
			break;//插入完成跳出循环
		}
		t=right[t];
	}
	//输出链表中所有的数
	t=1;
	while(t!=0)
	{
		printf("%d ",data[t]);
		t=right[t];
		getchar();
		getchar();
		return 0;
	}
}

使用模拟链表也可以实现双向链表和循环链表。

猜你喜欢

转载自blog.csdn.net/qq_38446366/article/details/89522550