文章目录
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)
说如果兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子。假设所有兔都不死,那么一年以后可以繁殖多少对兔子呢?
我们拿新出生的一对兔子分析一下:第一个月小兔子没有繁殖能力,所以还是一对;两个月后,生下一对,小兔子数共有两对;三个月后,老兔子又生下一对,因为小兔子还没有繁殖能力,所以一共是三对…以此类推可以列出下表
所经过月数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
兔子对数 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 |
表中数字 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 队列的链式存储结构
链队的操作实际上是单链表的操作,只不过是删除在表头进行,插入在表尾进行。插入、删除时分别修改不同的指针。