栈
概念:栈是一种特殊的线性表,其只允许在固定的一段进行插入和删除元素操作。进行数据插入和删除的一端称为栈顶,另一端称为栈底。栈中的数据元素遵循后进先出的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫出栈。出数据在栈顶。
栈可以通过数组或链表实现。使用数组实现的栈称为顺序栈,使用链表实现的栈称为链式栈。在实现栈时,需要注意栈的大小,防止栈溢出的问题。
栈的使用
在Java中,可以使用内置的数据结构java.util.Stack
来实现栈。
以下是使用Stack
类的一些常见操作:
(1)创建栈对象:
Stack<Integer> stack = new Stack<>();
(2)压入元素到栈中:
stack.push(5);
stack.push(10);
stack.push(15);
(3)弹出栈顶元素:
int top = stack.pop(); // 返回并删除栈顶元素
(4)获取栈顶元素但不删除:
int top = stack.peek(); // 返回栈顶元素
(5)判断栈是否为空:
boolean isEmpty = stack.isEmpty();
(6)获取栈的大小:
int size = stack.size();
(7)遍历栈中的元素:
for (Integer element : stack) {
System.out.println(element);
}
需要注意的是,Stack
类是一个遗留的类,推荐使用Deque
接口的实现类LinkedList
来代替它。使用LinkedList
实现的栈可以实现相同的功能,同时还具备其他更多的操作和灵活性。
以下是使用LinkedList
来实现栈的示例:
Deque<Integer> stack = new LinkedList<>();
stack.push(5);
stack.push(10);
stack.push(15);
int top = stack.pop();
int size = stack.size();
for (Integer element : stack) {
System.out.println(element);
}
栈的模拟实现
public class MyStack {
public Object[] elem;
public int usedSize;
public MyStack2() {
this.elem = new Object[10];
}
public void push(E val) {
if(isFull()) {
//扩容
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
usedSize++;
}
public boolean isFull() {
return usedSize == elem.length;
}
public E pop() {
if(empty()) {
return null;
}
E oldVal = (E)elem[usedSize-1];
usedSize--;
return oldVal;
}
public E peek() {
if(empty()) {
return null;
}
//int oldVal = elem[usedSize-1];
//usedSize--;
//return oldVal;
return (E)elem[usedSize-1];
}
public boolean empty() {
return usedSize == 0;
}
}
栈是一种常见的数据结构,在许多应用场景中都有广泛的应用。
以下是一些常见的栈的应用场景:
-
表达式求值:在编程语言中,栈常常用于计算表达式的值。例如,将中缀表达式转换为后缀表达式,并使用栈来计算后缀表达式的值。
-
括号匹配:栈可以用于检查表达式中的括号是否匹配。通过遍历表达式中的字符,将左括号压入栈中,当遇到右括号时,弹出栈顶元素并检查是否为对应的左括号。
-
函数调用:在函数调用中,栈用于存储函数的局部变量、参数和返回地址。每次调用函数时,将相关信息压入栈中,在函数返回时再弹出。
-
浏览器的后退与前进:浏览器的后退和前进功能可以通过栈实现。每次浏览一个页面时,将该页面的 URL 压入栈中,当用户点击后退或前进时,从栈中弹出相应的 URL 进行导航。
-
撤销操作:在许多应用程序中,栈用于实现撤销操作。每次用户进行操作时,将相关的操作记录压入栈中,当用户撤销时,从栈中弹出最近的操作并恢复到上一个状态。
-
浏览器的历史记录:浏览器的历史记录可以使用栈来实现。每次访问一个页面时,将该页面的 URL 压入栈中,当用户查看历史记录时,依次弹出栈中的 URL 进行展示。
-
递归算法:一些算法问题可以通过递归方式解决,而递归中常常使用栈来保存递归的状态。
这些仅是栈的一些应用场景,实际上栈在计算机科学中还有许多其他的应用。栈的特性使其非常适合处理需要"后进先出"(LIFO)的数据操作。
队列
概念:队列是只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出(FIFO)的特性。
入队列:进行插入操作的一端称为队尾。
出队列:进行删除操作的一端称为队头。
队列的使用
在Java中,队列的使用可以通过Java集合框架中提供的Queue接口及其实现类来实现。Queue接口继承自java.util.Collection接口,并提供了一系列用于操作队列的方法。
以下是一些Java队列的常见使用方法:
-
创建队列对象:
Queue<String> queue = new LinkedList<>();
-
入队操作(添加元素到队尾):
queue.offer("element"); // 返回true或false,表示添加成功与否 queue.add("element"); // 添加元素到队尾,添加成功时返回true,否则抛出异常
-
出队操作(移除并返回队头元素):
String element = queue.poll(); // 返回队头元素,如果队列为空则返回null String element = queue.remove(); // 移除并返回队头元素,如果队列为空则抛出异常
-
获取队头元素(不移除):
String element = queue.peek(); // 返回队头元素,如果队列为空则返回null String element = queue.element(); // 返回队头元素,如果队列为空则抛出异常
-
判断队列是否为空:
boolean isEmpty = queue.isEmpty(); // 返回true或false
队列可以根据实际的需求选择不同的实现类,常见的实现类包括LinkedList、ArrayDeque。在选择实现类时,可以考虑队列的使用场景和性能需求。
需要注意的是,Java中的队列接口并没有定义固定容量的限制,所以队列可以按需动态增长。若需要限制队列的容量,可以在使用LinkedList实现队列时,结合队列的大小进行判断和控制。
队列的模拟实现
由于队列是先进先出的特点,所以在模拟队列时,使用链式结构更佳。
public class MyQueue {
static class ListNode {
public int val;
public ListNode prev ;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
public ListNode last;
public void offer(int val) {
ListNode node = new ListNode(val);
if(head == null) {
head = last = node;
}else {
last.next = node;
node.prev = last;
last = last.next;
}
}
public int poll() {
if(head == null) {
return -1;
}
int ret = head.val;
if(head.next == null) {
head = null;
last = null;
}else {
head = head.next;
head.prev = null;
}
return ret;
}
public int peek() {
if(head == null) {
return -1;
}
return head.val;
}
public boolean isEmpty() {
return head == null;
}
}
队列在计算机科学和软件开发中有广泛的应用场景,以下是一些常见的应用场景:
-
消息队列:用于异步处理系统之间的通信,可以实现解耦和削峰填谷的效果,常见的消息队列包括RabbitMQ、ActiveMQ、Kafka等。
-
多线程任务调度:多线程环境下,可以使用队列来实现任务的调度和同步,例如线程池中的任务调度。
-
数据缓冲:在数据传输或处理中,队列可以作为缓冲区,临时存储数据,以平衡生产者和消费者之间的速度差异。
-
广度优先搜索(BFS):在图的遍历中,使用队列来实现广度优先搜索,即逐层遍历图的节点。
-
任务排队系统:用于处理任务的等待和执行顺序,例如批处理系统或作业调度中的任务队列。
-
网络请求处理:在服务器端处理网络请求时,可以使用队列来管理请求的顺序,以确保公平处理和资源的合理利用。
-
系统调用处理:操作系统内核中的系统调用请求可以使用队列来管理和调度。
需要根据具体的应用场景选择合适的队列数据结构和算法,以满足需求。不同的队列实现方式可能在性能、并发性、持久化等方面有所差异,需根据具体情况进行选择。