数据结构学习总结(三)栈与队列

1. 栈

1.1 栈的定义

栈
栈(stack)是限定仅在表尾进行插入和删除操作的线性表。 允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out) 的线性表,简称 LIFO 结构。

栈的插入操作,叫作进栈,也称压栈、入栈,类似子弹入弹夹;栈的删除操作,叫作出栈、弹栈,类似子弹出夹。

进栈出栈变化形式
最先进栈的元素是不是就只能是最后出栈呢?
答案是不一定,最理想的情况下,从小到大入栈,从大到小出栈,但是因为出栈是随机的,也就是在入栈还没结束的情况下就可以出栈。这样,就会出现一些小的数提前出栈的现象。但是那些“守规矩”的小数相对位置是没有变化的。比如入栈 12345,结果在 4 入栈之前 3 不听话先出去了。但是这不会影响12 的顺序,12 的相对顺序还是保持着 12 入栈,21 出栈。因为 12 是守规矩的。

举例,现在是有 3 个整形数字元素 1、2、3 依次进栈,可能的出栈次序有以下几种:
第一种:1、2、3 进,再 3、2、1 出,出栈次序为 321。
第二种:1 进,1 出,2 进,2 出,3 进,3 出,出栈次序为 123。
第三种:1 进,2 进,2 出,1 出,3 进,3 出,出栈次序为 213。
第四种:1 进,1 出,2 进,3 进,3 出,2 出,出栈次序为 132。
第五种:1 进,2 进,2 出,3 进,3 出,1 出,出栈次序为 231。
有没有可能是 312 这样是次序出栈呢?答案是肯定不会。因为 3 先出栈,就意味着,3 曾经进栈,也就意味着 1 、2 已经进栈了,此时,2 一定是在 1 的上面,就是更接近栈顶,若要出栈的话,2 肯定是在 1 之前先出栈,那么出栈只可能是 321,不然不满足 123 依次进栈的要求。

1.2 栈的抽象数据类型

ADT Stack{
	数据对象:D ={ai | ai∈ElemSet, i=1,2,…,n,n≥0}
	数据关系:R ={<ai-1, ai>|ai-1,ai∈D, i=2,3,…,n}
	基本操作:初始化、进栈、出栈、取栈顶元素等
} ADT Stack

1.3 栈的顺序存储结构

栈的顺序存储结构简称为顺序栈,和线性表相类似,用一维数组来存储栈。根据数组是否可以根据需要增大,又可分为静态顺序栈和动态顺序栈。

静态顺序栈 Java 实现:

public class Stack {
	private int elementCount; // 栈的大小
	private Object[] elementData;
	private int top = -1;

	/**
	 * 初始化
	 * @param size
	 */
	public Stack(int size) {
		elementCount = size;
		elementData = new Object[elementCount];
	}

	/**
	 * 入栈
	 * @param elem
	 */
	public void push(Object elem) {
		if (!isFull())
			elementData[++top] = elem;
	}

	/**
	 * 出栈
	 * @return
	 */
	public Object pop() {
		if (!isEmpty())
			return elementData[top--];
		return null;
	}

	/**
	 * 查看栈顶元素
	 * @return
	 */
	public Object peek() {
		return elementData[top];
	}

	/**
	 * 判断栈是否为空
	 * @return
	 */
	public boolean isEmpty() {
		return top == -1;
	}

	/**
	 * 判断栈是否已满
	 * @return
	 */
	private boolean isFull() {
		return top == elementCount;
	}
}

1.4 栈的链式存储结构

栈的链式存储结构称为链栈,是运算受限的单链表。其插入和删除操作只能在表头位置上进行。因此,链栈没有必要像单链表那样附加头结点,栈顶指针top就是链表的头指针。

链栈 Java 实现:

public class LinkStack<T> {
	private StackNode top; // 存放栈顶结点
	private int size; // 存放栈中元素个数

	// 创建空链栈
	public LinkStack() {
		this.top = null;
		this.size = 0;
	}

	public void push(T data) {
		top = new StackNode(data, top);
		size++;
	}

	public T pop() {
		if (size == 0) {
			return null;
		}

		StackNode oldTop = top.next;
		// top下移
		top = oldTop;

		// 释放原栈顶元素的引用
		oldTop.next = null;
		size--;

		return oldTop.data;
	}

	public T peek() {
		if (size == 0) {
			return null;
		}
		return top.data;
	}

	public int getSize() {
		return this.size;
	}

	private class StackNode {
		T data;
		StackNode next;

		public StackNode(T data, StackNode next) {
			this.data = data;
			this.next = next;
		}
	}
}

1.5 栈的应用

1.5.1 递归(斐波那契数列)

如果一个函数在内部直接或间接调用自身本身,这个函数就是递归函数。每个递归定义必须至少有一个条件,满足时递归不在进行,即不再引用自身而是返回值退出。

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。

下面介绍一个经典的递归例子:斐波那契数列(Fibonacci)
fibonacci
说如果兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子。假设所有兔都不死,那么一年以后可以繁殖多少对兔子呢?

我们拿新出生的一对兔子分析一下:第一个月小兔子没有繁殖能力,所以还是一对;两个月后,生下一对,小兔子数共有两对;三个月后,老兔子又生下一对,因为小兔子还没有繁殖能力,所以一共是三对…以此类推可以列出下表

所经过月数123456789101112
兔子对数1123581321345589144

表中数字 1,1,2,3,5,8,13…所构成的序列就是斐波那契数列

fibonacci Java实现:

public int fib(int n) {
	if (n < 2)
		return n == 0 ? 0 : 1;
	return fib(n - 1) + fib(n - 2);
}

迭代使用的是循环结构,递归使用的是选择结构
递归函数分为调用和回退阶段,递归的回退顺序是它调用顺序的逆序。利用这一点可以简单实现将一个字符串反向输出的功能。

1.5.2 四则运算表达式求值(逆波兰表达式)

我们平常所做的标准四则运算表达式,即 “9+(3-1)*3+10/2” 叫做 中缀表达式。但对于计算机来说,使用 中缀表达式 处理四则运算比较复杂,于是波兰科学家想出了一种不需要括号的后缀表达法,我们也把它称为逆波兰(RPN)表达式,例如,将上述 中缀表达式 转换为 后缀表达式:“9 3 1 - 3 * + 10 2 / +”。

1.5.3 后缀表达式计算

规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到获得最终结果。

示例:
后缀表达式9 3 1 - 3 * + 10 2 / +
1、后缀表达式前三个都是数字,所以 9、3、1 进栈。
2、接下来是 - ,所以 1、3 出栈,并运算 3 - 1 得到 2,再将 2 进栈。
3、接着是数字 3 进栈,此时栈中元素是 9、2、3。
4、后面是 * ,所以 3、2 出栈,并运算 2 * 3 得 6,并将 6 进栈。
5、下面是 + ,所以 6、9 出栈,并运算 6 + 9,得 15,将 15 进栈。
6、接着是 10、2 进栈,此时栈中元素是 15、10、2.
7、接下来是符号 / ,因此 2、10 出栈,并运算 10 / 2 得 5,将 5 进栈。
8、最后一个是符号 + ,所以 5、15 出栈并相加得到 20,将 20 进栈。
9、结果是 20 出栈,栈变为空。

1.5.4 中缀表达式转后缀表达式

规则:从左到右遍历 中缀表达式 的每个数字和符号,若是数字就输出,即成为 后缀表达式 的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号,则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出 后缀表达式 为止。

示例:
中缀表达式9+(3-1)*3+10/2” 转换为 后缀表达式:“9 3 1 - 3 * + 10 2 / +”。
1、第一个字符是数字 9,输出 9,后面是符号 + ,进栈。
2、第三个字符是 ( ,依然是符号,因其是左括号,还未配对,故进栈。
3、第四个字符是数字 3,输出,总表达式为 9 3,接着是 - ,进栈, 此时栈中元素是 + 、( 、- 。
4、接下来是数字 1,输出,总表达式为 9 3 1,后面是符号 ) ,此时,需要去匹配之前是 ( ,所以栈顶元素依次出栈,并输出,直到 ( 出栈为止。总的输出表达式为 9 3 1 - 。
5、紧接着是符号 * ,因为此时栈顶元素为 + ,优先级低于 * ,因此不输出,* 进栈,此时栈中元素为 + 、* 。接着是数字 3,输出,总的表达式为 9 3 1 - 3。
6、之后是符号 + ,此时当前栈顶元素 * 比 + 的优先级高,因此栈顶元素出栈并输出,而 + 的优先级不高于此时栈顶元素 + ,因此栈顶元素 + 再次出栈,总输出表达式为 9 3 1 - 3 * + 。然后将当前这个符号 + 进栈。
7、紧接着数字 10,输出,总表达式变为 9 3 1 - 3 * + 10。后面是符号 / 进栈。
8、最后一个数字 2,输出,总的表达式为 9 3 1 - 3 * + 10 2。
9、因为已经到最后,所以将栈中符号全部出栈并输出。最终输出的后缀表达式结果为 9 3 1 - 3 * + 10 2 / + 。

因此要想让计算机处理我们通常的标准(中缀)表达式的能力,最重要的就是两步:
(1)将中缀表达式转化为后缀表达式(栈用来进出运算的符号)
(2)将后缀表达式进行运算得出结果(栈用来进出运算的数字)
整个过程都利用了栈的后进先出的特性来处理。

2. 队列

2.1 队列的定义

队列
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。 允许插入的一端称为队尾(rear),允许删除的一端称为队头(front),队列是一种先进先出(First In First Out) 的线性表,简称 FIFO 结构。

2.2 队列的抽象数据类型

ADT Queue{
数据对象:D = { ai | ai∈ElemSet,  i=1, 2, …, n, n >= 0}
数据关系:R = {<ai-1, ai> | ai-1, ai∈D,  i=2,3,…,n}
		约定a1端为队首,an端为队尾。
基本操作:
	InitQueue():初始化操作,创建一个空队列;
	EmptyQueue():若队列为空,则返回true,否则返回flase;
	...
	EnQueue(e):向队尾插入元素e;
	DelQueue():删除队首元素;
} ADT Queue

2.3 循环队列

2.3.1 队列顺序存储的不足

利用一组连续的存储单元(一维数组) 依次存放从队首到队尾的各个元素,称为顺序队列

在非空队列里,队首指针始终指向队头元素,而队尾指针始终指向队尾元素的下一位置。

顺序队列中存在“假溢出”现象。因为在入队和出队操作中,头、尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。因此,尽管队列中实际元素个数可能远远小于数组大小,但可能由于尾指针巳超出向量空间的上界而不能做入队操作。该现象称为假溢出。如图所示是数组大小为5的顺序队列中队首、队尾指针和队列中元素的变化情况。
队列

2.3.2 循环队列定义

为充分利用向量空间,克服上述“假溢出”现象的方法是:将为队列分配的向量空间看成为一个首尾相接的圆环,并称这种队列为循环队列(Circular Queue)

在循环队列中进行出队、入队操作时,队首、队尾指针仍要加 1,朝前移动。只不过当队首、队尾指针指向向量上界(MAX_QUEUE_SIZE-1)时,其加 1 操作的结果是指向向量的下界 0。用模运算可简化为:i = (i+1)%MAX_QUEUE_SIZE; i 代表队首指针(front)或队尾指针(rear)。

显然,为循环队列所分配的空间可以被充分利用,除非向量空间真的被队列元素全部占用,否则不会上溢。因此,真正实用的顺序队列是循环队列。
循环队列
入队时尾指针向前追赶头指针,出队时头指针向前追赶尾指针,故队空和队满时头尾指针均相等。因此,无法通过front=rear来判断队列空还是满。解决此问题的方法是:约定入队前,测试尾指针在循环意义下加 1 后是否等于头指针,若相等则认为队满。即:
◆ rear 所指的单元始终为空。
◆ 循环队列为空:front = rear 。
◆ 循环队列满:(rear+1)%MAX_QUEUE_SIZE = front。

循环队列 Java实现:

public class CirQueue<T> {
	private int MAX_QUEUE_SIZE; // 队列大小
	private int front; // 队头
	private int rear; // 队尾
	private T[] queueArray;

	// 队列初始化
	public CirQueue(int size) {
		this.MAX_QUEUE_SIZE = size;
		this.queueArray = (T[]) new Object[MAX_QUEUE_SIZE];
		front = rear = 0;
	}

	// 返回队列中元素个数
	public int size() {
		return (rear - front + MAX_QUEUE_SIZE) % MAX_QUEUE_SIZE;
	}

	// 入队操作
	public void enQueue(T e) {
		if ((rear + 1) % MAX_QUEUE_SIZE == front) { // 判断队满
			return;
		}
		queueArray[rear] = e;
		rear = (rear + 1) % MAX_QUEUE_SIZE;
	}

	// 出队操作
	public T delQueue() {
		if (rear == front) { // 判断队空
			return null;
		}
		T e = queueArray[front];
		front = (front + 1) % MAX_QUEUE_SIZE;
		return e;
	}
}

2.4 队列的链式存储结构

链队的操作实际上是单链表的操作,只不过是删除在表头进行,插入在表尾进行。插入、删除时分别修改不同的指针。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值