博客为参考《啊哈!算法》一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。
1.解密QQ号-队列
假设有一串加密的数字为“6 3 1 7 5 8 9 2 4”。解密的规则为:首先将第1个数删除,紧接着将第2个数放到这串数的末尾,再将第3个数删除并将第4个数放到这串数的末尾,再将第5个数删除,……,直到剩下最后一个数,将最后一个数也删除。按照刚才删除的顺序,把这些删除的数连在一起就是解密后的结果。解密后得到的一串数字应该是“6 1 5 9 4 7 2 8 3”。
如果我们把待解密的数放在数组里,解密的第一步是将第一个数删除,最简单的方法是将所有后面的数都往前面挪动一位,将前面的数覆盖。但是这样的做法很耗费时间:
在这里,我们引入两个整型变量head和tail。head用来记录队列的队首(即第一位),tail用来记录队列的队尾(即最后一位)的下一个位置。
现在有9个数,9个数全部放入队列之后head=1,tail=10;此时head和tail之间的数就是目前队列中“有效”的数。如果要删除一个数的话,就将head++就OK了,这样仍然可以保持head和tail之间的数为目前队列中“有效”的数。这样做虽然浪费了一个空间,却节省了大量的时间,这是非常划算的。新增加一个数也很简单,把需要增加的数放在队尾即q[tail]之后再tail++就OK了。
我们来小结一下,在队首删除一个数的操作是head++:
在队尾增加一个数(假设这个数是x)的操作是q[tail]=x,tail++:
整个解密过程可表示为:
代码实现见下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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),称为空队列。队列遵循“先进先出”(First In First Out,FIFO)原则。
我们可以将队列封装为一个结构体类型,如下:
1
2
3
4
5
6
struct queue
{
int data[100]; //队列的主体,用来存储内容
int head; //队首
int tail; //队尾
};
接着我们使用定义的结构体来实现队列操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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=0; 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;
}
实际上,C++的STL库已经有队列的实现,无需我们自己再定义了。
2.解密回文-栈
第1部分介绍了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构,它叫做栈。栈限定为只能在一端进行插入和删除操作。
栈的实现也很简单,只需要一个一维数组和一个指向栈顶的变量top就可以了。我们通过top来对栈进行插入和删除操作。
栈究竟有哪些作用呢?我们来看一个例子。“xyzyx”是一个回文字符串,所谓回文字符串就是指正读反读均相同的字符序列。通过栈这个数据结构我们将很容易判断一个字符串是否为回文。实现代码见下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#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; i++)
{
if(a[i] != s[top])
break;
top--;
}
//如果top值为0,则说明栈内所有的字符都被一一匹配了
if(top == 0)
printf("YES");
else
printf("NO");
getchar();getchar();
return 0;
}
3.纸牌游戏-小猫钓鱼
接下来我们用第1和第2部分学到的知识实现一个“接竹竿”的纸牌小游戏:将一副扑克牌平均分成两份,每人拿一份,两人交替出牌。出牌时,如果某人打出的牌与桌子上某张牌的牌面相同,即可将两张相同的牌及其中间所夹的牌全部取走,并依次放到自己手中牌的末尾。当任意一人手中的牌全部出完时,游戏结束,对手获胜。
假如游戏开始时,玩家1手中有6张牌,顺序为2 4 1 2 5 6,玩家2手中也有6张牌,顺序为3 1 3 5 6 4,最终谁会获胜呢?假设牌面只有1~9。
我们先来分析一下,玩家有两种操作,分别是出牌和赢牌。这恰好对应队列的两个操作,出牌就是出队,赢牌就是入队。而桌子就是一个栈,每打出一张牌放到桌上就相当于入栈。当有人赢牌的时候,依次将牌从桌上拿走,这就相当于出栈。因此我们可以用两个队列,一个栈来实现上述游戏,代码见下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#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个数
for(i=1; i<=6; i++)
{
scanf("%d", &q1.data[q1.tail]);
q1.tail++;
}
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]; //玩家1出一张牌
//判断玩家1当前打出的牌是否能赢牌
if(book[t] == 0) //表明桌上没有牌面为t的牌
{
//玩家1此轮没有赢牌
q1.head++; //玩家1已经打出一张牌,所以要把打出的牌出队
s.top++;
s.data[s.top] = t; //再把打出的牌放到桌上,即入栈
book[t] = 1; //标记桌上现在已经有牌面为t的牌
}
else
{
//玩家1此轮可以赢牌
q1.head++; //玩家1已经打出一张牌,所以要把打出的牌出队
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]; //玩家2出一张牌
//判断玩家2当前打出的牌是否能赢牌
if(book[t] == 0) //表明桌上没有牌面为t的牌
{
//玩家2此轮没有赢牌
q2.head++; //玩家2已经打出一张牌,所以要把打出的牌出队
s.top++;
s.data[s.top] = t; //再把打出的牌放到桌上,即入栈
book[t] = 1; //标记桌上现在已经有牌面为t的牌
}
else
{
//玩家2此轮可以赢牌
q2.head++; //玩家2已经打出一张牌,所以要把打出的牌出队
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--; //栈中少了一张牌,所以栈顶要减1
}
}
}
if(q2.head == q2.tail)
{
printf("player1 win\n");
printf("cards in player1 :");
for(i=q1.head; i<=q1.tail-1; i++)
printf(" %d", q1.data[i]);
if(s.top > 0) //如果桌上有牌则依次输出桌上的牌
{
printf("\ncards on desk :");
for(i=1; i<=s.top; i++)
printf(" %d", s.data[i]);
}
else
printf("\nno cards on desk");
}
else
{
printf("player2 win\n");
printf("cards in player2 :");
for(i=q2.head; i<=q2.tail-1; i++)
printf(" %d", q2.data[i]);
if(s.top > 0) //如果桌上有牌则依次输出桌上的牌
{
printf("\ncards on desk :");
for(i=1; i<=s.top; i++)
printf(" %d", s.data[i]);
}
else
printf("\nno cards on desk");
}
getchar();getchar();
return 0;
}
可以输入以下数据进行验证:
1
2
2 4 1 2 5 6
3 1 3 5 6 4
运行结果是:
1
2
3
player1 win
cards in player1 : 5 6 2 3 1 4 6 5
cards on desk : 2 1 3 4
4.链表
在存储一大波数的时候,我们通常使用的是数组,但有时候数组显得不够灵活,比如下面这个例子。
有一串已经从小到大排好序的数2 3 5 8 9 10 18 26 32。现需要往这串数中插入6使其得到的新序列仍符合从小到大排列。如我们使用数组来实现这一操作,则需要将8和8后面的数都依次往后挪一位,如下:
这样操作显然很耽误时间,如果使用链表则会快很多。那什么是链表呢?请看下图:
此时如果需要在8前面插入一个6,就只需像下图这样更改一下就可以了,而无需再将8及后面的数都依次往后挪一位。这样就很节省时间:
我们使用指针和动态分配内存函数malloc来实现链表。我们想在程序中存储一个整数10,除了使用int a
这种方式在内存中申请一块区域来存储,还有另外一种动态存储方法。
1
malloc(4);
malloc函数的作用就是从内存中申请分配指定字节大小的内存空间。上面这行代码就申请了4个字节。如果你不知道int类型是4个字节的,还可以使用sizeof(int)获取int类型所占用的字节数,如下:
1
malloc(sizeof(int));
现在我们已经成功地从内存中申请了4个字节的空间来准备存放一个整数,可是如何来对这个空间进行操作呢?这里我们就需要用一个指针来指向这个空间,即存储这个空间的首地址。
1
2
int* p;
p = (int*)malloc(sizeof(int));
需要注意,malloc函数的返回类型是void*类型。void*表示未确定类型的指针。在C和C++中,void*类型可以强制转换为任何其他类型的指针。上面代码中我们将其强制转化为整型指针,以便告诉计算机这里的4个字节作为一个整体用来存放整数。指针变量存储的是一个内存空间的首地址(第一个字节的地址),但是这个空间占用了多少个字节,用来存储什么类型的数,则是由指针的类型来标明的。这样系统才知道应该取多少个连续内存作为一个数据。
链表的每一个结点都由两个部分组成。左边的部分用来存放具体的数值,本例中用一个整型变量就可以;右边的部分需要存储下一个结点的地址,可以用指针来实现(也称为后继指针)。这里我们定义一个结构体类型来存储这个结点:
1
2
3
4
5
struct node
{
int data;
struct node *next;
}
如何建立链表呢?首先我们需要一个头指针head指向链表的最开始。当链表还没有建立的时候头指针head为空(也可以理解为指向空结点)。
1
2
struct node *head;
head = NULL; //头指针初始为空
现在我们来创建第一个结点,并用临时指针p指向这个结点。
1
2
3
struct node *p;
//动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
p = (struct node *)malloc(sizeof(struct node));
接下来分别设置新创建的这个结点的左半部分和右半部分。
1
2
3
scanf("%d",&a);
p->data=a; //将数据存储到当前结点的data域中
p->next=NULL; //设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空
下面来设置头指针并设置新创建结点的*next指向空。头指针的作用是方便以后从头遍历整个链表。
1
2
3
4
if(head == NULL)
head=p; //如果这是第一个创建的结点,则将头指针指向这个结点
else
q->next=p; //如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
如果这是第一个创建的结点,则将头指针指向这个结点。
如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点。
最后要将指针q也指向当前结点,因为待会儿临时指针p将会指向新创建的结点。
1
q=p; //指针q也指向当前结点
假设我们需要往链表中插入6,操作如下:
代码实现见下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#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也指向当前结点
}
scanf("%d",&a); //读入待插入的数
t = head; //从链表头部开始遍历
while (t != NULL) //当没有到达链表尾部的时候循环
{
if(t->next->data > a) //如果当前结点下一个结点的值大于待插入数,将数插入到中间
{
p = (struct node *)malloc(sizeof(struct node)); //动态申请一个空间,用来存放新增结点
p->data = a;
p->next = t->next; //新增结点的后继指针指向当前结点的后继指针所指向的结点
t->next = p; //当前结点的后继指针指向新增结点
break; //插入完毕退出循环
}
t = t->next; //继续下一个结点
}
//输出链表中的所有数
t = head;
while(t != NULL)
{
printf("%d ",t->data);
t = t->next; //继续下一个结点
}
getchar();getchar();
return 0;
}
需要说明的一点是:上面这段代码没有释放动态申请的空间。
可以输入以下数据进行验证。
1
2
3
9
2 3 5 8 9 10 18 26 32
6
运行结果是:
1
2 3 5 6 8 9 10 18 26 32
5.模拟链表
链表还有另外一种使用数组来实现的方式,叫做模拟链表。
链表中的每一个结点只有两个部分。我们可以用一个数组data来存储每序列中的每一个数。然后再用另一个数组right来存放序列中每一个数右边的数。
上图的两个数组中,第一个整型数组data是用来存放序列中具体数字的,另外一个整型数组right是用来存放当前序列中每一个元素右边的元素在数组data中位置的。right[9]的值为0,就表示当前序列中9号元素的右边没有元素。
现在需要在8前面插入一个6,只需将6直接存放在数组data的末尾即data[10]=6。接下来只需要将right[3]改为10,表示新序列中3号元素右边的元素存放在data[10]中。再将right[10]改为4,表示新序列中10号元素右边的元素存放在data[4]中。这样我们通过right数组就可以从头到尾遍历整个序列了(序列的每个元素的值存放在对应的数组data中),如下。
完整的代码实现如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#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;
}
可以输入以下数据进行验证。
1
2
3
9
2 3 5 8 9 10 18 26 32
6
运行结果是:
1
2 3 5 6 8 9 10 18 26 32
使用模拟链表也可以实现双向链表和循环链表。