这两个数据结构,熟悉编程的小伙伴肯定听过或者很熟悉。今天我们来详细介绍一下这两个,从基础开始系统的介绍,新手和老司机都会有所收获
栈
什么是栈
- 堆叠(stack)又称为栈或堆叠,是计算机科学中的一种抽象资料型别,只允许在有序的线性资料集合的一端(称为堆叠顶端,top)进行加入数据(push)和移除数据(pop)的运算。
- 因而按照后进先出(LIFO, Last In First Out)的原理运作,堆叠常用一维数组或连结串列来实现
- 栈也是一种线性结构
- 相比数组,栈对应的操作是数组的子集
栈的应用
- 我们常常使用的 “撤销” 操作
- 例如我们在编码的过程中,可以每次的操作都放入栈中。依次敲入a b c d(其中a在栈底,d在栈顶)这时候我们想撤销到上一步
- 在撤销的时候,利用栈的特性,从栈顶元素开始出栈
- 撤销第一次 将d弹出,我们的文本变成abc
- 撤销第二次,将c弹出,我们的文本变成ab
- 借助栈的特性 就实现了撤销的动作,(当然实际中可能没有这么简单,这里只是做一个简单例子)
- 还有一个比较重要的应用-
程序调用栈- 方法a中有调用方法b,方法b中有调用方法c。以这种方法之间存在调用来举例
- 程序执行从a开始,到调用b方法的时候会暂停a方法的执行,转到b方法的执行中,这个时候会压入栈内一个元素例如a1。同理在b方法中执行c方法的时候,将压入栈内一个b1。
- c方法执行结束之后,从栈内弹出元素,b1,程序就会知道接着执行b方法。同理,b方法执行完毕之后,弹出a1,继续执行a方法,a方法执行完毕之后,栈内无元素,方法执行完毕。
- 这里做个简单介绍。感兴趣小伙伴可以搜索专门的文章来查看
动手编码
- 自定义实现一个栈
- 借助上一篇的数组结构,只不过栈只能从一端加入元素和释放元素
public class MyStack<E>{
private E[] data;
private int size;
//构造函数,初始化一个数组结构
//数组头当作栈低,数组末尾当作栈顶
public MyStack(int capacity){
data = (E[])new Object[capacity];
size = 0;
}
//将数据放入栈中,也即像数组的末尾添加元素
public void push(E e){
if(size == data.length){
resize(2 * data.length);
}
data[size] = e;
size ++;
}
//从栈中弹出元素,也即删除数组末尾元素
public E pop(){
size --;
E ret = data[size];
data[size] = null;
if(size == data.length / 4 && data.length / 2 != 0){
resize(data.length / 2);
}
return ret;
}
// 将数组空间的容量变成newCapacity大小
private void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity];
for(int i = 0 ; i < size ; i ++)
newData[i] = data[i];
data = newData;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Stack: ");
res.append('[');
for(int i = 0 ; i < array.getSize() ; i ++){
res.append(array.get(i));
if(i != array.getSize() - 1)
res.append(", ");
}
res.append("] top");
return res.toString();
}
}
- 想必有一部分小伙伴已经发现了,这里的 push和pop方法的实现和上一篇数组的add方法和remove方法一样,因为是基于数组来实现的,这也证实了开头的定义,
是数组功能的子集
数组实现栈的时间复杂度
- push:O(1)
- pop: O(1)
- 两个方法都是O(1)的想必小伙伴们没有什么疑问吧,因为都是通过索引快速操作。
- 如果有疑问,可以看看我的上篇文章-“数据结构-数组” 里面有对时间复杂度的介绍
队列
什么是队列
- 是计算机科学中的一种抽象资料型别,是先进先出(FIFO, First-In-First-Out)的线性表。
- 在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。
- 队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加
- 综上所述,队列也是一个线性结构
- 相比较数组,队列的操作也是数组的子集
动手编码
public class MyQueue<E> {
private E[] data;
private int size;
//构造方法,数组的头为队首,数组的尾为队尾
public MyQueue(int capacity){
data = (E[])new Object[capacity];
size = 0;
}
//入队操作,也即从数组的末尾 加元素,和入栈的操作一样
public void push(E e){
if(size == data.length){
resize(2 * data.length);
}
data[size] = e;
size ++;
}
//出队操作,也即从数组的头 删除一个元素
public E dequeue(){
E ret = data[0];
for(int i = 1 ; i < size ; i ++){
data[i - 1] = data[i];
}
size --;
data[size] = null;
if(size == data.length / 4 && data.length / 2 != 0){
resize(data.length / 2);
}
return ret;
}
// 将数组空间的容量变成newCapacity大小
private void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity];
for(int i = 0 ; i < size ; i ++)
newData[i] = data[i];
data = newData;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Queue: ");
res.append("front [");
for(int i = 0 ; i < array.getSize() ; i ++){
res.append(array.get(i));
if(i != array.getSize() - 1)
res.append(", ");
}
res.append("] tail");
return res.toString();
}
}
- 我知道,当小伙伴看到出队这个操作的时候,我想整个人都是懵的,因为从数组头删除一个元素,还需要将后面的所有元素向前移动一个空间,这典型的O(n) 操作啊
- 别急,我们先分析完时间复杂度再说
数组实现队列的时间复杂度
- push:O(1)
- pop: O(n)
- 出队是一个O(n)的操作,我想大家肯定是忍不了吧,效率太差
- 那么怎么能把出队这个操作变成O(1) 呢,那就是想删除头元素的时候,其余元素不必向前移动一个空间,
循环队列它来了
循环队列
- 什么是循环队列呢,简单的解释是:队列首尾相接,形成一个环形
- 当出队元素的时候,仅仅删除对应的元素,其余元素不移动。入队元素的时候,如果末尾已经满了,由于是首尾相连的,会向队首依次添加
- 所以出现两个新的名字 front:代表队首元素的索引位置,rear:代表队尾元素的位置
- 当 front = rear的时候 说明队列为空 front = rear+1 的时候 说明队列满
循环队列实现
public class MyQueue<E> {
private E[] data;
private int front, rear;
private int size;
//构造方法,和上面的队列基本一致,只是因为循环队列会有一个空间空余,所以这里容量要+1
public MyQueue(int capacity){
data = (E[])new Object[capacity + 1];
front = 0;
rear = 0;
size = 0;
}
//入队操作,小伙伴可以细细体会一下这个取余操作
public void push(E e){
if((rear + 1) % data.length == front)
resize(getCapacity() * 2);
data[rear] = e;
rear = (rear + 1) % data.length;
size ++;
}
//出队
public E pop(){
if(front == rear){
throw new IllegalArgumentException("队列为空");
}
E ret = data[front];
data[front] = null;
front = (front + 1) % data.length;
size --;
int capacity = data.length - 1;
if(size == capacity / 4 && capacity / 2 != 0)
resize(capacity / 2);
return ret;
}
}
- 到此,循环队列实现完成了,小伙伴可以体会一下
循环这个感觉,类似一个环~ - 这样出队这个操作的时间复杂度就是O(1) 了
到此,本文就结束了,将栈和队列放在一起,一来是因为 这两个都是线性结构,二来 通过对比队列的实现细节,来发现其中缺点。大家可以想象循环队列的优缺点。试想有没有其他的实现方案