浅析数据结构之“栈”和“队列”
引言:
以前刚开始学习数据结构的时候,链表是一大难关,但是在学习链表的时候,老师有事没事就提到“栈”,于是总觉得“栈”很恐怖,很复杂。但是当我学完“栈”这种数据结构的时候,才知道“栈”只不过是纸老虎,甚至还有点“残疾”,功能很少。队列和栈有异曲同工之妙,所以今天就让我们一举攻破“栈”和“队列”吧,其实很简单。一枪扎破纸老虎。
概念简述:
栈,是一种非常基本的数据结构,它的特点就是先进后出,后进先出,那么”先进后出,后进先出“是什么意思呢?形象的说“栈”就是一口井,“栈”有两个操作,入栈(push)和出栈(pop),入栈就是小动物掉进井里,甲乙丙丁四个小动物从甲开始乙丙丁依次掉进了井里,甲要想被救出去必须需要等到最后掉进去的丁爬出去以后,甲才能出去。小动物们爬出去就可以形象的称为“出栈”。
而“队列”呢,就像一个队列本身,它的特点就是先进先出,后进后出,“队列”有关的操作就是入队(enqueue),出队(dequeue),入队就是排在队伍的后面如果没有队伍就自成一队。出队的时候就是排在前面的先出去。
如上图所示,这就是基本的栈和队列这两种最基本的数据结构。恭喜你,看到这基本上你就已经掌握了栈和队列的基本概念了。
代码实现
栈的实现方式有两种,分别为链式栈(由链表实现)和顺序栈(由数组实现),同样队列也有用链表实现和用数组实现两种方式。
链式栈:
话不多说,先上代码。解释都在注释里面。
public class LinkedlistStack {
int N=0;//记录栈中元素的个数。
private Node first = null;
private class Node{
int item;
Node next;
}
public boolean isEmpty(){return (first==null);}//判断栈是否为空。
public int size(){return N;}//返回栈中元素的个数。
public void push(int num){
Node oldfirst = first;
Node first = new Node();//看着很复杂,其实就是在头节点插入元素。
first.item = num;
first.next = oldfirst;
N++;
}
public int pop(){
int tmp = first.item;
first = first.next;//直接删除头节点的元素。
N--;
return tmp;
}
}
这就是链式栈,内部实现非常简单。
顺序栈:
我们都有思路,就是申请一个数组,向里面加入元素为入栈,取出元素为出栈。
public class ArrayStack {
int N=0;
int[] array;
public ArrayStack(int num){
int[] array = new int[num];
}
public boolean isEmpty(){return array.length==0;}
public int size(){return array.length;}
public void push(int num){
array[N++]=num;
}
public int pop(){
return array[--N];
}
}
这种的实现就非常简单了,但是美中不足的是必须人为定义数组容量,而且还不能扩容,所以有诸多缺点。
经过学习,我们有了进阶版顺序栈,我们直接在类中定义数组,如果数组满了,重新申请一个两倍的新的数组,将原来数组复制到新数组。出栈时,直到只剩1/4的元素时,我们在释放掉一半的数组空间。代码实现如下:
public class FixedArrayStack {
int[] array = new int[1];//申请一个大小为1的数组。
private int N=0;
public boolean isEmpty(){ return N==0;}
public int size(){return N;}
public void push(int num){
if(array.length==N){resize(array.length*2);}//如果数组满了重新申请一个二倍大的数组。
array[N++]=num;
}
public int pop(){
int item = array[--N];
//array[N]=null;//这里是为了防止对象游离,编译器无法回收数组中的元素,但是我们永远也用不到了。
if(N>0&&array.length/N>=4){resize(array.length/2);}//之所以只剩1/4的元素时,我们在释放掉一半的数组空间,如果剩下一半释放一半,此时我们连续出栈入栈会连续申请数组释放数组,严重增加时间复杂度。
return item;
}
public void resize(int length){
int[] s = new int[length];
for(int i=0;i<array.length;i++){
s[i]=array[i];
}
array=s;//此时array指向的就是s.
}
}
好了,这就是我们的进阶版了,不用用户提供大小,而且支持扩容的进阶版了。
队列
链式队列:
先上代码:因为队列是先进先出,后来的排队,所以我们需要两个指针。
public class LinkedListQuene {
private Node first;//申请两个指针一个指向头节点,一个只想尾节点。
private Node last;
private int N=0;//记录队列的数量
private class Node
{
int val;
Node next;
}
public boolean isEmpty(){return (first==null);}
public int size(){ return N;}
public void enquene(int num){
Node oldlast = last;
last = new Node();
last.val = num;
if(isEmpty()){first = last;}//如果队列为空,我们就直接让first等于null。否则的话直接在队尾添加元素。
else{oldlast.next = last;}
N++;
}
public int dequene(){
int tmp = first.val;
first = first.next;//删除头节点。
if (isEmpty()){last=null;}//删除头节点以后如果队列为空,则必须让last指向null。
N--;
return tmp;
}
}
顺序队列:
简洁版,我们都能想到的一个思路就是申请一个数组,用两个指针,一个指针指向头部,一个指针指向数组最后一个元素的后面用来添加元素。
// 用数组实现的队列
public class ArrayQueue {
// 数组:items,数组大小:n
private String[] items;
private int n = 0;
// head表示队头下标,tail表示队尾下标
private int head = 0;
private int tail = 0;
// 申请一个大小为capacity的数组
public ArrayQueue(int capacity) {
items = new String[capacity];
n = capacity;
}
// 入队
public boolean enqueue(String item) {
// 如果tail == n 表示队列已经满了
if (tail == n) return false;
items[tail] = item;
++tail;
return true;
}
// 出队
public String dequeue() {
// 如果head == tail 表示队列为空
if (head == tail) return null;
// 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了
String ret = items[head];
++head;
return ret;
}
}
//此代码来自极客时间王争老师,数据结构与算法之美,我的代码弄丢了。
但是这里有个致命的问题就是,每次入队或者出队我们都将两个指针往后移一位,这样会出现数组中还有剩余空间时,因为head指向了队尾,所以无法在入队。我们想到了每次入队满了之后,前面还有剩余空间的时候,我们将数组向前搬运。就这样我们就解决了上面的有空间无法入队的情况。
// 入队操作,将item放入队尾
public void enqueue(String item) {
// tail == n表示队列末尾没有空间了
if (tail == n) {
// tail ==n && head==0,表示整个队列都占满了
if (head == 0) return false;
// 数据搬移
for (int i = head; i < tail; ++i) {
items[i-head] = items[i];
}
// 搬移完之后重新更新head和tail
tail -= head;
head = 0;
}
items[tail] = item;
++tail;
}
好了,这就是初学者应该掌握的基本与栈和队列有关的概念以及简单的应用。
本人良弓,初来乍到,请多关照~