目录
引言
线性表:一次保存单个同类型元素,多个元素之间逻辑上连续数组,链表,栈,队列,字符串(内部是char[])。
栈和队列其实是操作受限的线性表。
之前的数组、链表,既可以在头部插入和删除,也能在尾部插入和删除,甚至在任意位置都可以插入和删除。而"栈和队列"只能在一端插入元素和删除元素。
栈
概念
只能从一端插入元素,也只能从这一端取出元素(栈顶)。
栈的特点:先进后出,后进先出的线性表。添加元素和删除元素的一端称为栈顶,另—端称为栈底。
栈在现实生活中的应用(无处不在的栈的应用)
1. 无处不在的undo(撤销)操作。
a. 在任何一个编辑器中,输错了一个内容,使用ctrl +z就返回到了上一次输入的内容。b. 在任何一个浏览器点击 <- 就能返回上—次浏览的网页。
都是栈这个结构的应用。
撤销ctrl +z操作,相当于从栈顶取出这个错误的元素,然后文本编辑器就会取出当前的栈顶元素。
2. 操作系统栈:程序在执行过程中,从A函数调用B函数,从B函数调用C函数,返回执行时如何得知从哪开始继续执行呢,其实背后就是栈这个结构。
栈的实现
栈:只能在栈顶插入元素,在栈顶删除元素的结构。
基于数组实现的栈——顺序栈。
栈的三个核心操作:
push(E e):向栈中添加元素,即入栈(压栈)。
E pop():出栈操作,弹出栈顶元素。
E peek():查看栈顶元素,但不出栈。package seqlist.stack; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; /** * 基于数组实现的顺序栈 */ public class MyStack<E> { // 记录当前栈的元素个数 private int size; // 实际存储数据的动态数组,此时在栈中只能在数组末尾添加和删除元素 private List<E> data = new ArrayList<>(); /** * 向当前栈中添加元素,压栈操作 */ public void push(E val) { //在数组尾部插入元素 data.add(val); size++; } /** * 弹出当前的栈顶元素,返回栈顶元素的值 */ public E pop() { if (data.isEmpty()) { throw new NoSuchElementException("stack is empty!cannot pop!"); } //在数组的末尾删除元素 E val = data.remove(size - 1); size--; return val; } /** * 查看当前栈顶元素值,但是不弹出 */ public E peek() { if (data.isEmpty()) { throw new NoSuchElementException("stack is empty!cannot pop!"); } return data.get(size - 1); } public String toString() { StringBuilder sb = new StringBuilder(); sb.append("["); for (int i = 0;i < size;i++) { sb.append(data.get(i)); if (i != size-1) { //还没到数组末尾 sb.append(", "); } } sb.append("] top"); return sb.toString(); } private boolean isEmpty() { return size == 0; } }
package seqlist.stack; import java.util.ArrayList; public class StackTest { public static void main(String[] args) { MyStack<Integer> stack = new MyStack<>(); stack.push(1); stack.push(2); stack.push(3); System.out.println(stack); System.out.println(stack.pop()); System.out.println(stack); } }
队列
概念
栈和队列是一码事,都是对只能在线性表的一端进行插入和删除。因此,栈和队列可以相互转换。
队列:FIFO,先进先出的数据结构,元素从"队尾"添加到队列中,元素从"队首"出队列。元素的出队顺序和入队顺序保持一致。
现实生活中,有各式各样的"排队"操作。
同样的,队列也有基于数组实现的队列和基于链表实现的队列。出队操作只能在队列的头部进行。若采用数组的方案,每次出队一个元素就得搬移剩下的所有元素向前移动一个单位。此时采用链表的方案更加适合队列的结构。
出队列:删除头节点,添加元素:在链表的尾部添加。
队列接口
package seqlist.queue; /** * 相对于栈来说,队列实现的子类比较多 * 普通的FIFO队列、双端队列Deque、循环队列LoopQueue * 优先级队列PriorityQueue */ public interface Queue<E> { //入队操作 void offer(E val); //出队操作 E poll(); //查看队首元素 E peek(); boolean isEmpty(); }
普通LFLO队列
package seqlist.queue.impl; import seqlist.queue.Queue; import java.util.NoSuchElementException; /** * 基于链表实现的普通队列 */ class Node<E> { E val; Node next; public Node(E val) { this.val = val; } } public class LinkedQueue<E> implements Queue<E> { //当前元素个数 int size; //当前队列的队首元素 private Node<E> head; //当前队列的队尾元素 private Node<E> tail; @Override public void offer(E val) { //产生一个新节点,尾插 Node<E> node = new Node<>(val); if (head == null) { head = tail = node; } else { tail.next = node; tail = node; } size++; } @Override public E poll() { if (isEmpty()) { throw new NoSuchElementException("queue is empty!cannot poll!"); } //删除队首元素,即head E val = head.val; Node node = head; head = head.next; node.next = node = null; size--; return val; } @Override public E peek() { if (isEmpty()) { throw new NoSuchElementException("queue is empty!cannot poll!"); } return head.val; } @Override public boolean isEmpty() { return size == 0; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("front ["); for (Node x = head; x != null; x = x.next) { sb.append(x); if (x.next!=null) { sb.append(", "); } } sb.append("] tail"); return sb.toString(); } }
public static void main(String[] args) { Queue<Integer> queue = new LinkedQueue<>(); queue.offer(1); queue.offer(3); queue.offer(5); queue.offer(7); System.out.println(queue); //front [1, 3, 5, 7] tail queue.poll(); System.out.println(queue); //front [3, 5, 7] tail }
JDK内置的队列实现
java.util.Queue;
java.util.LinkedList;
public static void main(String[] args) { //JDK的Queue实现,子类LinkedQUEUE Queue<Integer> queue = new LinkedList<>(); queue.offer(1); queue.offer(3); queue.offer(5); System.out.println(queue); queue.poll(); System.out.println(queue); }
循环队列
概念
循环队列:基本都是使用长度固定的数组来实现。数组在实现队列时,若从数组头部删除元素需要频繁的移动后面的元素,带来效率比较低。
出队和入队操作,使用两个引用:一个head,一个tail。添加元素在数组尾部添加,删除元素只需要移动head引用指向的地址即可(逻辑删除)。
循环队列就是使用长度固定的数组来实现,数组头部就是队首(head),数组的尾部就是队尾(tail)。数组[head,tail)是循环队列的有效元素。应用场景:操作系统的生产消费者模型,MySQL数据库的InnoDB存储引擎中的redo日志。
head永远指向循环队列的第一个元素,tail永远指向循环队列有效元素的后一个位置。
所谓的循环队列指的就是当head或者tail引用走到数组末尾时,下一次再继续向后移动,其实返回数组的头部继续操作。
循环队列在删除元素时,不需要进行数据的搬移。循环队列出队一个元素,当有新的元素在添加时就会覆盖掉之前的元素。
我们此时数组为空和数组满,都会使head == tail,因此没法区分当前循环队列到底是空还是满。
修改方案:
1. 在循环队列中浪费一个空间,判断队列是否已满。若此时(tail + 1) % n == head; 认为此时循环队列已满。
2. head和tail的移动不能简单的+1,应该使用取模操作,%数组长度n。
tail = (tail + 1) % n;
对数组长度取模的本质:
head和tail走到数组最后—个索引位置时,下一次要返回数组头部,就必须使用+1对n取模。3. 若head == tail; 认为队列为空。
代码实现
package seqlist.queue.impl; import seqlist.queue.Queue; import java.util.NoSuchElementException; /** * 基于整型的循环队列 */ public class LoopQueue implements Queue<Integer> { // 定长数组 private Integer[] data; // 指向队首元素 private int head; // 指向队尾元素的下一个索引 private int tail; public LoopQueue(int size) { // 因为循环队列中要浪费一个空间判断是否已满 data = new Integer[size + 1]; } @Override public void offer(Integer val) { if (isFull()) { throw new ArrayIndexOutOfBoundsException("loopQueue is full!cannot offer"); } data[tail] = val; tail = (tail + 1) % data.length; } @Override public Integer poll() { if (isEmpty()) { throw new NoSuchElementException("loopQueue is empty!cannot pool"); } //移动队首元素 Integer val = data[head]; //移动 +1再&n head = (head + 1) % data.length; return val; } @Override public Integer peek() { if (isEmpty()) { throw new NoSuchElementException("loopQueue is empty!cannot peek"); } return data[head]; } @Override public boolean isEmpty() { return head == tail; } public boolean isFull() { return (tail + 1) % data.length == head; } public String toString() { StringBuilder sb = new StringBuilder(); sb.append("front ["); //最后一个有效元素的索引 int lastIndex = tail == 0 ? data.length - 1 : tail - 1; for (int i = head; i != tail; ) { sb.append(data[i]); //最后一个有效元素的索引是什么? if (i != lastIndex) { sb.append(", "); } i = (i + 1) % data.length; } sb.append("] tail"); return sb.toString(); } }
测试
package seqlist.queue.impl; import seqlist.queue.Queue; public class LoopQueueTest { public static void main(String[] args) { Queue<Integer> queue = new LoopQueue(4); queue.offer(1); queue.offer(2); queue.offer(3); queue.offer(4); System.out.println(queue); queue.poll(); System.out.println(queue); } }
数组越界情况
没有找到元素情况
双端队列
双端队列:Deque——>Queue的子接口。
这个队列,既可以从尾插,头出也可以从头插,尾出。
package seqlist.queue.impl; import java.util.ArrayList; import java.util.LinkedList; /** * 双端队列 */ public class DequeTest { public static void main(String[] args) { Deque<Integer> stack = new LinkedList<>(); stack.push(1); stack.push(2); stack.push(3); System.out.println(stack); System.out.println(stack.pop()); System.out.println(stack); System.out.println(stack.peek()); } }
package seqlist.queue.impl; import java.util.ArrayList; import java.util.LinkedList; /** * 双端队列 */ public class DequeTest { public static void main(String[] args) { Deque<Integer> queue = new ArrayDeque<>(); queue.offer(1); queue.offer(2); queue.offer(3); System.out.println(queue); System.out.println(queue.poll()); System.out.println(queue); System.out.println(queue.peek()); } }
以后无论使用的是栈还是接口,统—使用双端队列接口。
不推荐使用Stack这个类,其效率很低,都是同步操作。
双端队列的一个常用子类就是LinkedList。