数据结构与算法笔记3 栈Stack和队列Sequence

91 阅读8分钟

1. 栈的定义

栈是只允许在一端进行插入或删除操作的线性表。

逻辑结构:与普通线性表相同。 数据运算:插入、删除操作有区别。

1.1 重要术语:栈顶、栈底、空栈

栈顶:允许插入和删除的一端。 栈底:不允许插入和删除的一端。

特点:后进先出。LIFO Last In First Out

1.2 栈要实现的基本操作

  1. 初始化栈(创)
  2. 销毁栈(销)
  3. 进栈:若栈s未满,则将x加入使之成为新的栈顶。(增)
  4. 出栈:若栈s非空,则弹出栈顶元素,并用x返回。(删)
  5. 读栈顶元素:若栈s非空,则用x返回栈顶元素。(查)
  6. 判断栈是否为空。

nn 个不同元素进栈,出栈元素不同排列的个数为 1n+1C2nn \frac{1}{n+1}C^{n}_{2n} 。该公式称为卡特兰数(Catalan)

2. 顺序栈:用顺序存储的方式实现的栈

基本操作:创、销、删、查、判空、判满

2.1 顺序栈的基本操作

2.1.1 顺序栈基本操作:初始化操作、判空

image.png

2.1.2 顺序栈基本操作:进栈

image.png

2.1.3 顺序栈基本操作:出栈

image.png

2.1.4 顺序栈基本操作:读栈顶元素

跟出栈区别仅有不改变top游标指针 image.png

top初始化也可以设为 0 :

则判空和判满的条件改变,判空为 s.top = 0; 判满为 s.top = MAXSIZE

进栈和出栈代码也发生相应变化:

进栈
    s.data[s.top++] = x;
出栈
    x=s.data[--s.top];

2.2 共享栈

顺序栈容量不可以改变,可以通过链式存储或设置较大内存空间解决,为避免初始设置较大内存空间的浪费,可以设置共享栈。

image.png

3. 链栈:用链式存储实现的栈

本质是只能一端进行插入和删除的单链表。

image.png

推荐使用不带头结点的链表实现。

3.1 示例:

3.1 定义链栈的存储结构

typedef int Elem;  
typedef struct Snode{
	Elem data;  //数据类型,这样定义容易修改
	Snode *next;
}Snode, *LinkList;

3.2 初始化

void initStack(LinkList &s){ // 初始化
s = NULL;
return;
}

3.3 判断栈空

bool isEmpty(LinkList &s)
{
    return (s == NULL);
}

3.4 入栈

void push(LinkList &s){ // 入栈 
	Elem e;
	Snode *p;
	printf("入栈的值:\n");
	scanf("%d",&e);	
	p = new Snode;
	p->data = e;
	p->next = s; 
	s = p;
	return;
}

3.5 出栈

void pop(LinkList &s){  // 出栈 
    Elem e;
	if(isEmpty(s)){
		printf("栈空\n");
		return;
	} 
	e = s->data;
	LinkList p = s;
	s = s->next;
	delete p;
	printf("%d 出栈成功\n",e);
	return;	
}

3.6 获取栈顶元素并返回

Elem getTop(LinkList &s)
{
    if(!isEmpty(s))
    {
        printf("%d\n",s->data);
        return s->data;
    }
}

3.7 遍历栈

void show(LinkList &s){ // 显示栈 
	if(!isEmpty(s)){
		LinkList p = s;
		while(p != NULL){
			printf("%d  ",p->data);
			p = p->next;
		}
		printf("\n");
		return;
	}
}

3.8 清空栈


void toEmpty(LinkList &s){  // 清空 
	LinkList p;
	while(s != NULL){
		p = s;
		s = s->next;
		delete p;
	}
	return;
}

4. 队列

只允许在一端插入,在另一端删除的线性表。

4.1 重要术语:队头、队尾、空队列

队头:允许删除的一端。

队尾:允许插入的一端。

特点:先进先出。FIFO First In First Out。

image.png

4.2 基本操作

  1. 初始化队列,构造一个空队列。
  2. 销毁队列,销毁并释放队列Q所占用的内存空间。
  3. 入队,若队列Q未满,将x加入,使之成为新的队尾。
  4. 出队,若队列Q非空,删除队头元素,并用x返回。
  5. 读队头元素,若队列Q非空,则将队头元素赋值给x。

5. 顺序存储实现队列

可以通过静态数组实现队列的数据元素,设置两个标记队列的队头和队尾。 定义为:

# define MAXSIZE 10
typedef struct 
{
	int data[MAXSIZE]; 
	int front, rear; //这种情况下为了区分判空和判满需要浪费一个存储空间
	//int size; 可以用来判空判满的另一种辅助方式
	//int tag;  可以用来判空判满的另一种辅助方式,插入时tag置为1,删除时为0
}SqQueue;

5.1 顺序队列的基本操作:初始化和判空操作

image.png

5.2 顺序队列的基本操作:判满操作

为了避免浪费存储空间,顺序队列通常采用循环结构,即循环队列。其形式如下: image.png 在此情况下采用上述的数据结构定义方式,会导致判断队列空满为一个形式,因此此处采用:

//判断队满,循环队列情况下,在此种定义方式中判断队满需要浪费一个存储单元
bool isFull(SqQueue q) 
{
	return ((q.rear + 1) % MAXSIZE == q.front);
}

也可以采用数据结构定义中增加记录队列状态的参数。

5.2 顺序队列的基本操作:入队(只能从队尾插入)

image.png

5.3 顺序队列的基本操作:出队(只能从队头出队)

//出队
bool OutQueue(SqQueue& q, int x) 
{
	if (isEmpty(q)) //判断队空
		return false;
	x = q.data[q.front];
	q.front = (q.front + 1) % MAXSIZE; //队头指针后移
	return true;
}

5.4 顺序队列的基本操作:获取队头元素的值

//获取队头元素的值
bool OutQueue(SqQueue& q, int x)
{
	if (isEmpty(q)) //判断队空
		return false;
	x = q.data[q.front];
	//跟出队操作的区别就是没有队头指针后移
	return true;
}

注意:队尾指针可能指向队尾元素,而非队尾元素的后一个位置(下一个应该插入的位置),那么代码会有区别。

判空:
     (q.rear+1)% MAXSIZE = q.front
判满:
     牺牲一个存储单元或增加一个辅助变量

6. 链队列:链式存储实现队列

6.1 链式存储队列的定义

image.png

6.2 链式存储队列的基本操作:初始化和判空

1) 带头结点

image.png

2) 不带头结点

image.png

6.3 链式存储队列的基本操作:入队

1) 带头结点

image.png

2) 不带头结点

注意第一个元素入队的处理。

image.png

6.4 链式存储队列的基本操作:出队

1) 带头结点

注意最后一个元素出队的处理。 image.png

2) 不带头结点

注意最后一个元素出队的处理。 image.png

链式存储一般不会队满,除非内存不足。

7. 双端队列

只允许从两端插入、两端删除的线性表。 同时还有输入受限的双端队列和输出受限的双端队列。 若数据元素输入序列为 1,2,3,...,n, 1,2,3,...,n,,则输出序列有 Ann=n! A^{n}_{n} = n! 种。

8. 栈的应用

8.1 括号匹配

利用栈的先进后出特性,遇到左括号就入栈,遇到右括号就将左括号出栈。

image.png

image.png

8.2 表达式求值

8.2.1 中缀表达式

中缀表达式由操作符、运算符、界限符三部分组成,中缀表达式中界限符是必不可少的。

image.png

8.2.2 后缀表达式(逆波兰表达式)

中缀转后缀的方法,机算转换遵循左优先原则,可保证运算顺序唯一,遵循左优先原则。 image.png 后缀表达式的手算方法,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数。应注意两个操作数的左右顺序。

1)中缀表达式转后缀表达式(机算)

image.png

2) 用栈实现后缀表达式计算

    1. 从左往右扫描下一个元素,直到处理完所有元素
    1. 若扫描到操作数则压入栈,并回到1,否则执行3
    1. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1
  • 注意:先出栈的是右操作数

3)用栈实现中缀表达式的求值

相当于中缀转后缀表达式和后缀表达式计算的结合。

    1. 初始化两个栈,操作数栈和运算符栈。
    1. 若扫描到操作数,压入操作数栈
    1. 若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑亚茹运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)

8.2.3 前缀表达式(波兰表达式)

中缀转前缀的方法,机算转换遵循右优先原则,只要右边的运算符能先计算,就优先算右边的。

image.png

1)用栈实现前缀表达式计算

    1. 从右往左扫描下一个元素,直到处理完所有元素
    1. 若扫描到操作数则压入栈,并回到1,否则执行3
    1. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1
  • 注意:先出栈的是左操作数

8.3 栈用:递归

函数调用特点:最后被调用的函数最先执行结束(LIFO)

8.3.1 函数调用背后过程

image.png

8.3.2 栈在递归中的应用

image.png image.png

1)递归缺点:

效率低,太多层递归可能会导致栈溢出,可能包含很多重复计算

解决方法可以尝试: 自定义栈将递归算法改造成非递归算法?

9. 队列的应用

9.1 队列应用:树的层次遍历

每处理一个结点,将该结点的左右孩子放在队尾,处理完的结点在头部出队。

9.2 队列应用:图的广度优先遍历

9.3 队列在操作系统中的应用

多个进程争抢着使用有限的系统资源时,FCFS(First Come First Service 先来先服务)是一种常用策略。

EG:打印数据缓冲区:用队列组织打印数据