计算机二级考点
考点1.算法的基本概念:
算法的特性:
1.有穷性:一个算法必须在执行有穷步骤后结束且每一步都可在有穷的时间内完成,不能无限的执行下去。如果没有上限,那么程序将无休止的执行下去,也就是所谓的死循环。
2.确定性:算法的每一个步骤应当是确切定义的,对于每一个过程不能有二义性,将要执行的每个动作必须做出严格而清楚的规定。
3.可行性:算法中的每一步都应当能有效的执行,并要求最终得到正确的结果。
4.拥有足够多的情报。
算法的优劣:
1.正确性:也就是所写的代码能满足具体问题的需求,并且任何合法的输入算法都会得出正确的结果。
2.可读性:是指算法被写好之后,该算法理解的难易程度,一个算法可读性的好坏十分重要。如果一个算法比较抽象且难以理解,那么这个算法就不利于交流和推广使用,对于修改、扩展、维护来说都十分不方便,因此,在追求高效的同时,也应是算法尽量简明易懂。
3.健壮性:这个就是指当输入数据非法时,算法也会做出相应的判断,而不会因为输入的错误而造成瘫痪。
4.时间复杂度和空间复杂度:简单的说,时间复杂度就是算法运行所需的时间。不同的算法具有不同的时间复杂度,当一个程序较小时,就感觉不到时间复杂度的重要性,但是当一个程序特别大时,便会察觉到时间复杂度实际上是十分中要的。因此写出一个高效的算法一直是算法不断改进的目标。空间的复杂度是指算法运行所需的存储空间的多少,随着计算机硬件的发展,空间复杂度已经不再显的那么重要。
算法的描述方式:
算法包含算法设计和算法分析两方面内容,算法设计主要研究怎样针对某一特定类型的问题设计出求解的步骤,算法分析则要讨论所涉及出来的算法步骤的正确性和复杂性。
因此对一些问题的求解的步骤,需要一种表达方式,也就是所谓的算法描述。算法描述有很多种方式,例如:
1.自然语言:自然语言就是人们通常用的语言,这种表示方式通俗易懂。
任意输入3个数,求者三个数中的最小数
①:定义4个变量分别为x、y、z以及min。
②:输入大小不同的三个数分别赋值给x、y、z。
③:判断x是否小于y,如果小于,则将x的值赋值给min,否则将y的值赋值给min。
④:将min和z进行比较,如果小于则执行第⑤步,否则就将z的值赋值给min。
⑤:将min的值输出。
2.流程图:使用流程图的话,大家首先要了解一下流程的的一些常用基本符号。
流程图符号:
输入一个数,如果大于0输出1,等于0输出0,否则输出-1。
Bohra和Jacopini为了提高算法的质量,经过研究提出了3种基本结构:即顺序结构,选择结构和循环结构。因为任何一个算法都可以用这3中基本结构组成。这3中基本结构之间可以并列,可以相互包含,但是绝对不允许交叉。整个算法都是由3种基本结构组成的,所以只要规定好3种基本结构的流程图的画法,就可以画出任何算法的流程图。
3.N-S 流程图:N-S图另一种算法表示法,它的根据:既然任何算法都是上面介绍的3种结构组成的,则各基本结构之间的流程线就是多余的,因此去掉了所有的流程线,将全部的算法写在一个矩形框中。他同样也具有三种基本结构。
求0~100的和。
4.伪代码表示:用传统的流程图和N-S图表示算法直观易懂,但画起来比较费事,在设计一个算法时,可能要反复修改,而修改流程图是比较麻烦的。因此,流程图适宜于表示一个算法,但在设计算法过程中使用不是很理想的(尤其是当算法比较复杂、需要反复修改时)。为了设计算法时方便,常用一种称为伪代码的工具。伪代码是用介于自然语言和计算机语言之间的文字和符号来描述算法。它如同一篇文章一样,自上而下地写下来。每一行(或几行)表示一个基本操作。它不用图形符号,因此书写方便、格式紧凑,易懂也便于向计算机语言算法(即程序)过渡。
考点2.数据结构的基本概念。
1.数据结构的定义:
数据结构(Data Structure)是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。
2. 数据结构中的一些基本术语:
1>. 数据(data)
数据是指计算机能够接受和处理的一切对象, 例如数字、字符、图像、声音、动画等等。
2>. 数据元素(data element)
数据元素是指在计算机中作为一个整体考虑和处理的对象, 是组成数据的基本单位。在学生信息管理系统中, "学生"和"学生的成绩"可以看做是数据元素, 数据元素又可由若干数据项(data item)组成, 在学生信息管理系统中, 学生的姓名、年龄、班级等是"学生"这个数据元素的的数据项, 而"学生的成绩"数据元素是由语文、数学、英语等数据项组成。
3>. 数据对象(data object)
数据对象是指性质相同的若干数据元素的集合, 是数据的子集。 如: 正整数的数据对象是集合N* = {1, 2, 3, ...}, 动物的数据对象是集合A = {猫, 狗, 猪, 牛, ...}等。
数据对象也可以是由其他的数据对象复合而成, 例如数据对象实数 = {有理数, 无理数}, 而有理数是由整数和分数复合而成, 整数和分数又有自己的数据元素集合。
3. 数据结构的研究方向:
数据结构是研究数据与数据之间关系的一门学科, 主要包括以下三个方面:
1>. 数据元素之间的逻辑关系, 称为数据的逻辑结构(logic structure);
2>. 数据在计算机中的表示方式, 称之为数据的存储结构(storage structure)或物理结构(physical structure);
3>. 数据之间所要进行的运算, 称之为数据的操作(operation)。
数据的逻辑结构是从逻辑上开描述数据之间的关系, 它与数据在计算机中的存储无关, 独立于计算机。
数据的存储结构是指数据在计算机中是如何存储的, 它依赖于计算机。
4. 数据的的逻辑结构:
数据的逻辑结构按关系分为线性结构(关系是线性的)和非线性结构(关系是非线性的)。
1>. 线性结构包括线性表、栈和队列、 串、数组、广义表。
2>. 非线性数据结构包括树、二叉树、有向图、无向图以及集合类型。
图示如下:
5. 数据的存储结构:
计算机的存储器是由许多个存储单元组成的, 每个存储单元有唯一的地址, 数据在计算机中的存放有4种基本的存储方式:
1>. 顺序(Sequential)存储方法:
顺序存储方法就是把每个结点的数据, 按照某种顺序存放在一段连续的存储单元中。
2>. 链式(Linked)存储方法:
把每个结点的数据, 零散地存放在一些存储单元中。
3>. 索引(Index)存储方法:
用结点的索引号i来确定结点的存储单元地址, 把每一个结点的数据按一定规律顺序或链式存放在存储单元中。
4>. 散列(Hash)存储方法:
把每个结点的数据通过一个预设的散列函数, 来决定该结点的存储单元。
6. 数据结构的研究方式:
研究数据结构的的方式较为常用的是ADT(抽象数据类型, Abstract Data Type 简称ADT), ADT实际上是指一个数学模型以及定义在此数学模型上的一组操作。抽象数据类型可以使我们更容易描述现实世界。例如: 用线性表描述学生成绩表,用树或图描述遗传关系。 抽象数据类型的关键在于使用它的人可以只关心它的逻辑特征,不需要了解它的存储方式。定义它的人同样不必要关心它如何存储。
抽象数据类型的特点:
1>. 信息的隐蔽性
2>. 模块化
3>. 描述的精确性
4>. 简单性
5>. 完整性
6>. 实现的独立性
数据结构的研究与其描述语言无关, 各种计算机语言, 比如C、C++、JAVA、C#、Python等都可以用来对数据结构进行描述, 虽说其描述的语言不同, 但其描述的思想是相同的。
与语言有关的是抽象数据类型的定义与其实现方式。
一个抽象数据类型通常包括定义、表示、实现三个部分:
定义即为公共属性, 与编程语言无关, 对编程者是可见的;
表示和实现与语言有关, 是私有属性, 对其他编程者是不可见的。
7. 抽象数据类型的表示:
抽象数据类型可用以下三元祖表示: (D, S, P):
D表示数据对象, S是D上的关系, P是对D的基本操作集。
ADT抽象数据类型名{
数据对象 : (数据对象的定义)
关系 : (数据关系的定义)
基本操作 : (基本操作的定义)
}ADT抽象数据类型名
其中数据对象的数据关系的定义可用伪代码描述, 基本操作的定义格式为:
基本操作名(参数列表)
初始条件 : (初始条件描述)
操作结构 : (操作结构的描述)
考点3.线性表及其顺序存储结构。
线性表可以说是最简单的数据结构,它的描述为:n个数据元素的有限序列。
记为:L=(a1,a2,...,an)
按照存储结构它又可以分为顺序存储结构和链式存储结构。
而其中线性表的顺序存储结构是最简单最常用的数据结构:用一段连续地址依次存储表中的数据元素。
看到这里,我们会自然的联想到C语言中的数组。
下面我要实现的是线性表中的元素为整型的顺序存储结构,及它的主要运算:增删查。
先来简单的定义一下这个线性表的顺序存储结构:
[cpp] view
plain copy
-
#define MAXLENGTH 20
-
-
struct sequencelist
-
{
-
int data[MAXLENGTH];
-
int length;
-
};
其中data数组为这个线性表的主要部分,数据元素就存在于此数组中,而对这个线性表的操作都是基于这个数组。
length是这个线性表的一个属性,表示这个线性表包含元素的个数。
对线性表的插入就是对data数组的插入,然后将其length增加。
[cpp] view
plain copy
-
//insert opration
-
int insert(struct sequencelist *list,int index,int element)
-
{
-
int length = list->length;
-
if(length ==0 || index < 0 || index > length || length >= MAXLENGTH)
-
return ERROR;
-
list->data[index] = element;
-
for(int i = length - 1;i>index;i--)
-
{
-
list->data[i+1] = list->data[i];
-
}
-
list->length++;
-
return OK;
-
}
类似增的相反操作。
[cpp] view
plain copy
-
// Delete opration
-
int delete(struct sequencelist *list,int index)
-
{
-
int length = list->length;
-
if(length ==0 || index < 0 || index > length-1 )
-
return ERROR;
-
for(int i = index;i<length-1;i++)
-
{
-
list->data[i] = list->data[i+1];
-
}
-
list->data[length-1] = '\0';//delete the last element.
-
list->length--;
-
return OK;
-
}
用索引值查找元素的值。
[cpp] view
plain copy
-
//get list elements
-
//make sure elemet is NOT NULL when calling.
-
int getElement(struct sequencelist list,int index,int *element)
-
{
-
printf("\ngetElement\n");
-
int length = list.length;
-
printf("length is %d\n",length);
-
if(length ==0 || index < 0 || index >= length)
-
return ERROR;
-
*element = list.data[index];
-
return OK;
-
}
从程序中可以看出增删操作的时间复杂度都是0(n),所以这两项操作都是不是它的强项。而查找操作的时间复杂度是O(1),那么线性表的顺序存储结构的优势就是可以快速的取出任意位置的元素。
上面的3种操作作为较为基本的操作,是本人的学习笔记。如果大虾们发现哪里有不妥,请不吝赐教。
而线性表的其他操作如求前驱元素、求后驱元素、判断表中是否存在某元素值、清空操作等等有意思的操作还待空闲时去完成。
较为完整的调试例子:
[cpp] view
plain copy
-
//2013.2
-
//lincoln
-
//linear list
-
//Sequence Storage Structure
-
//
-
#include <stdio.h>
-
-
#define OK 1
-
#define ERROR -1
-
#define TURE 1
-
#define FALSE 0
-
#define MAXLENGTH 20
-
-
struct sequencelist
-
{
-
int data[MAXLENGTH];
-
int length;
-
};
-
-
//get list elements
-
//make sure elemet is NOT NULL when calling.
-
int getElement(struct sequencelist list,int index,int *element)
-
{
-
printf("\ngetElement\n");
-
int length = list.length;
-
printf("length is %d\n",length);
-
if(length ==0 || index < 0 || index >= length)
-
return ERROR;
-
*element = list.data[index];
-
return OK;
-
}
-
-
//insert opration
-
//
-
int insert(struct sequencelist *list,int index,int element)
-
{
-
printf("\ninsert\n");
-
int length = list->length;
-
printf("length is %d\n",length);
-
if(length ==0 || index < 0 || index > length || length >= MAXLENGTH)
-
return ERROR;
-
list->data[index] = element;
-
for(int i = length - 1;i>index;i--)
-
{
-
list->data[i+1] = list->data[i];
-
}
-
list->length++;
-
return OK;
-
}
-
-
// Delete opration
-
//
-
int delete(struct sequencelist *list,int index)
-
{
-
printf("\ndelete\n");
-
int length = list->length;
-
printf("length is %d\n",length);
-
if(length ==0 || index < 0 || index > length-1 )
-
return ERROR;
-
for(int i = index;i<length-1;i++)
-
{
-
printf("delete data[%d]\n",i);
-
list->data[i] = list->data[i+1];
-
}
-
list->data[length-1] = '\0';//delete the last element.
-
list->length--;
-
return OK;
-
}
-
-
int main()
-
{
-
struct sequencelist list =
-
{
-
{3,1,5,7,12,78,34},
-
7
-
};
-
-
printf("list length : %d\n",list.length);
-
//Test get
-
int *element = 0, test = 8;
-
element = &test;
-
if(OK == getElement(list,2,element))
-
{
-
printf("list get 2 :%d\n", *element);
-
}
-
//Test insert
-
if(OK == insert(&list,7,520))
-
{
-
printf("list insert 7 ok!\n");
-
}
-
if(OK == getElement(list,7,element))
-
{
-
printf("list get 7 :%d\n", *element);
-
}
-
if(OK == insert(&list,3,520))
-
{
-
printf("list insert 3 ok!\n");
-
}
-
if(OK == getElement(list,3,element))
-
{
-
printf("list get 3 :%d\n", *element);
-
}
-
-
//Test delete
-
if(OK == delete(&list,3))
-
{
-
printf("list delete 3 ok!\n");
-
}
-
if(OK == getElement(list,3,element))
-
{
-
printf("list get 3 :%d\n", *element);
-
}
-
if(OK == delete(&list,6))
-
{
-
printf("list delete 6 ok!\n");
-
}
-
if(OK == getElement(list,6,element))
-
{
-
printf("list get 6 :%d\n", *element);
-
}
-
else
-
{
-
printf("list get ERROR!\n");
-
}
-
}
考点4.栈和队列
1.1 栈的定义
栈是一种特殊的线性表。其特殊性在于限定插入和删除数据元素的操作只能在线性表的一端进行。如下所示:
结论:后进先出(Last In First Out),简称为LIFO线性表。
栈的基本运算有六种:
构造空栈:InitStack(S)、
判栈空: StackEmpty(S)、
判栈满: StackFull(S)、
进栈: Push(S,x)、可形象地理解为压入,这时栈中会多一个元素
退栈: Pop(S) 、 可形象地理解为弹出,弹出后栈中就无此元素了。
取栈顶元素:StackTop(S),不同与弹出,只是使用栈顶元素的值,该元素仍在栈顶不会改变。
由于栈也是线性表,因此线性表的存储结构对栈也适用,通常栈有顺序栈和链栈两种存储结构,这两种存储结构的不同,则使得实现栈的基本运算的算法也有所不同。
我们要了解的是,在顺序栈中有"上溢"和"下溢"的概念。顺序栈好比一个盒子,我们在里头放了一叠书,当我们要用书的话只能从第一本开始拿(你会把盒子翻过来吗?真聪明^^),那么当我们把书本放到这个栈中超过盒子的顶部时就放不下了(叠上去的不算,哼哼),这时就是"上溢","上溢"也就是栈顶指针指出栈的外面,显然是出错了。反之,当栈中已没有书时,我们再去拿,看看没书,把盒子拎起来看看盒底,还是没有,这就是"下溢"。"下溢"本身可以表示栈为空栈,因此可以用它来作为控制转移的条件。
链栈则没有上溢的限制,它就象是一条一头固定的链子,可以在活动的一头*地增加链环(结点)而不会溢出,链栈不需要在头部附加头结点,因为栈都是在头部进行操作的,如果加了头结点,等于要在头结点之后的结点进行操作,反而使算法更复杂,所以只要有链表的头指针就可以了。
1.2 栈的顺序存储
使用c++的面向对象封装:
结论:由于栈的插入和删除操作具有它的特殊性,所以用顺序存储结构表示的栈并不存在插入删除数据元素时需要移动的问题,但栈容量难以扩充的弱点仍就没有摆脱。
1.3 栈的链式存储
若是栈中元素的数目变化范围较大或不清楚栈元素的数目,就应该考虑使用链式存储结构。人们将用链式存储结构表示的栈称作"链栈"。链栈通常用一个无头结点的单链表表示。如图所示:
栈的操作是线性表操作的特例。
简单用c实现:
1.4 栈的应用
1) 数制转换
2)语法词法分析
3)表达式求值等
1.5 栈的递归和实现
汉诺塔的问题:
解决:
1)如果有一个盘子,直接从X移到Z即可。
2)如果有n个盘子要从X移到Z,Y作为辅助。问题可以转化为,先将上面n-1个从X移动到Y,Z作为辅助,然后将第n个从X移动到Z,最后将剩余的n-1个从Y移动到Z,X作为辅助。
完整实现代码,包括栈的实现:
1.队列
1.1 队列定义
队列(Queue)也是一种运算受限的线性表,它的运算限制与栈不同,是两头都有限制,插入只能在表的一端进行(只进不出),而删除只能在表的另一端进行(只出不进),允许删除的一端称为队尾(rear),允许插入的一端称为队头 (Front)
,队列的操作原则是先进先出的,所以队列又称作FIFO表(First In First Out)
队列的基本运算也有六种:
置空队 :InitQueue(Q)
判队空: QueueEmpty(Q)
判队满: QueueFull(Q)
入队 : EnQueue(Q,x)
出队 : DeQueue(Q)
取队头元素: QueueFront(Q),不同与出队,队头元素仍然保留。
队列也有顺序存储和链式存储两种存储结构,前者称顺序队列,后者为链队。
对于顺序队列,我们要理解"假上溢"的现象。
我们现实中的队列比如人群排队买票,队伍中的人是可以一边进去从另一头出来的,除非地方不够,总不会有"溢出"的现象,相似地,当队列中元素完全充满这个向量空间时,再入队自然就会上溢,如果队列中已没有元素,那么再要出队也会下溢。
那么"假上溢"就是怎么回事呢?
因为在这里,我们的队列是存储在一个向量空间里,在这一段连续的存储空间中,由一个队列头指针和一个尾指针表示这个队列,当头指针和尾指针指向同一个位置时,队列为空,也就是说,队列是由两个指针中间的元素构成的。在队列中,入队和出队并不是象现实中,元素一个个地向前移动,走完了就没有了,而是指针在移动,当出队操作时,头指针向前(即向量空间的尾部)增加一个位置,入队时,尾指针向前增加一个位置,在某种情况下,比如说进一个出一个,两个指针就不停地向前移动,直到队列所在向量空间的尾部,这时再入队的话,尾指针就要跑到向量空间外面去了,仅管这时整个向量空间是空的,队列也是空的,却产生了"上溢"现象,这就是假上溢。
为了克服这种现象造成的空间浪费,我们引入循环向量的概念,就好比是把向量空间弯起来,形成一个头尾相接的环形,这样,当存于其中的队列头尾指针移到向量空间的上界(尾部)时,再加1的操作(入队或出队)就使指针指向向量的下界,也就是从头开始。这时的队列就称循环队列。
通常我们应用的大都是循环队列。由于循环的原因,光看头尾指针重叠在一起我们并不能判断队列是空的还是满的,这时就需要处理一些边界条件,以区别队列是空还是满。方法至少有三种,一种是另设一个布尔变量来判断(就是请别人看着,是空还是满由他说了算),第二种是少用一个元素空间,当入队时,先测试入队后尾指针是不是会等于头指针,如果相等就算队已满,不许入队。第三种就是用一个计数器记录队列中的元素的总数,这样就可以随时知道队列的长度了,只要队列中的元素个数等于向量空间的长度,就是队满。
2.2 队列的顺序存储
顺序存储如图:
由于是顺序存储结构的存储空间是静态分配的,所以在添加数据的时,有可能没有剩余空间的情况。
解决这种“假溢出”情况,使用循环队列。在c语言中,不能用动态分配的一维数组来实现循环队列。若使用循环队列,必须设置最大队列长度,若无法估计最大长度,就使用链式队列。
c实现:
注意:InitQueue(QUEUE *&Q) 传的是指针的地址。
2.3 链式队列:
2. 4.队列的应用
【举例1】银行排队
【举例2】模拟打印机缓冲区。
在主机将数据输出到打印机时,会出现主机速度与打印机的打印速度不匹配的问题。这时主机就要停下来等待打印机。显然,这样会降低主机的使用效率。为此人们设想了一种办法:为打印机设置一个打印数据缓冲区,当主机需要打印数据时,先将数据依次写入这个缓冲区,写满后主机转去做其他的事情,而打印机就从缓冲区中按照先进先出的原则依次读取数据并打印,这样做即保证了打印数据的正确性,又提高了主机的使用效率。由此可见,打印机缓冲区实际上就是一个队列结构。
【举例3】CPU分时系统
在一个带有多个终端的计算机系统中,同时有多个用户需要使用CPU运行各自的应用程序,它们分别通过各自的终端向操作系统提出使用CPU的请求,操作系统通常按照每个请求在时间上的先后顺序,将它们排成一个队列,每次把CPU分配给当前队首的请求用户,即将该用户的应用程序投入运行,当该程序运行完毕或用完规定的时间片后,操作系统再将CPU分配给新的队首请求用户,这样即可以满足每个用户的请求,又可以使CPU正常工作。
1.1 栈的定义
栈是一种特殊的线性表。其特殊性在于限定插入和删除数据元素的操作只能在线性表的一端进行。如下所示:
结论:后进先出(Last In First Out),简称为LIFO线性表。
栈的基本运算有六种:
构造空栈:InitStack(S)、
判栈空: StackEmpty(S)、
判栈满: StackFull(S)、
进栈: Push(S,x)、可形象地理解为压入,这时栈中会多一个元素
退栈: Pop(S) 、 可形象地理解为弹出,弹出后栈中就无此元素了。
取栈顶元素:StackTop(S),不同与弹出,只是使用栈顶元素的值,该元素仍在栈顶不会改变。
由于栈也是线性表,因此线性表的存储结构对栈也适用,通常栈有顺序栈和链栈两种存储结构,这两种存储结构的不同,则使得实现栈的基本运算的算法也有所不同。
我们要了解的是,在顺序栈中有"上溢"和"下溢"的概念。顺序栈好比一个盒子,我们在里头放了一叠书,当我们要用书的话只能从第一本开始拿(你会把盒子翻过来吗?真聪明^^),那么当我们把书本放到这个栈中超过盒子的顶部时就放不下了(叠上去的不算,哼哼),这时就是"上溢","上溢"也就是栈顶指针指出栈的外面,显然是出错了。反之,当栈中已没有书时,我们再去拿,看看没书,把盒子拎起来看看盒底,还是没有,这就是"下溢"。"下溢"本身可以表示栈为空栈,因此可以用它来作为控制转移的条件。
链栈则没有上溢的限制,它就象是一条一头固定的链子,可以在活动的一头*地增加链环(结点)而不会溢出,链栈不需要在头部附加头结点,因为栈都是在头部进行操作的,如果加了头结点,等于要在头结点之后的结点进行操作,反而使算法更复杂,所以只要有链表的头指针就可以了。
1.2 栈的顺序存储
使用c++的面向对象封装:
结论:由于栈的插入和删除操作具有它的特殊性,所以用顺序存储结构表示的栈并不存在插入删除数据元素时需要移动的问题,但栈容量难以扩充的弱点仍就没有摆脱。
1.3 栈的链式存储
若是栈中元素的数目变化范围较大或不清楚栈元素的数目,就应该考虑使用链式存储结构。人们将用链式存储结构表示的栈称作"链栈"。链栈通常用一个无头结点的单链表表示。如图所示:
栈的操作是线性表操作的特例。
简单用c实现:
1.4 栈的应用
1) 数制转换
2)语法词法分析
3)表达式求值等
1.5 栈的递归和实现
汉诺塔的问题:
解决:
1)如果有一个盘子,直接从X移到Z即可。
2)如果有n个盘子要从X移到Z,Y作为辅助。问题可以转化为,先将上面n-1个从X移动到Y,Z作为辅助,然后将第n个从X移动到Z,最后将剩余的n-1个从Y移动到Z,X作为辅助。
完整实现代码,包括栈的实现:
1.队列
1.1 队列定义
队列(Queue)也是一种运算受限的线性表,它的运算限制与栈不同,是两头都有限制,插入只能在表的一端进行(只进不出),而删除只能在表的另一端进行(只出不进),允许删除的一端称为队尾(rear),允许插入的一端称为队头 (Front)
,队列的操作原则是先进先出的,所以队列又称作FIFO表(First In First Out)
队列的基本运算也有六种:
置空队 :InitQueue(Q)
判队空: QueueEmpty(Q)
判队满: QueueFull(Q)
入队 : EnQueue(Q,x)
出队 : DeQueue(Q)
取队头元素: QueueFront(Q),不同与出队,队头元素仍然保留。
队列也有顺序存储和链式存储两种存储结构,前者称顺序队列,后者为链队。
对于顺序队列,我们要理解"假上溢"的现象。
我们现实中的队列比如人群排队买票,队伍中的人是可以一边进去从另一头出来的,除非地方不够,总不会有"溢出"的现象,相似地,当队列中元素完全充满这个向量空间时,再入队自然就会上溢,如果队列中已没有元素,那么再要出队也会下溢。
那么"假上溢"就是怎么回事呢?
因为在这里,我们的队列是存储在一个向量空间里,在这一段连续的存储空间中,由一个队列头指针和一个尾指针表示这个队列,当头指针和尾指针指向同一个位置时,队列为空,也就是说,队列是由两个指针中间的元素构成的。在队列中,入队和出队并不是象现实中,元素一个个地向前移动,走完了就没有了,而是指针在移动,当出队操作时,头指针向前(即向量空间的尾部)增加一个位置,入队时,尾指针向前增加一个位置,在某种情况下,比如说进一个出一个,两个指针就不停地向前移动,直到队列所在向量空间的尾部,这时再入队的话,尾指针就要跑到向量空间外面去了,仅管这时整个向量空间是空的,队列也是空的,却产生了"上溢"现象,这就是假上溢。
为了克服这种现象造成的空间浪费,我们引入循环向量的概念,就好比是把向量空间弯起来,形成一个头尾相接的环形,这样,当存于其中的队列头尾指针移到向量空间的上界(尾部)时,再加1的操作(入队或出队)就使指针指向向量的下界,也就是从头开始。这时的队列就称循环队列。
通常我们应用的大都是循环队列。由于循环的原因,光看头尾指针重叠在一起我们并不能判断队列是空的还是满的,这时就需要处理一些边界条件,以区别队列是空还是满。方法至少有三种,一种是另设一个布尔变量来判断(就是请别人看着,是空还是满由他说了算),第二种是少用一个元素空间,当入队时,先测试入队后尾指针是不是会等于头指针,如果相等就算队已满,不许入队。第三种就是用一个计数器记录队列中的元素的总数,这样就可以随时知道队列的长度了,只要队列中的元素个数等于向量空间的长度,就是队满。
2.2 队列的顺序存储
顺序存储如图:
由于是顺序存储结构的存储空间是静态分配的,所以在添加数据的时,有可能没有剩余空间的情况。
解决这种“假溢出”情况,使用循环队列。在c语言中,不能用动态分配的一维数组来实现循环队列。若使用循环队列,必须设置最大队列长度,若无法估计最大长度,就使用链式队列。
c实现:
注意:InitQueue(QUEUE *&Q) 传的是指针的地址。
2.3 链式队列:
【举例1】银行排队
【举例2】模拟打印机缓冲区。
在主机将数据输出到打印机时,会出现主机速度与打印机的打印速度不匹配的问题。这时主机就要停下来等待打印机。显然,这样会降低主机的使用效率。为此人们设想了一种办法:为打印机设置一个打印数据缓冲区,当主机需要打印数据时,先将数据依次写入这个缓冲区,写满后主机转去做其他的事情,而打印机就从缓冲区中按照先进先出的原则依次读取数据并打印,这样做即保证了打印数据的正确性,又提高了主机的使用效率。由此可见,打印机缓冲区实际上就是一个队列结构。
【举例3】CPU分时系统
在一个带有多个终端的计算机系统中,同时有多个用户需要使用CPU运行各自的应用程序,它们分别通过各自的终端向操作系统提出使用CPU的请求,操作系统通常按照每个请求在时间上的先后顺序,将它们排成一个队列,每次把CPU分配给当前队首的请求用户,即将该用户的应用程序投入运行,当该程序运行完毕或用完规定的时间片后,操作系统再将CPU分配给新的队首请求用户,这样即可以满足每个用户的请求,又可以使CPU正常工作。