第二节——栈、队列、链表

专栏

算法(C语言)

第二节——栈、队列、链表

队列

我们先来看一串加密过的数字“6 3 1 7 5 8 9 2 4”,解密规则是这样的:首先将第 1 个数删除,紧接着将第 2 个数放到这串数的末尾,再将第 3 个数删除并将第 4 个数放到这串数的末尾,再将第 5 个数删除······直到剩下最后一个数,将最后一个数也删除。安装刚才删除的顺序,把这些删除的数连在一起就是解密后的数字了。如果没有理解错误解密规则的话,解密后的数字应该是“6 1 5 9 4 7 2 8 3”。

其实解密的过程就像是将这些数排队,既然现在以及搞清楚了解密法则,我们不妨自己尝试一下编程。

解密的第一步是将第一个数删除,你可以想一下如何在数组中删除一个数呢。最简单的方法是将所有后面的数都往前面挪动一位,但是这样的做法很耗费时间。

删除队列第一个数

在这里,我将引入两个整型变量 headtailhead 用来记录队列的队首(即第一位),tail 用来记录队列的队尾(即最后一位)的下一个位置。当队列中只剩下一个元素时,队首和队尾重回会带来一些麻烦。我们这里规定队首和队尾重回时,队列为空。

现在有 9 个数, 9 个数全部放入队列之后 head=1;tail=10; 此时 headtail 之间的数就是目前队列中“有效”的数。如果要删除一个的话,就将 head++OK 了,这样仍然可以保持 headtail 之间的数为目前队列中“有效”的数。这样做虽然浪费了一个空间,却节省了大量的时间,这是非常划算的。新增加一个数也很简单,把需要增加的数放到队尾即 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 OutFIFO)原则。

队列将是我们今后学习广度优先搜索以及队列优化的 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 使其得到的新序列仍然符合从小到大排列。如果我们使用数组来实现这一操作,则需要将 88后面的数都往后挪一位,如下:

数组实现插入

这样操作显然很耽误时间,如果使用链表则会快很多。那么说明是链表呢?请看下图。

链表1

此时如果需要在 8 前面插入一个 6 ,就只需像下图这样更改一下就可以了,而无需再将 8 及后面的数都依次往后挪一位。是不是很节省时间呢?

链表2

那么如何实现链表呢?在 C 语言中可以使用指针和动态分配内存函数 malloc 来实现。

首先我们来看一下,链表中的每一个结点应该如何存储。

链表1

每一个结点都由两个部分组成。左边的部分用来存放具体的数值,那么用一个整形变量就可以;右边的部分需要存储下一个结点的地址,可以用指针来实现(也称为后继指针)。这里我们定义一个结构体类型来存储这个结点,如下:

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

链表3

上面代码中,我们定义了一个叫做 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;//设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空

链表4

下面来设置头指针并设置新创建结点 *next 指向空,头指针的作用是方便以后从头遍历整个链表。

if(head==NULL)
    head=p;//如果这是第一个创建的结点,则头指针指向这个结点
else
    q->next=p;//如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点

如果这是第一个创建的结点,则头指针指向这个结点。

链表5

如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点。

链表6

最后要将指针 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

参考:《啊哈!算法》

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FantasyQin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值