数据结构与算法(五)栈、队列
栈(Stack)
栈是一种特殊的线性表,只能在一端进行操作
往栈中添加元素的操作,一般叫做push
,入栈
从栈中移除元素的操作,一般叫做pop
,出栈(只能移除栈顶元素,也叫做弹出栈顶元素)
后进先出的原则,Last In First Out,LIFO
这里说的栈和内存中的栈空间
是两个不同的概念
栈的接口设计
下面我们就来自己设计一个栈的接口
栈的结构我们可以使用之前学过的动态数组和链表来进一步实现
首先,我们需要确定对外的接口有哪些?
完整的设计代码如下
import com.company.list.ArrayList;
import com.company.list.List;
public class Stack<E> {
private List<E> list = new ArrayList<>();
public void clear() {
list.clear();
}
public int size() {
return list.size();
}
public boolean isEmpty() {
return list.isEmpty();
}
public void push(E element) {
list.add(element);
}
public E pop() {
return list.remove(list.size() - 1);
}
public E top() {
return list.get(list.size() - 1);
}
}
设计点的详细讲解:
1.我们可以将动态数组或者链表作为成员变量来在内部调用
对应要引入其相关父类已经声明接口,这里就不再重复粘贴代码了
public class Stack<E> {
private List<E> list = new ArrayList<>();
// private List<E> list = new LinkedList<>();
....
}
2.push操作
就是在list
最后添加元素,所以选择动态数组或者链表的复杂度都是一样的
public void push(E element) {
list.add(element);
}
3.pop操作
就是移除list
最后的元素
public E pop() {
return list.remove(list.size() - 1);
}
4.top操作
就是获取栈顶的元素,也就是获取list
最后的元素
public E top() {
return list.get(list.size() - 1);
}
栈的应用场景
浏览器的前进和后退
软件的撤销和恢复功能
练习题
1.给定一段字符串,判断有效的括号
题目概述
题目链接:
https://leetcode-cn.com/problems/valid-parentheses/
题解:
第一种方式
可以循环遍历字符串,只有有成对的符号就置为空字符,循环结束后查看字符串是否为空,如果不为空证明有无效的字符串
这种方式最简单,但是效率很低,循环遍历不说,还会一直创建新的字符串来分配,不建议
public class Solution {
public boolean isValid(String s) {
while (s.contains("{}")
|| s.contains("[]")
|| s.contains("()")) {
s = s.replace("{}", "");
s = s.replace("()", "");
s = s.replace("[]", "");
}
return s.isEmpty();
}
}
第二种方式
1.利用栈的特性,将左字符压入栈
2.然后遇到右字符,如果栈是空的,说明括号无效;如果栈不为空,则让左字符出栈进行对比
如果左右字符不匹配,说明括号无效;如果匹配继续循环下一个字符
3.所有字符遍历完毕后,如果栈为空,说明括号有效;如果栈不为空,说明括号无效
public class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i); // 获取某一个位置的字符
if (c == '(' || c == '{' || c == '[') { // 左括号
stack.push(c);
} else { // 右括号
if (stack.isEmpty()) return false;
char left = stack.pop();
if (left == '(' && c != ')') return false;
if (left == '{' && c != '}') return false;
if (left == '[' && c != ']') return false;
}
}
return stack.isEmpty();
}
}
第三种方式
在第二种方式的基础上,利用HashMap
提前先将括号以key-value
的形式存储,然后遍历时都从HashMap
中取出value
来做对比
public class Solution {
private static HashMap<Character, Character> map = new HashMap<>();
static {
// key - value
map.put('(', ')');
map.put('{', '}');
map.put('[', ']');
}
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (map.containsKey(c)) { // 左括号
stack.push(c);
} else { // 右括号
if (stack.isEmpty()) return false;
if (c != map.get(stack.pop())) return false;
}
}
return stack.isEmpty();
}
}
队列(Queue)
单端队列
队列是一种特殊的线性表,只能在头尾两端
进行操作
队尾(rear):只能从队尾添加元素,一般叫做enQueue
,入队
队头(front):只能从队头移除元素,一般叫做deQueue
,出队
先进先出原则,Frist In Frist Out,FIFO
队列的接口设计
下面我们就来自己设计一个队列的接口
队列的结构我们也可以使用之前学的动态数组和链表来进一步实现
首先,我们需要确定对外的接口有哪些?
完整的设计代码如下
import com.company.list.LinkedList;
import com.company.list.List;
public class Queue<E> {
private List<E> list = new LinkedList<>();
public int size() {
return list.size();
}
public boolean isEmpty() {
return list.isEmpty();
}
public void clear() {
list.clear();
}
// 入队
public void enQueue(E element) {
list.add(element);
}
// 出队
public E deQueue() {
return list.remove(0);
}
// 获取队头元素
public E front() {
return list.get(0);
}
}
设计点的详细讲解:
1.队列主要是往头尾操作元素,所以优先使用双向链表
public class Queue<E> {
private List<E> list = new LinkedList<>();
....
}
2.入队操作,就是在链表尾节点增加元素
public void enQueue(E element) {
list.add(element);
}
3.出队操作,就是删除链表首节点的元素
public E deQueue() {
return list.remove(0);
}
4.获取队头元素就是获取链表首节点的元素
public E front() {
return list.get(0);
}
双端队列(Deque)
双端队列是能在头尾两端添加、删除
的队列
英文deque
是double ended queue
的意思
双端队列的接口设计
下面我们就来自己设计一个双端队列的接口
双端队列的结构也是在链表的基础上来实现的
对外的接口有以下这些
完整的设计代码如下
import com.company.list.LinkedList;
import com.company.list.List;
public class Deque<E> {
private List<E> list = new LinkedList<>();
public int size() {
return list.size();
}
public boolean isEmpty() {
return list.isEmpty();
}
public void clear() {
list.clear();
}
// 从队尾入队
public void enQueueRear(E element) {
list.add(element);
}
// 从队头出队
public E deQueueFront() {
return list.remove(0);
}
// 从队头入队
public void enQueueFront(E element) {
list.add(0, element);
}
// 从队尾出队
public E deQueueRear() {
return list.remove(list.size() - 1);
}
// 获取队列的头元素
public E front() {
return list.get(0);
}
// 获取队列的尾元素
public E rear() {
return list.get(list.size() - 1);
}
}
设计点的详细讲解:
其实同队列相似,更多注意的是对于链表头尾节点入队出队时的操作
循环队列(Circle Queue)
其实队列底层也可以使用动态数组
实现,并且各项接口也可以优化到0(1)
的时间复杂度
这个用数组实现并且优化之后的队列也叫做循环队列
循环队列的接口设计
下面我们就来自己设计一个循环队列的接口
完整的设计代码如下
@SuppressWarnings("unchecked")
public class CircleQueue<E> {
private int front; // 用来记录队头在哪里
private int size;
private E[] elements;
private static final int DEFAULT_CAPACITY = 10; // 默认容量
public CircleQueue() {
elements = (E[]) new Object[DEFAULT_CAPACITY];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void clear() {
for (int i = 0; i < size; i++) {
elements[index(i)] = null;
}
front = 0;
size = 0;
}
// 入队
public void enQueue(E element) {
ensureCapacity(size + 1);
elements[index(size)] = element;
size++;
}
// 出队
public E deQueue() {
E frontElement = elements[front];
elements[front] = null;
front = index(1);
size--;
return frontElement;
}
public E front() {
return elements[front];
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("capcacity=").append(elements.length)
.append(" size=").append(size)
.append(" front=").append(front)
.append(", [");
for (int i = 0; i < elements.length; i++) {
if (i != 0) {
string.append(", ");
}
string.append(elements[i]);
}
string.append("]");
return string.toString();
}
// 索引映射
private int index(int index) {
index += front;
return index - (index >= elements.length ? elements.length : 0);
}
/**
* 保证要有capacity的容量
* @param capacity
*/
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
if (oldCapacity >= capacity) return;
// 新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[index(i)];
}
elements = newElements;
// 重置front
front = 0;
}
}
设计点的详细讲解:
1.增加一个成员变量front
来记录队头的位置
public class CircleQueue<E> {
private int front;
....
}
2.封装索引映射函数,传进来的对外索引值会转换成数组的真实索引
索引映射的原因:
-
入队操作是在队尾增加元素,而在数组中对外的索引可能是处于数组的最后一个位置,那么增加元素就需要往数组头部插入,所以要做好对外索引和真实索引的转换
-
反之出队操作的索引位置也可能是在数组的起始位置,那么删除元素的位置就是
front
的上一个元素位置,也就是数组的最后的位置
一开始采用的计算公式如下:
private int index(int index) {
return (index + front) % elements.length
}
由于乘*、除/、模%、浮点数
运算,CPU都会执行更多操作来实现运算,所以效率会低
对于模运算也有一些规律可循:
已知n >= 0, m > 0
n % m 等价于 n - (m > n ? 0 : m)
能实现规律的前提是 n < 2m
然后我们将映射函数做了优化:
而且index + front
所得的值永远都是小于elements.length的2倍的
,那么模运算的规律就可以使用
private int index(int index) {
index += front;
return index - (index >= elements.length ? elements.length : 0);
}
3.入队操作时先判断是否需要扩容,如果需要扩容,就建一个新的数组,然后将元素按照真实的索引对应添加到新数组中,这时数组的对外索引和真实索引是一致的
扩容操作时,记录队头的front
字段要重置为0
入队操作就是通过对外索引找到真实索引在数组中添加元素,front
指向新的队头元素
public void enQueue(E element) {
ensureCapacity(size + 1);
elements[index(size)] = element;
size++;
}
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
if (oldCapacity >= capacity) return;
// 新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[index(i)];
}
elements = newElements;
// 重置front
front = 0;
}
4.出队操作就是通过front
找到数组的真实索引位置,置空元素
public E deQueue() {
E frontElement = elements[front];
elements[front] = null;
front = index(1);
size--;
return frontElement;
}
5.清空队列时,需要遍历所有数组元素,然后找到其真实索引的元素置空
front
也要归零
public void clear() {
for (int i = 0; i < size; i++) {
elements[index(i)] = null;
}
front = 0;
size = 0;
}
循环双端队列(Circle Deque)
循环双端队列是可以进行两端添加、删除操作的循环队列
循环双端队列的接口设计
下面我们就来自己设计一个循环双端队列的接口
循环双端队列是在循环队列的基础上增加了从头部入队和从尾部出队两个函数
@SuppressWarnings("unchecked")
public class CircleDeque<E> {
private int front; // 用来记录队头在哪里
private int size;
private E[] elements;
private static final int DEFAULT_CAPACITY = 10;
public CircleDeque() {
elements = (E[]) new Object[DEFAULT_CAPACITY];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void clear() {
for (int i = 0; i < size; i++) {
elements[index(i)] = null;
}
front = 0;
size = 0;
}
/**
* 从尾部入队
* @param element
*/
public void enQueueRear(E element) {
ensureCapacity(size + 1);
elements[index(size)] = element;
size++;
}
/**
* 从头部出队
* @param element
*/
public E deQueueFront() {
E frontElement = elements[front];
elements[front] = null;
front = index(1);
size--;
return frontElement;
}
/**
* 从头部入队
* @param element
*/
public void enQueueFront(E element) {
ensureCapacity(size + 1);
front = index(-1);
elements[front] = element;
size++;
}
/**
* 从尾部出队
* @param element
*/
public E deQueueRear() {
int rearIndex = index(size - 1);
E rear = elements[rearIndex];
elements[rearIndex] = null;
size--;
return rear;
}
public E front() {
return elements[front];
}
public E rear() {
return elements[index(size - 1)];
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("capcacity=").append(elements.length)
.append(" size=").append(size)
.append(" front=").append(front)
.append(", [");
for (int i = 0; i < elements.length; i++) {
if (i != 0) {
string.append(", ");
}
string.append(elements[i]);
}
string.append("]");
return string.toString();
}
private int index(int index) {
index += front;
if (index < 0) { // 如果计算小于0,说明真实的首元素索引也是0,需要往数组最后一位插入
return index + elements.length;
}
return index - (index >= elements.length ? elements.length : 0);
}
/**
* 保证要有capacity的容量
* @param capacity
*/
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
if (oldCapacity >= capacity) return;
// 新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[index(i)];
}
elements = newElements;
// 重置front
front = 0;
}
}
设计点的详细讲解:
1.从头部入队操作,也要先考虑是否需要扩容
插入元素的位置也就是队头的上一个,所以找到真实的索引位置添加元素,并且更新front
的值
public void enQueueFront(E element) {
ensureCapacity(size + 1);
front = index(-1);
elements[front] = element;
size++;
}
2.从尾部出队操作,通过索引映射找到其真实的索引位置,然后将该元素置空
public E deQueueRear() {
int rearIndex = index(size - 1);
E rear = elements[rearIndex];
elements[rearIndex] = null;
size--;
return rear;
}
练习题
1.用栈实现队列
题目概述
题目链接:
https://leetcode-cn.com/problems/implement-queue-using-stacks/
题解:
1.准备两个栈:inStack、outStack
2.入队时,push
到inStack
中
3.出队时,如果outStack
为空,将inStack
的元素全部逐一出栈,push
到outStack,outStack
弹出栈顶元素
如果outStack
不为空,弹出栈顶元素
import java.util.Stack;
public class MyQueue {
private Stack<Integer> inStack;
private Stack<Integer> outStack;
/** Initialize your data structure here. */
public MyQueue() {
inStack = new Stack<>();
outStack = new Stack<>();
}
/** 入队 */
public void push(int x) {
inStack.push(x);
}
/** 出队 */
public int pop() {
checkOutStack();
return outStack.pop();
}
/** 获取队头元素 */
public int peek() {
checkOutStack();
// 获取队头的顺序是从outStack中取出的
return outStack.peek();
}
/** 是否为空 */
public boolean empty() {
// 两个栈里都没有元素了才能叫空
return inStack.isEmpty() && outStack.isEmpty();
}
private void checkOutStack() {
// 如果outStack为空,就把inStack的元素都压入栈
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
}
}