(1)栈:栈实际上是一种线性表,它只允许在固定的一段进行插入或者删除元素,在进行数据插入或者删除的一段称之为栈顶,剩下的一端称之为栈顶。其遵循的原则是后进先出。
(2)栈的核心操作:三大核心操作,入栈,出栈,取栈顶元素
(3)对于栈的形象理解:子弹的弹夹我们一定见过,子弹在被压入的时候就相当于是一个个元素,而弹夹就相当于是栈。先被压入的子弹是最后被打出的,先压入的元素是最后出来的,也就是后进先出。
(1)队列:首先队列也是一种特殊的线性表,它允许在一端进行插入数据,在另一端进行删除数据的。队列里边有队首,队尾,队首元素。其遵循的原则是先进先出。
(2)队列的核心操作:三大核心操作分别是入队列,出队列,取队首元素。
(3)对于队列的形象理解:火车穿越隧道,火车的头相当于是队列的首,火车的尾相当于是队列的尾部。火车在穿越隧道的时候,头部先进入隧道头部也先出隧道,尾部后进入尾部后出隧道。队列也就是先入的元素先出队列,后进入的元素后出队列。
(1)栈和队列的出入方式不同:栈是后进先出、队列是先进先出。
(2)栈和队列在具体实现的时候操作的位置不同:因为栈是后进先出,它在一段进行操作;而队列是先进先出,实现的时候在两端进行。在Java标准库中实现队列时是按照链表实现的。
上边我们提到过:栈和队列都是一种典型的线性表,都是基于线性表(顺序表和链表)来实现的,那么我们研究栈和队列的目的何在?因为在栈和队列定义后,只有那三种操作,而那三种操作都是最常用的,它支持的操作越少,我们在使用的时候关心的点也就越少,用起来就越不容易出错。在计算机中“少即是多”,少意味着功能比较少、比较呆板。多意味着功能很多,用的时候要操的心就越多,就越容易出错。综上:栈和队列存在的意义就是减少线性表的基本操作,提取常用操作,让人们使用起来更方便,更不容易出错。
栈的操作是在一端进行的,所以我们选择在顺序表的尾部进行操作:入栈用尾插来实现、出栈用尾删来实现、取栈顶元素就是取尾部元素。
注意:我们在用顺序表来实现栈的时候选取的是在顺序表的尾部来进行的,但这并不是说在顺序表的头部就不能实现。只是在头部实现的时候,不管是头插还是头删都要进行元素的搬运,时间复杂度太高,所以不选取。
下边是基于顺序表来实现的栈:
public class MyStackForArrayList {
// 用顺序表来实现栈
// 栈的特点:后进先出,所以在顺序表中入栈用尾插,出栈用尾删,取栈顶元素用[]操作
// 创建数组用来表示顺序表,栈的容量是100,要是不够后边可以进行扩容
private int[] data = new int[100];
// 记录顺序表的当前值
private int size = 0;
// 一,栈的实现:顺序表实现
// 1.入栈,就是顺序表中的尾插,头插也能实现,但是要进行搬运,效率太低
public void push(int val) {
// 特殊情况的考虑,这里也可以进行扩容
if (size >= data.length) {
return;
}
// 一般情况的处理
data[size] = val;
size++;// 入栈要让size++
}
// 2.出栈,是有返回值的,用Integer来接收,它可以返回null。
public Integer pop() {
// 特殊情况
if (size == 0) {
return null;
}
// 一般情况
int ret = data[size-1];// 保存那个出栈的元素,后边进行返回
size--;// 出栈要让size--
return ret;
}
// 3.取栈顶元素
public Integer peek() {
// 特殊情况
if (size == 0) {
return null;
}
return data[size - 1];
}
}
用链表来实现栈:在用链表实现栈的时候,我们操作的一段选取头端。用头插表示入栈,用头删来表示出栈,取栈顶元素就是取链表的head的值。
注意:选取在链表的头部实现并不是说在链表的尾部不能进行,而是在尾部进行操作的时候,因为每一次那个tail都会进行更新,所以要用一个引用来记录tail的位置,这样就占据了一块内存空间,因此不选它。
下边是基于链表实现的栈:
class Node {
int val;
Node next;
public Node(int val) {
this.val = val;
}
}
// 链表实现栈,头插表示入栈,头删表示出栈,取栈顶元素。链表头插,头删还是为了减少内存的开销
public class MyStackForLinkedList {
// 先搞一个链表
private Node head = null;
// 1.入栈,头插,不需要返回要插入的值
public void push(int val) {
// 将要插入的元素放在链表里边
Node newNode = new Node(val);
// 特殊情况的处理,空链表
if (head == null) {
// 链表本来是空的
head = newNode;
return;
}
// 处理一般情况
newNode.next = head;
head = head.next;
}
// 2.出栈,就是头删,出栈要返回一个值
public Integer pop() {
// 用Integer来为了可以返回那个null
// 特殊情况处理,链表是空
if (head == null) {
return null;
}
// 链表之中只有一个元素,删除就没有元素了
if (head.next == null) {
int ret = head.val;
head = null;
return ret;
}
// 一般情况的处理
int ret = head.val;
head = head.next;
return ret;
}
// 3.取栈顶元素
public Integer peek() {
// 特殊情况:要是链表是空的,没得取,返回null
if (head == null) {
return null;
}
return head.val;
}
}
队列的操作在两端进行,所以我们选择在链表的尾部进行插入表示入队列,头删来表示出队列,用 head.val 操作来表示取队首元素。
注意:这里的头删和尾插的顺序是可以进行交换的,头和尾只是选的两个引用罢了。咋样选取都是一样的。
下边是基于链表来实现的队列:
// 基于链表来实现队列
// 因为队列是先进先出的,所以用尾插表示入队列。头删表示出队列,.操作表示取队列元素
public class MyQueueForLinkedList {
// 弄一个Node内部类,要带上static,为了和外部的实例无关
static class Node {
int val;
Node next;
public Node(int val) {
this.val = val;
}
}
// 先创建一个链表,并记录其头节点和尾节点,方便后边的进行尾插
Node newHead = null;
Node newTail = null;
// 1.入队列,就是尾插,为了和Java库中的队列保持一致,用boolean返回值
public boolean offer(int val) {
// 将要插入的值放在Node 节点里面
Node newNode = new Node(val);
// 情况特殊的处理
if (newHead == null) {
// 头节点和尾节点都是要插入的值
newHead = newNode;
newTail = newNode;
return true;
}
// 一般情况的处理
newTail.next = newNode;
newTail = newTail.next;
return true;
}
// 2.出队列,就是头删,注意头删也是要返回那个要删除的值
public Integer poll() {
// 特殊情况的处理,链表为空没得删
if (newHead == null) {
return null;
}
// 链表只要一个元素
if (newHead.next == null) {
int ret = newHead.val;
// 新头节点就没有了,就是null
newHead = null;
return ret;
}
// 处理一般情况的处理
int ret = newHead.val;
newHead = newHead.next;
return ret;
}
// 3.取队列首元素
public Integer peek() {
// 特殊情况的处理
// 链表为空,没得取
if (newHead == null) {
return null;
}
// 一般情况的处理
return newHead.val;
}
}
(1)基本思想:创建两个引用head和tail表示队列的头部和尾部,开始的时候head和tail都表示null,此时head == tail表示的是队列是空的 。
(2)出现的问题:每入一次队列给tail进行++,要是tail已经满了的时候就让tail == head,此时head和tail也是相等的,所以就不能区别队列到底是空的还是满的。
(3)解决方法:
方法1:是空上让出一个内存空间,不让tail和head重合。这种方法浪费了一块内存空间,直接让那个空间是空的,也就是用这种方法后:head == tail表示空, tail == head - 1表示队列是满的。看起来非常不好看。
方法2:创建一个变量size,时刻记录队列里边元素的多少,没进行一次入队操作进行size++,每进行一次出队操作让size–,最后用size的值来区别队列是空的还是满的。
下边是基于顺序表实现的队列:
public class MyQueueForArrayList {
// 用顺序表来实现队列
// 基本思路:让head = 0,tail = 0;队列的长度是[head,tail),包含head不包含tail。
// 入队:tail++,入完判断tail的值,要是 == data.length,让tail从0又开始
// 出队列:让head++
// 这样操作当head == tail时,有两种结果:1,是队列是空的时候 2,是队列是满的时候
// 所以用size来记录一下顺序表的具体的大小,根据size来判断队列是否为满。
// 创建数组
public int[] data = new int[100];
public int head = 0;
private int tail = 0;
// 这个用来判断队列到底是空的还是满的
private int size = 0;
// 1.入队操作,tail++,然后判断size的值
public boolean offer(int val) {
// 特殊情况的处理,先判断size的值的大小,每一次都是以size来判断队列是否是空的
if (size == data.length) {
// 队列已经满了
return false;
}
// 一般情况的处理
data[tail] = val;
// 完成插入之后,判断一下tail的值的
if (tail == data.length) {
// 要是让tail从0开始
tail = 0;
}
// 否则更新size的值
size++;
return true;
}
// 2.出队列,核心操作,头删,head--
public Integer poll() {
// 特殊情况的处理
// 队列为空,没得取
if (size == 0) {
return null;
}
// 一般情况的处理
int ret = data[head];
head++;
// 每一次要判断head的值是否到达了末尾
// 要是到达了末尾,让head也是0
if (head == data.length) {
head = 0;
}
size--;
return ret;
}
// 3.取队首元素
public Integer peek() {
// 特殊情况的处理
if (size == 0) {
// 为空,没得取
return null;
}
return data[head];
}
}
好了以上就是我学完栈和队列所了解到的内容,希望对大家有所帮助,要是有错误的地方还望大家海涵并指出。