快速掌握Java线性数据结构:数组、链表、栈与队列
概率型数据结构:快速掌握Java概率型数据结构:布隆过滤器
一、引子
数据结构是算法的基石,更是高效解决实际问题的关键工具。本文详解Java中五大核心线性数据结构:数组(Array)、链表(Linked List)、栈(Stack)、队列(Queue)和双端队列(Deque)。通过代码实例、时间复杂度分析和应用场景对比,助你掌握不同场景下的最优选择策略。
二、 数组 (Array)
概念: 数组是最基础且重要的线性数据结构。它在内存中分配连续的存储空间,用于存储相同类型元素的集合。数组通过从0
开始的整数索引直接访问任意元素。
Java实现核心: Java 提供了内置的数组语法(int[] arr
)以及功能更强大的 java.util.ArrayList
类(基于数组的动态扩容实现)。
-
基本数组声明与初始化:
// 声明并初始化一个固定大小的整型数组 int[] staticArray = new int[5]; // 初始化为默认值0 staticArray[0] = 10; // 赋值 staticArray[1] = 20; // 声明并直接初始化 String[] names = {"Alice", "Bob", "Charlie"};
-
ArrayList
(动态数组):import java.util.ArrayList; // 创建一个存储整数的动态数组 ArrayList<Integer> dynamicArray = new ArrayList<>(); // 添加元素 (自动扩容) dynamicArray.add(10); // O(1) 摊销时间复杂度 dynamicArray.add(20); dynamicArray.add(1, 15); // 在索引1处插入15, 后续元素后移 O(n) // 访问元素 int firstElement = dynamicArray.get(0); // O(1) // 修改元素 dynamicArray.set(1, 25); // O(1) // 删除元素 (移除索引1处的元素) dynamicArray.remove(1); // 后续元素前移 O(n) // 获取大小 int size = dynamicArray.size(); // O(1)
核心操作时间复杂度:
- 访问 (Access by Index):
O(1)
- 直接计算内存地址偏移。 - 搜索 (Search):
O(n)
- 最坏情况下需要遍历整个数组(无序时)。 - 插入 (Insertion):
- 在末尾插入 (
add(elem)
):O(1)
摊销 (Amortized) - 大多数情况下很快,扩容时O(n)
。 - 在指定索引插入 (
add(index, elem)
):O(n)
- 需要移动后续元素。
- 在末尾插入 (
- 删除 (Deletion):
- 删除末尾元素:
O(1)
。 - 删除指定索引元素 (
remove(index)
):O(n)
- 需要移动后续元素。
- 删除末尾元素:
特点与适用场景:
- 优点: 随机访问极快;内存连续,缓存友好(Cache Locality);实现简单。
- 缺点: 大小固定(基本数组)或扩容有成本(
ArrayList
);在非末尾位置插入/删除效率低(需要移动元素)。 - 典型应用:
- 存储已知大小或变化不大的数据集。
- 需要频繁随机访问元素的场景(如通过下标获取)。
- 作为其他数据结构(如堆、哈希表)的基础实现。
- 多维数组表示矩阵、图像像素等。
三、 链表 (Linked List)
概念: 链表由一系列结点 (Node) 组成,每个结点包含数据域 (data
) 和指向下一个结点地址的指针域 (next
)。结点在内存中不必连续存储。链表主要有两种基本形式:单向链表 (Singly Linked List) 和双向链表 (Doubly Linked List)。
Java实现核心: Java 标准库提供了 java.util.LinkedList
(基于双向链表)。理解链表原理通常需要手动实现结点类。
-
结点类定义 (单向链表):
class ListNode<T> { T data; // 存储的数据 ListNode<T> next; // 指向下一个结点的引用 ListNode(T data) { this.data = data; this.next = null; } }
-
手动实现链表基本操作示例:
public class SinglyLinkedList<T> { private ListNode<T> head; // 头结点引用 private int size; // 在链表头部插入 public void addFirst(T data) { ListNode<T> newNode = new ListNode<>(data); newNode.next = head; head = newNode; size++; } // O(1) // 在链表尾部插入 (需遍历找到尾结点) public void addLast(T data) { ListNode<T> newNode = new ListNode<>(data); if (head == null) { head = newNode; } else { ListNode<T> current = head; while (current.next != null) { current = current.next; } current.next = newNode; } size++; } // O(n) // 查找元素 (按值) public boolean contains(T data) { ListNode<T> current = head; while (current != null) { if (current.data.equals(data)) { return true; } current = current.next; } return false; } // O(n) // 删除第一个匹配项 (按值) public boolean remove(T data) { if (head == null) return false; if (head.data.equals(data)) { head = head.next; size--; return true; } ListNode<T> prev = head; ListNode<T> current = head.next; while (current != null) { if (current.data.equals(data)) { prev.next = current.next; size--; return true; } prev = current; current = current.next; } return false; } // O(n) }
-
使用
LinkedList
:import java.util.LinkedList; LinkedList<String> list = new LinkedList<>(); // 添加元素 list.add("Apple"); // 尾部添加 O(1) list.addFirst("Banana"); // 头部添加 O(1) list.addLast("Cherry"); // 尾部添加 O(1) list.add(1, "Orange"); // 指定索引插入 O(n) // 访问元素 String first = list.getFirst(); // O(1) String last = list.getLast(); // O(1) String elem = list.get(2); // O(n) - 需要遍历 // 删除元素 list.removeFirst(); // O(1) list.removeLast(); // O(1) list.remove("Orange"); // O(n) - 按值查找 list.remove(0); // O(1) - 移除头/尾是O(1), 中间是O(n)
核心操作时间复杂度 (双向链表 LinkedList
):
- 访问 (Access by Index):
O(n)
- 需要从头或尾(取决于哪个更近)遍历。 - 搜索 (Search):
O(n)
- 需要遍历。 - 插入 (Insertion):
- 在头部 (
addFirst
,push
):O(1)
- 在尾部 (
addLast
,add
):O(1)
- 在指定索引 (
add(index, elem)
):O(n)
- 需要遍历到该位置。
- 在头部 (
- 删除 (Deletion):
- 删除头部 (
removeFirst
,pop
,poll
):O(1)
- 删除尾部 (
removeLast
):O(1)
- 删除指定索引 (
remove(index)
):O(n)
- 需要遍历到该位置。 - 删除指定元素 (
remove(Object)
):O(n)
- 需要查找。
- 删除头部 (
特点与适用场景:
- 优点: 动态大小,插入/删除操作(尤其在头尾)效率高(
O(1)
);不需要连续内存空间。 - 缺点: 随机访问慢(
O(n)
);需要额外空间存储指针;缓存局部性较差。 - 典型应用:
- 实现栈、队列、双端队列等抽象数据类型。
- 需要频繁在列表头部或尾部进行插入/删除的场景。
- 内存分配管理(如操作系统内存池)。
- 实现图结构的邻接表表示法。
LinkedList
在需要频繁操作头尾或作为队列/栈使用时效率很高。
四、 栈 (Stack)
概念: 栈是一种遵循 后进先出 (Last-In-First-Out, LIFO) 原则的线性数据结构。元素的添加(压栈,Push)和移除(弹栈,Pop)操作都发生在同一端,称为栈顶 (Top)。另一端称为栈底 (Bottom)。
Java实现核心: Java 提供了 java.util.Stack
类(继承自 Vector
,线程安全但通常不推荐),更常用的是 java.util.Deque
接口的实现类(如 ArrayDeque
, LinkedList
)来模拟栈,因为它们提供了更高效和一致的 API。
-
使用
Deque
作为栈 (推荐):import java.util.ArrayDeque; import java.util.Deque; Deque<Integer> stack = new ArrayDeque<>(); // 通常用ArrayDeque, 效率高 // 压栈 (Push) stack.push(10); // 或 stack.addFirst(10) stack.push(20); stack.push(30); // 查看栈顶元素 (Peek) - 不移除 int top = stack.peek(); // 返回30, 栈不变 System.out.println