碎碎念:今天在学习数据结构,刷Leetcode的时候偶尔栈和队列不会了,还得刷,正所谓勤能补拙是良训,一分辛苦一分才。
这里感谢大佬的博客1.http://c.biancheng.net/data_structure/stack_queue/。 2.https://blog.csdn.net/whl190412/article/details/94565236 3.https://blog.csdn.net/wyqwilliam/article/details/82725637
本人对其加以理解并整理。
定义
栈和队列严格来说属于线性表,但是都都比较特殊:
- 栈 :先进后出
- 队列:先进先出
既然栈和队列都属于线性表,根据线性表分为顺序表和链表的特点,栈也可分为顺序栈和链表,队列也分为顺序队列和链队列,这些内容都会在本章做详细讲解。
栈
和顺序表的链表一样也是存储一对一的关系的数据结构。
说明一下哈:
- 顺序表是数组的表,可以直接访问数据的位置所产生的表结构
- 链表是链式结构,不能访问到及具体的位置信息,只能通过链式查找
简单理解:栈只能从一头存储数据,并且从那个位置来取数据。
因此,我们可以给栈下一个定义,即栈是一种只能从表的一端存取数据且遵循 “先进后出” 原则的线性存储结构。
通常,栈的开口端被称为栈顶;相应地,封口端被称为栈底。因此,栈顶元素指的就是距离栈顶最近的元素,拿图 2 来说,栈顶元素为元素 4;同理,栈底元素指的是位于栈最底部的元素,图 2 中的栈底元素为元素 1。
这里:要注意,栈顶是指距离最近的位置的元素,千万不要搞混。
进栈出栈
基于栈结构的特点,在实际应用中,通常只会对栈执行以下两种操作:
- 向栈中添加元素,此过程被称为"进栈"(入栈或压栈);
- 从栈中提取出指定元素,此过程被称为"出栈"(或弹栈);
应用
例如,浏览器的回退,以及括号的编程问题,等等,当然在面试与笔试中也占了很大的一部分,希望学会。
顺序表结构栈
顺序栈,即用顺序表实现栈存储结构。例如,我们先使用顺序表(a 数组)存储 {1,2,3,4},存储状态如图 1 所示:
使用栈存储的结构情况为
通过图 1 和图 2 的对比不难看出,使用顺序表模拟栈结构很简单,只需要将数据从 a 数组下标为 0 的位置依次存储即可。
了解了顺序表模拟栈存储数据后,接下来看如何模拟栈中元素出栈的操作。由于栈对存储元素出栈的次序有"先进后出"的要求,如果想将图 1 中存储的元素 1 从栈中取出,需先将元素 4、元素 3 和元素 2 依次从栈中取出。这里给出使用顺序表模拟栈存储结构常用的实现思路,即在顺序表中设定一个实时指向栈顶元素的变量(一般命名为 top),top 初始值为 -1,表示栈中没有存储任何数据元素,及栈是"空栈"。
这里简单来讲就是从栈顶向外弹出元素,栈顶top 初始为-1 表示为空
栈的结构
public class Stack implements IStack {//实现接口
private int[] mIndex;
private Object[] stackElem;
public Object[] getStackElem() {
return stackElem;
}
public int[] getmIndex() {
return mIndex;
}
private int top;
public Stack(int maxSize) {
stackElem = new Object[maxSize];
mIndex = new int[maxSize];
top = 0;
}
public void clear() {
// TODO Auto-generated method stub
top = 0;
}
public boolean isEmpty() {
// TODO Auto-generated method stub
return top == 0;//当top为0,栈为空;
}
public int length() {
// TODO Auto-generated method stub
return top;
}
// 取栈顶元素的函数
public Object peek() {
// TODO Auto-generated method stub
if (!isEmpty())
top = top - 1;
return stackElem[top+1];//top指向最新插入的元素,即栈顶。
else
return null;
}
(这里绝对我讲的不透彻可以看看上面大神的链接)
基本内容和大神的过程,如下:
入栈
/**
* 进栈操作 要求:时间复杂度是O(1)
* */
public boolean push(Object x) {
// TODO Auto-generated method stub
if (top == stackElem.length) {
System.out.print("栈已满");
return false;
}
if (top == 0) {
mIndex[top] = 0;
} else if ((Integer)x <(Integer)peek()&& peek() != null) {
mIndex[top] = top;
} else if (peek() != null) {
mIndex[top] = mIndex[top - 1];
}
stackElem[top++] = x;
return true;
}
出栈
/**
* 顺序栈的出栈操作 时间复杂度为:O(1)
* */
public Object pop() throws Exception {
// TODO Auto-generated method stub
if (!isEmpty()) {// 若栈不空,则移去栈顶元素并返回其值
mIndex[top - 1] = -1;
return stackElem[--top];
} else
return null;// 若栈空,则返回空值
}
寻找最小的元素
/**
* 返回栈中最小的元素,需要一个辅助栈 要求:时间复杂度是O(1)
* 解决这道题的思路在于:用空间换时间!很关键,很直白,但并不是很个人都能很灵活的运用!比如我就不能!
* 代码实现也很简单,定义一个结构体,结构体中两个等大的数组stackElem,mIndex;一个存储数据,另一个存储最小值的下标,
* mIndex[i]表示[0,i]区间中最小值的下标。
* */
public Object min() {
if (top == 0)
return null;
else
return stackElem[mIndex[top - 1]];
}
颠倒栈的结构
@Override
/**
* 要求用递归的方法,在不浪费空间的情况下,颠倒栈。
* */
public void reverseStack(Stack s) throws Exception {
if (!s.isEmpty()) {
Object t = s.pop();
reverseStack(s);
pushStackButtom(s, t);
}
}
public void pushStackButtom(Stack s, Object t) throws Exception {
if (s.isEmpty()) {
s.push(t);
} else {
Object top = s.pop();
pushStackButtom(s, t);
s.push(top);
}
}
}
链表式结构
以下代码来源于:博主「东北一绝、英俊侠」的原创文章,原文链接https://blog.csdn.net/whl190412/article/details/94565236,后续本人将从Leetcode的题型中进行实现
栈结构
public class LinkedListStack<Item> {
// 栈中元素的总数
private int N = 0;
// 链表头元素
private Node front;
// 内部结点类
private class Node {
Item item;
Node next;
}
插入元素
/**
* @description: 向栈顶插入元素
*/
public void push (Item item) {
Node oldFront = front;
// 向链表头部插入新的结点
front = new Node();
front.item = item;
// 将新头结点的next指针指向旧的头结点
front.next = oldFront;
// 栈的长度加1
N++;
}
弹出元素
/**
* @description: 向栈顶删除元素,并将删除的元素返回
*/
public Item pop () {
// 当栈还是空的时候, 不删除并且返回空
if(isEmpty()) return null;
// 保存待删除的项以便返回
Item item = front.item;
// 删除原头结点
front = front.next;
// 栈的长度减1
N--;
return item;
}
判断是否为空
/**
* @description: 判断栈是否为空
*/
public boolean isEmpty () {
return N == 0;
}
/**
* @description: 返回栈的大小
*/
public int size () {
return N;
}
创建实例对象
public static void main (String args []) {
// 创建栈
LinkedListStack<Integer> stack = new LinkedListStack<>();
// 向栈顶依次添加3个元素
stack.push(1);
stack.push(2);
stack.push(3);
// 依次从栈顶删除3个元素
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());
}
队列
定义:队列即遵循先进先出的原则,就相当于现实生活中的排队
同样:实现队列也有两种方式,一种是链表, 另一种是循环数组 ------------队列和栈在实现上的不同
栈主要是从数据或者链表的一端进行操作,
队列从两端进行操作
链表实现
public class LinkedListQueue<Item> {
// 链表中的结点数目
private int N = 0;
// 链表头结点
private Node front = null;
// 链表尾结点
private Node rear = null;
// 结点内部类
private class Node {
Item item;
Node next;
}
入列
/**
* @description: 元素入列(在链表尾部添加)
*/
public void enqueue (Item item) {
Node oldRear = rear;
rear = new Node();
rear.item = item;
if (isEmpty()) front = rear;
else oldRear.next = rear;
N++;
}
出列
/**
* @description: 元素出列(在链表头部删除)
*/
public Item dequeue () {
if(isEmpty()) return null;
Item item = front.item;
front = front.next;
N--;
if(isEmpty()) rear = null;
return item;
}
判断是否为空,返回长度
/**
* @description: 判断队列是否为空
*/
public boolean isEmpty () {
return N == 0;
}
/**
* @description: 返回队列长度
*/
public int size () {
return N;
}
测试
public static void main (String args []) {
LinkedListQueue<String> queue = new LinkedListQueue<>();
queue.enqueue("A");
queue.enqueue("B");
queue.enqueue("C");
queue.enqueue("D");
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
}
}
数组的队列
这里采用循环的方式遍历数组,如果是普通的数组会产生很大的浪费如下
代码的方式如下:
代码如下图所示, 可以看到,实现循环的关键是使用的一个取余数的操作,使得指针在移动到数组尾部的时候,能够重新移动到数组的头部:
栈结构
public class CircleArrayQueue<Item> {
// 队列元素总数
private int N = 0;
// 数组长度
private int M;
// 队列头部元素指针
private int front = 0;
// 队列尾部元素指针
private int rear = 0;
private Item [] items;
public CircleArrayQueue (int M) {
this.M = M;
items = (Item [])new Object[M];
}
入列操作
/**
* @description: 入列操作
*/
public void enqueue (Item item) {
// 当队列为空时, 不能进行入列操作
if (isFull()) return;
// 向队列尾部插入元素
items[rear] = item;
// 用数组长度M取余, 使得rear到达数组尾部时能返回数组头部
rear = (rear + 1) % M;
// 增加队列长度
N++;
}
出列操作
/**
* @description: 出列,并返回被删除项
*/
public Item dequeue () {
// 当队列为满时, 不能进行出列操作
if (isEmpty()) return null;
// 保存待删除元素, 以待返回
Item item = items[front];
// 删除队列头部元素
items[front] = null;
// 用数组长度M取余, 使得front到达数组尾部时能返回数组头部
front = (front + 1) % M;
// 减少队列长度
N--;
// 返回删除元素
return item;
}
判断是否满是否空等。
/**
* @description: 判断队列是否满了
*/
public boolean isFull () {
return N == M;
}
/**
* @description: 判断队列是否为空
*/
public boolean isEmpty () {
return N == 0;
}
/**
* @description: 返回队列元素总数
*/
public int size () {
return N;
}
循环数据方式
判断循环数组的满状态和空状态
在循环数组的实现中,一个非常重要的操作就是区分数组是处在"满"状态还是“空”状态,因为当front和rear指向同一个元素位置时,既可能处在满状态也可能处在空状态。上面的代码里我们是通过一个表示队列元素总数的变量N去判断的,除此之外,我们也可以通过另外一种不依赖于变量N的方式去判断数组的满和空的状态, 但代价是少用一个元素空间,例如:
public class CircleArrayQueue2<Item> {
private int M;
private int front = 0;
private int rear = 0;
private Item [] items;
public CircleArrayQueue2 (int M) {
this.M = M;
items = (Item [])new Object[M];
}
public void enqueue (Item item) {
if (isFull()) return;
items[rear] = item;
rear = (rear + 1) % M;
}
public Item dequeue () {
if (isEmpty()) return null;
Item item = items[front];
items[front] = null;
front = (front + 1) % M;
return item;
}
public boolean isFull () {
return (rear + 1) % M == front;
}
public boolean isEmpty () {
return rear == front;
}
public static void main (String args []) {
CircleArrayQueue2<Integer> queue = new CircleArrayQueue2<>(3);
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
}
}