专栏
第二节——栈、队列、链表
队列
我们先来看一串加密过的数字“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++; 。
整个解密过程,请看下面这个图。
最后输出的就是6 1 5 9 4 7 2 8 3,代码实现如下。
#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)原则。
队列将是我们今后学习广度优先搜索以及队列优化的 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;
}
栈
在上面我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构,它叫做栈。栈限定为只能在一端进行插入和删除操作。
栈的实现也很简单,只需要一个一维数组和一个指向栈顶的变量 top 就可以了。我们通过 top 来对栈进行插入和删除操作。
栈究竟有哪些作用呢?我们来看一个例子。“xyzyx”是一个回文字符串,所谓回文字符串就是指正读反读均相同的字符序列,如“席主席”、“记书记”、“aha”和“ahaha”均是回文,但“ahah”不是回文。通过栈这个数据结构我们将很容易判断一个字符串是否为回文。
首先我们需要读取这行字符串,并求出这个字符串的长度。
char a[101];
int len;
gets(a);
len=strlen(a);
如果一个字符串是回文的话,那么它必须是中间对称的,我们需要求中点,即:
mid=len/2-1
接下来就轮到栈出场了。
我们先将mid之前的字符全部入栈。因为这里的栈是用来存储字符的,所以这里用来实现栈的数组类型是字符数组即 char s[101]; ,初始化栈很简单, top=0; 就可以了。入栈的操作是 top++;s[top]=x; (假设需要入栈的字符暂存在字符变量 x 中),其实可以简写为 s[++top]=x; 。
现在我们就来将 mid 之前的字符依次全部入栈。
for(i=0;i<=mid;i++)
{
s[++top]=a[i];
}
接下来进入判断回文的关键步骤。将当前栈中的字符依次出栈,看看能否与 mid 之后的字符一一匹配,如果都能匹配则说明这个字符串是回文字符串,否则这个字符串就不是回文字符串。
for(i=mid+1);i<=len-1;i++)
{
if(a[i]!=s[top])
{
break;
}
top--;
}
if(top==0)
printf("YES");
else
printf("NO");
最后如果 top 的值为 0 ,就说明栈内所有的字符都被一一匹配了,那么这个字符串就是回文字符串。完整的代码如下。
#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;
}
可以输入以下数据进行验证。
ahaha
运行结果是:
YES
栈还可以用来进行验证括号的匹配。比如输入一行只包含“()[]{}”的字符串,请判断形如“([{}()])”或者“{()[]{}}”的是否可以正确匹配。显然上面两个例子都是可以正确匹配的。“([)]”是不能匹配的。有兴趣的同学可以自己动手来试一试。
链表
在存储一大波数的时候,我们通常使用的是数组,但有时候数组显得不够灵活,比如下面这个例子。
有一串已经从小到大排序号的数 2 3 5 8 9 10 18 26 32。现需要往这串数中插入 6 使其得到的新序列仍然符合从小到大排列。如果我们使用数组来实现这一操作,则需要将 8 和 8后面的数都往后挪一位,如下:
这样操作显然很耽误时间,如果使用链表则会快很多。那么说明是链表呢?请看下图。
此时如果需要在 8 前面插入一个 6 ,就只需像下图这样更改一下就可以了,而无需再将 8 及后面的数都依次往后挪一位。是不是很节省时间呢?
那么如何实现链表呢?在 C 语言中可以使用指针和动态分配内存函数 malloc 来实现。
首先我们来看一下,链表中的每一个结点应该如何存储。
每一个结点都由两个部分组成。左边的部分用来存放具体的数值,那么用一个整形变量就可以;右边的部分需要存储下一个结点的地址,可以用指针来实现(也称为后继指针)。这里我们定义一个结构体类型来存储这个结点,如下:
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;//设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空
下面来设置头指针并设置新创建结点 *next 指向空,头指针的作用是方便以后从头遍历整个链表。
if(head==NULL)
head=p;//如果这是第一个创建的结点,则头指针指向这个结点
else
q->next=p;//如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
如果这是第一个创建的结点,则头指针指向这个结点。
如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点。
最后要将指针 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 命令。
可以输入以下数据进行验证。
9
2 3 5 8 9 10 18 26 32
运行的结果是:
2 3 5 8 9 10 18 26 32
接下来需要往链表中插入6,操作如下。
首先用一个临时指针 t 从链表的头部开始遍历。
t=head;//从链表头部开始遍历
等到指针 t 的下一个结点的值比 6 大的时候,将 6 插入到中间。即 t->next->data 大于 6 时进行插入,代码如下。
scanf("%d",&a);//读入待插入的数
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;//继续下一个结点
}
完整代码如下。
#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;
}
可以输入以下数据进行验证。
9
2 3 5 8 9 10 18 26 32
6
运行结果是:
2 3 5 6 8 9 10 18 26 32
参考:《啊哈!算法》