490 likes | 641 Views
数据结构 第 3 章 栈和队列. 什么是栈和队列?? 栈和队列的基本运算 栈和队列的应用. a n. 栈顶. a n-1. …. a 2. 栈底. a 1. 栈. 栈 (Stack) : 是一种特殊的线性表。其特殊性在于限定插入和删除数据元素的操作只能在线性表的一端(尾端)进行 。. 假设栈 S=(a1 , a2 , a3 , … an) , 栈中元素按 a1 , a2 , … an 的次序进栈,退栈的第一个元素应为栈顶元素,即 a n 。换句话说,栈的修改是按后进先出的原则进行的。因此,栈称为后进先出表。( LIFO )。.
E N D
数据结构第3章 栈和队列 • 什么是栈和队列?? • 栈和队列的基本运算 • 栈和队列的应用 数据结构
a n 栈顶 a n-1 … a2 栈底 a1 栈 • 栈(Stack) :是一种特殊的线性表。其特殊性在于限定插入和删除数据元素的操作只能在线性表的一端(尾端)进行。 • 假设栈S=(a1,a2,a3,…an), 栈中元素按a1,a2,…an的次序进栈,退栈的第一个元素应为栈顶元素,即an。换句话说,栈的修改是按后进先出的原则进行的。因此,栈称为后进先出表。(LIFO)。 数据结构
例1:家里吃饭的碗,通常在洗干净后一个一个地落在一起存放,在使用时,若一个一个地拿,一定最先拿走最上面的那只碗,而最后拿出最下面的那只碗。例1:家里吃饭的碗,通常在洗干净后一个一个地落在一起存放,在使用时,若一个一个地拿,一定最先拿走最上面的那只碗,而最后拿出最下面的那只碗。 • 例2:在建筑工地上,使用的砖块从底往上一层一层地码放,在使用时,将从最上面一层一层地拿取。 数据结构
栈的抽象数据类型定义 ADT Stack{ 数据对象: D={ai| ai(- ElemSet,i=1,2,...,n,n>=0} 数据关系: R1={<ai-1,ai>| ai-1,ai(- D,i=2,...,n} 基本操作: InitStack(&S) 构造一个空栈S DestroyStack(&S) 栈S存在则栈S被销毁 ClearStack(&S) 栈S存在则清为空栈 StackEmpty(S) 栈S存在则返回TRUE,否则FALSE StackLength(S) 栈S存在则返回S的元素个数,即栈的长度 GetTop(S,&e) 栈S存在且非空则返回S的栈顶元素 Push(&S,e) 栈S存在则插入元素e为新的栈顶元素 Pop(&S,&e) 栈S存在且非空则删除S的栈顶元素并用e返回其值 StackTraverse(S,visit())栈S存在且非空则从栈底到栈顶依次对S的每个数据元素调用visit()一旦visit()失败,则操作失败 }ADT Stack 数据结构
栈的顺序表示和实现 • 栈的顺序存储结构是用一组连续的存储单元依次存放栈中的每个数据元素,并用起始端作为栈底。 • 类似于顺序表,我们可用如下的结构来顺序地定义栈。 #define STACK_INIT_SIZE 100 //存储空间的初始分配量 #define STACKINCREMENT 10 //存储空间分配增量 typedef struct{ SElemType *base; //存储空间基址 int top; //栈顶指针(栈的长度) int stacksize; //当前分配的存储容量(以一数据元素 存储长度为单位) }SqStack; 数据结构
顺序栈典型操作的实现 • 顺序栈的建立 Status InitStack_Sq(SqStack &S) { // 构造一个空的栈S。 S.base = (SElemType*)malloc(STACK_INIT_SIZE*sizeof(SElemType)); if (!S.base) exit(OVERFLOW); // 存储分配失败 S.top = 0; // 空栈长度为0 S.listsize = LIST_INIT_SIZE; // 初始存储容量 return OK; } // InitStack_Sq 此算法的时间复杂度为O (1)。 数据结构
线性栈的销毁 void DestroyStack( SqStack &S ){// 释放顺序栈S所占存储空间if (S.base) free(S.base); S.top = 0; S.stacksize = 0;} // DestroyStack_Sq 此算法的时间复杂度为:O (1) • 线性栈的清空 void ClearStack( SqStack &S ){// 将顺序栈S的长度置0 S.top = 0; } // ClearStack_Sq 此算法的时间复杂度为:O (1) 数据结构
判断线性栈是否为空 int StackEmpty(SqStack S){ if (S.length==0) return TRUE; else return FALSE; } 此算法的时间复杂度为:O (1) • 求线性栈S的长度 int StackLength(SqStack S){ return S.top; } 此算法的时间复杂度为:O (1) 数据结构
获取线性栈S中的栈顶元素的内容 int GetTop(SqStack S,SElemType *e){ if (S.top == 0) return ERROR; *e=*(S.base + S.top – 1); return OK; } 此算法的时间复杂度为:O (1) 数据结构
插入新的栈顶元素 Status Push_Sq(SqStack &S, ElemType e){ // 在顺序栈的栈顶插入新的元素e if (S.top >= S.stacksize) { // 当前存储空间已满,增加容量 newbase = (SElemType *)realloc(S.base, (S.stacksize+STACKINCREMENT)*sizeof(SElemType)); if (!newbase) return ERROR; // 存储分配失败 S.base = newbase; // 新基址 S.stacksize += STACKINCREMENT; // 增加存储容量 } *(S.base + S.top) = e; // 插入e ++S.top; // 栈长增1 return OK; } // StackInsert_Sq 此算法的最坏时间复杂度为:O (S.top) 数据结构
删除栈顶元素 Status StackDelete_Sq(SqStack &S, int i, SElemType &e) { // 在顺序栈L中删除栈顶元素 if (S.top == 0) return ERROR; e = *(S.base + S.top – 1); // 被删除元素的值赋给e --S.top; // 栈长减1 return OK; } // StackDelete_Sq 此算法的最坏时间复杂度为:O (S.top) 数据结构
栈的链式表示和实现 • 栈的链式存储结构称为链栈,它是运算受限的单链表,其插入和删除操作仅限制在表头位置上进行。 • 由于只是在链栈的头部进行操作,故链栈没有必要像单链表那样附加头结点。栈顶指针就是链表的头指针。 • 类似于单链表,链栈的类型说明如下: typedef struct StackNode{ ElemType data; struct StackNode *next }stacknode, *LinkStack; • 链栈的建立 Status InitStack_L(LinkStack &S) { S = NULL; return OK; } // InitStack_L 此算法的时间复杂度为O (1)。 数据结构
链栈的插入 Status Push_L(LinkStack &S, ElemType e) { // 在链栈S的头部插入元素e p = (LinkStack)malloc(sizeof(StackNode));// 生成新结点 if(!p) return ERROR; p->data = e; p->next = S; // 插入L中 S->next = p; return OK; } // StackInsert_L 此算法的最坏时间复杂度为:O (StackLength(S)) 数据结构
链栈的删除 Status Pop_L(LinkStack &S, ElemType &e) { // 在链栈S中,删除第一个元素,并由e返回其值 if(!S) return ERROR; q = S; S = S->next; // 删除并释放结点 e = q->data; free(q); return OK; } // StackDelete_L 此算法的最坏时间复杂度为:O (StackLength(L)) 数据结构
栈的应用举例 • 由于栈结构具有的后进先出的固有特性,那么凡应用问题求解的过程具有"后进先出"的天然特性的话,则求解的算法中也必然需要利用"栈"。 • 例1:数制转换:对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数。 • 十进制数N和其他d进制数的转换的基本原理是:N = (N div d)×d + N mod d (其中:div 为整除运算,mod 为求余运算) • 这八进制的各个数位产生的顺序是从低位到高位的,而打印输出的顺序是从高位到低位,这恰好和计算过程相反。 • 因此,需要先保存在计算过程中得到的八进制数的各位,然后逆序输出,因为它是按“后进先出”的规律进行的,所以用栈最合适。 数据结构
void conversion (){// 对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数InitStack(S); // 构造空栈scanf(”%d”,N);// 输入一个十进制数while(N){Push(S,N % 8);// “余数”入栈N = N/8;// 非零“商”继续运算} // whilewhile (!StackEmpty(S)){ // 和"求余"所得相逆的顺序输出八进制的各位数Pop(S,e);printf(“%d”, e);} // while} // conversion 数据结构
例2:括弧匹配检验 • 算术表达式中各种括号的使用规则为:出现左括号,必有相应的右括号与之匹配,并且每对括号之间可以嵌套,但不能出现交叉情况。 • 因此我们可以利用一个栈结构保存每个出现的左括号,当遇到右括号时,从栈中弹出左括号,检验匹配情况。在检验过程中,若遇到以下几种情况之一,就可以得出括号不匹配的结论。 (1)当遇到某一个右括号时,栈已空,说明到目前为止,右括号多于左括号; (2)从栈中弹出的左括号与当前检验的右括号类型不同,说明出现了括号交叉情况; (3)算术表达式输入完毕,但栈中还有没有匹配的左括号,说明左括号多于右括号。 数据结构
例3:行编辑程序 • 行编辑器的功能为:接受用户从终端输入的程序或数据,并存入用户的数据区。 • 由于用户在终端进行输入时,不能保证不出差错,因此,在编辑程序中,“每接受一个字符即存入用户数据区”的做法不当。 • 因此,在编辑程序中,设立一个输入缓冲区,用于接受用户输入的一行字符,然后逐行存入用户数据区。允许用户输入错误,并在发现有误时可以及时更正。 • 如果约定#为退格符,以表示前一个字符无效,@为退行符,表示当前行所有字符均无效。则 whli##ilr#e(s#*s) outcha@putchar(*s = #++) 应为 while(*s) putchar(*s++) • 字符在存入缓冲区时是按“后进先出”的规律进行的,所以用栈表示输入缓冲区最合适。当从终端接受了一个字符后做如下判断: • 如果它既不是退格符也不是退行符,则将该字符压入栈顶; • 如果它是一个退格符,则从栈顶删去一个字符; • 如果它是一个退行符,则将栈清空。 数据结构
void LineEdit() { //利用字符栈S,从终端接收一行并传送至调用过程的数据区。 char ch,*temp; SqStack S; InitStack(S); //构造空栈S printf("请输入一行(#:退格;@:清行):\n"); ch = getchar(); //从终端接收第一个字符 while (ch != EOF) { //EOF为全文结束符 while (ch != EOF && ch != '\n') { switch (ch) { case '#': Pop(S, ch); break; // 仅当栈非空时退栈 case '@': ClearStack(S); break; // 重置S为空栈 default : Push(S, ch); break; // 有效字符进栈,未考虑栈满情形 } ch = getchar(); // 从终端接收下一个字符 } temp=S.base; while(temp!=S.top) { printf("%c",*temp); ++temp; }// 将从栈底到栈顶的栈内字符传送至调用过程的数据区; ClearStack(S); // 重置S为空栈 printf("\n"); if (ch != EOF) { printf("请输入一行(#:退格;@:清行):\n"); ch = getchar(); } }// end while DestroyStack(S); } 数据结构
入口 • 例4:迷宫求解(穷举求解) 出口 数据结构
计算机解迷宫时,通常用的是“穷举求解”的方法。计算机解迷宫时,通常用的是“穷举求解”的方法。 • 即从入口出发,顺某一方向向前探索,若能走通,则继续往前走;否则沿原路退回,换一个方向再继续探索,直至所有可能的通路都探索到为止,如果所有可能的通路都试探过,还是不能走到终点,那就说明该迷宫不存在从起点到终点的通道。 • 显然为了保证在任何位置上都能沿原路退回,需要用一个“后进先出”的结构来保存从入口到当前位置的路径,因而自然而然地用栈。并且在走出出口之后,栈中保存的正是一条从入口到出口的路径。 • 设在计算机中可以用上页所示的方块图表示迷宫,“当前位置”指的是“在搜索过程中某一时刻所在图中某个方块位置”。由此,求迷宫中一条路径的算法的基本思想是:若当前位置“可通”,则纳入“当前路径”,并继续朝“下一位置”探索;若当前位置“不可通”,则应顺着“来的方向”退回到“前一通道块”,然后朝着除“来向”之外的其他方向继续探索;若该通道块的四周四个方块均“不可通”,则应从“当前路径”上删除该通道块。 • 假设以栈S记录"当前路径",则栈顶中存放的是"当前路径上最后一个通道块"。"纳入路径"的操作即为"当前位置入栈";"从当前路径上删除前一通道块"的操作即为"出栈"。 数据结构
Status MazePath(MazeType &maze, PosType start, PosType end) { …… } // MazePath 数据结构
例5:表达式求值 • 在计算机中,任何一个表达式都是由操作数(operand)、运算符(operator)和界限符(delimiter)组成的。 • 操作数可以是常数,也可以是变量或常量的标识符; • 运算符可以是算术运算体符、关系运算符和逻辑符; • 界限符为左右括号和标识表达式结束的结束符。 • 在本节中,仅讨论简单算术表达式的求值问题。在这种表达式中只含加、减、乘、除四则运算,所有的运算对象均为单变量。表达式的结束符为“#”。 • 算术四则运算的规则为: • 先乘除、后加减; • 同级运算时先左后右; • 先括号内,后括号外。 数据结构
如:4 + 2 * 3 – 10 / 5 = 4 + 6 - 10 /5 = 10 – 2 = 8 • 也就是说,对表达式中出现的每一个运算符是否即刻进行运算取决于在它后面出现的运算符,如果它的优先数“高或等于”后面的运算,则它的运算先进行,否则就得等待在它之后出现的所有优先数高于它的“运算”都完成之后再进行。 • 那么显然需要某种结构保存先出现的优先级较低的运算符及其操作数。由上述叙述可知,优先级越低的运算符越先进入存储结构,而其运算却是越后进行的。那么反之,优先级越高的运算符越后进入存储结构,而其运算却是越先进行的。因此要求存储结构具有“后进先出”的特性。所以选择栈,从栈底到栈顶的运算符的优先数是从低到高的,而它们运算是从栈顶到栈底进行的。同理,也选择栈存储操作数。 • 因此,为实现表达式求值,可以使用两个工作栈,一个称为OPTR,用于寄存运算符;另一个称作OPND,用以寄存操作数或运算结果。 数据结构
那么进行表达式求值的算法基本思想是: 1. 操作数栈置空,表达式起始符“#”入运算符栈 2. 从左到右依次读出表达式中的各个符号(操作数或运算符),每读出一个符号后,根据运算规则作如下的处理: (1)假如是操作数,则将其压入操作数栈,并依次读下一个符号。 (2)假如是运算符,则: 1)如果读出的运算符的优先级大于运算符栈栈顶运算符的优先级,则将其压入运算符栈,并依次读下一个符号。 2)如果读出的是表达式结束符“#”, A)如果运算符栈栈顶的运算符也为“#”,则表达式处理结束,最后的表达式的计算结果在操作数栈的栈顶位置。 B)否则依次退栈。 3)假如读出的是“(”,则将其压入运算符栈,并且它将对其之前后的运算符起隔离作用。 4)假如读出的是“)”,则其可视为自相应左括弧开始的表达式的结束符。 A)若运算符栈栈顶不是“(”,则从操作数栈连续退出两个操作数,从运算符栈中退出一个运算符,然后作相应的运算,并将运算结果压入操作数栈,然后继续执行A)。 B)若运算符栈栈顶为“(”,则从运算符栈退出“(”,依次读下一个符号。 5)假如读出的运算符的优先级不大于运算符栈栈顶运算符的优先级,则从操作数栈连续退出两个操作数,从运算符栈中退出一个运算符,然后作相应的运算,并将运算结果压入操作数栈。此时读出的运算符下次重新考虑(即不读入下一个符号)。 • 如:5 + 3 * (7 – 2) 数据结构
以上讨论的表达式一般都是运算符在两个操作数中间,这种表达式被称为中缀表达式。在编译系统中,对表达式的处理采用的是另外一种方法,即将中缀表达式转变为后缀表达式,然后对后缀式表达式进行处理,后缀表达式也称为逆波兰式。以上讨论的表达式一般都是运算符在两个操作数中间,这种表达式被称为中缀表达式。在编译系统中,对表达式的处理采用的是另外一种方法,即将中缀表达式转变为后缀表达式,然后对后缀式表达式进行处理,后缀表达式也称为逆波兰式。 • 波兰表示法(也称为前缀表达式)是由波兰逻辑学家(Lukasiewicz)提出的,其特点是将运算符置于运算对象的前面,如a+b表示为+ab;逆波兰式则是将运算符置于运算对象的后面,如a+b表示为ab+。 • 中缀表达式转变为后缀表达式后,运算时按从左到右的顺序进行,不需要括号。而在计算表达式时,可以设置一个栈,从左到右扫描后缀表达式,每读到一个操作数就将其压入栈中;每到一个运算符时,则从栈顶取出两个操作数进行运算,并将结果压入栈中,一直到后缀表达式读完。最后栈顶就是计算结果。 • 5 + 3 * (7 – 2)的后缀表达式为5372-*+ • (a + b)*(a - b)的后缀表达式为ab+ab-* 数据结构
栈与递归的实现 • 递归函数(recursive function) :一个直接或间接调用自己的函数,称为递归函数。 • 如阶乘函数n! = n*(n-1)! • 当一个函数在运行期间调用另一个函数时,在运行该被调用函数之前,系统需先完成三件事:1) 将所有的实在参数、返回地址等信息传递给被调用函数保存;2) 为被调用函数的局部变量分配存储区;3) 将控制转移到被调用函数的入口。 • 而从被调用函数返回调用函数之前,应该完成:1) 保存被调函数的计算结果;2) 释放被调函数的数据区;3) 依照被调函数保存的返回地址将控制转移到调用函数。 • 当多个函数嵌套调用时,由于函数的运行规则是:后调用先返回,因此各函数占有的存储管理应实行"栈式管理"。 数据结构
一个递归函数的运行过程类似于多个函数的嵌套调用,差别仅在于“调用函数和被调用函数是同一个函数”。那么为了保证"每一层的递归调用"都是对"本层"的数据进行操作,在执行递归函数的过程中需要一个"递归工作栈"。它的作用是:一个递归函数的运行过程类似于多个函数的嵌套调用,差别仅在于“调用函数和被调用函数是同一个函数”。那么为了保证"每一层的递归调用"都是对"本层"的数据进行操作,在执行递归函数的过程中需要一个"递归工作栈"。它的作用是: • 将递归调用时的实在参数和函数返回地址传递给下一层执行的递归函数; • 保存本层的参数和局部变量,以便从下一层返回时重新使用它们。 • 递归过程执行过程中所占用的数据区,称之为递归工作栈。 • 每一层的递归参数等数据合成一个记录,称之为递归工作记录。 • 栈顶记录指示当前层的执行情况,称之为当前活动记录。 • 递归工作栈的栈顶指针,称之为当前环境指针。 数据结构
出队 入队 队头 队尾 队列 • 队列(Queue) :也是一种特殊的线性表。其特殊性在于限定只能在表的一端进行插入,而在另一端进行删除。允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。 a1 a2… an • 当队列中没有元素时称为空队列。在空队列中依次加入元素a1,a2,…an之后,a1是队头元素,an是队尾元素。那么退出队列的次序也只能是a1,a2,…an ,也就是说队列的修改是依先进先出的原则进行的。因此,队列称为先进先出表。(FIFO)。 数据结构
例1:排队购物。 • 例2:操作系统中的作业排队 在多道程序运行的计算机系统中,可以同时有多个作业运行,它们的运算结果都需要通过通道输出,若通道尚未完成输出,则后来的作业应排队等待,每当通道完成输出时,则从队列的队头退出作业作输出操作,而凡是申请该通道输出的作业都从队尾进入该队列。 数据结构
队列的抽象数据类型定义 ADT Queue{ 数据对象: D={ai| ai(- ElemSet,i=1,2,...,n,n>=0} 数据关系: R1={<ai-1,ai>| ai-1,ai(- D,i=2,...,n} 基本操作: InitQueue(&Q) 构造一个空队列Q DestroyQueue(&Q)队列Q存在则队列Q被销毁 ClearQueue(&Q)队列Q存在则清为空队列 QueueEmpty(Q)队列Q存在则返回TRUE,否则FALSE QueueLength(Q)队列Q存在则返回Q的元素个数,即队列的长度 GetHead(Q,&e)队列Q存在且非空则返回Q的队头元素 EnQueue(&Q,e)队列Q存在则插入元素e为新的队尾元素 DeQueue(&Q,&e)队列Q存在且非空则删除Q的队头元素并用e返回其值 QueueTraverse(Q,visit())队列Q存在且非空则从队头到队尾依次对Q的每个数据元素调用visit()一旦visit()失败,则操作失败 }ADT Queue 数据结构
队列的链式表示和实现 • 队列的链式存储结构称为链队列,它是运算受限的单链表,其插入限制在表尾进行,删除则限制在表头位置上进行,因此需要头、尾两个指针。 • 类似于单链表,同样为链队列附加头结点。则空的链队列的判断条件为头、尾指针均指向头结点。 • 类似于单链表,链队列的类型说明如下: typedef struct QNode{ ElemType data; struct QNode *next }QNode, *QueuePtr; typedef struct{ QueuePtr front; QueuePtr rear; }LinkQueue; 数据结构
链队列的建立 Status InitQueue(LinkQueue &Q){ //构造一个空队列Q Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode)); if(!Q.front)exit(OVERFLOW); //存储分配失败 Q.front->next=NULL; return OK; } 此算法的时间复杂度为O (1)。 数据结构
链队列的销毁 Status DestroyQueue(LinkQueue &Q){//销毁队列Q while(Q.front){ Q.rear = Q.front->next; free(Q.front); Q.front = Q.rear; } return OK; } 此算法的时间复杂度为O (Q.Length)。 • 链队列的清空 Status ClearQueue( LinkQueue &Q ){// 将链队列Q的长度置0 while (Q.front->next){p = Q.front->next; Q.front->next = p->next;free(p);} // while Q.rear = Q.front; } 此算法的时间复杂度为:O (Q.Length) 数据结构
判断链队列Q是否为空 Status QueueEmpty(LinkQueue Q){ if (Q.front == Q.rear) return TRUE; else return FALSE; } 此算法的时间复杂度为:O (1) • 求链队列Q的长度 int ListLength(LinkQueue Q){ length = 0; p = Q.front; while(p! = Q.rear){ length++: p = p->next; }//while return length; } 此算法的时间复杂度为:O (Q.Length) 数据结构
获取队头元素的内容 Status GetHead(LinkQueue &Q, QElemType &e) { // 用e返回队头元素 if ( ! QueueEmpty(Q)) return ERROR; e = Q.front->next->data; return OK; } 此算法的时间复杂度为:O (1) 数据结构
队列的插入 Status EnQueue(LinkQueue &Q, QElemType e) { // 插入元素e为Q的新的队尾元素 p = (QueuePtr)malloc(sizeof(QNode)); if(!p) exit(OVERFLLOW); //存储分配失败 p->data = e; p->next = NULL; Q.rear->next = p; Q.rear = p; return OK; } 此算法的时间复杂度为:O (1) 数据结构
队列的删除 Status DeQueue(LinkQueue &Q, QElemType e) { // Q为非空队列,删除Q的队头元素,并用e返回其值 if(QueueEmpty(Q)) return ERROR; p = Q.front->next; e = p->data; Q.front->next = p -> next; if(Q.rear == p) Q.rear = Q.front; free(p); return OK; } 此算法的时间复杂度为:O (1) 在一般情况下,删除队头元素只需修改队头指针。但当原队中只有一个结点时,该结点既是队头也是队尾,故删去此结点时亦需修改尾指针,且删去此结点后队列变空。 数据结构
队列的顺序表示和实现 ?? • 队列的顺序存储结构是用一组连续的存储单元依次存放队列中的每个数据元素。但与顺序表不同的是,我们尚需附设两个指针front和rear分别指示队列头元素及队列尾元素。 • 我们约定,初始化建空队列时,令front = rear = 0,每当插入一个新的队尾元素后,头指针 front增1;每当删除一个队头元素之后,尾指针rear增1。因此,在非空队列中,头指针始终指向队头元素,而尾指针指向队尾元素的"下一个"位置。 front rear front rear front rear front rear 数据结构
在顺序队列中,当队尾指针已经指向了队列的最后一个位置时,此时若有元素入列,就会发生“溢出”,如上图;但在上图中,虽然队尾指针已经指向最后一个位置,但事实上队列中还有空位置。也就是说,队列的存储空间并没有满,但队列却发生了溢出,我们称这种现象为“假溢出”。在顺序队列中,当队尾指针已经指向了队列的最后一个位置时,此时若有元素入列,就会发生“溢出”,如上图;但在上图中,虽然队尾指针已经指向最后一个位置,但事实上队列中还有空位置。也就是说,队列的存储空间并没有满,但队列却发生了溢出,我们称这种现象为“假溢出”。 • 解决这个问题有两种可行的方法: (1)采用平移元素的方法,当发生假溢出时,就把整个队列的元素平移到存储区的首部,然后再插入新元素。 (2)将顺序队列的存储区当作为一个环状的空间,当发生假溢出时,将新元素插入到第一个位置上,这就是循环队列(Circular Queue)。 • 显然,方法(1)需移动大量的元素,因而效率低;而方法(2)中不需要移动元素,因而效率高,空间的利用率也很高。 数据结构
在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。只不过当头尾指针指向数组上界(QueueSize-1)时,其加1操作的结果是指向数组的下界0。(见例图)在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。只不过当头尾指针指向数组上界(QueueSize-1)时,其加1操作的结果是指向数组的下界0。(见例图) • 如上述使用循环链表时,队空和队满时头尾指针均相等。因此,我们无法通过front==rear来判断队列“空”还是“满”。 • 解决此问题有两种方法: (1)设一个布尔变量以区别队列的空和满; (2)少用一个元素的空间,约定入队前,测试尾指针在循环意义下加1后是否等于头指针,若相等则认为队满(注意:rear所指的单元始终为空); 数据结构
顺序栈典型操作的实现 • 我们可用如下的结构来顺序地定义队列。 #define MAXQSIZE 100 typedef struct{ QElemType base[MAXQSIZE]; int front; int rear; }SqQueue; • 如何使当头尾指针指向数组上界(MAXQSIZE-1)时,其加1操作的结果是指向数组的下界0? • 可以利用取模运算(一个整数数值整除以另一个整数数值的余数)实现。如下所示: front=(front+1)%MAXQSIZE; rear=(rear+1)%MAXQSIZE; 当front或rear为MAXQSIZE时,上述两个公式计算的结果就为0,这样,就使得指针自动由后面转到前面,形成循环的效果。 数据结构
线性队列的建立 Status InitQueue_Sq(SqStack &S) { // 构造一个空队列Q。 S.base = (QElemType*)malloc(MAXQSIZE*sizeof(QElemType)); if (!Q.base) exit(OVERFLOW); // 存储分配失败 Q.front = Q.rear = 0; // 空队列长度为0 S.listsize = LIST_INIT_SIZE; // 初始存储容量 return OK; } // InitQueue_Sq 此算法的时间复杂度为O (1)。 数据结构
线性队列的销毁 void DestroyQueue( SqQueue &Q ){// 释放顺序队列Q所占存储空间if(Q.base) free(Q.base); Q.front = Q.rear = 0; } 此算法的时间复杂度为:O (1) • 线性队列的清空 void ClearQueue( SqQueue &Q ){// 将顺序队列Q的长度置0 Q.front = Q.rear = 0; } 此算法的时间复杂度为:O (1) 数据结构
判断线性队列是否为空 Status QueueEmpty(SqQueue Q){ if (Q.front == Q.rear) return TRUE; else return FALSE; } 此算法的时间复杂度为:O (1) • 求线性队列Q的长度 int QueueLength(SqQueue Q){ return (Q.rear – Q.front + MAXQSIZE) % MAXQSIZE; } 此算法的时间复杂度为:O (1) 数据结构
获取线性队列Q中队头元素的内容 int GetHead(SqQueue Q,QElemType *e){ if(QueueEmpty(Q)) return ERROR; *e=Q.base[Q.front]; return OK; } 此算法的时间复杂度为:O (1) 数据结构
插入新的队尾元素 Status EnQueue(SqQueue &Q, QElemType e){ // 在顺序队列的队尾插入新的元素e if((Q.rear+1)%MAXQSIZE == Q.front) return ERROR; //队列满 Q.base[Q.rear] = e; // 插入e Q.rear = (Q.rear+1)%MAXQSIZE; return OK; } 此算法的最坏时间复杂度为:O (1) 数据结构
删除队头元素 Status DeQueue(SqQueue &Q, QElemType &e) { // 在顺序队列Q中删除队头元素 if (Q.front == Q.rear) return ERROR; e = Q.base[Q.front]; // 被删除元素的值赋给e Q.front = (Q.front + 1)%MAXQSIZE; return OK; } 此算法的最坏时间复杂度为:O (1) 数据结构
队列的应用举例(离散事件模拟) • 见书。 数据结构