目录
1. Java中的栈(Stack)
• 概念
• Java中的实现(Stack类)
• 主要方法:
• 自定义栈的实现(基于数组)
(1)实现思路:
(2)示例代码:
• 应用场景
2. Java中的栈(Stack)底层实现原理
• 基于数组实现的栈(顺序栈)
(1)存储结构:
(2)入栈操作(push):
(3) 出栈操作(pop):
(4)查看栈顶元素(peek):
• 基于链表实现的栈(链栈)
(1)存储结构:
(2)入栈操作(push):
(3)出栈操作(pop):
(4) 查看栈顶元素(peek):
3. Java中的队列(Queue)
• 概念
• Java中的实现(Queue接口及其实现类)
• 主要方法(对于一般队列):
• LinkedList作为队列:
• PriorityQueue(优先队列)
• 应用场景
4. Java中的队列(Queue)底层实现原理
• 基于数组实现的队列(顺序队列) - 简单循环队列示例
(1)存储结构:
(2)入队操作(enqueue):
(3) 出队操作(dequeue):
• 基于链表实现的队列(链队)
(1)存储结构:
(2) 入队操作(enqueue):
(3) 出队操作(dequeue):
5.PriorityQueue(优先队列)底层实现原理
• 基于堆结构:
• 元素存储与排序:
1. Java中的栈(Stack)
• 概念
栈是一种遵循后进先出(LIFO - Last In First Out)原则的数据结构。可以把栈想象成一摞盘子,最后放上去的盘子最先被拿走。
• Java中的实现(Stack类)
继承关系:在Java中,java.util.Stack类继承自java.util.Vector类。虽然Stack类可以直接使用,但由于它继承自Vector,这种设计存在一些不符合现代设计理念的地方(例如,Vector是一个线程安全的动态数组,而栈的操作不需要这么复杂的底层结构以及这种线程安全机制带来的性能开销在不需要线程安全的场景下是多余的)。
• 主要方法:
1.push(E item):将元素item压入栈顶。例如:
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
2.pop():移除并返回栈顶元素。如果栈为空,会抛出EmptyStackException异常。例如:
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
int topElement = stack.pop();// 返回2
3.peek():返回栈顶元素,但不移除它。如果栈为空,会抛出EmptyStackException异常。例如:
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
int top = stack.peek();// 返回2,栈不变
4.empty():检查栈是否为空,为空返回true,否则返回false。例如:
Stack<Integer> stack = new Stack<>();
boolean isEmpty = stack.empty();// true
stack.push(1);
isEmpty = stack.empty();// false
• 自定义栈的实现(基于数组)
(1)实现思路:
使用数组来存储栈中的元素,定义一个变量来表示栈顶的位置。
(2)示例代码:
class MyStack<E> {
private Object[] elements;
private int top;
private static final int DEFAULT_INITIAL_CAPACITY = 10;
public MyStack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
top = -1;
}
public void push(E item) {
if (top == elements.length - 1) {
// 栈满,扩容
Object[] newElements = new Object[elements.length * 2];
System.arraycopy(elements, 0, newElements, 0, elements.length);
elements = newElements;
}
top++;
elements[top] = item;
}
@SuppressWarnings("unchecked")
public E pop() {
if (top == -1) {
throw new RuntimeException("Stack is empty");
}
E item = (E) elements[top];
top--;
return item;
}
@SuppressWarnings("unchecked")
public E peek() {
if (top == -1) {
throw new RuntimeException("Stack is empty");
}
return (E) elements[top];
}
public boolean isEmpty() {
return top == -1;
}
}
• 应用场景
(a)表达式求值:例如在计算中缀表达式的值时,可以利用栈来存储操作数和运算符。对于表达式“3 + 4 * 2”,可以使用栈来按照运算符的优先级处理操作数和运算符。
(b)函数调用栈:在程序执行函数调用时,系统会使用栈来存储函数的局部变量、参数以及返回地址等信息。当一个函数调用另一个函数时,当前函数的执行状态就被压入栈中,当被调用函数执行完毕后,再从栈中弹出恢复执行。
2. Java中的栈(Stack)底层实现原理
• 基于数组实现的栈(顺序栈)
(1)存储结构:
使用一个数组来存储栈中的元素。数组具有连续的内存空间,这使得元素的访问速度相对较快。例如,定义一个Object[]数组来存储任意类型的元素(在Java中,由于泛型擦除,实际存储时会转换为Object类型)。
(2)入栈操作(push):
• 当执行入栈操作时,首先需要检查栈是否已满。如果栈顶指针(假设为top,初始值为 - 1,表示空栈)等于数组长度减1,说明栈已满,可能需要进行扩容操作(如创建一个更大的新数组,并将原数组元素复制到新数组中)。
• 如果栈未满,将栈顶指针top加1,然后将元素存储到数组的top索引位置。例如,在简单的顺序栈实现中:
private Object[] elements;
private int top;
public void push(Object element) {
if (top == elements.length - 1) {
// 扩容逻辑
}
top++;
elements[top] = element;
}
(3) 出栈操作(pop):
• 首先检查栈是否为空,即top是否等于 - 1。如果为空,则不能进行出栈操作(可以抛出异常或者返回特殊值表示栈空)。
• 如果栈不为空,取出数组中top索引位置的元素,然后将top减1,表示栈顶元素已经被移除。例如:
public Object pop() {
if (top == - 1) {
throw new EmptyStackException();
}
Object element = elements[top];
top--;
return element;
}
(4)查看栈顶元素(peek):
• 同样先检查栈是否为空。如果不为空,直接返回数组中top索引位置的元素,但不改变top的值。例如:
public Object peek() {
if (top == - 1) {
throw new EmptyStackException();
}
return elements[top];
}
• 基于链表实现的栈(链栈)
(1)存储结构:
• 由节点组成的链表,每个节点包含一个数据域和一个指向下一个节点的引用(指针)。例如,定义一个内部节点类:
private class Node {
Object data;
Node next;
Node(Object data) {
this.data = data;
this.next = null;
}
}
(2)入栈操作(push):
• 创建一个新的节点,将新节点的next指针指向当前的栈顶节点(如果栈为空,则新节点的next为null),然后将栈顶指针指向新创建的节点。例如:
private Node top;
public void push(Object data) {
Node newNode = new Node(data);
newNode.next = top;
top = newNode;
}
(3)出栈操作(pop):
• 首先检查栈是否为空,如果为空则不能进行出栈操作。如果栈不为空,取出栈顶节点的数据,然后将栈顶指针指向栈顶节点的下一个节点(如果栈顶节点的下一个节点为null,则表示栈为空)。例如:
public Object pop() {
if (top == null) {
throw new EmptyStackException();
}
Object data = top.data;
top = top.next;
return data;
}
(4) 查看栈顶元素(peek):
• 检查栈是否为空,如果不为空则返回栈顶节点的数据,但不改变栈顶指针。例如:
public Object peek() {
if (top == null) {
throw new EmptyStackException();
}
return top.data;
}
3. Java中的队列(Queue)
• 概念
队列是一种遵循先进先出(FIFO - First In First Out)原则的数据结构,就像排队买票一样,先来的人先得到服务。
• Java中的实现(Queue接口及其实现类)
Queue接口:java.util.Queue是Java中表示队列的接口,它定义了队列的基本操作方法。常见的实现类有LinkedList和PriorityQueue等。
• 主要方法(对于一般队列):
1.add(E e):将指定元素e插入队列。如果队列已满(对于有界队列),则抛出IllegalStateException异常。例如:
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.add(2);
2.offer(E e):将指定元素e插入队列。如果队列已满(对于有界队列),则返回false,与add方法类似但处理满队列的方式不同。例如:
Queue<Integer> queue = new LinkedList<>();
boolean success = queue.offer(1);
success = queue.offer(2);
3.remove():移除并返回队列的头元素。如果队列为空,则抛出NoSuchElementException异常。例如:
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.add(2);
int head = queue.remove();// 返回1
4.poll():移除并返回队列的头元素。如果队列为空,则返回null,与remove方法类似但处理空队列的方式不同。例如:
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.add(2);
int head = queue.poll();// 返回1
5.element():返回队列的头元素,但不移除它。如果队列为空,则抛出NoSuchElementException异常。例如:
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.add(2);
int head = queue.element();// 返回1,队列不变
6.peek():返回队列的头元素,但不移除它。如果队列为空,则返回null,与element方法类似但处理空队列的方式不同。例如:
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.add(2);
int head = queue.peek();// 返回1,队列不变
• LinkedList作为队列:
实现原理:LinkedList类实现了Queue接口,可以作为队列使用。它内部是一个双向链表结构,对于队列操作,头结点用于出队操作,尾结点用于入队操作。
示例代码:
Queue<Integer> linkedQueue = new LinkedList<>();
linkedQueue.add(1);
linkedQueue.add(2);
int first = linkedQueue.poll();// 1
• PriorityQueue(优先队列)
实现原理:PriorityQueue是基于堆(默认为小根堆,可以通过自定义比较器实现大根堆)实现的队列。元素按照它们的自然顺序或者自定义的比较器顺序进行排序,每次出队的元素都是队列中优先级最高(对于小根堆是值最小,对于大根堆是值最大)的元素。
• 示例代码:
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.add(3);
priorityQueue.add(1);
priorityQueue.add(2);
int min = priorityQueue.poll();// 1
• 应用场景
• 任务调度:在操作系统或任务管理系统中,任务可以按照提交的先后顺序(FIFO队列)或者任务的优先级(优先队列)进行排队处理。例如,在打印任务管理中,普通打印任务可以按照提交顺序排队,而紧急打印任务可以设置较高优先级,优先处理。
• 广度优先搜索(BFS)算法:在图的广度优先搜索算法中,需要使用队列来存储待访问的节点。从起始节点开始,将其相邻节点依次加入队列,然后按照先进先出的顺序依次访问这些节点及其相邻节点,直到搜索完所有可达节点。
4. Java中的队列(Queue)底层实现原理
• 基于数组实现的队列(顺序队列) - 简单循环队列示例
(1)存储结构:
使用一个数组来存储队列中的元素。例如,定义一个Object[]数组。为了实现循环队列的效果,需要两个指针:一个表示队头(front),一个表示队尾(rear)。初始时,front = rear = 0。
(2)入队操作(enqueue):
首先检查队列是否已满。判断队列满的条件通常是(rear + 1) % array.length== front(循环队列的满的判断方式,这是因为有一个位置是浪费的,用来区分队空和队满的情况)。如果队列已满,可能需要进行扩容操作(创建一个更大的新数组,并将原队列元素按照顺序复制到新数组中)。
如果队列未满,将元素放入队尾位置(rear指向的位置),然后更新队尾指针rear=(rear + 1) % array.length(实现循环效果)。例如:
private Object[] elements;
private int front;
private int rear;
public void enqueue(Object element) {
if ((rear + 1) % elements.length == front) {
// 扩容逻辑
}
elements[rear] = element;
rear = (rear + 1) % elements.length;
}
(3) 出队操作(dequeue):
首先检查队列是否为空,判断队列空的条件是front == rear。如果队列为空,则不能进行出队操作。
如果队列不为空,取出队头元素(front指向的元素),然后更新队头指针front=(front + 1) % elements.length。例如:
public Object dequeue() {
if (front == rear) {
throw new NoSuchElementException();
}
Object element = elements[front];
front = (front + 1) % elements.length;
return element;
}
• 基于链表实现的队列(链队)
(1)存储结构:
由节点组成的链表,包含一个队头指针(front)和一个队尾指针(rear)。定义内部节点类,与链栈的节点类类似:
private class Node {
Object data;
Node next;
Node(Object data) {
this.data = data;
this.next = null;
}
}
(2) 入队操作(enqueue):
创建一个新的节点,将新节点的数据域设置为要入队的元素。如果队列为空(front == null),则将队头和队尾指针都指向新节点;否则,将队尾节点的next指针指向新节点,然后更新队尾指针为新节点。例如:
private Node front;
private Node rear;
public void enqueue(Object data) {
Node newNode = new Node(data);
if (front == null) {
front = newNode;
rear = newNode;
} else {
rear.next = newNode;
rear = newNode;
}
}
(3) 出队操作(dequeue):
首先检查队列是否为空,如果为空则不能进行出队操作。如果队列不为空,取出队头节点的数据。如果队头节点是队尾节点(即front == rear),说明队列中只有一个元素,出队后队列为空,此时将队头和队尾指针都设置为null;否则,将队头指针指向下一个节点。例如:
public Object dequeue() {
if (front == null) {
throw new NoSuchElementException();
}
Object data = front.data;
if (front == rear) {
front = null;
rear = null;
} else {
front = front.next;
}
return data;
}
5.PriorityQueue(优先队列)底层实现原理
• 基于堆结构:
优先队列内部通常基于堆来实现。在Java中,PriorityQueue默认是基于小根堆(最小堆)实现的,即堆顶元素是堆中最小的元素。
• 元素存储与排序:
当向优先队列中添加一个元素时,会将该元素插入到合适的位置,以维护堆的性质。具体来说,会从下往上进行调整(上浮操作 - siftUp),比较新插入元素与其父元素的大小,如果新元素小于父元素,则交换它们的位置,直到满足堆的性质(父元素小于等于子元素)。
当从优先队列中取出元素(总是取出堆顶元素)时,会先将堆顶元素(优先级最高的元素)取出,然后将堆的最后一个元素移到堆顶,再从上往下进行调整(下沉操作 - siftDown),比较堆顶元素与其子元素的大小,将堆顶元素与较小(对于小根堆)的子元素交换位置,直到满足堆的性质。例如:
// 简单的上浮操作示例(假设是小根堆)
private void siftUp(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
// 简单的下沉操作示例(假设是小根堆)
private void siftDown(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}