# #34
## 今天规划
算法
双端队列 优先级队列 阻塞队列(和多线程一起看 难) 堆 二叉树
复习java基础内容
all
写 java底层代码
stringbuild
## 双端队列
### 接口
```java
package com.itheima.Deque;
public interface Deque<E> {
boolean offerFirst(E e);
boolean offerLast(E e);
E pollFirst();
E pollLast();
E peekFirst();
E peekLast();
boolean isEmpty();
boolean isFull();
}
```
### 链表实现
```java
package com.itheima.Deque;
import java.util.Iterator;
public class LinkedListDeque<E> implements Deque<E>, Iterable<E> {
@Override
public boolean offerFirst(E e) {
if (isFull()) {
return false;
}
size++;
Node<E> a = sentinel;
Node<E> b = sentinel.next;
Node<E> offered = new Node<>(a, e, b);
a.next = offered;
b.prev = offered;
return true;
}
@Override
public boolean offerLast(E e) {
if (isFull()) {
return false;
}
size++;
Node<E> a = sentinel.prev;
Node<E> b = sentinel;
Node<E> offered = new Node<>(a, e, b);
a.next = offered;
b.prev = offered;
return true;
}
@Override
public E pollFirst() {
if (isEmpty()) {
return null;
}
Node<E> a = sentinel;
Node<E>polled = sentinel.next;
Node<E> b = polled.next;
a.next = b;
b.prev = a;
size--;
return polled.value;
}
@Override
public E pollLast() {
if(isEmpty()){
return null;
}
Node<E> polled = sentinel.prev;
Node<E> a = polled.prev;
Node<E> b = sentinel;
a.next = b;
b.prev = a;
size--;
return polled.value;
}
@Override
public E peekFirst() {
if (isEmpty()) {
return null;
}
return sentinel.next.value;
}
@Override
public E peekLast() {
if (isEmpty()) {
return null;
}
return sentinel.prev.value;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == Capacity;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node<E> p = sentinel.next;
@Override
public boolean hasNext() {
return p!=sentinel;
}
@Override
public E next() {
E value = p.value;
p = p.next;
return value;
}
};
}
static class Node<E> {
E value;
Node<E> prev;
Node<E> next;
public Node(Node<E> prev,E value, Node<E> next) {
this.value = value;
this.next = next;
this.prev = prev;
}
}
Node<E> sentinel = new Node<>(null,null,null);
int Capacity;
int size;
public LinkedListDeque(int capacity) {
Capacity = capacity;
sentinel.next = sentinel;
sentinel.prev = sentinel;
}
}
```
> ### 代码结构与实现说明
>
> 这个双端队列实现采用了双向链表结构,核心设计特点如下:
>
> 1. **哨兵节点 (sentinel)**:
> - 使用一个特殊的哨兵节点来简化边界条件处理
> - 初始化时哨兵节点的 next 和 prev 都指向自己,形成一个空循环链表
> - 所有实际元素都存储在哨兵节点的前后
> 2. **双向链表节点**:
> - 每个节点包含值、前驱指针和后继指针
> - 通过双向指针实现 O (1) 时间复杂度的前后操作
> 3. **核心操作**:
> - 两端插入 (offerFirst/offerLast):通过修改指针实现节点添加
> - 两端删除 (pollFirst/pollLast):通过修改指针实现节点移除
> - 两端查询 (peekFirst/peekLast):直接访问哨兵节点的前后节点
> - 迭代器:从哨兵节点的后继开始,按顺序遍历到哨兵节点
> 4. **容量控制**:
> - 通过 Capacity 字段限制队列最大长度
> - 插入操作前检查是否已满,避免溢出
>
> 这种实现方式在处理双端队列操作时具有高效性,所有基本操作均为 O (1) 时间复杂度,同时通过哨兵节点简化了边界条件的处理,使代码更加简洁可靠。
### 注释
```java
package com.itheima.Deque;
import java.util.Iterator;
/**
* 双向链表实现的双端队列(Deque)
* 特点:
* 1. 基于双向链表实现,使用哨兵节点(sentinel)简化边界条件处理
* 2. 支持在队列两端进行插入和删除操作
* 3. 具有固定容量限制,可防止内存溢出
* 4. 实现Iterable接口,支持迭代遍历
*/
public class LinkedListDeque<E> implements Deque<E>, Iterable<E> {
/**
* 向双端队列头部添加元素
* @param e 要添加的元素
* @return 添加成功返回true,队列已满返回false
* 实现原理:
* 1. 检查队列是否已满,已满则返回false
* 2. 创建新节点,设置其前驱为哨兵节点,后继为哨兵节点的原后继节点
* 3. 更新哨兵节点和原后继节点的指针,完成插入
* 4. 队列大小加1
*/
@Override
public boolean offerFirst(E e) {
if (isFull()) {
return false;
}
size++;
Node<E> a = sentinel;
Node<E> b = sentinel.next;
Node<E> offered = new Node<>(a, e, b);
a.next = offered;
b.prev = offered;
return true;
}
/**
* 向双端队列尾部添加元素
* @param e 要添加的元素
* @return 添加成功返回true,队列已满返回false
* 实现原理:
* 1. 检查队列是否已满,已满则返回false
* 2. 创建新节点,设置其前驱为哨兵节点的原前驱节点,后继为哨兵节点
* 3. 更新原前驱节点和哨兵节点的指针,完成插入
* 4. 队列大小加1
*/
@Override
public boolean offerLast(E e) {
if (isFull()) {
return false;
}
size++;
Node<E> a = sentinel.prev;
Node<E> b = sentinel;
Node<E> offered = new Node<>(a, e, b);
a.next = offered;
b.prev = offered;
return true;
}
/**
* 从双端队列头部移除并返回元素
* @return 队列头部元素,队列为空返回null
* 实现原理:
* 1. 检查队列是否为空,为空则返回null
* 2. 获取哨兵节点的后继节点(即队列头部节点)
* 3. 更新哨兵节点和原头部节点后继的指针,断开原头部节点
* 4. 队列大小减1,返回原头部节点的值
*/
@Override
public E pollFirst() {
if (isEmpty()) {
return null;
}
Node<E> a = sentinel;
Node<E> polled = sentinel.next;
Node<E> b = polled.next;
a.next = b;
b.prev = a;
size--;
return polled.value;
}
/**
* 从双端队列尾部移除并返回元素
* @return 队列尾部元素,队列为空返回null
* 实现原理:
* 1. 检查队列是否为空,为空则返回null
* 2. 获取哨兵节点的前驱节点(即队列尾部节点)
* 3. 更新原尾部节点前驱和哨兵节点的指针,断开原尾部节点
* 4. 队列大小减1,返回原尾部节点的值
*/
@Override
public E pollLast() {
if (isEmpty()) {
return null;
}
Node<E> polled = sentinel.prev;
Node<E> a = polled.prev;
Node<E> b = sentinel;
a.next = b;
b.prev = a;
size--;
return polled.value;
}
/**
* 获取双端队列头部元素但不移除
* @return 队列头部元素,队列为空返回null
*/
@Override
public E peekFirst() {
if (isEmpty()) {
return null;
}
return sentinel.next.value;
}
/**
* 获取双端队列尾部元素但不移除
* @return 队列尾部元素,队列为空返回null
*/
@Override
public E peekLast() {
if (isEmpty()) {
return null;
}
return sentinel.prev.value;
}
/**
* 检查队列是否为空
* @return 队列为空返回true,否则返回false
*/
@Override
public boolean isEmpty() {
return size == 0;
}
/**
* 检查队列是否已满
* @return 队列已满返回true,否则返回false
*/
@Override
public boolean isFull() {
return size == Capacity;
}
/**
* 返回队列的迭代器,按从头部到尾部的顺序遍历
* @return 迭代器对象
*/
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node<E> p = sentinel.next; // 迭代器当前指向的节点,初始为哨兵节点的后继(队列头部)
/**
* 检查是否还有下一个元素
* @return 存在下一个元素返回true,否则返回false
*/
@Override
public boolean hasNext() {
return p != sentinel; // 当指向哨兵节点时,表示已遍历完所有元素
}
/**
* 获取下一个元素并移动迭代器
* @return 下一个元素
*/
@Override
public E next() {
E value = p.value;
p = p.next;
return value;
}
};
}
/**
* 双向链表节点类
* 每个节点包含:
* - value: 存储的元素值
* - prev: 指向前驱节点的指针
* - next: 指向后继节点的指针
*/
static class Node<E> {
E value; // 节点存储的值
Node<E> prev; // 前驱节点引用
Node<E> next; // 后继节点引用
/**
* 构造节点
* @param prev 前驱节点
* @param value 存储的值
* @param next 后继节点
*/
public Node(Node<E> prev, E value, Node<E> next) {
this.value = value;
this.next = next;
this.prev = prev;
}
}
Node<E> sentinel = new Node<>(null, null, null); // 哨兵节点,用于简化边界条件处理
int Capacity; // 队列容量
int size; // 当前队列元素数量
/**
* 构造函数
* @param capacity 队列容量
* 初始化操作:
* 1. 设置队列容量
* 2. 初始化哨兵节点,使其自环(next和prev都指向自己)
* 3. 初始队列大小为0
*/
public LinkedListDeque(int capacity) {
Capacity = capacity;
sentinel.next = sentinel;
sentinel.prev = sentinel;
}
}
```
### 数组实现
```java
package com.itheima.Deque;
import java.util.Iterator;
/**
* 基于循环数组实现的双端队列(Deque)
* 特点:
* 1. 使用数组作为底层存储结构
* 2. 通过head和tail指针实现循环利用数组空间
* 3. 数组容量比实际可存储元素数量多1,用于区分队列空和满的状态
* 4. 实现了双端队列接口,支持从队列两端进行元素操作
*/
public class ArrayDeque1<E> implements Deque<E>, Iterable<E> {
E[] array; // 存储队列元素的数组
int head; // 队头指针,指向队列第一个元素
int tail; // 队尾指针,指向队列最后一个元素的下一个位置
/**
* 构造函数,初始化队列容量
* @param capacity 队列的容量
* 注意:实际创建的数组大小为capacity+1,这是为了区分队列满和队列空的状态
*/
@SuppressWarnings("all") // 抑制数组类型转换的警告
public ArrayDeque1(int capacity) {
// 创建泛型数组需要进行类型转换
array = (E[]) new Object[capacity + 1];
}
/**
* 循环递增索引,用于处理数组的循环特性
* @param i 当前索引
* @param length 数组长度
* @return 递增后的索引,如果达到数组末尾则返回0
*/
static int inc(int i, int length) {
if (i + 1 >= length) {
return 0;
} else {
return i + 1;
}
}
/**
* 循环递减索引,用于处理数组的循环特性
* @param i 当前索引
* @param length 数组长度
* @return 递减后的索引,如果达到数组开头则返回数组末尾索引
*/
static int dec(int i, int length) {
if (i - 1 < 0) {
return length - 1;
} else {
return i - 1;
}
}
/**
* 向队列头部添加元素
* @param e 要添加的元素
* @return 添加成功返回true,队列已满返回false
* 实现逻辑:
* 1. 检查队列是否已满
* 2. 将head指针向前移动一位(循环移动)
* 3. 在新的head位置放置元素
*/
@Override
public boolean offerFirst(E e) {
if (isFull()) {
return false;
}
head = dec(head, array.length); // 移动head指针
array[head] = e; // 放置元素
return true;
}
/**
* 向队列尾部添加元素
* @param e 要添加的元素
* @return 添加成功返回true,队列已满返回false
* 实现逻辑:
* 1. 检查队列是否已满
* 2. 在当前tail位置放置元素
* 3. 将tail指针向后移动一位(循环移动)
*/
@Override
public boolean offerLast(E e) {
if (isFull()) {
return false;
}
array[tail] = e; // 放置元素
tail = inc(tail, array.length); // 移动tail指针
return true;
}
/**
* 从队列头部移除并返回元素
* @return 队列头部元素,如果队列为空返回null
* 实现逻辑:
* 1. 检查队列是否为空
* 2. 获取当前head位置的元素
* 3. 将该位置置为null(帮助垃圾回收)
* 4. 将head指针向后移动一位(循环移动)
*/
@Override
public E pollFirst() {
if (isEmpty()) {
return null;
}
E e = array[head]; // 获取元素
array[head] = null; // 置空位置
head = inc(head, array.length); // 移动head指针
return e;
}
/**
* 从队列尾部移除并返回元素
* @return 队列尾部元素,如果队列为空返回null
* 实现逻辑:
* 1. 检查队列是否为空
* 2. 将tail指针向前移动一位(循环移动)
* 3. 获取移动后位置的元素
* 4. 将该位置置为null(帮助垃圾回收)
*/
@Override
public E pollLast() {
if (isEmpty()) {
return null;
}
tail = dec(tail, array.length); // 移动tail指针
E e = array[tail]; // 获取元素
array[tail] = null; // 置空位置
return e;
}
/**
* 获取队列头部元素但不移除
* @return 队列头部元素,如果队列为空返回null
*/
@Override
public E peekFirst() {
if (isEmpty()) {
return null;
}
return array[head];
}
/**
* 获取队列尾部元素但不移除
* @return 队列尾部元素,如果队列为空返回null
*/
@Override
public E peekLast() {
if (isEmpty()) {
return null;
}
// 注意:tail指向最后一个元素的下一个位置,所以需要先减1
return array[tail];
}
/**
* 检查队列是否为空
* @return 队列为空返回true,否则返回false
* 判断条件:head == tail 表示队列中没有元素
*/
@Override
public boolean isEmpty() {
return head == tail;
}
/**
* 检查队列是否已满
* @return 队列已满返回true,否则返回false
* 判断条件:
* 1. 当tail > head时,tail - head == array.length - 1 表示队列已满
* 2. 当tail < head时,head - tail == 1 表示队列已满
*/
@Override
public boolean isFull() {
if(tail > head){
return tail - head == array.length - 1;
}else{
return head - tail == 1;
}
}
/**
* 返回队列的迭代器(当前实现为返回null,需要完善)
* @return 迭代器对象,用于遍历队列元素
*/
@Override
public Iterator<E> iterator() {
return null;
}
}
```
> ### 代码核心逻辑说明
>
> 1. **循环数组设计**:
> - 使用固定大小数组 + 头尾指针实现循环队列
> - 数组容量比实际可用空间多 1,用于区分队列空和满的状态
> - 通过`inc`和`dec`方法实现索引的循环移动
> 2. **双端操作实现**:
> - `offerFirst`/`pollFirst`操作队头 (head) 指针
> - `offerLast`/`pollLast`操作队尾 (tail) 指针
> - 所有操作均为 O (1) 时间复杂度
> 3. **空满状态判断**:
> - 队列空:head == tail
> - 队列满:tail 和 head 之间相差 1 个位置(循环意义下)
> 4. **迭代器实现**:
> - 目前返回 null,需要进一步实现以支持 foreach 等遍历操作
> 5. **泛型数组处理**:
> - 使用`@SuppressWarnings("all")`抑制泛型数组创建时的警告
> - 采用`(E[]) new Object[capacity + 1]`方式创建泛型数组
## 题
### E1
```java
package com.itheima.Deque;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* E01: 二叉树的锯齿形层序遍历(LeetCode 103)
* 使用双端队列实现Z字形层次遍历,即奇数层从左到右,偶数层从右到左
*/
public class E01Leetcode103 {
/**
* 锯齿形层序遍历二叉树
* <p>
* 算法思路:
* 1. 使用队列进行层序遍历,双端队列存储当前层节点值
* 2. 通过leftToRight标志控制当前层的遍历方向
* 3. 每处理完一层后翻转遍历方向
*
* @param root 二叉树根节点
* @return 按锯齿形顺序排列的节点值列表
*/
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
// 队列用于层序遍历,存储待处理的节点
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean leftToRight = true; // 遍历方向标志,true表示从左到右
int currentLevelNodes = 1; // 当前层的节点数量
while (!queue.isEmpty()) {
int nextLevelNodes = 0; // 下一层的节点数量
LinkedList<Integer> levelDeque = new LinkedList<>(); // 存储当前层节点值的双端队列
// 处理当前层的所有节点
for (int i = 0; i < currentLevelNodes; i++) {
TreeNode node = queue.poll(); // 取出队首节点
// 根据当前方向决定将节点值添加到双端队列的前端或后端
if (leftToRight) {
levelDeque.offerLast(node.val); // 从左到右:添加到队尾
} else {
levelDeque.offerFirst(node.val); // 从右到左:添加到队首
}
// 将下一层的子节点加入队列
if (node.left != null) {
queue.offer(node.left);
nextLevelNodes++;
}
if (node.right != null) {
queue.offer(node.right);
nextLevelNodes++;
}
}
// 更新当前层节点数,并翻转遍历方向
currentLevelNodes = nextLevelNodes;
leftToRight = !leftToRight;
result.add(levelDeque); // 将当前层结果添加到最终结果
}
return result;
}
/**
* 二叉树节点定义
*/
private static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
}
```
### 不懂点
~~~markdown
### `result` 集合的建立过程详解
在锯齿形层序遍历算法中,`result` 是最终存储遍历结果的二维集合,其建立过程可以分为以下几个关键步骤,结合代码逻辑和数据结构特性进行详细解析:
#### **一、result 的数据结构定义**
```java
List<List<Integer>> result = new ArrayList<>();
```
- **类型解析**:`result` 是一个 **ArrayList of ArrayLists**,即二维集合
- 外层 `List<List<Integer>>` 表示每一层的节点值列表
- 内层 `List<Integer>` 表示某一层的所有节点值(按锯齿形顺序排列)
- **初始化**:使用无参构造函数创建空的 ArrayList,初始容量为 10(ArrayList 默认初始容量)
#### **二、result 的填充过程(核心逻辑)**
```java
while (!queue.isEmpty()) {
// 处理当前层的节点...
result.add(levelDeque); // 将当前层结果添加到最终结果
}
```
1. **每一层的处理循环**:
- 外层 `while` 循环控制层序遍历的层次,每次循环处理一层节点
- 每次循环中,`levelDeque` 存储当前层的节点值(按锯齿形顺序)
2. **添加当前层到 result**:
```java
result.add(levelDeque);
```
- 每次处理完一层后,将该层的 `levelDeque` 直接添加到 `result` 中
- 由于 `levelDeque` 是 `LinkedList<Integer>` 类型,而 `LinkedList` 实现了 `List<Integer>` 接口,因此可以直接添加
- `result` 的大小随着层数增加而递增,每层对应一个子列表
3. **示例流程(以三层二叉树为例)**:
```
初始 result: []
第一层处理后:
levelDeque = [root.val]
result = [[root.val]]
第二层处理后(假设从右到左):
levelDeque = [rightChild.val, leftChild.val]
result = [[root.val], [rightChild.val, leftChild.val]]
第三层处理后(从左到右):
levelDeque = [leftGrandchild.val, rightGrandchild.val, ...]
result = [[root.val], [rightChild.val, leftChild.val], [leftGrandchild.val, rightGrandchild.val, ...]]
```
#### **三、levelDeque 与 result 的关系**
```java
LinkedList<Integer> levelDeque = new LinkedList<>(); // 每层新建一个双端队列
```
- **每层独立的双端队列**:
- 每次循环(处理一层)时,创建新的 `levelDeque`
- 该双端队列仅存储当前层的节点值,按锯齿形顺序排列
- **添加到 result 的时机**:
- 当一层的所有节点处理完毕后,`levelDeque` 被添加到 `result`
- 后续层的 `levelDeque` 是新的对象,不会覆盖之前的层
#### **四、锯齿形顺序的实现对 result 的影响**
1. **奇数层(leftToRight = true)**:
```java
levelDeque.offerLast(node.val); // 添加到队尾,顺序为左到右
```
- 例如节点顺序为 A → B → C,`levelDeque` 存储为 [A, B, C]
- 添加到 `result` 后,该层显示为 [A, B, C]
2. **偶数层(leftToRight = false)**:
```java
levelDeque.offerFirst(node.val); // 添加到队首,顺序为右到左
```
- 例如节点顺序为 D → E → F,`levelDeque` 存储为 [F, E, D]
- 添加到 `result` 后,该层显示为 [F, E, D]
3. **最终 result 的结构**:
```
result = [
[A], // 第一层(左到右)
[C, B], // 第二层(右到左)
[D, E, F, G], // 第三层(左到右)
[K, J, I, H] // 第四层(右到左)
]
```
#### **五、result 的内存与性能分析**
1. **内存占用**:
- 每个 `levelDeque` 存储当前层的所有节点值,空间复杂度为 O(n)
- `result` 存储所有层的列表,总空间复杂度为 O(n)(n 为节点总数)
2. **动态扩容**:
- `ArrayList` 的默认扩容策略:当元素数量超过容量时,容量翻倍
- 例如初始容量 10,添加第 11 个元素时容量变为 20
- 扩容操作的时间复杂度为均摊 O(1),不影响整体算法复杂度
3. **与其他数据结构的对比**:
- 若使用 `LinkedList` 作为外层集合,插入效率与 `ArrayList` 相近
- `ArrayList` 的随机访问效率更高,更适合结果集的存储和返回
#### **六、常见问题与解决方案**
1. **为什么使用 LinkedList 作为内层集合?**
```java
LinkedList<Integer> levelDeque = new LinkedList<>();
```
- 原因:`LinkedList` 支持在两端高效插入(`offerFirst`/`offerLast`)
- 若使用 `ArrayList`,在偶数层从右到左插入时需要频繁移动元素,效率较低
2. **能否使用 ArrayList 代替 LinkedList?**
- 可以,但偶数层的插入效率会降低:
```java
List<Integer> levelList = new ArrayList<>();
if (leftToRight) {
levelList.add(node.val); // 尾部插入,O(1)
} else {
levelList.add(0, node.val); // 头部插入,O(n)
}
```
- 当层数较多时,`LinkedList` 的效率优势更明显
3. **result 的遍历方式**:
```java
for (List<Integer> level : result) {
for (int val : level) {
System.out.print(val + " ");
}
System.out.println();
}
```
通过以上分析可以看出,`result` 的建立过程本质是将每层的节点值按锯齿形顺序收集到独立的列表中,最终组合成二维集合。这种结构清晰地反映了二叉树的层次关系和锯齿形遍历顺序,是层序遍历算法的标准结果表示方式。
~~~
### EX2
leetcode641
同上 也是差别不大,略
## 优先级队列
### 无序数组实现
```java
package com.itheima.PriorityQueue;
import com.itheima.Queue.Queue;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 优先级队列接口定义
* 定义元素优先级比较的规范
*/
public interface Priority {
/**
* 获取元素的优先级
* @return 优先级值,值越大优先级越高
*/
int getPriority();
}
package com.itheima.PriorityQueue;
import com.itheima.Queue.Queue;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 基于数组实现的优先级队列
* 元素按照优先级从高到低出队(优先级值大的先出队)
* <p>
* 核心特性:
* 1. 每次入队时间复杂度:O(1)
* 2. 每次出队时间复杂度:O(n)(遍历查找最高优先级元素)
* 3. 不支持动态扩容,容量固定
*/
public class PriorityQueue1<E extends Priority> implements Queue<E> {
private E[] array; // 存储元素的数组
private int size; // 当前队列中的元素数量
@SuppressWarnings("unchecked")
public PriorityQueue1(int capacity) {
// 初始化数组,容量为指定大小
array = (E[]) new Priority[capacity];
size = 0;
}
/**
* 将元素添加到优先级队列
* <p>
* 直接添加到数组末尾,时间复杂度O(1)
* @param e 要添加的元素,必须实现Priority接口
* @return 添加成功返回true,队列已满返回false
*/
@Override
public boolean offer(E e) {
if (isFull()) {
return false;
}
array[size++] = e;
return true;
}
/**
* 查找优先级最高的元素索引
* <p>
* 遍历数组找到优先级最大的元素,时间复杂度O(n)
* @return 最高优先级元素的索引
*/
private int selectMax() {
if (isEmpty()) {
throw new NoSuchElementException("PriorityQueue is empty");
}
int maxIndex = 0;
for (int i = 1; i < size; i++) {
if (array[i].getPriority() > array[maxIndex].getPriority()) {
maxIndex = i;
}
}
return maxIndex;
}
/**
* 移除并返回优先级最高的元素
* <p>
* 先查找最高优先级元素,然后将其从数组中移除,时间复杂度O(n)
* @return 优先级最高的元素
*/
@Override
public E poll() {
if (isEmpty()) {
return null;
}
int maxIndex = selectMax();
E maxElement = array[maxIndex];
remove(maxIndex);
return maxElement;
}
/**
* 从数组中移除指定索引的元素
* <p>
* 将后续元素前移,时间复杂度O(n)
* @param index 要移除元素的索引
*/
private void remove(int index) {
for (int i = index; i < size - 1; i++) {
array[i] = array[i + 1];
}
array[--size] = null; // 帮助垃圾回收
}
/**
* 返回优先级最高的元素但不移除
* <p>
* 查找最高优先级元素,时间复杂度O(n)
* @return 优先级最高的元素
*/
@Override
public E peek() {
if (isEmpty()) {
return null;
}
int maxIndex = selectMax();
return array[maxIndex];
}
/**
* 判断队列是否为空
* @return 为空返回true,否则返回false
*/
@Override
public boolean isEmpty() {
return size == 0;
}
/**
* 判断队列是否已满
* @return 已满返回true,否则返回false
*/
@Override
public boolean isFull() {
return size == array.length;
}
/**
* 返回队列的迭代器(按优先级从高到低)
* <p>
* 迭代器实现为内部类,支持按优先级顺序遍历
* @return 迭代器实例
*/
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
private int iterSize = size; // 迭代器使用的独立size,避免遍历时修改影响
@Override
public boolean hasNext() {
return iterSize > 0;
}
@Override
public E next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
int maxIndex = 0;
// 查找当前剩余元素中的最大优先级
for (int i = 0; i < size; i++) {
if (array[i] != null && array[i].getPriority() > array[maxIndex].getPriority()) {
maxIndex = i;
}
}
E maxElement = array[maxIndex];
// 标记为已访问(避免重复返回)
array[maxIndex] = null;
iterSize--;
return maxElement;
}
@Override
public void remove() {
throw new UnsupportedOperationException("不支持删除操作");
}
};
}
}
```
### 有序数组实现
```java
package com.itheima.PriorityQueue;
import com.itheima.Queue.Queue;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 基于有序数组实现的优先级队列
* 元素按照优先级从低到高排列(升序)
* 队首(数组末尾)为优先级最高的元素
*
* 特性:
* - 入队操作时间复杂度为 O(n),需移动元素维持有序性
* - 出队和查看操作时间复杂度为 O(1)
*/
public class PriorityQueue2<E extends Priority> implements Queue<E> {
private Priority[] array; // 存储元素的数组,按优先级升序排列
private int size; // 当前队列中的元素数量
@SuppressWarnings("unchecked")
public PriorityQueue2(int capacity) {
// 初始化数组,容量为指定大小
array = new Priority[capacity];
size = 0;
}
/**
* 向队列中添加元素,保持元素按优先级升序排列
*
* @param o 待添加的元素(需实现 Priority 接口)
* @return 添加成功返回 true,队列已满返回 false
*/
@Override
public boolean offer(E o) {
if (isFull()) {
return false;
}
insert(o); // 将元素插入到正确位置
size++; // 更新元素数量
return true;
}
/**
* 将元素插入到正确位置,保持数组按优先级升序排列
*
* @param e 待插入的元素
*/
private void insert(E e) {
int i = size - 1;
// 从后向前查找插入位置,将较大元素后移
while (i >= 0 && array[i].getPriority() > e.getPriority()) {
array[i + 1] = array[i]; // 后移元素
i--;
}
array[i + 1] = e; // 插入新元素
}
/**
* 移除并返回优先级最高的元素(队首元素)
*
* @return 优先级最高的元素,队列为空时返回 null
*/
@Override
public E poll() {
if (isEmpty()) {
return null;
}
E e = (E) array[size - 1]; // 获取队首元素(优先级最高)
array[size - 1] = null; // 置空原位置,帮助垃圾回收
size--; // 更新元素数量
return e;
}
/**
* 返回优先级最高的元素(队首元素)但不移除
*
* @return 优先级最高的元素,队列为空时返回 null
*/
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return (E) array[size - 1]; // 返回队首元素(优先级最高)
}
/**
* 判断队列是否为空
*
* @return 队列为空返回 true,否则返回 false
*/
@Override
public boolean isEmpty() {
return size == 0;
}
/**
* 判断队列是否已满
*
* @return 队列已满返回 true,否则返回 false
*/
@Override
public boolean isFull() {
return size == array.length;
}
/**
* 返回队列的迭代器(未实现)
*
* @return null(需后续实现)
*/
@Override
public Iterator<E> iterator() {
// TODO: 实现迭代器
return null;
}
}
```
### 堆实现
```java
package com.itheima.PriorityQueue;
import com.itheima.Queue.Queue;
import java.util.Iterator;
/**
* 基于最大堆实现的优先级队列
* 元素按优先级从高到低出队(优先级值大的元素先出队)
*
* 堆特性:
* - 完全二叉树结构,使用数组存储
* - 父节点优先级 >= 子节点优先级
* - 根节点(索引0)为优先级最高的元素
*/
public class PriorityQueue3<E extends Priority> implements Queue<E> {
private Priority[] array; // 存储堆元素的数组
private int size; // 当前队列中的元素数量
@SuppressWarnings("unchecked")
public PriorityQueue3(int capacity) {
array = new Priority[capacity];
size = 0;
}
/**
* 向队列添加元素,维护最大堆特性
*
* @param offered 待添加的元素(需实现 Priority 接口)
* @return 添加成功返回 true,队列已满返回 false
*/
@Override
public boolean offer(E offered) {
if (isFull()) {
return false;
}
int child = size++; // 新元素初始位置为数组末尾
int parent = (child - 1) / 2; // 父节点索引
// 上浮调整:若新元素优先级高于父节点,则交换
while (child > 0 && offered.getPriority() > array[parent].getPriority()) {
array[child] = array[parent]; // 父节点下移
child = parent; // 继续向上比较
parent = (child - 1) / 2;
}
array[child] = offered; // 插入新元素到正确位置
return true;
}
/**
* 交换数组中两个元素的位置
*/
private void swap(int i, int j) {
Priority temp = array[i];
array[i] = array[j];
array[j] = temp;
}
/**
* 移除并返回优先级最高的元素(堆顶元素)
*
* @return 优先级最高的元素,队列为空时返回 null
*/
@Override
public E poll() {
if (isEmpty()) {
return null;
}
swap(0, size - 1); // 将堆顶元素与最后一个元素交换
size--; // 减少队列大小
Priority e = array[size]; // 取出原堆顶元素
array[size] = null; // 置空原位置,帮助垃圾回收
shiftDown(0); // 下沉调整堆结构
return (E) e;
}
/**
* 下沉调整:维护最大堆特性
* 将指定位置的元素与其子节点比较,若小于子节点则交换
*/
private void shiftDown(int parent) {
int left = 2 * parent + 1; // 左子节点索引
int right = left + 1; // 右子节点索引
int max = parent; // 优先级最高的节点索引
// 找出父子节点中优先级最高的节点
if (left < size && array[left].getPriority() > array[max].getPriority()) {
max = left;
}
if (right < size && array[right].getPriority() > array[max].getPriority()) {
max = right;
}
// 若父节点不是最大,交换并继续下沉
if (max != parent) {
swap(max, parent);
shiftDown(max);
}
}
/**
* 返回优先级最高的元素(堆顶元素)但不移除
*
* @return 优先级最高的元素,队列为空时返回 null
*/
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return (E) array[0]; // 堆顶元素为优先级最高的元素
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == array.length;
}
@Override
public Iterator<E> iterator() {
// TODO: 实现迭代器
return null;
}
}
```
> ### 核心算法解析
>
> 这个优先级队列基于最大堆实现,主要包含两个核心操作:
>
> 1. **上浮调整(Offer 操作)**
> - 将新元素添加到数组末尾(完全二叉树的最后位置)
> - 与父节点比较,若优先级更高则交换位置
> - 重复此过程直到满足堆特性或到达根节点
> - 时间复杂度:O (log n)
> 2. **下沉调整(Poll 操作)**
> - 将堆顶元素(优先级最高)与最后一个元素交换
> - 移除并返回原堆顶元素
> - 对新的堆顶元素执行下沉操作:与子节点比较,若小于子节点则交换
> - 重复此过程直到满足堆特性
> - 时间复杂度:O (log n)
>
> ### 数据结构特性
>
> - **数组存储**:使用数组实现完全二叉树结构
> - 父节点索引:`(child - 1) / 2`
> - 左子节点索引:`2 * parent + 1`
> - 右子节点索引:`2 * parent + 2`
> - **优先级比较**:通过元素实现的 `getPriority()` 方法获取优先级值,值越大优先级越高
>
> ### 性能分析
>
> - **入队(Offer)**:O(log n)
> - **出队(Poll)**:O(log n)
> - **查看队首(Peek)**:O(1)
> - **空间复杂度**:O(n)
>
> 这种实现方式在处理大量元素时效率较高,尤其适合需要频繁获取最高优先级元素的场景,如任务调度系统、事件处理等。
### 题 E01. 合并多个有序链表-Leetcode 23
```java
package com.itheima.PriorityQueue;
import java.util.List;
/**
* 最小堆(Min-Heap)实现,用于存储ListNode节点
* 最小堆是一种完全二叉树,父节点值小于等于子节点值
* 主要用于高效获取和删除最小值元素
*/
public class MinHeap {
ListNode[] array; // 存储堆元素的数组
int size; // 当前堆中元素数量
/**
* 构造指定容量的最小堆
* @param capacity 堆的初始容量
*/
public MinHeap(int capacity) {
array = new ListNode[capacity];
}
/**
* 向最小堆中添加节点,维护最小堆性质
* 算法:上浮调整(percolate up)
* 1. 将新节点添加到堆末尾
* 2. 与父节点比较,若新节点值更小则交换位置
* 3. 重复直到满足最小堆性质
* @param offered 待添加的ListNode节点
*/
public void Offer(ListNode offered) {
int child = size++; // 新节点初始位置为堆末尾
int parent = (child - 1) / 2; // 计算父节点索引
// 上浮调整:若新节点值小于父节点,则交换位置
while (child > 0 && offered.val < array[parent].val) {
array[child] = array[parent]; // 父节点下移
child = parent; // 继续向上比较
parent = (child - 1) / 2;
}
array[child] = offered; // 插入新节点到正确位置
}
/**
* 移除并返回堆顶元素(最小值节点),维护最小堆性质
* 算法:下沉调整(percolate down)
* 1. 交换堆顶与堆尾元素
* 2. 移除并返回原堆顶元素
* 3. 对新堆顶执行下沉调整,确保最小堆性质
* @return 堆顶元素(最小值节点),堆为空时返回null
*/
public ListNode poll() {
if (isEmpty()) {
return null;
}
swap(0, size - 1); // 交换堆顶与堆尾元素
size--; // 减少堆大小
ListNode e = array[size]; // 保存原堆顶元素
array[size] = null; // 置空原位置,帮助垃圾回收
down(0); // 下沉调整堆结构
return e;
}
/**
* 交换数组中两个位置的元素
* @param i 第一个位置索引
* @param i1 第二个位置索引
*/
private void swap(int i, int i1) {
ListNode temp = array[i];
array[i] = array[i1];
array[i1] = temp;
}
/**
* 下沉调整:维护最小堆性质
* 1. 比较当前节点与左右子节点,找到值最小的节点
* 2. 若当前节点不是最小值,则与最小值子节点交换
* 3. 递归调整交换后的子节点
* @param parent 要调整的节点索引
*/
private void down(int parent) {
int left = 2 * parent + 1; // 左子节点索引
int right = left + 1; // 右子节点索引
int min = parent; // 初始假设当前节点为最小值
// 找出父节点和左右子节点中的最小值节点
if (left < size && array[left].val < array[min].val) {
min = left;
}
if (right < size && array[right].val < array[min].val) {
min = right;
}
// 若当前节点不是最小值,交换并继续下沉调整
if (min != parent) {
swap(parent, min);
down(min);
}
}
/**
* 判断堆是否为空
* @return 堆为空返回true,否则返回false
*/
private boolean isEmpty() {
return size == 0;
}
/**
* E01: 合并k个升序链表(LeetCode 23)
* 使用最小堆实现高效合并k个升序链表
* 时间复杂度:O(N log k),N为所有链表节点总数,k为链表数量
* 空间复杂度:O(k)
*/
class E01Leetcode23 {
public ListNode mergeKLists(ListNode[] lists) {
// 边界条件处理
if (lists == null || lists.length == 0) {
return null;
}
// 创建最小堆,容量为链表数量
MinHeap minHeap = new MinHeap(lists.length);
// 将所有链表的头节点加入堆
for (ListNode head : lists) {
if (head != null) {
minHeap.Offer(head);
}
}
// 创建哑节点简化操作
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
// 从堆中不断取出最小值节点,构建结果链表
while (!minHeap.isEmpty()) {
ListNode node = minHeap.poll(); // 取出当前最小值节点
p.next = node; // 连接到结果链表
p = p.next; // 移动指针
// 将当前节点的下一个节点加入堆
if (node.next != null) {
minHeap.Offer(node.next);
}
}
return dummy.next; // 返回合并后的链表(跳过哑节点)
}
}
/**
* 链表节点定义
*/
static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
}
```
> ### 代码解析
>
> #### 最小堆核心功能
>
> 1. **数据结构**:
> - 使用数组实现最小堆,满足完全二叉树性质
> - 父节点与子节点关系:`parent = (child - 1) / 2`,`left = 2*parent + 1`,`right = 2*parent + 2`
> 2. **核心操作**:
> - **上浮调整 (Offer)**:新节点从堆尾插入,与父节点比较并交换,直到满足最小堆性质
> - **下沉调整 (down)**:堆顶元素与子节点比较并交换,确保堆顶始终为最小值
> - **删除 (poll)**:移除堆顶元素,用堆尾元素替代后下沉调整
>
> #### 合并 k 个链表算法
>
> 1. **算法思路**:
> - 使用最小堆存储 k 个链表的头节点
> - 每次从堆中取出值最小的节点,添加到结果链表
> - 将取出节点的下一个节点加入堆中,重复直到堆为空
> 2. **复杂度分析**:
> - 时间复杂度:O (N log k),其中 N 是所有链表的节点总数,k 是链表数量
> - 空间复杂度:O (k),用于存储堆中的 k 个节点
> 3. **优化点**:
> - 使用哑节点 (dummy node) 避免处理头节点的特殊情况
> - 堆操作保证每次获取最小值的时间复杂度为 O (log k)
>
> 这个实现高效地解决了合并 k 个升序链表的问题,充分利用了最小堆在获取最小值场景下的优势。
## 阻塞队列
### 单锁实现
```java
/**
* 基于单锁实现的阻塞队列,支持线程安全的元素插入和取出操作
* 当队列满时插入操作会阻塞,队列空时取出操作会阻塞
* <p>
* 实现特点:
* - 使用循环数组存储元素,通过head/tail指针管理队列
* - 单ReentrantLock实现互斥访问
* - 两个Condition条件变量分别控制生产者和消费者等待
* @param <E> 队列存储的元素类型
*/
public class BlockingQueue1<E> implements BlockingQueue<E> {
private final E[] array; // 存储元素的数组
private int head = 0; // 队首指针,指向队首元素
private int tail = 0; // 队尾指针,指向队尾元素的下一个位置
private int size = 0; // 队列中元素的数量
@SuppressWarnings("all")
public BlockingQueue1(int capacity) {
// 初始化存储数组,容量为用户指定大小
array = (E[]) new Object[capacity];
}
// 用于互斥访问的锁
private final ReentrantLock lock = new ReentrantLock();
// 生产者等待条件:当队列满时,生产者线程在此等待
private final Condition tailWaits = lock.newCondition();
// 消费者等待条件:当队列空时,消费者线程在此等待
private final Condition headWaits = lock.newCondition();
/**
* 向队列尾部添加元素,队列满时阻塞当前线程
* <p>
* 实现逻辑:
* 1. 获取锁并处理中断
* 2. 队列满时通过tailWaits等待
* 3. 添加元素到队尾,更新tail指针
* 4. 通知等待的消费者
* @param e 要添加的元素
* @throws InterruptedException 当线程被中断时抛出
*/
@Override
public void offer(E e) throws InterruptedException {
lock.lockInterruptibly();
try {
// 队列满时等待
while (isFull()) {
tailWaits.await();
}
// 添加元素到队尾
array[tail] = e;
// 更新队尾指针(循环数组)
if (++tail == array.length) {
tail = 0;
}
size++;
// 通知等待的消费者队列已有元素
headWaits.signal();
} finally {
lock.unlock();
}
}
/**
* 带超时的元素添加操作,队列满时等待指定时间
* <p>
* 实现逻辑:
* 1. 获取锁并处理中断
* 2. 队列满时等待指定时间,超时则返回
* 3. 添加元素到队尾,更新tail指针
* 4. 通知等待的消费者
* @param e 要添加的元素
* @param timeout 等待时间
* @throws InterruptedException 当线程被中断时抛出
*/
@Override
public void offer(E e, long timeout) throws InterruptedException {
lock.lockInterruptibly();
try {
// 将毫秒转换为纳秒
long nanos = TimeUnit.MILLISECONDS.toNanos(timeout);
// 队列满时等待指定时间
while (isFull()) {
if (nanos <= 0) {
return; // 超时返回
}
// 等待并返回剩余等待时间
nanos = tailWaits.awaitNanos(nanos);
}
// 添加元素到队尾
array[tail] = e;
// 更新队尾指针(循环数组)
if (++tail == array.length) {
tail = 0;
}
size++;
// 通知等待的消费者队列已有元素
headWaits.signal();
} finally {
lock.unlock();
}
}
/**
* 从队列头部取出元素,队列空时阻塞当前线程
* <p>
* 实现逻辑:
* 1. 获取锁并处理中断
* 2. 队列空时通过headWaits等待
* 3. 取出队首元素,更新head指针
* 4. 通知等待的生产者队列已有空间
* @return 队列头部元素
* @throws InterruptedException 当线程被中断时抛出
*/
@Override
public E poll() throws InterruptedException {
lock.lockInterruptibly();
try {
// 队列空时等待
while (isEmpty()) {
headWaits.await();
}
// 取出队首元素
E e = array[head];
// 置空原位置帮助垃圾回收
array[head] = null;
// 更新队首指针(循环数组)
if (++head == array.length) {
head = 0;
}
size--;
// 通知等待的生产者队列已有空间
tailWaits.signal();
return e;
} finally {
lock.unlock();
}
}
/**
* 判断队列是否为空
* @return 队列为空返回true,否则返回false
*/
private boolean isEmpty() {
return size == 0;
}
/**
* 判断队列是否已满
* @return 队列已满返回true,否则返回false
*/
private boolean isFull() {
return size == array.length;
}
}
```
### 双锁实现
```java
/**
* 基于双锁实现的阻塞队列,支持线程安全的元素插入和取出操作
* 采用生产者-消费者模式,通过分离锁提高并发性能
* <p>
* 实现特点:
* - 使用循环数组存储元素,head/tail指针管理队列
* - 分离的headLock和tailLock分别控制队首和队尾操作
* - AtomicInteger保证元素数量的原子性操作
* - 精准的信号通知机制减少线程竞争
* @param <E> 队列存储的元素类型
*/
public class BlockingQueue2<E> implements BlockingQueue<E> {
private final E[] array; // 存储元素的循环数组
private int head = 0; // 队首指针,指向队首元素
private int tail = 0; // 队尾指针,指向队尾下一个位置
private final AtomicInteger size = new AtomicInteger(0); // 元素数量(原子操作)
// 分离锁:headLock控制队首操作,tailLock控制队尾操作
private final ReentrantLock headLock = new ReentrantLock();
private final Condition headWaits = headLock.newCondition(); // 消费者等待条件(队列空时)
private final ReentrantLock tailLock = new ReentrantLock();
private final Condition tailWaits = tailLock.newCondition(); // 生产者等待条件(队列满时)
@SuppressWarnings("all")
public BlockingQueue2(int capacity) {
array = (E[]) new Object[capacity]; // 初始化存储数组
}
/**
* 向队列尾部添加元素,队列满时阻塞当前线程
* <p>
* 实现优化点:
* 1. 使用tailLock保护队尾操作,与headLock分离提高并发
* 2. 精准通知:仅在必要时唤醒等待线程,减少竞争
* 3. 先获取tailLock,后释放,保证队尾操作原子性
* @param e 要添加的元素
* @throws InterruptedException 当线程被中断时抛出
*/
@Override
public void offer(E e) throws InterruptedException {
int c;
tailLock.lockInterruptibly(); // 获取队尾锁
try {
// 队列满时等待
while (isFull()) {
tailWaits.await();
}
// 添加元素到队尾
array[tail] = e;
// 更新队尾指针(循环数组)
if (++tail == array.length) {
tail = 0;
}
// 原子性增加元素数量并获取旧值
c = size.getAndIncrement();
// a. 队列未满且不是从满->不满,唤醒其他生产者
if (c + 1 < array.length) {
tailWaits.signal(); // 通知其他offer线程队列仍有空位
}
} finally {
tailLock.unlock(); // 释放队尾锁
}
// b. 队列从空->不空,唤醒等待的消费者
if (c == 0) {
headLock.lock(); // 轻量级获取headLock
try {
headWaits.signal(); // 通知poll线程队列已有元素
} finally {
headLock.unlock();
}
}
}
/**
* 从队列头部取出元素,队列空时阻塞当前线程
* <p>
* 实现优化点:
* 1. 使用headLock保护队首操作,与tailLock分离
* 2. 精准通知:根据元素数量决定唤醒哪些线程
* 3. 先获取headLock,后释放,保证队首操作原子性
* @return 队列头部元素
* @throws InterruptedException 当线程被中断时抛出
*/
@Override
public E poll() throws InterruptedException {
E e;
int c;
headLock.lockInterruptibly(); // 获取队首锁
try {
// 队列空时等待
while (isEmpty()) {
headWaits.await();
}
// 取出队首元素
e = array[head];
// 更新队首指针(循环数组)
if (++head == array.length) {
head = 0;
}
// 原子性减少元素数量并获取旧值
c = size.getAndDecrement();
// b. 队列不空且不是从空->不空,唤醒其他消费者
if (c > 1) {
headWaits.signal(); // 通知其他poll线程队列仍有元素
}
} finally {
headLock.unlock(); // 释放队首锁
}
// a. 队列从满->不满,唤醒等待的生产者
if (c == array.length) {
tailLock.lock(); // 轻量级获取tailLock
try {
tailWaits.signal(); // 通知offer线程队列已有空位
} finally {
tailLock.unlock();
}
}
return e;
}
/**
* 判断队列是否为空(使用原子变量保证一致性)
*/
private boolean isEmpty() {
return size.get() == 0;
}
/**
* 判断队列是否已满(使用原子变量保证一致性)
*/
private boolean isFull() {
return size.get() == array.length;
}
}
```
## 堆
> ### 核心算法解析
>
> #### 1. 第 K 个最大元素(LeetCode 215)
>
> - **数据结构选择**:使用最小堆,因为我们需要维护最大的 K 个元素,堆顶即为第 K 大元素
>
> - 算法关键点
>
> :
>
> - 堆的容量固定为 K,始终保存当前最大的 K 个元素
> - 当新元素大于堆顶时,替换堆顶并调整堆,确保堆中始终是最大的 K 个元素
> - 最终堆顶元素即为第 K 个最大元素
>
> #### 2. 数据流中的第 K 个最大元素(LeetCode 703)
>
> - 动态维护方案
>
> :
>
> - 使用最小堆动态维护数据流中最大的 K 个元素
> - add 方法中,若堆未满则直接添加,否则比较新元素与堆顶
> - 新元素更大时替换堆顶,保证堆中始终是当前最大的 K 个元素
>
> #### 3. 数据流的中位数(LeetCode 295)
>
> - 双堆解决方案
>
> :
>
> - 左堆(最大堆)存储较小的一半元素,右堆(最小堆)存储较大的一半元素
> - 添加元素时保持两堆大小平衡,左堆元素数≥右堆
> - 中位数计算:两堆大小相等时取平均值,不等时取左堆顶
>
> - 平衡策略
>
> :
>
> - 两堆大小相等时,新元素加入右堆,然后右堆顶移到左堆
> - 两堆大小不等时,新元素加入左堆,然后左堆顶移到右堆
>
> 这些算法充分展示了堆在解决 "top K" 和中位数问题中的高效应用,时间复杂度均为 O (n log k),空间复杂度为 O (k),是处理海量数据时的经典解决方案。
```java
package com.itheima.Heap;
import java.util.Arrays;
/**
* 堆的应用算法集合,包含三个基于堆的LeetCode问题解决方案
* <p>
* 涉及算法:
* 1. 寻找数组中第K个最大元素(LeetCode 215)
* 2. 数据流中的第K个最大元素(LeetCode 703)
* 3. 数据流的中位数(LeetCode 295)
*/
public class Heap_E {
// ------------------------ 最小堆实现 ------------------------
class MinHeap {
private int[] array; // 堆数组
private int size; // 堆中元素数量
private int capacity; // 堆容量
public MinHeap(int capacity) {
this.capacity = capacity;
this.array = new int[capacity];
this.size = 0;
}
// 判断堆是否已满
public boolean isFull() {
return size == capacity;
}
// 向堆中添加元素
public void offer(int val) {
if (size == capacity) {
throw new IllegalStateException("Heap is full");
}
array[size] = val;
up(size++);
}
// 替换堆顶元素并调整堆
public void replace(int val) {
array[0] = val;
down(0);
}
// 获取堆顶元素
public int peek() {
if (size == 0) {
throw new IllegalStateException("Heap is empty");
}
return array[0];
}
// 上浮调整
private void up(int index) {
int parent = (index - 1) / 2;
while (index > 0 && array[parent] > array[index]) {
swap(parent, index);
index = parent;
parent = (index - 1) / 2;
}
}
// 下沉调整
private void down(int index) {
int left = 2 * index + 1;
while (left < size) {
int right = left + 1;
int smallest = left;
// 找到左右子节点中较小的
if (right < size && array[right] < array[left]) {
smallest = right;
}
// 如果父节点已经是最小的,无需调整
if (array[index] <= array[smallest]) {
break;
}
swap(index, smallest);
index = smallest;
left = 2 * index + 1;
}
}
// 交换数组元素
private void swap(int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
// ------------------------ E2: LeetCode 215 ------------------------
/**
* 寻找数组中第K个最大元素
* <p>
* 算法思路:
* 1. 使用最小堆维护当前最大的K个元素
* 2. 堆中始终保存最大的K个元素,堆顶为第K大元素
* 3. 时间复杂度:O(n log k),空间复杂度:O(k)
*
* @param numbers 输入数组
* @param k 第K个最大元素
* @return 第K个最大元素的值
*/
public int findKthLargest(int[] numbers, int k) {
if (numbers == null || numbers.length == 0 || k <= 0 || k > numbers.length) {
throw new IllegalArgumentException("Invalid input");
}
MinHeap heap = new MinHeap(k); // 创建容量为k的最小堆
// 先将前k个元素加入堆
for (int i = 0; i < k; i++) {
heap.offer(numbers[i]);
}
// 遍历剩余元素,若大于堆顶则替换
for (int i = k; i < numbers.length; i++) {
int top = heap.peek(); // 获取当前第k大元素
if (numbers[i] > top) {
heap.replace(numbers[i]); // 替换堆顶并调整堆
}
}
return heap.peek(); // 堆顶即为第k大元素
}
// ------------------------ E3: LeetCode 703 ------------------------
/**
* 数据流中的第K个最大元素(LeetCode 703)
* <p>
* 数据结构设计:
* - 使用最小堆维护最大的k个元素
* - 堆中始终保存当前数据流中最大的k个元素
* - add方法时间复杂度:O(log k),peek时间复杂度:O(1)
*/
class KthLargest {
private MinHeap heap; // 最小堆,容量为k
/**
* 构造函数
* @param k 第k大元素
* @param nums 初始数据流
*/
public KthLargest(int k, int[] nums) {
if (k <= 0) {
throw new IllegalArgumentException("k must be positive");
}
heap = new MinHeap(k); // 创建容量为k的最小堆
for (int num : nums) {
add(num); // 将初始数据加入堆
}
}
/**
* 向数据流中添加元素并返回第k大元素
* @param val 新元素
* @return 当前第k大元素
*/
public int add(int val) {
if (!heap.isFull()) {
heap.offer(val); // 堆未满时直接添加
} else if (val > heap.peek()) {
heap.replace(val); // 新元素更大时替换堆顶
}
return heap.peek(); // 返回当前第k大元素
}
}
// ------------------------ E4: LeetCode 295 ------------------------
/**
* 数据流的中位数(LeetCode 295)
* <p>
* 数据结构设计:
* - 左堆:最大堆,存储较小的一半元素
* - 右堆:最小堆,存储较大的一半元素
* - 两堆大小差不超过1,左堆元素数≥右堆
*/
class MedianFinder {
private MaxHeap left; // 最大堆,存储较小一半元素
private MinHeap right; // 最小堆,存储较大一半元素
/**
* 构造函数
*/
public MedianFinder() {
left = new MaxHeap(); // 最大堆
right = new MinHeap(); // 最小堆
}
/**
* 向数据流中添加数字
* <p>
* 平衡策略:
* - 两堆大小相等时,新元素加入右堆,然后右堆顶移到左堆
* - 两堆大小不等时,新元素加入左堆,然后左堆顶移到右堆
*/
public void addNum(int num) {
if (left.size() == right.size()) {
right.offer(num);
left.offer(right.poll());
} else {
left.offer(num);
right.offer(left.poll());
}
}
/**
* 获取当前数据流的中位数
* <p>
* 中位数规则:
* - 两堆大小相等时,取两堆顶的平均值
* - 两堆大小不等时,取左堆顶(左堆元素数≥右堆)
*/
public double findMedian() {
if (left.size() == right.size()) {
return (left.peek() + right.peek()) / 2.0;
} else {
return left.peek();
}
}
}
// ------------------------ 最大堆实现 ------------------------
class MaxHeap {
private int[] array; // 堆数组
private int size; // 堆中元素数量
public MaxHeap() {
this.array = new int[10];
this.size = 0;
}
// 向堆中添加元素
public void offer(int val) {
if (size == array.length) {
resize(); // 堆满时扩容
}
array[size] = val;
up(size++);
}
// 移除并返回堆顶元素
public int poll() {
if (size == 0) {
throw new IllegalStateException("Heap is empty");
}
int top = array[0];
array[0] = array[--size];
down(0);
return top;
}
// 获取堆顶元素
public int peek() {
if (size == 0) {
throw new IllegalStateException("Heap is empty");
}
return array[0];
}
// 获取堆中元素数量
public int size() {
return size;
}
// 上浮调整
private void up(int index) {
int parent = (index - 1) / 2;
while (index > 0 && array[parent] < array[index]) {
swap(parent, index);
index = parent;
parent = (index - 1) / 2;
}
}
// 下沉调整
private void down(int index) {
int left = 2 * index + 1;
while (left < size) {
int right = left + 1;
int largest = left;
// 找到左右子节点中较大的
if (right < size && array[right] > array[left]) {
largest = right;
}
// 如果父节点已经是最大的,无需调整
if (array[index] >= array[largest]) {
break;
}
swap(index, largest);
index = largest;
left = 2 * index + 1;
}
}
// 交换数组元素
private void swap(int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 堆扩容
private void resize() {
int newCapacity = size * 2;
array = Arrays.copyOf(array, newCapacity);
}
}
}
```
## 二叉树
```java
package com.itheima.BinaryTree;
// 二叉树节点遍历工具类,包含递归和非递归(栈实现)的前序、中序、后序遍历
// 内部集成链表栈 LinkedListStack,无需外部导包
public class TreeNodeTravel {
// 二叉树节点类:存储节点值、左子树、右子树
static class TreeNode {
String val;
TreeNode left;
TreeNode right;
// 无参构造
TreeNode() {}
// 带值构造
TreeNode(String val) {
this.val = val;
}
// 带值、左右子树构造
TreeNode(String val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
// 重写 toString,简化节点值打印
@Override
public String toString() {
return this.val;
}
}
// ===================== 内部类:链表实现的栈 =====================
// 作用:为二叉树遍历提供栈支持,避免外部导包依赖
private static class LinkedListStack<E> {
// 栈节点类:存储元素和下一个节点引用
private static class Node<E> {
E item; // 当前节点存储的元素
Node<E> next; // 指向下一个节点的引用
Node(E item, Node<E> next) {
this.item = item;
this.next = next;
}
}
private Node<E> top; // 栈顶指针(链表头节点)
private int size; // 栈中元素数量
private final int capacity; // 栈容量限制(默认无限制)
// 无参构造:默认容量设为 Integer.MAX_VALUE(表示不限制容量)
public LinkedListStack() {
this.capacity = Integer.MAX_VALUE;
}
// 带容量构造:指定栈最大容量
public LinkedListStack(int capacity) {
this.capacity = capacity;
}
// 入栈操作:将元素压入栈顶
public void push(E item) {
// 检查容量限制
if (size < capacity) {
// 新节点成为新栈顶(链表头插法)
top = new Node<>(item, top);
size++;
} else {
// 容量不足时可扩展逻辑(如抛异常、返回布尔值),这里简单打印提示
System.out.println("栈已满,无法入栈:" + item);
}
}
// 出栈操作:弹出并返回栈顶元素
public E pop() {
// 栈空时返回 null(可扩展抛异常)
if (isEmpty()) {
return null;
}
E item = top.item; // 取栈顶元素
top = top.next; // 栈顶指针后移(链表头节点变更)
size--; // 元素数量减少
return item;
}
// 查看栈顶元素:不弹出栈顶
public E peek() {
return isEmpty() ? null : top.item;
}
// 判断栈是否为空
public boolean isEmpty() {
return size == 0;
}
}
// ===================== 内部类:链表栈结束 =====================
// ===================== 二叉树遍历方法 =====================
// 1. 递归 - 前序遍历(根 -> 左 -> 右)
static void preOrder(TreeNode node) {
// 递归终止条件:节点为空
if (node == null) {
return;
}
// 访问当前节点(打印值)
System.out.print(node.val + " ");
// 递归遍历左子树
preOrder(node.left);
// 递归遍历右子树
preOrder(node.right);
}
// 2. 递归 - 中序遍历(左 -> 根 -> 右)
static void inOrder(TreeNode node) {
if (node == null) {
return;
}
// 先递归遍历左子树
inOrder(node.left);
// 访问当前节点
System.out.print(node.val + " ");
// 递归遍历右子树
inOrder(node.right);
}
// 3. 递归 - 后序遍历(左 -> 右 -> 根)
static void postOrder(TreeNode node) {
if (node == null) {
return;
}
// 递归遍历左子树
postOrder(node.left);
// 递归遍历右子树
postOrder(node.right);
// 访问当前节点
System.out.print(node.val + " ");
}
// 4. 非递归 - 前序遍历(栈实现:根 -> 左 -> 右)
static void preOrder2(TreeNode root) {
if (root == null) {
return;
}
// 创建栈(容量无限制)
LinkedListStack<TreeNode> stack = new LinkedListStack<>();
// 根节点入栈
stack.push(root);
// 栈不为空时循环
while (!stack.isEmpty()) {
// 弹出栈顶节点(当前访问节点)
TreeNode node = stack.pop();
System.out.print(node.val + " ");
// 【关键】右子树先入栈(利用栈 "后进先出" 特性,保证左子树先处理)
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
}
// 5. 非递归 - 中序遍历(栈实现:左 -> 根 -> 右)
static void inOrder2(TreeNode root) {
if (root == null) {
return;
}
LinkedListStack<TreeNode> stack = new LinkedListStack<>();
TreeNode cur = root; // 当前遍历节点
// 条件:当前节点非空 或 栈非空(还有节点未处理)
while (cur != null || !stack.isEmpty()) {
// 1. 一路向左入栈
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
// 2. 弹出栈顶(最左节点,即当前要访问的节点)
TreeNode node = stack.pop();
System.out.print(node.val + " ");
// 3. 处理右子树
cur = node.right;
}
}
// 6. 非递归 - 后序遍历(栈实现:左 -> 右 -> 根)
static void postOrder2(TreeNode root) {
if (root == null) {
return;
}
LinkedListStack<TreeNode> stack = new LinkedListStack<>();
TreeNode cur = root; // 当前遍历节点
TreeNode lastVisit = null; // 记录上一次访问的节点(避免重复处理右子树)
while (cur != null || !stack.isEmpty()) {
// 1. 一路向左入栈
if (cur != null) {
stack.push(cur);
cur = cur.left;
} else {
// 2. 查看栈顶(不弹出)
TreeNode peekNode = stack.peek();
// 3. 若右子树存在且未被访问过:处理右子树
if (peekNode.right != null && peekNode.right != lastVisit) {
cur = peekNode.right;
} else {
// 4. 弹出并访问节点(左右子树已处理完毕)
TreeNode node = stack.pop();
System.out.print(node.val + " ");
lastVisit = node; // 标记为已访问
cur = null; // 重置当前节点,继续处理栈中元素
}
}
}
}
// ===================== 测试用例(可选) =====================
public static void main(String[] args) {
// 构建测试二叉树
TreeNode root = new TreeNode("A",
new TreeNode("B",
new TreeNode("D"),
new TreeNode("E")
),
new TreeNode("C",
new TreeNode("F"),
new TreeNode("G")
)
);
System.out.println("递归前序遍历:");
preOrder(root); // A B D E C F G
System.out.println("\n非递归前序遍历:");
preOrder2(root); // A B D E C F G
System.out.println("\n递归中序遍历:");
inOrder(root); // D B E A F C G
System.out.println("\n非递归中序遍历:");
inOrder2(root); // D B E A F C G
System.out.println("\n递归后序遍历:");
postOrder(root); // D E B F G C A
System.out.println("\n非递归后序遍历:");
postOrder2(root); // D E B F G C A
}
}
```
### 题
#### E01. 前序遍历二叉树-Leetcode 144
#### E02. 中序遍历二叉树-Leetcode 94
#### E03. 后序遍历 145
同上
#### E04.对称二叉树101
```java
package com.itheima.BinaryTree;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
/**
* 判断二叉树是否对称
* 对称条件:树的左右子树镜像对称
*/
public class SymmetricTree {
/**
* 二叉树节点定义
*/
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
/**
* 递归方法:判断二叉树是否对称
* @param root 树根节点
* @return 对称返回true,否则返回false
*/
public boolean isSymmetricRecursive(TreeNode root) {
if (root == null) {
return true; // 空树视为对称
}
return isMirror(root.left, root.right);
}
/**
* 递归辅助方法:判断两个节点是否镜像对称
* @param left 左子树节点
* @param right 右子树节点
* @return 对称返回true,否则返回false
*/
private boolean isMirror(TreeNode left, TreeNode right) {
// 递归终止条件:两个节点都为空
if (left == null && right == null) {
return true;
}
// 一个为空另一个不为空,不对称
if (left == null || right == null) {
return false;
}
// 值不相等,不对称
if (left.val != right.val) {
return false;
}
// 递归检查子树:左的左子树与右的右子树对称,且左的右子树与右的左子树对称
return isMirror(left.left, right.right) && isMirror(left.right, right.left);
}
/**
* 迭代方法(队列实现):判断二叉树是否对称
* @param root 树根节点
* @return 对称返回true,否则返回false
*/
public boolean isSymmetricIterative(TreeNode root) {
if (root == null) {
return true; // 空树视为对称
}
Queue<TreeNode> queue = new LinkedList<>();
// 初始将左右子树入队
queue.offer(root.left);
queue.offer(root.right);
while (!queue.isEmpty()) {
// 每次取出两个节点进行比较
TreeNode left = queue.poll();
TreeNode right = queue.poll();
// 两个节点都为空,跳过当前循环
if (left == null && right == null) {
continue;
}
// 一个为空另一个不为空,不对称
if (left == null || right == null) {
return false;
}
// 值不相等,不对称
if (left.val != right.val) {
return false;
}
// 按镜像顺序将子节点入队
queue.offer(left.left); // 左子树的左子节点
queue.offer(right.right); // 右子树的右子节点
queue.offer(left.right); // 左子树的右子节点
queue.offer(right.left); // 右子树的左子节点
}
return true; // 所有节点检查完毕,树对称
}
/**
* 迭代方法(栈实现):判断二叉树是否对称
* @param root 树根节点
* @return 对称返回true,否则返回false
*/
public boolean isSymmestricwithStack(TreeNode root) {
if (root == null) {
return true; // 空树视为对称
}
Stack<TreeNode> stack = new Stack<>();
// 初始将左右子树压栈
stack.push(root.left);
stack.push(root.right);
while (!stack.isEmpty()) {
// 每次弹出两个节点进行比较
TreeNode right = stack.pop();
TreeNode left = stack.pop();
// 两个节点都为空,跳过当前循环
if (left == null && right == null) {
continue;
}
// 一个为空另一个不为空,不对称
if (left == null || right == null) {
return false;
}
// 值不相等,不对称
if (left.val != right.val) {
return false;
}
// 按镜像顺序将子节点压栈
stack.push(left.left); // 左子树的左子节点
stack.push(right.right); // 右子树的右子节点
stack.push(left.right); // 左子树的右子节点
stack.push(right.left); // 右子树的左子节点
}
return true; // 所有节点检查完毕,树对称
}
}
```
> ### 代码逻辑说明
>
> 这个程序提供了三种判断二叉树是否对称的方法:
>
> 1. **递归方法**:
> - 核心思路:检查根节点的左右子树是否镜像对称
> - 递归过程:比较左子树的左节点与右子树的右节点,以及左子树的右节点与右子树的左节点
> 2. **队列迭代方法**:
> - 使用队列存储待比较的节点对
> - 每次从队列取出两个节点进行比较
> - 按镜像顺序将子节点对入队(左左 - 右右,左右 - 右左)
> 3. **栈迭代方法**:
> - 使用栈存储待比较的节点对
> - 每次从栈弹出两个节点进行比较
> - 按镜像顺序将子节点对压栈(与队列逻辑相同)
>
> 三种方法的时间复杂度均为 O (n),空间复杂度最坏情况下为 O (n),适用于不同的应用场景。递归方法代码简洁,迭代方法则避免了栈溢出的风险。
#### E05求最大深度 104
```java
// E05 二叉树最大深度 LeetCode 104
public class MaxDepthOfBinaryTree {
// 二叉树节点定义
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
/**
* 递归方法:计算二叉树的最大深度
* 核心思路:后序遍历思想,深度 = max(左子树深度, 右子树深度) + 1
* 时间复杂度:O(n),每个节点访问一次
* 空间复杂度:O(h),递归栈深度(h为树高)
*/
public int maxDepth(TreeNode node) {
// 终止条件:空节点深度为0
if (node == null) {
return 0;
}
// 递归计算左子树深度
int leftDepth = maxDepth(node.left);
// 递归计算右子树深度
int rightDepth = maxDepth(node.right);
// 当前节点深度为左右子树深度的最大值加1
return Integer.max(leftDepth, rightDepth) + 1;
}
/**
* 非递归方法:基于后序遍历计算二叉树最大深度
* 核心思路:用栈模拟递归过程,记录遍历路径深度
* 时间复杂度:O(n),每个节点访问一次
* 空间复杂度:O(h),栈空间(h为树高)
*/
public int maxDepth2(TreeNode root) {
if (root == null) return 0;
TreeNode curr = root; // 当前遍历节点
LinkedList<TreeNode> stack = new LinkedList<>(); // 模拟递归栈
int maxDepth = 0; // 记录最大深度
TreeNode lastVisited = null; // 记录上一个访问的节点(用于后序遍历判断)
while (curr != null || !stack.isEmpty()) {
// 1. 向左子树深入,同时更新当前路径深度
if (curr != null) {
stack.push(curr);
// 栈的大小即当前路径深度,更新最大值
if (stack.size() > maxDepth) {
maxDepth = stack.size();
}
curr = curr.left;
} else {
// 2. 查看栈顶节点(未访问过的右子树)
TreeNode peek = stack.peek();
// 条件1:右子树为空 或 右子树已访问过(后序遍历完成)
if (peek.right == null || peek.right == lastVisited) {
// 弹出节点并标记为已访问
lastVisited = stack.pop();
} else {
// 条件2:右子树未访问,转向右子树
curr = peek.right;
}
}
}
return maxDepth;
}
}
```