目录
一、栈(Stack)
栈(Stack)是一种后进先出(Last In First Out, LIFO)的数据结构。在栈中,最后添加的元素最先被访问和移除。栈的操作包括压入(Push)元素,将元素添加到栈的顶部,和弹出(Pop)元素,从栈的顶部移除元素。栈的示例包括浏览器的后退按钮、函数调用的执行和撤销操作。
1.栈的常用方法
方法
|
功能
|
Stack()
|
构造一个空的栈
|
E push(E e)
|
将
e
入栈,并返回
e
|
E pop()
|
将栈顶元素出栈并返回
|
E peek()
|
获取栈顶元素
|
int size()
|
获取栈中有效元素个数
|
boolean empty()
| 检测栈是否为空 |
2.栈的使用
import java.util.*;
public class StackExample {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
// 入栈操作
stack.push(1);
stack.push(2);
stack.push(3);
// 查看栈顶元素
System.out.println("栈顶元素: " + stack.peek());
// 出栈操作
int item = stack.pop();
System.out.println("出栈元素: " + item);
// 判断栈是否为空
System.out.println("栈是否为空: " + stack.isEmpty());
}
}
运行结果:
栈顶元素: 3
出栈元素: 3
栈是否为空: false
3.栈的模拟实现
用数组实现(入栈、出栈时间复杂度为O(1)):
MyStack:
import java.util.Arrays;
public class MyStack {
public int[] elem;
public int usedSize;
public MyStack() {
this.elem = new int[10];
}
public void push(int val) {
if(isFull()) {
this.elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize++] = val;
}
public boolean isFull() {
return usedSize == elem.length;
}
public int pop() {
if(isEmpty()){
throw new EmptyStackException();
}
int val = elem[usedSize-1];
usedSize--;
return val;
}
//获取栈顶元素 但是不删除
public int peek() {
if(isEmpty()){
throw new EmptyStackException();
}
return elem[usedSize - 1];
}
public boolean isEmpty() {
return usedSize == 0;
}
}
EmptyStackException:
public class EmptyStackException extends RuntimeException{
public EmptyStackException(String message) {
super(message);
}
public EmptyStackException() {
}
}
Test:
public class Test {
public static void main(String[] args) {
MyStack stack = new MyStack();
stack.push(12);
stack.push(23);
stack.push(34);
stack.push(45);
stack.push(56);
System.out.println(stack.pop());
System.out.println(stack.peek());
System.out.println(stack.isEmpty());
}
}
运行结果:
56
45
false
若使用单链表实现:
采用尾插法入栈,入栈时间复杂度为O(n),如果有last,那么是O(1),但是出栈操作一定是O(n)。采用头插法入栈,入栈时间复杂度为O(1),同时出栈复杂度也是O(1)。
若使用双向链表实现:
不管是头插法还是尾插法,时间复杂度都是O(1)。
4.栈的应用场景
a.将递归转化为循环(以逆序打印链表为例)
递归方法:
void printList(Node head){
if(null != head){
printList(head.next);
System.out.print(head.val + " ");
}
}
循环方法:
void printList(Node head){
if(null == head){
return;
}
Stack<Node> s = new Stack<>();
// 将链表中的结点保存在栈中
Node cur = head;
while(null != cur){
s.push(cur);
cur = cur.next;
}
// 将栈中的元素出栈
while(!s.empty()){
System.out.print(s.pop().val + " ");
}
}
b.括号匹配 答题
思路:
- 先把左括号入栈(push),若栈为空,直接返回false;
- 再判断是否匹配(peek & pop);
- 若栈不为空,返回false。
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(int i = 0;i < s.length();i++) {
char ch1 = s.charAt(i);
if(ch1 == '(' || ch1 == '[' || ch1 == '{'){
stack.push(ch1);
}else{
if(stack.isEmpty()){
return false;
}
char ch2 = stack.peek();
if((ch2=='('&&ch1==')')||(ch2=='['&&ch1==']')||(ch2=='{'&&ch1=='}')){
stack.pop();
}else{
return false;
}
}
}
if(!stack.isEmpty()){
return false;
}
return true;
}
}
思路:创建一个栈,依次把数字放进去,遇到运算符时取出,先取出的作右运算数,将所得结果再放回栈中,直到栈为空为止。
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for(String str : tokens){
if(!isOperations(str)){
int num = Integer.parseInt(str);
stack.push(num);
}else{
int val2 = stack.pop();
int val1 = stack.pop();
switch(str){
case "+":
stack.push(val1+val2);
break;
case "-":
stack.push(val1-val2);
break;
case "*":
stack.push(val1*val2);
break;
case "/":
stack.push(val1/val2);
break;
}
}
}
return stack.pop();
}
private boolean isOperations(String ch){
if(ch.equals("+")||ch.equals("-")||ch.equals("*")||ch.equals("/")){
return true;
}
return false;
}
}
d.出栈入栈次序匹配 答题
思路:遍历pushV数组,每次入栈一个元素之后,用栈顶元素和popV的下标比较,若一样则出栈,若不一样,继续遍历,直到遍历完整个pushV数组。
public boolean IsPopOrder (int[] pushV, int[] popV) {
Stack<Integer> stack = new Stack<>();
int j = 0;
for(int i = 0;i<pushV.length;i++){
stack.push(pushV[i]);
while(!stack.isEmpty() && j<popV.length && stack.peek()==popV[j]){
stack.pop();
j++;
}
}
return stack.isEmpty();
}
e.最小栈 答题
class MinStack {
public Stack<Integer> stack;
public Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
stack.push(val);
if(minStack.isEmpty()){
minStack.push(val);
}else{
if(val <= minStack.peek()){
minStack.push(val);
}
}
}
public void pop() {
if(stack.empty()) {
return;
}
int popVal = stack.pop();
if(popVal == minStack.peek()) {
minStack.pop();
}
}
public int top() {
if(stack.isEmpty()){
return -1;
}
return stack.peek();
}
public int getMin() {
if(stack.isEmpty()){
return -1;
}
return minStack.peek();
}
}
二、队列(Queue)
队列是一种线性数据结构,它遵循先进先出(FIFO)的原则。在队列中,新元素被插入到队尾,而只有队首元素可以被删除。队列通常用于需要按顺序处理元素的场景,比如任务调度、请求处理等。队列的操作包括入队(将元素插入队尾)、出队(将队首元素删除并返回)、获取队首元素(返回队首元素但不删除)等。
1.队列的常用方法
方法
|
功能
|
boolean offer(E e)
|
入队列
|
E poll()
|
出队列
|
peek()
|
获取队头元素
|
int size()
|
获取队列中有效元素个数
|
boolean isEmpty()
| 检测队列是否为空 |
2.队列的使用
注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。
import java.util.LinkedList;
import java.util.Queue;
public class QueueExample {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
// 入队
queue.add("Alice");
queue.add("Bob");
queue.add("Charlie");
// 出队
String firstPerson = queue.remove();
System.out.println("出队的人:" + firstPerson);
// 获取队首元素
String peekPerson = queue.peek();
System.out.println("队首的人:" + peekPerson);
// 遍历队列
System.out.println("队列中的人:");
for (String person : queue) {
System.out.println(person);
}
}
}
运行结果:
出队的人:Alice
队首的人:Bob
队列中的人:
Bob
Charlie
3.队列的模拟实现
使用双向链表实现(普通数组无法实现):
MyQueue:
package demo;
public class MyQueue {
static class ListNode {
public ListNode prev;
public ListNode next;
public int val;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
public ListNode last;
public int size = 0;
public void offer(int e){
ListNode node = new ListNode(e);
if (head == null) {
head = last = node;
} else {
last.next = node;
node.prev = last;
last = node;
}
size++;
}
public int poll() {
if (head == null) {
return -1;
}
ListNode po = head;
head = head.next;
if (head != null) {
head.prev = null;
}
size--;
return po.val;
}
public int peek() {
if (head == null) {
return -1;
}
return head.val;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
Test:
package demo;
public class Test {
public static void main(String[] args) {
MyQueue queue = new MyQueue();
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue.poll());
System.out.println(queue.peek());
System.out.println(queue.size());
System.out.println(queue.isEmpty());
}
}
运行结果:
1
2
3
false
4.循环队列
循环队列是一种特殊的队列数据结构,它通过使用固定大小的数组来实现。在循环队列中,当队列的尾部达到数组的末尾时,如果队列的头部还有空闲位置,就可以将尾部元素循环移动到数组的起始位置,从而实现循环的效果。
循环队列有一些特点:
- 队列的容量是固定的,一旦定义后就无法改变。
- 队列有一个头指针和一个尾指针,分别指向队列的头部和尾部。
- 当队列为空时,头指针和尾指针都指向同一个位置。
- 当队列满时,头指针和尾指针指向的位置相邻。
循环队列的主要操作包括:
- 入队(enqueue):将元素添加到队列的尾部。
- 出队(dequeue):从队列的头部移除并返回元素。
- 判空(isEmpty):判断队列是否为空。
- 判满(isFull):判断队列是否已满。
- 获取队头元素(getFront):返回队列的头部元素。
设计循环队列:
class MyCircularQueue {
public int front;
public int rear;
public int[] arr;
public MyCircularQueue(int k) {
arr = new int[k + 1];
}
public boolean enQueue(int value) {
if(isFull()){
return false;
}
arr[rear] = value;
rear = (rear+1)%arr.length;
return true;
}
public boolean deQueue() {
if(isEmpty()){
return false;
}
front = (front+1)%arr.length;
return true;
}
public int Front() {
if(isEmpty()){
return -1;
}
return arr[front];
}
public int Rear() {
if(isEmpty()){
return -1;
}
int index = (rear == 0)?arr.length-1:rear-1;
return arr[index];
}
public boolean isEmpty() {
return front == rear;
}
public boolean isFull() {
return (rear+1)%arr.length == front;
}
}
5.双端队列
双端队列(Deque)是一种特殊的队列数据结构,它允许从队列两端进行插入和删除操作。双端队列可以在头部和尾部同时进行入队和出队操作,因此它可以被看作是一种兼具栈和队列性质的数据结构。
双端队列的实现可以使用数组或链表。在使用数组实现时,需要考虑数组的动态扩容和缩容问题;在使用链表实现时,可以直接在头部和尾部进行插入和删除操作。双端队列的选择主要取决于具体的应用场景和需求。
三、练习题
题目:用队列实现栈。 答题
思路:模拟入栈:将数据放入不为空的队列当中;
模拟出栈:把不为空的队列中的size-1个元素放到另一个队列当中,剩下的这一个就 是要出栈的元素。
class MyStack {
public Queue<Integer> qu1;
public Queue<Integer> qu2;
public MyStack() {
qu1 = new LinkedList<>();
qu2 = new LinkedList<>();
}
public void push(int x) {
if (!qu1.isEmpty()) {
qu1.offer(x);
} else if (!qu2.isEmpty()) {
qu2.offer(x);
} else {
qu1.offer(x);
}
}
public int pop() {
if (empty()) {
return -1;
}
if (!qu1.isEmpty()) {
int size = qu1.size();
for (int i = 0; i < size - 1; i++) {
qu2.offer(qu1.poll());
}
return qu1.poll();
} else {
int size = qu2.size();
for (int i = 0; i < size - 1; i++) {
qu1.offer(qu2.poll());
}
return qu2.poll();
}
}
public int top() {
if (empty()) {
return -1;
}
if (!qu1.isEmpty()) {
int size = qu1.size();
int val = 0;
for (int i = 0; i < size; i++) {
val = qu1.poll();
qu2.offer(val);
}
return val;
} else {
int size = qu2.size();
int val = 0;
for (int i = 0; i < size; i++) {
val = qu2.poll();
qu1.offer(val);
}
return val;
}
}
public boolean empty() {
return qu1.isEmpty() && qu2.isEmpty();
}
}
题目:用栈实现队列。 答题
思路:模拟入队:都放到第一个栈;
模拟出队:判断第二个栈是不是空的,若是,把第一个栈中所有元素都放到第二个栈 里,取出第二个栈的栈顶元素,否则,直接取出第二个栈的栈顶元素。
class MyQueue {
public ArrayDeque<Integer> stack1;
public ArrayDeque<Integer> stack2;
public MyQueue() {
stack1 = new ArrayDeque<>();
stack2 = new ArrayDeque<>();
}
public void push(int x) {
stack1.push(x);
}
public int pop() {
if(empty()) {
return -1;
}
if(stack2.isEmpty()) {
//第一个栈里面所有的元素 放到第二个栈当中
while(!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
public int peek() {
if(empty()) {
return -1;
}
if(stack2.isEmpty()) {
//第一个栈里面所有的元素 放到第二个栈当中
while(!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
链栈与顺序栈相比,比较明显的优点是( )
A.插入操作更加方便
B.删除操作更加方便
C.入栈时不需要扩容
A错误,如果是链栈,一般需要进行头插或者头删操作,而顺序栈一般进行尾插和尾删操作,链表的操作比顺序表复杂,因此使用顺序结构实现栈更简单
B错误,原因参考A
C正确,链式结构实现栈时,每次入栈相当于链表中头插一个节点,没有扩容一说
下列关于栈的叙述中,正确的是()
A.栈底元素一定是最后入栈的元素
B.栈顶元素一定是最先入栈的元素
C.栈操作遵循先进后出的原则
D.以上说法均错误
栈是先进后出,队列先进先出,因此:
A错误:栈底元素是最先入栈的
B错误:栈顶元素一定是最后入栈的
C正确,栈的特性后进先出或者先进后出
D错误:C说法明显是正确的
故选择C。
用无头单链表存储队列,front引用队头,back引用队尾,则在进行出队列操作时( )
A.仅修改front
B.front 和 back 都要修改
C.front 和 back 可能都要修改
D.仅修改back
出队列时:
- 如果队列中有多个节点时,只需要修改front
- 如果队列中只有一个节点时,front和back都需要修改
故应该选择C。