一、数组(Array)与动态数组(ArrayList)
数组是连续内存存储的同类型元素集合,ArrayList 是动态扩容的数组实现。
1. 基本数组(固定长度)
声明与初始化:
java
// 声明方式1:指定长度
int[] intArr = new int[5]; // 初始值为0
String[] strArr = new String[3]; // 初始值为null
// 声明方式2:直接赋值
int[] nums = {1, 2, 3, 4};
核心操作:
java
// 访问元素(通过索引,O(1))
int val = nums[0]; // 获取第一个元素
// 修改元素
nums[1] = 10; // 将索引1的元素改为10
//获取长度
int len = nums.length
// 遍历
// 方式1:for循环
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
// 方式2:增强for循环
for (int num : nums) {
System.out.println(num);
}
2. 动态数组(ArrayList)
声明与初始化:
java
// 声明泛型集合(只能存引用类型,基本类型需用包装类)
List<Integer> list = new ArrayList<>(); // 初始容量为10
List<String> strList = new ArrayList<>(Arrays.asList("a", "b")); // 初始化并赋值
核心操作:
java
// 添加元素(尾部添加,O(1))
list.add(1); // [1]
list.add(2); // [1, 2]
// 指定位置插入(O(n),需移动后续元素)
list.add(1, 3); // [1, 3, 2]
// 访问元素(O(1))
int first = list.get(0); // 1
// 修改元素(O(1))
list.set(0, 10); // [10, 3, 2]
// 删除元素(O(n))
list.remove(1); // 删除索引1的元素 → [10, 2]
list.remove(Integer.valueOf(2)); // 删除值为2的元素 → [10]
// 查找元素位置(O(n))
int index = list.indexOf(10); // 0(不存在返回-1)
//获取长度
int len = list.size
// 遍历
for (int num : list) {
System.out.println(num);
}
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
适用场景:需要频繁访问元素、已知大致长度的场景(如存储查询结果)。
数组列表的容量与数组的大小有一个非常重要的区别。如果分配一个有100个元素的数组,数组就有100个空位置(槽)可以使用。而容量为100个元素的数组列表只是可能保存100个元素(实际上也可以超过100,不过要以重新分配空间为代价),但是在一开始,甚至完成初始化构造之后,数组列表并不包含任何元素。
-
对于数组
new Employee[100]
:- 它确实有 100 个 "槽位",每个位置都可以直接存放元素(初始为 null)
- 你可以直接通过
array[0]
到array[99]
访问或赋值 - 数组的长度是固定的,不能改变
-
对于
new ArrayList<>(100)
:- 这里的 100 是 "容量",表示内部数组的初始长度为 100
- 但ArrayList 在初始化后仍然是空的(size=0),没有任何元素
- 这些 "槽位" 是 ArrayList 内部管理的缓冲空间,用户不能直接访问
- 只有当你调用
add()
方法时,才会真正填充元素,size 才会增加 - 当元素数量超过容量时,ArrayList 会自动扩容(创建更大的新数组,复制元素)
二、链表(LinkedList)
链表通过节点引用连接,内存不连续,适合频繁插入 / 删除的场景。Java 中 LinkedList
是双向链表实现,也可自定义单向链表。
1. 自定义单向链表
节点定义:
java
class ListNode {
int val; // 节点值
ListNode next; // 下一个节点引用
// 构造方法
ListNode(int x) {
val = x;
next = null;
}
}
核心操作:
java
// 1. 创建链表(1 → 2 → 3)
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
// 2. 遍历链表
ListNode cur = head;
while (cur != null) {
System.out.println(cur.val); // 输出1,2,3
cur = cur.next;
}
// 3. 插入节点(在2后面插入4)
ListNode newNode = new ListNode(4);
newNode.next = head.next.next; // 4 → 3
head.next.next = newNode; // 2 → 4 → 3 → null
// 4. 删除节点(删除4)
head.next.next = head.next.next.next; // 2 → 3 → null
// 5. 反转链表(迭代法)
ListNode reverse(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next; // 保存下一个节点
cur.next = prev; // 反转当前节点指针
prev = cur; // 移动prev
cur = next; // 移动cur
}
return prev; // 新头节点
}
2. Java 内置 LinkedList(双向链表)
LinkedList
是 java.util.LinkedList
类的实例,它实现了 List 接口和 Deque 接口,因此既支持普通列表的增删改查操作,也支持双端队列的特性(如首尾快速操作)。以下是其核心增删改查操作的详细说明,结合方法示例和特性分析:
声明与初始化:
java
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
特有操作(与 ArrayList 相比):
java
// 头部/尾部操作(O(1))
linkedList.addFirst(0); // 头部添加 → [0,1,2]
linkedList.addLast(3); // 尾部添加 → [0,1,2,3]
linkedList.removeFirst(); // 删除头部 → [1,2,3]
linkedList.removeLast(); // 删除尾部 → [1,2]
// 获取首尾元素
int first = linkedList.getFirst(); // 1
int last = linkedList.getLast(); // 2
增删改查操作详解
以下操作均以 LinkedList<String> list = new LinkedList<>();
为基础示例。
1. 增(添加元素)
LinkedList
提供多种添加元素的方法,覆盖 "尾部添加""指定索引添加""首尾快速添加" 等场景:
方法 | 功能描述 | 示例代码 | 注意事项 |
---|---|---|---|
boolean add(E e) |
向链表尾部 添加元素(继承自 List ),成功返回 true |
list.add("apple"); |
通用添加方式,时间复杂度 O (1)(双向链表尾部有指针,直接定位) |
void add(int index, E e) |
向指定索引位置插入元素 | list.add(1, "banana"); |
需先遍历到索引 index ,时间复杂度 O (n);插入后后续元素的索引自动后移 |
void addFirst(E e) |
向链表头部 添加元素(继承自 Deque ) |
list.addFirst("orange"); |
尾部添加的互补操作,时间复杂度 O (1)(直接操作头部指针) |
void addLast(E e) |
向链表尾部 添加元素(同 add(E e) ,继承自 Deque ) |
list.addLast("grape"); |
与 add(E e) 功能一致,属于双端队列特性的显式写法 |
boolean offer(E e) |
向尾部添加元素(同 add(E e) ,继承自 Queue ),成功返回 true |
list.offer("mango"); |
队列风格的添加方式,本质与 add 无区别(LinkedList 无容量限制,永远成功) |
boolean offerFirst(E e) |
向头部添加元素(同 addFirst ,队列风格) |
list.offerFirst("pear"); |
与 addFirst 功能一致,返回 true (无容量限制,永远成功) |
2. 删(删除元素)
支持 "按索引删""按元素删""首尾快速删",同样利用双向链表的结构优势:
方法 | 功能描述 | 示例代码 | 注意事项 |
---|---|---|---|
E remove(int index) |
删除指定索引的元素,返回被删除的元素 | String removed = list.remove(0); |
需遍历到索引 index ,时间复杂度 O (n);删除后后续元素索引自动前移 |
boolean remove(Object o) |
删除首次出现 的指定元素(需重写 equals 方法判断相等),成功返回 true |
list.remove("banana"); |
需遍历查找元素,时间复杂度 O (n);若元素不存在,返回 false |
E removeFirst() |
删除并返回头部 元素(继承自 Deque ) |
String first = list.removeFirst(); |
时间复杂度 O (1);若链表为空,抛出 NoSuchElementException |
E removeLast() |
删除并返回尾部 元素(继承自 Deque ) |
String last = list.removeLast(); |
时间复杂度 O (1);若链表为空,抛出 NoSuchElementException |
E poll() |
删除并返回头部元素(队列风格,继承自 Queue ) |
String head = list.poll(); |
与 removeFirst 功能类似,但链表为空时返回 null (不抛异常) |
E pollFirst() |
同 poll() ,删除并返回头部元素 |
String head = list.pollFirst(); |
双端队列风格的写法,空列表返回 null |
E pollLast() |
删除并返回尾部元素 | String tail = list.pollLast(); |
空列表返回 null ,时间复杂度 O (1) |
3. 改(修改元素)
仅支持 "按索引修改",因为链表无随机访问特性,需先定位到索引:
方法 | 功能描述 | 示例代码 | 注意事项 |
---|---|---|---|
E set(int index, E e) |
将指定索引 位置的元素替换为 e ,返回被替换的旧元素 |
String old = list.set(1, "pear"); |
需先遍历到索引 index ,时间复杂度 O (n);若索引越界,抛出 IndexOutOfBoundsException |
4. 查(查询元素)
支持 "按索引查""按元素查""查首尾元素""遍历查询" 等:
方法 | 功能描述 | 示例代码 | 注意事项 |
---|---|---|---|
E get(int index) |
获取指定索引位置的元素 | String elem = list.get(2); |
需遍历到索引 index ,时间复杂度 O (n);索引越界抛异常 |
E getFirst() |
获取头部元素(不删除) | String first = list.getFirst(); |
时间复杂度 O (1);空列表抛 NoSuchElementException |
E getLast() |
获取尾部元素(不删除) | String last = list.getLast(); |
时间复杂度 O (1);空列表抛 NoSuchElementException |
E peek() |
获取头部元素(队列风格,不删除) | String head = list.peek(); |
与 getFirst() 类似,但空列表返回 null (不抛异常) |
E peekFirst() |
同 peek() ,获取头部元素 |
String head = list.peekFirst(); |
双端队列风格写法,空列表返回 null |
E peekLast() |
获取尾部元素 | String tail = list.peekLast(); |
空列表返回 null ,时间复杂度 O (1) |
int indexOf(Object o) |
返回首次出现 元素 o 的索引,不存在则返回 -1 |
int idx = list.indexOf("apple"); |
需从头部遍历查找,时间复杂度 O (n);依赖 equals 方法判断 |
int lastIndexOf(Object o) |
返回最后出现 元素 o 的索引,不存在则返回 -1 |
int idx = list.lastIndexOf("apple"); |
需从尾部遍历查找,时间复杂度 O (n) |
boolean contains(Object o) |
判断列表是否包含元素 o ,包含返回 true |
boolean has = list.contains("banana"); |
本质调用 indexOf(o) != -1 ,时间复杂度 O (n) |
适用场景:频繁在首尾插入 / 删除(如实现队列、栈)、未知长度的动态数据。
三、哈希表(HashMap)
基于键值对存储,通过哈希函数快速定位元素,查询 / 插入 / 删除平均 O (1)。
声明与初始化
java
// 声明泛型哈希表(key和value类型需指定)
Map<String, Integer> map = new HashMap<>();
// 初始化并赋值
Map<Integer, String> initMap = new HashMap<>() {{
put(1, "a");
put(2, "b");
}};
核心操作
java
// 添加键值对(若key已存在,会覆盖value)
map.put("apple", 5);
map.put("banana", 3);
// 获取value(key不存在返回null)
int count = map.get("apple"); // 5
// 判断key是否存在
boolean hasApple = map.containsKey("apple"); // true
// 判断value是否存在
boolean hasValue3 = map.containsValue(3); // true
// 删除键值对
map.remove("banana"); // 删除key为"banana"的条目
// 获取所有key/value
Set<String> keys = map.keySet(); // [apple]
Collection<Integer> values = map.values(); // [5]
// 遍历(推荐entrySet,同时获取key和value)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
int value = entry.getValue();
System.out.println(key + ":" + value);
}
适用场景:计数(如统计字符出现次数)、缓存(存储键值映射)、快速查找(通过 key 定位 value)。
四、栈(Stack/Deque)
栈是 "后进先出"(LIFO)结构,Java 推荐用 Deque
接口的 ArrayDeque
实现(Stack
类已过时)。
声明与核心操作
java
// 声明栈(Deque的push/pop/peek对应栈操作)
Deque<Integer> stack = new ArrayDeque<>();
// 入栈(压栈,O(1))
stack.push(1);
stack.push(2); // 栈顶为2 → [1,2]
// 出栈(弹栈顶元素,O(1))
int top = stack.pop(); // 返回2,栈变为[1]
// 查看栈顶元素(不弹出,O(1))
int peek = stack.peek(); // 1
// 判断是否为空
boolean isEmpty = stack.isEmpty(); // false
// 遍历(需弹出所有元素,会清空栈)
while (!stack.isEmpty()) {
System.out.println(stack.pop());
}
适用场景:括号匹配、表达式求值、回溯算法(记录路径)。
五、队列(Queue)
队列是 "先进先出"(FIFO)结构,常用 LinkedList
或 ArrayDeque
实现。
声明与核心操作
java
// 声明队列
Queue<Integer> queue = new LinkedList<>();
// 入队(尾部添加,O(1))
queue.offer(1);
queue.offer(2); // 队列:[1,2]
// 出队(头部删除,O(1))
int front = queue.poll(); // 返回1,队列变为[2]
// 查看队首元素(不删除,O(1))
int peek = queue.peek(); // 2
// 判断是否为空
boolean isEmpty = queue.isEmpty(); // false
// 遍历(需出队所有元素,会清空队列)
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
适用场景:BFS(广度优先搜索)、任务调度、缓冲队列。
六、树(二叉树)
二叉树由节点组成,每个节点最多有左右两个子节点,常用递归或迭代遍历。
节点定义
java
class TreeNode {
int val;
TreeNode left; // 左子节点
TreeNode right; // 右子节点
TreeNode(int x) {
val = x;
left = null;
right = null;
}
}
遍历方式(以如下二叉树为例)
plaintext
1
/ \
2 3
/ \
4 5
1. 深度优先遍历(DFS)
-
前序遍历(根 → 左 → 右):1 → 2 → 4 → 5 → 3
java// 递归实现 void preorder(TreeNode root) { if (root == null) return; System.out.print(root.val + " "); // 访问根 preorder(root.left); // 遍历左子树 preorder(root.right); // 遍历右子树 } // 迭代实现(用栈) void preorderIterative(TreeNode root) { if (root == null) return; Deque<TreeNode> stack = new ArrayDeque<>(); stack.push(root); while (!stack.isEmpty()) { TreeNode node = stack.pop(); System.out.print(node.val + " "); // 右子节点先入栈(栈是LIFO,保证左子树先处理) if (node.right != null) stack.push(node.right); if (node.left != null) stack.push(node.left); } }
-
中序遍历(左 → 根 → 右):4 → 2 → 5 → 1 → 3
java// 递归实现 void inorder(TreeNode root) { if (root == null) return; inorder(root.left); System.out.print(root.val + " "); inorder(root.right); }
-
后序遍历(左 → 右 → 根):4 → 5 → 2 → 3 → 1
java// 递归实现 void postorder(TreeNode root) { if (root == null) return; postorder(root.left); postorder(root.right); System.out.print(root.val + " "); }
2. 广度优先遍历(BFS,层序遍历)
按层级访问:1 → 2 → 3 → 4 → 5
java
void levelOrder(TreeNode root) {
if (root == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size(); // 当前层节点数
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
System.out.print(node.val + " ");
// 下一层节点入队
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}
}
适用场景:二叉搜索树(BST)操作、路径问题、子树判断。
补充1:
Collection
是 Java 集合框架的根接口,定义了所有集合(List
、Set
、Queue
等)的通用操作。它的子接口(如 List
、Set
、Queue
)则在其基础上扩展了各自特性相关的方法。以下是具体说明:
一、Collection
接口的核心方法
Collection
接口定义了所有集合共有的增删改查、判断、迭代等操作,主要包括:
方法分类 | 核心方法 | 功能描述 |
---|---|---|
添加元素 | boolean add(E e) |
向集合添加元素,成功返回 true (Set 会去重,List 允许重复) |
boolean addAll(Collection<? extends E> c) |
添加另一个集合中的所有元素,成功返回 true |
|
删除元素 | boolean remove(Object o) |
删除指定元素(首次出现的),成功返回 true |
boolean removeAll(Collection<?> c) |
删除两个集合的交集元素(即删除本集合中包含在 c 中的元素) |
|
boolean retainAll(Collection<?> c) |
保留两个集合的交集元素(删除本集合中不在 c 中的元素) |
|
void clear() |
清空集合中所有元素 | |
判断与查询 | int size() |
返回集合中元素的数量 |
boolean isEmpty() |
判断集合是否为空(size() == 0 ) |
|
boolean contains(Object o) |
判断集合是否包含指定元素 | |
boolean containsAll(Collection<?> c) |
判断集合是否包含另一个集合的所有元素 | |
转换与迭代 | Object[] toArray() |
将集合转换为数组 |
<T> T[] toArray(T[] a) |
将集合转换为指定类型的数组 | |
Iterator<E> iterator() |
返回用于遍历集合的迭代器(Iterator ) |
二、子接口新增的常用方法
Collection
的子接口在继承上述方法的基础上,根据自身特性扩展了专属方法:
1. List
接口(有序、可重复、可按索引访问)
List
最大的特点是支持索引操作,新增方法主要围绕索引展开:
核心方法 | 功能描述 |
---|---|
E get(int index) |
获取指定索引位置的元素 |
E set(int index, E element) |
替换指定索引位置的元素,返回被替换的旧元素 |
void add(int index, E element) |
在指定索引位置插入元素(后续元素自动后移) |
E remove(int index) |
删除指定索引位置的元素,返回被删除的元素 |
int indexOf(Object o) |
返回指定元素首次出现的索引(不存在返回 -1 ) |
int lastIndexOf(Object o) |
返回指定元素最后出现的索引(不存在返回 -1 ) |
List<E> subList(int fromIndex, int toIndex) |
返回从 fromIndex 到 toIndex (左闭右开)的子列表(视图,修改会影响原列表) |
ListIterator<E> listIterator() |
返回支持双向遍历、添加、修改元素的迭代器(ListIterator ) |
2. Set
接口(无序、不可重复)
Set
强调元素唯一性 ,没有新增太多方法(主要依赖 Collection
的基础方法),但部分实现类有特色方法:
核心方法(主要来自实现类) | 功能描述 |
---|---|
SortedSet 子接口:Comparator<? super E> comparator() |
返回用于排序的比较器(若为自然排序则返回 null ) |
SortedSet 子接口:E first() |
返回第一个元素(按排序规则) |
SortedSet 子接口:E last() |
返回最后一个元素(按排序规则) |
HashSet 无新增,LinkedHashSet 继承 HashSet 并维护插入顺序,无额外方法。 |
3. Queue
接口(队列,FIFO 先进先出)
Queue
侧重队列操作,新增方法围绕 "队首 / 队尾" 的添加、删除、查询:
核心方法 | 功能描述 |
---|---|
boolean offer(E e) |
向队尾添加元素(队列满时返回 false ,区别于 add() 的抛异常) |
E poll() |
移除并返回队首元素(队列为空时返回 null ,区别于 remove() 的抛异常) |
E peek() |
返回队首元素(不移除,队列为空时返回 null ,区别于 element() 的抛异常) |
E element() |
返回队首元素(不移除,队列为空时抛 NoSuchElementException ) |
boolean add(E e) |
向队尾添加元素(队列满时抛 IllegalStateException ) |
E remove() |
移除并返回队首元素(队列为空时抛 NoSuchElementException ) |
4. Deque
接口(双端队列,首尾均可操作)
Deque
继承 Queue
,支持首尾双向操作,新增方法如下:
核心方法 | 功能描述 |
---|---|
void addFirst(E e) |
向队首添加元素(满时抛异常) |
void addLast(E e) |
向队尾添加元素(满时抛异常) |
boolean offerFirst(E e) |
向队首添加元素(满时返回 false ) |
boolean offerLast(E e) |
向队尾添加元素(满时返回 false ) |
E removeFirst() |
移除并返回队首元素(空时抛异常) |
E removeLast() |
移除并返回队尾元素(空时抛异常) |
E pollFirst() |
移除并返回队首元素(空时返回 null ) |
E pollLast() |
移除并返回队尾元素(空时返回 null ) |
E getFirst() |
返回队首元素(不移除,空时抛异常) |
E getLast() |
返回队尾元素(不移除,空时抛异常) |
E peekFirst() |
返回队首元素(不移除,空时返回 null ) |
E peekLast() |
返回队尾元素(不移除,空时返回 null ) |
三、总结
Collection
定义了集合的通用行为(增删、判断、迭代等),是所有集合的基础。List
新增索引操作 (get(index)
、add(index)
等),适合有序、可重复场景。Set
强调唯一性,核心方法与Collection
一致,排序相关方法在SortedSet
中。Queue
/Deque
新增队列 / 双端队列操作 (offer()
、poll()
、peek()
等),适合先进先出或双向操作场景。
这些接口的设计体现了 "接口隔离原则"------ 每个子接口只扩展自身特性所需的方法,使集合框架更灵活。
补充2:
ArrayDeque
是 Java 集合框架中的一个双端队列(double-ended queue)实现,它之所以能同时用作队列(Queue) 和栈(Stack) ,核心原因是其底层设计支持首尾两端的高效操作 ,同时实现了 Deque
接口(该接口继承了 Queue
接口并扩展了栈所需的操作)。
一、ArrayDeque
的底层特性
ArrayDeque
基于动态数组实现(并非链表),但数组的头尾是 "循环" 的(类似环形缓冲区),这使得:
- 可以在头部(head) 和尾部(tail) 进行
O(1)
时间复杂度的添加 / 删除操作; - 无需像普通数组那样移动元素,因此首尾操作效率极高。
二、同时支持队列和栈的关键:Deque
接口的方法设计
ArrayDeque
实现了 Deque
接口,该接口融合了队列 和栈的操作规范:
1. 作为队列(FIFO,先进先出)使用
队列的核心操作是 "尾部添加、头部删除",Deque
提供了对应的方法:
- 入队 :
addLast(e)
或offerLast(e)=offer(e)
(向尾部添加元素); - 出队 :
removeFirst()
或pollFirst()
(从头部删除并返回元素); - 查看队首 :
getFirst()
或peekFirst()
(返回头部元素,不删除)。
这些方法完全符合队列的 "先进先出" 特性,例如:
Deque<Integer> queue = new ArrayDeque<>();
queue.offerLast(1); // 入队:[1]
queue.offerLast(2); // 入队:[1, 2]
queue.pollFirst(); // 出队:返回1,队列变为[2]
2. 作为栈(LIFO,后进先出)使用
栈的核心操作是 "头部添加、头部删除"(或 "尾部添加、尾部删除",本质是同一个端点),Deque
提供了对应的方法:
- 压栈 :
addFirst(e)
或push(e)
(向头部添加元素,push
是栈的专用方法); - 弹栈 :
removeFirst()
或pop()
(从头部删除并返回元素,pop
是栈的专用方法); - 查看栈顶 :
getFirst()
或peekFirst()
(返回头部元素,不删除)。
这些方法符合栈的 "后进先出" 特性,例如:
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 压栈:[1]
stack.push(2); // 压栈:[2, 1]
stack.pop(); // 弹栈:返回2,栈变为[1]
三、相比传统 Stack
类和 LinkedList
的优势
- 比
Stack
类更高效 :java.util.Stack
是古老的遗留类,基于 Vector 实现,方法加了同步锁(性能低),且设计上继承了Vector
(不符合单一职责原则);而ArrayDeque
是专门为双端操作设计的,无同步开销,效率更高。 - 比
LinkedList
更高效 :LinkedList
虽然也实现了Deque
,但底层是链表(节点有额外的前驱 / 后继指针开销),而ArrayDeque
基于数组,内存连续,缓存友好,大多数情况下操作速度更快。
总结
ArrayDeque
能同时作为队列和栈使用的核心原因是:
- 底层动态数组支持首尾两端的高效操作(
O(1)
时间复杂度); - 实现了
Deque
接口,该接口同时定义了队列(FIFO)和栈(LIFO)所需的操作方法; - 通过不同的方法组合(尾部操作 / 头部操作),自然适配两种数据结构的特性。
- 但是 ArrayDeque不是线程安全的,要想同步需要额外的外部操作