slide1 n.
Download
Skip this Video
Loading SlideShow in 5 Seconds..
第三章 栈和队列 PowerPoint Presentation
Download Presentation
第三章 栈和队列

Loading in 2 Seconds...

play fullscreen
1 / 161

第三章 栈和队列 - PowerPoint PPT Presentation


  • 160 Views
  • Uploaded on

第三章 栈和队列.  栈 ( Stack)  栈的应用举例 栈与递归 队列 (Queue). 第三章 栈和队列.  从结点间的逻辑关系看, 栈 和 队列 也是 线性表 ,只不过是 操作受限的特殊的线性表 。  但从数据类型角度看,它们是和线性表 不相同的两种数据结构 。在计算机科学和程序设计中有广范的应用。. 3.1 栈 (Stack). 定义 限定 仅在表尾进行插入或删除操作 的线性表 允许进行插入和删除的一端是浮动端,通常被称为 栈顶 (top) ,并用一个“栈顶指针”指示;而另一端是固定端,通常被称为 栈底 (bottom) 。

loader
I am the owner, or an agent authorized to act on behalf of the owner, of the copyrighted work described.
capcha
Download Presentation

PowerPoint Slideshow about '第三章 栈和队列' - sana


An Image/Link below is provided (as is) to download presentation

Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author.While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server.


- - - - - - - - - - - - - - - - - - - - - - - - - - E N D - - - - - - - - - - - - - - - - - - - - - - - - - -
Presentation Transcript
slide1
第三章 栈和队列

栈(Stack)

栈的应用举例

栈与递归

队列(Queue)

slide2
第三章 栈和队列

从结点间的逻辑关系看, 栈和队列也是线性表,只不过是操作受限的特殊的线性表。

但从数据类型角度看,它们是和线性表不相同的两种数据结构。在计算机科学和程序设计中有广范的应用。

3 1 stack
3.1 栈(Stack)
  • 定义
    • 限定仅在表尾进行插入或删除操作的线性表
    • 允许进行插入和删除的一端是浮动端,通常被称为栈顶(top),并用一个“栈顶指针”指示;而另一端是固定端,通常被称为栈底(bottom)。
    • 没有元素时称空栈。
  • 特性
    • 后进先出(LIFO, Last In First Out)
slide4
栈的抽象数据类型定义及基本运算
  • 创建一个空栈;
  • 判断栈是否为空栈;
  • 往栈中插入(或称推入PUSH)一个元素;
  • 从栈中删除(或称弹出POP)一个元素;
  • 求栈顶元素的值。

P45

slide5
栈的表示和实现
  • 两种存储表示方法:
    • 顺序栈
      • 以一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top指示栈顶元素在顺序栈中的位置。
    • 链栈
      • 用链式存储结构表示的栈。通常用一个无头结点的单链表表示。
      • 由于栈的插入删除操作只能在一端进行,而对于单链表来说,在首端插入删除结点要比尾端相对地容易一些,所以,将单链表的首端作为栈顶端,即将单链表的头指针作为栈顶指针。
slide6
一、顺序栈
  • 顺序栈的类型定义:

#define STACK_INIT_SIZE 100 //存储空间初始分配量

#define STACKINCREMENT //存储空间分配增量

typedef struct //顺序栈类型定义

{

SElemType *base; //栈底顶指针

SElemType *top; //栈顶指针

int stacksize; //当前可使用的最大容量

}SqStack;

slide7
顺序栈的基本操作

top

base

空栈

1、栈的初始化

Status InItStack(SqStack &S) //构造一个空栈S

{

S.base=(SElemType *)malloc(STACK_INIT_SIZE*sizeof

(SElemType));

if(!base) exit(OVERFLOW);

S.top= S.base;

S.stacksize=STACK_INIT_SIZE;

return OK;

}

slide8
顺序栈的基本操作

top

b

a

base

e=*(S.top-1)

2、取栈顶元素

Status GetTop(SqStack S, SElemType &e)

//若栈不空,则用e返回S的栈顶元素,并返回OK;否则返回ERROR

{

if (S.top==S.base) return ERROR;

e=*(S.top-1);

return OK;

}

slide9
顺序栈的基本操作

3、入栈

Status push(SqStack &S,SElemType e)

//插入元素e为新的栈顶元素

slide10

b

a

base

base

base

top

top

top

top

top

top

top

a 进栈

空栈

b 进栈:

*S.top++=e

f

e

e

e

d

d

d

c

c

c

b

b

b

a

a

a

base

base

base

e 进栈

f 进栈,扩充容量

f 进栈

a

*S.top++=e

slide11
顺序栈的基本操作

3、入栈的实现

status push(SqStack &S,SElemType e)

{ //插入元素e为新的栈顶元素

if(S.top-S.base>=S.stacksize) //栈满,增加存储空间

{ S.base=(SElemType *)realloc(S.base,

(S.stacksize+STACKINCREMENT)*sizeof(SElemType))

If(!S.base) exit(OVERFLOW); //存储分配失败

S.top= S.base+S.stacksize; //重置栈顶指针

S.stacksize+=STACKINCREMENT; //修改栈的当前容量

}

*S.top++=e; //将元素e插入栈顶指针top指示位置后,top加1

return OK;

}

slide12
顺序栈的基本操作

3、出栈

status Pop(SqStack &S,SElemType &e)

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK; 否则返回ERROR

slide13

c

d

c

b

b

a

a

b

top

top

top

top

top

a

base

base

base

base

base

d 退栈

a

c 退栈

b 退栈

e=* - - S.top

a 退栈

空栈: S.top==S.base

slide14
顺序栈的基本操作

4、出栈的实现

status Pop(SqStack &S,SElemType &e)

/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK; 否则返回ERROR */

{

if (S.top==S.base) return ERROR;

e=* - - S.top;

return OK;

}

slide15

注意:

    • 由于栈的插入和删除操作具有它的特殊性,所以用顺序存储结构表示的栈并不存在插入删除数据元素时需要移动的问题;
    • 但栈容量难以扩充的弱点仍旧没有摆脱。
slide16
二、链栈

top

elem

next

栈顶

栈底

  • 用链式存储结构表示的栈。通常用一个无头结点的单链表表示。
  • 由于栈的插入删除操作只能在一端进行,而对于单链表来说,在首端插入删除结点要比尾端相对地容易一些,所以,将单链表的首端作为栈顶端,即将单链表的头指针作为栈顶指针。
slide17
链栈的类型定义

top

elem

next

栈顶

栈底

type struct node { //链栈的结点结构

StackElem elem; //栈的数据元素类型

struct node *next; //指向后继结点的指针

}NODE;

typedef struct stack{

NODE *top;

}L_STACK;

slide18
链栈的基本操作

1、初始化栈S

void InitStack(L_STACK &S)

{

S.top=NULL;

}

slide19
链栈的基本操作

2、入栈

void Push(L_STACK &S, StackElem e)

{

p=(NODE *)malloc(sizeof(NODE));

if (!p) exit(OVERFLOW);

else { p->elem = e;

p->next=S.top;

S.top=p;

}

}

slide20
链栈的基本操作

3、出栈

void Pop(L_STACK &S, StackElem &e)

{

if (StackEmpty(S)) exit(“Stack is empty”);

else {

e =S.top-> elem;

p=S.top;

S.top=p->next;

free(p);

}

}

slide21
链栈的基本操作

4、获取栈顶元素内容

void GetTop(L_STACK S, Stackelem &e)

{

if (StackEmpty(S)) exit(“Stack is empty”);

else e=S.top-> elem;

}

slide22
链栈的基本操作

5、判断栈是否为空

int StackEmpty(L_STACK S)

{

if (S.top==NULL) return TRUE;

else FALSE;

}

slide23
思考题

编号为1,2,3,4的四辆列车,顺序开进一个栈式结构的站台,问开出车站的顺序有多少种可能?请把它们具体写出来.

4,3,2,1; 3,2,1,4; 3,4,2,1; 3,2,4,1; 2,1,3,4; 2,1,4,3; 2,3,4,1; 2,3,1,4; 2,4,3,1; 1,2,3,4; 1,2,4,3; 1,3,4,2; 1,3,2,4; 1,4,3,2

slide24
3.2 栈的应用举例

数制转换

括号匹配的检验

表达式求值

slide25
一、数制转换

十进制N和其它进制数的转换是计算机实现计算的基本问题,其解决方法很多。

使用展转相除法将一个十进制数值转换成二进制数值。

  • 即用该十进制数值除以2,并保留其余数;重复此操作,直到该十进制数值为0为止。最后将所有的余数反向输出就是所对应的二进制数值。
  • 即,N=(ndivd)*d+nmodd

其中: div为整除运算, mod为求余运算。

slide26

2

2

19

0

2

1

9

2

4

1

2

2

0

2

1

0

0

1

例如 (38)10 = (???)2

其运算过程如下:

(38)10 = (100110)2

38

1

0

0

1

1

0

slide27
算法

void Decimal _ Binary_conversion ( )

{ //对于输入的任意一个非负十进制整数,打印输出其相应的二进制数

InitStack(S); //初始化栈S

scanf(“%d”,N); //输入十进制正整数

while (N)

{ Push(S, N%2); //余数入栈

N=N/2; //N整除以2,得到新的被除数

}

while (!StackEmpty(S))

{ //依次从栈中弹出每一个余数,并输出之

Pop(S, e);

printf(“%d”, e);

}

}

slide28
二、括号匹配的检验

算术表达式中包含三种括号:

  • 圆括号“(”和“)”,方括号“[”和“]”和花括号“{”和“}”;
  • 且这三种括号可按任意的次序嵌套使用;
  • 如,...[...{...}...[...]...]...[...]...(...)..。

设计算法,用来检验在输入的算术表达式中所使用括号的合法性。

算术表达式中各种括号的使用规则为:

  • 出现左括号,必有相应的右括号与之匹配,并且每对括号之间可以嵌套,但不能出现交叉情况。
slide29
分析

可利用一个栈结构保存每个出现的左括号,当遇到右括号时,从栈中弹出左括号,检验匹配情况。在检验过程中,若遇到以下几种情况之一,就可以得出括号不匹配的结论。

  • 当遇到某一个右括号时,栈已空,说明到目前为止,右括号多于左括号;
  • 从栈中弹出的左括号与当前检验的右括号类型不同,说明出现了括号交叉情况;
  • 算术表达式输入完毕,但栈中还有没有匹配的左括号,说明左括号多于右括号。
slide30
算法

typedef char SElemType;

int Check( )

{ char ch;

InitStack(S); //初始化栈S

while ((ch=getchar())!=’\n’)

{ //以字符序列的形式输入表达式

switch (ch)

{ case(ch==‘(’||ch==‘[’||ch==‘{’): //遇左括号入栈

Push(S,ch); break;

//在遇到右括号时,分别检测匹配情况

case (ch== ‘)’):

if (StackEmpty(S)) return FALSE;

else

{ Pop(S,e);

if (e!= ‘(’) return FALSE; }

break;

slide31

case (ch== ‘]’):

if (StackEmpty(S)) return FALSE;

else

{ Pop(S,e);

if (e!= ‘[’) return FALSE; }

break;

case (ch== ‘}’):

if (StackEmpty(S)) return FALSE;

else

{ Pop(S,e);

if (e!= ‘{’) return FALSE; }

break;

default:break;

}

}

if (StackEmpty(S)) return TRUE;

else return FALSE;

}

slide32
三、表达式求值

一个表达式由操作数(operand)、运算符 (operator) 和界限符(delimiter)组成。称为单词。

  • 操作数——可为常数、变量标识符、常量标识符;
  • 算符:
    • 运算符:算术、关系、逻辑运算符;
    • 界限符:括号、表达式结束符等。

算术表达式有三种表示:

  • 中缀(infix)表示

<操作数> <操作符> <操作数>,如 A+B;

  • 前缀(prefix)表示(波兰式)

<操作符> <操作数> <操作数>,如 +AB;

  • 后缀(postfix)表示(逆波兰式)

<操作数> <操作数> <操作符>,如 AB+。

slide33

rst4

rst1

rst5

rst2

rst3

rst6

表达式的中缀表示

a+b*(c-d) -e*f/g

  • 表达式中相邻两个操作符的计算次序为:
    • 优先级高的先计算;
    • 优先级相同的自左向右计算;
    • 当使用括号时从最内层括号开始计算。
slide34
中缀算术表达式求值
  • 使用两个栈:
    • 运算符栈OPTR (operator)
      • 存放处理表达式过程中的运算符。
      • 开始时,在运算符栈中先在栈底压入一个表达式的结束符“#”。
    • 操作数栈OPND(operand)
      • 存放处理表达式过程中的操作数。
slide35
中缀算术表达式求值算法基本思想
  • 建立并初始化OPTR和OPND栈,在OPTR栈中压入表达式起始符“#”;
  • 从头扫描表达式,取字符入c,根据运算规则作以下处理:
    • 当c==“#”时,且OPTR栈顶的运算符也是“#”时,表达式处理结束,在OPND栈的栈顶得到运算结果,结束算法。
    • 若c是操作数,进OPND栈,从中缀表达式取下一字符送入c;
    • 若c是操作符,则和OPTR中的栈顶运算符比较优先级:
      • 若优先级(c) > 优先级(OPTR),则c进OPTR栈,取下一字符送入c;
      • 若优先级(c)<优先级(OPTR),则从OPND栈退出a2和a1,从OPTR栈退出θ, 形成运算指令 (a1)θ(a2),结果进OPND栈。此时读出的运算符下次重新考虑(即不读入下一个符号);
      • 若优先级(c) ==优先级(OPTR) 且ch == “)”,则从OPTR栈退出栈顶的“(”,对消括号,然后从中缀表达式取下一字符送入c。
5 6 4 2 38

top

top

#

base

base

OPND

OPTR

中缀算术表达式5+(6-4/2)*3求值

/

2

-

4

(

6

+

5

slide56
另外
  • 中缀表达式有时必须借助括号才能将运算顺序表达清楚,处理起来比较复杂。
  • 在编译系统中,对表达式的处理用的是另一种方法:
    • 将中缀表达式转变为后缀表达式,然后对后缀式表达式进行处理。
    • 中缀表达式经过上述处理后,运算时按从左到右的顺序进行,不需要括号。
    • 得到后缀表达式后,我们在计算表达式时,可以设置一个栈,从左到右扫描后缀表达式,每读到一个操作数就将其压入栈中;每到一个运算符时,则从栈顶取出两个操作数进行运算,并将结果压入栈中,一直到后缀表达式读完。最后栈顶就是计算结果。
slide57
后缀表达式

中缀表达式:A + ( B – C / D) × E

后缀表达式:ABCD/-E×+

后缀表达式特点

1、与相应的中缀表达式中的操作数次序相同

2、没有括号

slide59
算符间的优先关系

当前运算符

栈顶运算符

slide60
中缀表达式到后缀表达式的转换的处理规则
  • 如为操作数,直接输出到队列;
  • 如为操作符,作如下处理:
    • 如当前运算符高于栈顶运算符,入栈;
    • 如当前运算符低于栈顶运算符,栈顶运算符退栈, 并输出到队列,当前运算符再与栈顶运算符比较;
    • 如当前运算符等于栈顶运算符,且栈顶运算符为 “(”,当前运算符为“)”,则栈顶运算符退栈, 继续读下一符号;
    • 如当前运算符等于栈顶运算符,且栈顶运算符为 “#”,当前运算符也为“#”,则栈顶运算符退栈, 继续读下一符号。
slide62
思考题

把中缀表达式转化为相应的后缀表达式:

(1)D-B+C

(2)A*B+C*D

(3)(A+B)*C-D*F+C

(1)D-B+CDB-C+

(2)A*B+C*DAB*CD*+

(3)(A+B)*C-D*F+CAB+C*DF*-C+

slide63
后缀算术表达式求值算法基本思想
  • 建立并初始化OPND栈;
  • 从左到右扫描表达式,取字符入c,根据运算规则作以下处理:
    • 每读到一个操作数就将其压入栈中;
    • 每到一个运算符时,则从栈顶取出两个操作数进行运算,并将结果压入栈中;
    • 一直到后缀表达式读完。最后栈顶就是计算结果。
slide64
3.3 栈与递归
  • 递归
    • 递归函数——一个直接调用自己或通过一系列的调用语句间接地调用自己的函数。
    • 一个递归定义必须一步比一步简单,最后是有终结的(称为递归定义的出口),决不能无限循环下去。
  • 计算机系统如何处理递归调用关系???
slide65
例:求阶乘n!的递归方法的思路
  • 相应的C语言函数

int fact(int n)

{ float s;

if (n= =0||n= =1) s=1;

else s=n*fact(n-1);

return (s);

}

调用函数本身

  • 在该函数中可以理解为求n!用fact(n)来表示,则求(n-1)!就用fact(n-1)来表示。
  • 若求5!,则有:

main()

{ printf(“5!=%d\n”,fact(5)); }

slide66
5!的递归调用的过程

第一层调用

n=5

s=5*fact(4)

第二层调用

n=4

s=4*fact(3)

第三层调用

n=3

s=3*fact(2)

第四层调用

n=2

s=2*fact(1)

第五层调用

n=1

s=1

fact(1)=1

fact(2)=2

fact(3)=6

fact(4)=24

fact(5)=120

输出:120

主函数

main()

printf(“fact(5)”)

1 main()

  • {
  • printf(“5!=%d\n”,fact(5));
  • }

1 int fact(int n)

2 { int s;

3 if (n= =0||n= =1) s=1;

4 else s=n*fact(n-1);

5 return (s); }

slide67
递归程序的一般形式

void Pname(参数表);

{

if (数据为递归出口) 简单操作;

else

{

简单操作;

Pname (实参表); 简单操作;

[Pname (实参表); 简单操作;] //可能有多次的调用

}

}

slide68
计算机系统处理递归的原则
  • 关键——正确处理执行过程中的递归调用层次和返回路径, 也就是要记住每一次递归调用时的返回地址。
  • 在系统中是用一个“递归工作栈”动态记忆调用过程中的路径,其处理原则为:
    • 开始执行程序前,建立一递归工作栈,其初始状态为空。
    • 当发生调用(递归)时,
      • 将当前的工作记录压入栈顶:
        • 工作记录——递归所需的信息,包括所有的实在参数、所有的局部变量以及返回地址;
      • 为被调用函数的局部变量分配存储区;
      • 将控制转移到被调函数的入口。
    • 当调用(递归)返回时,
      • 保存被调函数的输出结果;
      • 释放被调函数的数据区;
      • 从栈顶弹出工作记录,将控制转移回调用函数。若函数需要求值,,从回传变量中取出所保存的值并传送到相应的变量或位置上。
slide69
递归调用的内部实现

将递归调用当做是子程序调用的特殊情况,其特殊性在于所调用的乃是自身代码

可将递归调用理解为调用与自己有相同的代码和同名的局部变量的子程序

若递归子程序中有局部变量,则其各复制件也应有自己的局部变量,它们是独立的

内部实现不变

slide70
阅读递归程序的方法
  • 按次序写出程序当前调用层上实际执行的各语句,并用有向弧表示语句的执行次序。
  • 对程序中的每个调用语句或函数引用,写出其实际调用形式(带实参),然后在其右边或下边写出本次调用的子程序实际的执行语句,以区分调用主次和层次。另外,还要作如下操作:
    • 在被调层的前面标明各形参的值。
    • 从调用操作处画一有向弧指向被调子程序的入口,以表示调用路线。
    • 从被调子程序的末尾处画一有向弧指向调用操作的下面,表示返回路线。
  • 若为函数, 在返回路线上标出调用所得的函数值;若有要返回修改值的参数,在返回路线上增加标注语句:

实参=形参的值;

slide71

Cout<<’A’

Cout<<'B'

A

Cout<<’A’

Cout<<'B'

例1

Cout<<”AB”

  • 对下面程序,求出调用AB的运行结果。

void A()

{ cout<<'A'; }

void B()

{ cout<<'B';

A;

cout<<'B';

}

void AB()

{ cout<<”AB”;

A;

B;

cout<<”AB”;

} ;

A

运行结果:

AB A B A B AB

AB

B

Cout<<”AB”;

slide72

void P(int W);

{ if (W>0)

{ P(W-1);

cout<<W;

P(W-2);

}

}

w=1

w=2

P(0)

w=3

P(1)

Cout<<1

P(2)

Cout<<2

P(-1)

w=4

形参值

P(0)

Cout<<3

P(3)

P(0)

Cout<<1

P(1)

P(-1)

P(4)

Cout<<4

P(0)

P(1)

Cout<<1

P(-1)

Cout<<2

P(2)

P(0)

例2
  • 对下面的递归过程,写出调用P(4)的运行结果。

void P(int W)

{

if (W>0)

{ P(W-1);

cout<<W;

P(W-2);

}

}

运行结果:

1 2 3 1 4 1 2

slide73

F(6)

121

2*F(5) + 3*F(4)

13

41

2*F(3) + 3*F(2)

2*F(4) + 3*F(3)

5

1

13

5

2*F(2)+3*F(1)

2*F(2)+3*F(1)

2*F(3) + 3*F(2)

5

1

1

1

1

1

2*F(2)+3*F(1)

1

1

int F(int N)

{ if ( N<=2) return 1;

return(2*F(N-1)+3*F(N-2));

}

例3
  • 函数F定义如下, 用上述方法求出F(6)的值。

int F(int N)

{

if ( N<=2) return 1;

return (2*F(N-1)+3*F(N-2));

}

运行结果:

F(6)=121

slide74

写出递归出口的操作

写出非递归出口对应的操作

函 数

void Pname(参数表);

{ if (数据为递归出口) 简单操作;

else

{ 简单操作;

Pname (实参表); 简单操作;

[Pname (实参表); 简单操作;]

}

}

递归程序的编写

确定函数功能和变量含义,确定递归出口和描述

假设数据接近功能出口时,函数功能正常

适当调用这些功能实现本层函数功能

slide75

递归出口

非递归出口

例题
  • fibonacci序列Fn定义如下, 编写求解Fn的递归函数。

F0=0

F1=1

Fn=Fn-1+Fn-2n≥2

  • 设函数名为F, 用参数n表示序号, 即F(n)表示序列中的Fn, 讨论如下:
    • 递归出口:

两个, 即n=0和n=1, 相应操作用语句实现如下:

if (n==0 ) return 0;

else if (n==1) return 1;

    • 非递归出口:

即n≥2:假设在n<K时函数功能正确。

设F(n)为序列的第n项Fn, 则在n=K时, F(n)=F(n-1)+F(n-2)。

综上所述,得程序如下:

int F(int n)

{

if (n==0) return 0;

else if (n==1) return 1;

else return F(n-1)+F(n-2);

}

slide76
递归的应用
  • 阶乘函数
  • 2阶Fibonacci数列
  • Ackerman函数
  • n阶Hanoi塔问题
  • 背包问题
hanoi
汉诺塔(Hanoi塔)问题的递归算法

y

z

x

1

……

n-2

n-1

n

  • n阶Hanoi塔问题:
    • 假设:

有三个分别命名为X、Y和Z的塔座,在塔座X上插有n个直径大小各不相同、依小到大编号为1,2,…,n的圆盘。

hanoi1
汉诺塔(Hanoi塔)问题的递归算法

y

z

y

z

x

x

1

1

……

……

n-2

n-2

n-1

n-1

n

n

  • 现要求:

将塔座X上的n个圆盘移至塔座Z上并仍按同样顺序叠排,圆盘移动时必须遵守下列规则:

    • 每次只能移动一个圆盘;
    • 圆盘可以插在X、Y和Z中任一塔座上;
    • 任何时刻都不能将一个较大的圆盘压在较小的圆盘之上。
hanoi2
汉诺塔(Hanoi塔)问题的分析

y

z

x

  • n=1时:
    • hanoi(1,X,Y,Z)——(利用塔座Y为辅助塔座,将编号为1 的圆盘从塔座X移到塔座Z上。
    • 步骤:
      • 将编号为1的圆盘从塔座X直接移到塔座Z上。

——move(X,1,Z)

1

1

hanoi3
汉诺塔(Hanoi塔)问题的分析
  • n=2时:
    • hanoi(2,X,Y,Z)——利用塔座Y为辅助塔座,编号为1、2 的2个圆盘从X移到座Z上。
    • 步骤:
      • (利用塔座Z为辅助塔座,)将(压在编号为2的圆盘上面的)编号为1的1个圆盘从X移到座Y上;

——hanoi(1,X,Z,Y)

      • 将编号为2的圆盘从X移到Z上; ——move(X,2,Z)
      • (利用塔座X为辅助塔座,)将编号为1的圆盘从Y移到Z上; ——hanoi(1,Y,X,Z)
slide81
n=2

y

z

x

1

2

slide82
n=2

y

z

x

2

1

hanoi(1,X,Z,Y)

slide83
n=2

y

z

x

1

2

hanoi(1,X,Z,Y)

move(X,2,Z)

slide84
n=2

y

z

x

1

2

hanoi(1,X,Z,Y)

move(X,2,Z)

hanoi(1,Y,X,Z)

slide85

n=3时:

    • hanoi(3,X,Y,Z)——利用塔座Y为辅助塔座,将编号为1、 2、3的3个圆盘从X移到Z上。
    • 步骤:
      • 利用塔座Z为辅助塔座,将(压在编号为3的圆盘上面的)编号为1、2的2个圆盘从X移到Y上;

——hanoi(2,X,Z,Y)

      • 将编号为3的圆盘从X移到Z上; ——move(X,3,Z)
      • 利用塔座X为辅助塔座,编号为1、2的2个圆盘从Y移到Z上; ——hanoi(2,Y,X,Z)
slide86
n=3

y

z

x

1

2

3

slide87
n=3

y

z

x

1

3

2

hanoi(2,X,Z,Y)

slide88
n=3

y

z

x

1

2

3

hanoi(2,X,Z,Y)

move(X,3,Z)

slide89
n=3

y

z

x

1

2

3

hanoi(2,X,Z,Y)

move(X,3,Z)

hanoi(2,Y,X,Z)

slide90

n>1时:

    • hanoi(n,X,Y,Z)——利用塔座Y为辅助塔座,将编号为1、 2、……、n的n个圆盘从X移到Z上。
    • 步骤:
      • 利用塔座Z为辅助塔座,将(压在编号为n的圆盘上面的)编号为1、2 ……、n-1的n-1个圆盘从X移到Y上;

——hanoi(n-1,X,Z,Y)

      • 将编号为n的圆盘从X移到Z上; ——move(X,n,Z)
      • 利用塔座X为辅助塔座,将编号为1、2 ……、n-1的n-1个圆盘从Y移到Z上; ——hanoi(n-1,Y,X,Z)
slide91
n>1

y

z

x

1

……

n-2

n-1

n

slide92
n>1

y

z

x

1

……

n-2

n

n-1

hanoi(n-1,X,Z,Y)

slide93
n>1

y

z

x

1

……

n-2

n-1

n

hanoi(n-1,X,Z,Y)

move(X,n,Z)

slide94
n>1

y

z

x

1

……

n-2

n-1

n

hanoi(n-1,X,Z,Y)

move(X,n,Z)

hanoi(n-1,Y,X,Z)

slide95
算法

行号 void Hanoi(int n, char x, char y, char z)

1 {

2 if(n = = 1) move(x, 1, z);

3 else

  • {

5 Hanoi(n-1, x, z, y);

6 move(x, n, z);

7 Hanoi(n-1, y, x, z);

8 }

9 }

void move(char x, int n, char y)

{

printf(“Move disk %d from %c to %c\n”, n, x, y);

}

3 4 queue

a0a1a2 an-1



front

rear

3.4 队列(Queue)
  • 定义
    • 队列是只允许在一端删除,在另一端插入的顺序表。
    • 允许删除的一端叫做队头(front),允许插入的一端叫做队尾(rear)。
  • 特性
    • 先进先出(FIFO, First In First Out)
slide97

a1 a2a3 …… an

出队

入队

队头front

(删除元素)

队尾rear

(插入元素)

先进先出

图示
slide98
队列的基本运算
  • 创建一个空队列;
  • 判队列是否为空队列;
  • 往队列中插入一个元素;
  • 从队列中删除一个元素;
  • 求队列头部元素的值。

P59

slide99
队列的实现
  • 顺序表示
  • 链接表示
slide100
一、队列的顺序表示
  • 顺序表示
    • 用一组地址连续的存储单元依次存放从队列头到队列尾的元素;
    • 附设两个指针front和rear分别指示队列头元素及队列尾元素的位置。
      • 初始化时,front=rear=0;
      • 插入时,rear加1;
      • 删除时,front加1。
slide101
队列的进队和出队

A

空队列

front

rear

A进队

front

rear

A B C D

B, C, D进队

front

rear

B C D

C D

A退队

front

rear

B退队

front

rear

C D E F G

C D E F G

rear

front

front

rear

E,F,G进队

H进队,假溢出

front=rear=0;

rear=rear+1;

rear=maxSize;

front=front+1;

slide102
队列的进队和出队原则
  • 进队时
    • 新元素按 rear 指示位置加入,再将尾指针进一: rear = rear + 1;
  • 出队时
    • 将下标为 front 的元素取出,再将头指针进一: front = front + 1;
  • 队空时再出队作队空处理;
  • 队满时再进队将溢出出错。
    • 插入时可能会出现没有剩余单元的情况,而此时队列的实际可用空间并未占满—— “假溢出”现象。
    • 解决办法之一:
      • 将队列元素存放数组首尾相接,形成循环(环形)队列。
circular queue
二、循环队列 (Circular Queue)
  • 队列存放数组被当作首尾相接的表处理。
  • 队头、队尾指针的处理

——假设为队列开辟的数组单元数目为maxSize。加1时从 maxSize-1直接进到0,就使得指针自动由后面转到前 面,形成循环的效果。可用取模(一个整数数值整除以 另一个整数数值的余数)运算实现。

    • 删除时,队头指针进1: front = (front+1) % maxSize;
    • 插入时,队尾指针进1: rear = (rear+1) % maxSize;
    • 队列初始化:front = rear = 0;
    • 队空条件:front == rear;
    • 队满条件:(rear+1) % maxSize == front ;
slide104

front

6

7

6

7

front

6

7

front

rear

5

5

5

0

0

A

A

0

B

4

1

4

1

4

1

C

rear

3

2

3

2

2

rear

3

初始化空队列

B, C进队

A进队

6

7

6

7

6

7

G

H

5

5

5

0

A

0

F

I

0

B

B

E

J

4

1

4

1

4

1

C

C

D

C

rear

front

2

2

3

2

rear

3

rear

3

front

front

C,D,E,F,G,H,I,J进队

队满

A退队

B,C退队

空队列

front==rear

front==rear

front=rear=0;

front=(front+1)%8

rear=(rear+1)%8

slide105
循环队列的进队和出队原则
  • 进队时
    • 新元素按 rear 指示位置加入,再将尾指针进一: rear = (rear + 1)%maxSize;
  • 出队时
    • 将下标为 front 的元素取出,再将头指针进一: front = (front + 1)%maxSize;
  • 队空(front==rear)时再出队作队空处理;
  • 队满((rear+1)%maxSize==front)时再进队将溢出出错。
slide106
循环队列的类型定义

#define MAXQSIZE 100 //最大队列长度

typedef struct {

QElemType *base; //初始化的动态分配存储空间

int front; //头指针,若队列不空,指向队列头元素

int rear; //尾指针,若队列不空,指向队尾元素的下一个位置

}SqQueue;

slide107
循环队列的基本操作实现

1、初始化队列Q

status InitQueue(SqQueue &Q)

{ //构造一个空队列

Q.base=(QElemType *) malloc(MAXQSIZE*sizeof(QElemType));

if(! Q.base) exit(OVERFLOW);

Q.front=0;

Q.rear=0;

return OK;

}

slide108
循环队列的基本操作实现

2、入队

status EnQueue(SqQueue &Q, QElemType e )

{ //插入元素e为Q的新的队尾元素

if ((Q.rear+1)%MAXQSIZE==Q.front) return ERROR;

//队列满

*(Q.base+Q. rear)=e;

Q.rear=(Q. rear+1)%MAXQSIZE;

return OK;

}

slide109
循环队列的基本操作实现

3、出队

status DeQueue(SqQueue &Q, QElemType &e )

{ /*若队列不空,则删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR*/

if(Q. rear==Q. front) return ERROR;//队列空,出错

e=*(Q. base+Q. front);

Q. front=(Q. front+1)%MAXQSIZE;

return OK;

}

slide110
循环队列的基本操作实现

4、取队列头元素

status GetHead(SqQueue Q,QelemType &e)

{ //取队列头元素,并用e返回其值

if(Q. front==Q.rear) return ERROR;

//空队列,出错

e= *(Q.base+Q.front);

return OK;

}

slide111
循环队列的基本操作实现

4、队列判空

status QueueEmpty(SqQueue Q)

{

if(Q.front﹦﹦Q.rear) return OK;

else return ERROR;

}

5、求队列元素个数(长度)

int QueueLength(SqQueue Q)

{

return (Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;

}

slide112

front

rear

二、队列的链接表示——链队列
  • 用链表表示的队列——链队列。
  • 队头在链头,队尾在链尾。
  • 链式队列在进队时无队满问题,但有队空问题。
  • 两个指针:头指针front和尾指针rear。
  • 队空条件为 front == rear,且指向头结点。
slide113

front

front

A

p=front->next;

front->next=p->next;

if(rear==p) rear=front;

free(p);

front

A

B

C

rear

rear

rear

rear

rear

rear

C出队

front

A

B

C

front

C

front

链队列的基本操作

空表

A入队

p=front->next;

front->next=p->next;

free(p);

front=rear

B,C入队

rear->next=p;

rear=p;

A出队

slide114
链队列的类型定义

typedef struct QNode { //链式队列的结点结构QElemtype data; //队列的数据元素类型struct QNode *next; //指向后继结点的指针} QNode, *QueuePtr;

typedef struct { //链式队列QueuePtr front; //队头指针QueuePtr rear; //队尾指针}LinkQueue;

slide115
链队列的基本操作的实现
  • 初始化队列Q

status InitQueue(LinkQueue &Q)

  • 入队

status EnQueue(LinkQueue &Q, Elemtype e)

  • 出队

status DeQueue(LinkQueue &Q, Elemtype &e)

  • 获取队头元素内容

status GetFront(LinkQueue Q, Elemtype &e)

  • 判断队列Q是否为空

status QueueEmpty(LinkQueue Q)

slide116
链队列的基本操作的实现

4、获取队头元素内容

  • status GetFront(LinkQueue Q, Elemtype &e)

{

if(Q.front==Q.rear) return ERROR;

e=Q.front->next->data;

return OK;

}

status QueueEmpty(LinkQueue Q)

{

return (Q.front==Q.rear);

}

5、队列判空

slide117
链队列的应用

【例1】汽车加油站通常汽车加油站的结构基本上是:入口和出口为单行道,加油车道可能有若干条。每辆车加油都要经过三段路程,第一段是在入口处排队等候进入加油车道;第二段是在加油车道排队等候加油;第三段是进入出口处排队等候离开。实际上,这三段都是队列结构。若用算法模拟这个过程,就需要设置加油车道数加2个队列。

【例2】CPU分时系统在一个带有多个终端的计算机系统中,同时有多个用户需要使用CPU运行各自的应用程序,它们分别通过各自的终端向操作系统提出使用CPU的请求,操作系统通常按照每个请求在时间上的先后顺序,将它们排成一个队列,每次把CPU分配给当前队首的请求用户,即将该用户的应用程序投入运行,当该程序运行完毕或用完规定的时间片后,操作系统再将CPU分配给新的队首请求用户,这样即可以满足每个用户的请求,又可以使CPU正常工作。

slide118

两种操作受限的线性表:栈和队列。

    • 基本概念、存储结构、基本操作的实现及一些应用实例
    • 只允许在一端插入和删除的线性表。也被称为“后进先出”的线性表。
    • 两种结构:
      • 顺序存储结构
      • 链式存储结构
  • 队列
    • 只允许在一端插入、在另一端删除的线性表。也被称为“先进先出”表。
    • 循环队列可解决假溢出问题。
  • 递归问题是计算机科学中的一个根本性问题,本章的讨论仅限于它和栈的关系。
小结
slide119

简述栈和线性表的区别和联系。

  • 何为栈和队列?简述两者的区别和联系。
  • 若依次读入数据元素序列{a,b,c,d}进栈,进栈过程中允许出栈,试写出各种可能的出栈元素序列。
  • 将下列各算术运算式表示成逆波兰式:
    • (A*(B+C)+D)*E-F*G
    • A*(B-D)+H/(D+E)-S/N*T
    • (A-C)*(B+D)+(E-F)/(G+H)
  • 写出算术运算式3+4/25*8-6的操作数栈和运算符栈的变化情况。
  • 若堆栈采用链式存储结构,初始时为空,试画出a,b,c,d四个元素依次进栈后栈的状态,然后再画出此时的栈顶元素出栈后的状态。
  • 试写出函数Fibonacci数列:

F1=0 (n=1)

F2=1, (n=2)

Fn=Fn-1+Fn-2 (n>2)

的递归算法。

习题三
slide120

在一个类型为staticlist的一维数组A[0…m-1]存储空间建立二个链接堆栈,其中前2个单元的next域用来存储二个栈顶指针,从第3个单元起作为空闲存储单元空间提供给二个栈共同使用。试编写一个算法把从键盘上输入的n个整型数(n<=m-2,m>2)按照下列条件进栈:在一个类型为staticlist的一维数组A[0…m-1]存储空间建立二个链接堆栈,其中前2个单元的next域用来存储二个栈顶指针,从第3个单元起作为空闲存储单元空间提供给二个栈共同使用。试编写一个算法把从键盘上输入的n个整型数(n<=m-2,m>2)按照下列条件进栈:

    • 若输入的数小于100,则进第一个栈;
    • 若输入的数大于等于100,则进第三个栈;
  • 对于一个具有m个单元的循环队列,写出求队列中元素个数的公式。
  • 简述设计一个结点值为整数的循环队列的构思,并给出在队列中插入或删除一个结点的算法。
  • 有一个循环队列q(n),进队和退队指针分别为r和f;有一个有序线性表A[M],请编一个把循环队列中的数据逐个出队并同时插入到线性表中的算法。若线性表满则停止退队并保证线性表的有序性。
  • 设有栈stack,栈指针top=n-1,n>0;有一个队列Q(m),其中进队指针r,试编写一个从栈stack中逐个出栈并同时将出栈的元素进队的算法。
slide122
简化的背包问题
  • 问题:
    • 一个背包可以放入的物品重量为s,现有n件物品,重量分别为w1,w2,…,wn,问能否从这些物品中选若干件放入背包中,使得放入的重量之和正好是s。如果存在一种符合上述要求的选择,则称此背包问题有解,否则此问题无解。
  • 表示

int knap(int s, int n ) 其中,s>=0, n>=1

如果有解:

1.选择的一组物品中不包含wn;

knap( s, n-1 ) 的解就是knap( s, n)的解;

2.选择的一组物品中包含wn;

knap( s-wn, n -1 )的解就是knap( s, n)的解。

slide123

背包问题可递归定义如下:

1 s = 0

0 s < 0

Knap(s,n) = 0 s > 0 , n < 1

knap(s,n-1) 或

knap(s-wn, n-1) 当s>0,n>=1

slide124

背包问题的递归算法

int knap(int s, int n)

{ if( s==0)

return 1;

else if(s<0 || s>0 && n<1)

return 0;

else if(knap(s-w[n-1], n-1) == 1)

{ printf(“n=%d,w[%d]=%d\n”, n, n-1, w[n-1]);

return 1;

}

else return (knap(s, n-1));

}

a b i
队列的应用举例——逐行打印 二项展开式 (a + b)i 的系数
i i 1
分析第i 行元素与第 i+1行元素的关系

0 1 1 0

t

s

s+t

i = 2

0 1 2 1 0

i = 3

0 1 3 3 1 0

1 4 6 4 1

i = 4

从前一行的数据可以计算下一行的数据

i i 11
从第 i行数据计算并存放第 i+1 行数据

1 2 1 0 1 3 3 1 01 4 6

s=0 t=1 t=2 t=1 t=0 t=1 t=3 t=3 t=1 t=0 t=1

s+t s=t s=t s=t s=t s=t s=t s=t s=t

s+ts+ts+ts+ts+ts+t s+t s+t

slide128
利用队列打印二项展开式系数的程序

#include<stdio.h>

#include<iostream.h>

#include "queue.h"

void YANGVI ( int n )

{ Queue q; int s, t;

q.MakeEmpty ( ); //队列初始化

q.EnQueue (1); q.EnQueue (1);

s = 0;

for (inti = 1; i <= n; i++ ) //逐行计算

{ cout << endl;

q.EnQueue (0);

for ( int j = 1; j <= i+2; j++ ) //下一行

{ t = q.DeQueue ( ); q.EnQueue ( s + t ); s = t;

if ( j != i+2 ) cout << s << ' ';

}

}

}

slide130
C程序的结构
  • C程序是由函数构成的。
    • 一个C程序必须有且仅有一个名为main的主函数,C程序的执行总是从main函数开始的。
  • 一个函数由函数首部和函数体两部分组成。
    • 函数的首部包括:函数的类型、函数名及函数参数表;
    • 函数体是由一对花括号括起来的部分,它包括变量定义和一组执行语句。
  • C程序书写格式自由。
    • C既允许在一行内写多个语句,也允许将一个语句分写在多行上,但每个语句的最后必须有一个分号结尾。
  • 可以用/*…*/对C程序中的任何部分作注释。
    • 可以对整个函数的功能作注释,也可以对某个语句或某个变量的作用作注释,以增加程序的可读性。
slide131
C语句的概述
  • 表达式语句:

在一个表达式后面加一个分号,即构成表达式语句。

  • 控制语句 :
    • 选择结构控制语句:if ( )~else~ switch ( )~
    • 循环结构控制语句: for ( )~ while ( )~

do~ while ( ) continue break

    • 其它控制语句: goto return
  • 函数调用语句:由函数调用加分号构成的语句。
  • 空语句:空语句不进行任何操作,它由一个分号构成。
  • 复合语句:是由一对花括号括起来的一组语句。
slide132
常量
  • 常量是指在程序的执行过程中,其值不能被改变的量。常量不需要类型说明就可以直接使用,常量的类型是由常量本身隐含决定的。
    • 整型常量
      • 十进制整数:如2,-45等,其每个数字位可以是0~9。
      • 八进制整数:如012,-023等。八进制整数以0开头。
      • 十六进制整数:如0x12,-0x23等。十六进制整数以 0x开头。
    • 实型常量
      • 十进制数形式:如12.45,-0.567,3.1415等。
      • 指数形式:如23.45e4,0.38E-4等。
slide133
字符常量
    • 字符常量是由一对单引号括起来的单个字符。
  • 字符串常量
    • 字符串常量是由一对双引号括起来的一个字符序列。

***在C程序中,以上三种常量不需要说明就可以直接使用。

  • 符号常量
    • 有时为了增加程序的可读性和易修改性,在C程序中还经常使用符号常量。 符号常量一般用大写英文字母表示。
    • 其定义的一般形式为:

#define 符号常量 常量

    • 采用符号常量的意义在于:
      • 其一、增加了程序的可读性;
      • 其二、增加了程序的易修改性。
slide134

处理A

处理B

(a)

结构化程序设计的三种基本结构

顺序结构

顺序结构是最简单的基本结构,要求顺序地执行且必须执行

由先后顺序排列的每一个最基本的处理单位。

顺序结构的示意图如下图所示,表示先执行“处理A”,然后

再顺序执行“处理B”。

(a)图是用传统流程图表示的顺序结构。

slide135

条 件

处理A

处理B

(a)

分支结构

分支结构又称作选择结构。在分支结构中,要根据逻辑条件

的成立与否,分别选择执行不同的处理。

分支结构的示意图如下图所示,表示当逻辑条件成立是,执

行处理A,否则执行处理B。

(a)图是用传统流程图表示的顺序结构。

slide136
实现分支结构程序设计的语句

if语句

if语句的简单形式

if(表达式) 语句

if语句标准形式

if(表达式) 语句1

else 语句2

slide137
if语句的嵌套形式

if (表达式1) 语句1

else if(表达式2) 语句2

……

else if(表达式n) 语句n

else 语句n+1

slide138
关于if语句的注意事项
    • if后面的表达式一般为关系表达式或逻辑表达式。

由于系统对if语句的处理流程是:先对表达式的值进行判断,若为非零,则按“真”处理;若为零,则按“假”处理。因此if后面表达式的类型可以是任意的数值类型。

    • else子句不能作为单独的语句使用。

它必须是if语句的一部分,与if配对使用。

    • 在if和else后面可是一个简单语句,也可是复合语句。

复合语句是用花括号括起来的多个简单语句或符合语句。

    • 在if语句的嵌套中,要注意if和else的配对关系。

从最内层开始,else 总是与它上面最近的未曾配对的if配对。

slide139
条件运算符

形式: 表达式1?表达式2:表达式3

其运算过程如图4.5所示。

使用条件表达式时,应注意:

    • 条件运算符的优先级高于赋值

运算符,但低于关系运算符和

算术运算符。

    • 条件运算符具有右结合性,即

结合方向为“自右至左”,并允

许嵌套使用。

    • 在条件表达式中,当表达式2

与表达式3的类型不同时,条件

表达式的值的类型为二者中较

高的类型。

slide140
switch语句

形式: switch (表达式)

{ case 常量表达式1:语句组1;

case 常量表达式2:语句组2;

……

case 常量表达式n:语句组n;

[default: 语句组n+1;] }

执行过程:

先计算表达式的值。顺次同case后的常量表达式的值比较

①若找到相等的常量表达式i,则执行该常量表达式冒号后的 语句组i。在执行过程中,若遇到break语句则跳出switch 结构;否则依次执行其后的所有冒号后面的语句。

②若找不到匹配的常量表达式的值,则执行default后面的语 句组直到结束。

slide141
关于Switch结构的注意事项:
    • switch后表达式的类型,一般为整型、字符型或枚举类型。
    • 每个case后面常量表达式的值必须互不相同。
    • 把default放在最后是一种良好的程序设计习惯。
    • 多个case可共有一组执行语句。
    • case后常量表达式仅起语句标号的作用,并不进行条件判断。
    • switch语句中仅需执行一个分支时,须在该case后面的语句中加上break语句,让其跳出switch结构。
    • 当case后包含多条执行语句时,可以不用花括号括起来,系统会自动识别并顺序执行所有语句。
slide142

条 件

处理 A

(a)

循环结构

当型循环

在当型循环结构中,当逻辑条件成立时,就反复执行处理

A,直到逻辑条件不成立时结束。 如下图所示:

(a)图是用传统流程图表示的顺序结构。

slide143

处理A

条 件

(a)

直到型循环

在直到型循环结构中,反复执行处理A,直到逻辑条件成

立时结束,如图所示:

(a)图是用传统流程图表示的顺序结构。

slide144
实现循环结构的语句

while语句

    • while语句用于构成“当型”循环结构。
    • 形式: while (表达式)

{循环体语句

}

    • 其流程如右图所示:
  • 关于while循环结构的注意事项:
    • while循环结构的特点是“先判断,后执行”。
    • 若循环体中包含多个语句,则应以复合语句的形式出现。
    • 循环体内要有促使循环结束的条件。否则会形成“死循环”。
    • 为使循环能正确开始运行,要做好循环前的准备工作。
slide145
for语句
  • 形式:

for(表达式1;表达式2;表达式3)

循环体语句

  • 有关for循环结构的注意事项:
    • 具有“先判断,后执行”的特点。
    • 三个表达式的通常含义:

表达式1:循环变量初始化

表达式3:循环变量增(减)值。

表达式2:用来表示循环继续的条件。

    • 一种简单明了的for循环结构形式:

for(循环变量赋初值;循环条件;循环变量增(减)值)

循环体语句

slide146
do-while语句
  • do-while循环结构类似于“直到型”循环。
  • 形式: do

{ 循环体语句

}while (表达式);

  • 其流程如右图所示:
  • 有关do-while循环结构的注意事项:
    • do-while循环结构的特点是“先执行,后判断“,因此无论循环继续的条件是否成立,循环体中的语句都至少被执行一次。
    • 若循环体包含多个语句,则以复合语句的形式出现。
    • 循环体内要有促使循环趋向于结束的语句。
slide147
其它语句

break语句 形式:break;

  • break语句使执行从包含它的最内层循环或开关语句中跳出,转到switch结构或循环结构的下一语句。常与if语句配合使用。
  • 在嵌套的循环结构中使用时,break语句只能跳出包含它的最内层循环。而不能同时跳出多层循环。

continue语句 形式:continue;

  • continue语句只能用于循环结构中,用于结束本次循环,使得包含它的循环开始下一次重复。常与if语句配合使用。
  • continue语句只结束本次循环,而不终止整个循环的执行。
  • 二者虽都能实现程序执行的无条件转移,但continue语句只能结束本次循环;break语句则立即结束整个循环过程。
slide148

for(表达式1;表达式2;表达式3)

{……

break;

……}

for(表达式1;表达式2;表达式3)

{……

continue;

……}

以for循环为例,下图给出了continue和break语句的流程转向。

slide149
goto转向语句
  • 形式: goto标号;

……

标号:语句;

  • goto语句的功能是把程序控制转移到标号指定的语句处,使程序从指定的标号处的语句开始继续执行。
  • goto语句可以与if语句一起构成循环结构;
  • goto语句能够从循环体中跳到循环体外。
  • 使用goto语句不符合结构化程序设计的原则,无限制滥用goto语句将会影响程序结构的清晰,降低可读性。在程序设计中一般不宜采用goto语句。
slide150
函数的定义
  • 概述
    • 一个C程序至少由一个main函数构成,一般由一个main函数和若干其它函数构成。所有函数的定义是并列的、独立的,函数定义之间没有嵌套或从属的关系;函数之间可以相互调用(但不能调用main函数)。
    • 一个程序的执行总是从main函数开始的,调用了其它函数后再回到main函数中结束。函数是一个可以反复使用的程序段,函数是通过被调用而执行的(可认为main函数是由系统来调用的)。
    • 建立函数称“函数的定义”, 使用函数称“函数的调用”;主调函数在调用前对被调函数的说明称“函数的声明”。
    • 调用其它函数的函数称为“主调函数”,而被调用的函数称为“被调函数”;
slide151
函数定义的一般形式

类型标识符 函数名(形式参数表列)

{ 声明部分

执行部分 }

    • 函数名符合标识符的命名规则,同一程序中函数不能重名。
    • 对于无参函数,形参表是空的,但“()”不能省略。对于有参函数,需要说明每一个形式参数的类型;若多于一个形参,则形参之间用逗号分隔。
    • 函数名前的类型标识符用来说明函数返回值的类型,函数返回值通过return语句得到。若函数无返回值,可用类型标识符“void”表示.
    • 花括弧内部分称为“函数体”,由声明部分和执行部分构成。声明部分对函数内变量的类型和被调函数的原型进行定义和声明;执行部分是实现函数功能语句序列。
slide152
函数参数和返回值
    • 函数的参数
      • 定义函数时的参数称为形参。调用函数时的参数称为实参。函数定义时形参不占内存,只有发生调用时,形参才被分配内存单元,接受实参传来的值。
      • 函数的形参与实参要求在个数上相等,且对应的形参和实参的类型相同。形参和实参可以同名,但它们是不同的变量,占用不同的内存单元。
    • 函数的返回值
      • 函数的返回值是通过被调用函数中的return语句得到的。
      • 若函数值类型与return语句中表达式值的类型不一致,则以函数值类型为准。
      • 没有返回值的函数,用void定义其函数值类型。
      • return语句的另一项功能是结束被调用函数的运行。
slide153
函数调用
  • 函数的声明
    • 一个函数要调用另一个函数必具备的前提:
      • 被调函数已存在,即被调函数或者是库函数,或者是用户已定义的函数。
      • 对于库函数要用#include命令将相应头文件包含到主调函数所在文件中;对于用户函数要对其进行声明。
    • 对函数声明的格式:

类型标识符 函数名(形式参数表列);

    • 以下三种情况可以省略对被调函数的声明:
      • 若被调函数的定义出现在主调函数之前。
      • 被调函数的返回值类型是整型或字符型的。
      • 在函数的外部已做了对函数的声明。
slide154
函数的调用
    • 形式: 函数名(实际参数表列)
    • 调用函数的过程是:
      • 为函数的所有形参分配内存单元,计算各个实参表达式的值,一一对应的赋值给相应形参。
      • 进入函数体,执行函数中的语句,实现函数的功能,执行到return语句时,计算return语句中表达式的值,释放形参及本函数内的局部变量所占内存,返回主调函数。
      • 继续执行主调函数中的后继语句。
    • 注意:
      • 在发生函数调用时,不同的编译系统对实参的求值顺序是不同的,有的自左向右顺次求实参的值,有的自右向左顺次求实参的值,大部分编译系统按自右向左的顺序求值(如常用的Turbo C系统)。
slide155
函数调用的数据传递方式
    • C语言中,函数调用是值传递方式,即函数的实际参数和形式参数之间的数据传递方向是单向的,只能由实际参数传递给形式参数,而不能由形式参数传递给实际参数,是实际参数向形式参数单向赋值的关系。
    • 内存中,形式参数与实际参数占用不同的内存单元。当调用函数时,给形式参数分配内存单元,将实际参数的值赋值给形式参数,调用后,形式参数单元释放,实际参数仍保留调用前的值,形式参数值的变化不影响实际参数。
slide156
变量的作用域和存储类别
  • 变量的作用域、内部变量和外部变量
    • 作用域规则指变量只能在它的作用范围内使用。
    • 内部变量指在函数内部(或复合语句内部)定义的变量。
    • 外部变量指在函数外任意位置定义的变量。
    • 内部变量的作用域是定义它的函数内或复合语句内;即一个函数内定义的内部变量是不能被其它的函数所引用的。
    • 外部变量的作用域是从定义它的位置开始,直至它所在源程序文件的结束。
slide157

指针

数组

保存数据地址

保存数据

间接访问数据,首先取得指针的内容,把它作为地址,然后从这个地址提取数据。如果指针有个下标[i],就把指针的内容加上i作为地址,从中提取数据

直接访问数据,a[i]只是简单的以a+i为地址取的数据。

通常用于动态数据结构

通常用于存储固定数目且数据类型相同的元素

相关函数为malloc(),free()

隐式分配和删除

通常指向匿名数据

自身即为数据名

指针和数组

slide158

数组具有静态特征,而指针有更多的动态特性和灵活性。数组一经定义,其基址和大小便固定了,在该数组的有效使用范围内是不可变的;但是指针则具有很强的动态特征,可以动态地指向任一该类型(定义决定)变量,这也就决定了它有更大的灵活性(注:与此相关的三个重要函数是:Malloc、Calloc和Free)。数组具有静态特征,而指针有更多的动态特性和灵活性。数组一经定义,其基址和大小便固定了,在该数组的有效使用范围内是不可变的;但是指针则具有很强的动态特征,可以动态地指向任一该类型(定义决定)变量,这也就决定了它有更大的灵活性(注:与此相关的三个重要函数是:Malloc、Calloc和Free)。

  • 指针是变量,可以被赋值,数组名不是变量,不可以被赋值。如:array=pointer的表示方法便是错的。
  • 指针作为地址可以参加一些地址运算,如加法、减法,在特定的环境下(如两指针指向同一数组中的元素)还可进行比较运算,但不同的数组名之间则一般不进行比较运算。
  • 运算速度上的差异。一般来说,用指针要快些,因为在实际的运算中,总是把数组下标表示通过存储映象函数转换为指针表示,按其地址访问内存,这种转换要进行乘法和加法的运算。
  • 指针数组与多维数组。相对于多维数组,指针数组的每一行可以有不同的长度,有更大的灵活性。如:char a[10][20],*b[10];二维数组a[10][20]每一行有固定的长度20个字符,而指针数组*b[10]每一行的长度则随指针指向不同而发生变化。最常用的指针数组是为了存储不同长度的字符串,如:char *b[]={“hello!”,“how are you!”};
  • 数组具有较好的可读性,指针具有更强的灵活性。一般,对某些多维数组中非连续的元素的随机访问用下标表示比较方便,当按递增(减)顺序访问数组时,使用指针快捷而且方便。

指针和数组的区别

slide159

规则1:“表达式中的数组名”就是指针。

规则2:C语言把数组下标作为指针偏移量。

规则3:作为函数参数的数组名等同于指针。

指针和数组

slide160

用a[i]这样的形式对数组进行访问总是被编译器解释为如*(a+i)这样的指针访问。用a[i]这样的形式对数组进行访问总是被编译器解释为如*(a+i)这样的指针访问。

  • 指针始终就是指针。它绝不可以改写成数组。可以用下标形式访问指针,一般都是指针作为函数参数时,而且实际传递给函数的是一个数组。
  • 在特定的上下文环境中,也就是数组作为函数的参数(也只有这种情况)时,数组的声明可以看作是一个指针。作为函数参数的数组(就是在一个函数调用中)始终会被编译器修改成为指向第一个元素的指针。
  • 当把一个数组定义为函数参数时,可以选择把它定义为数组,也可定义为指针。不管用那种方法,在函数内部事实上获得的都是一个指针。
  • 在其他所有情况中,定义和声明必须匹配。如果定义了一个数组,在其他文件对他进行声明时也必须把它声明为数组,指针也是如此。

指针和数组的可交换性总结

slide161

任何可以由数组实现的操作也能由指针实现。这是因为指针和数组名都是地址,任一数组元素均可由指针表示。例如:设有char array[20];则对任何数组元素array[i](i=0,1…19),可由*(array+i)表示。任何可以由数组实现的操作也能由指针实现。这是因为指针和数组名都是地址,任一数组元素均可由指针表示。例如:设有char array[20];则对任何数组元素array[i](i=0,1…19),可由*(array+i)表示。

  • 在使用形式上,指针的使用亦可采用数组的表示法,以获得好的可读性。如:char *pointer,array[20];pointer=array;则array[i](i=0,1…19)的内容与*(pointer+i)一样。

指针和数组的联系