下列代码示例大部分由 Trae AI 生成。
1. 数组基本概念
1.1 定义与特性
定义:数组是存储在连续内存空间 中的相同类型元素的集合
核心特性:
- 元素类型相同
- 内存地址连续
- 通过索引随机访问
- 大小固定(静态数组):一旦数组被创建,它在内存中的容量(元素个数)就不能再改变。
数组必须存储在一块连续的内存区域里,且索引访问依赖固定的长度和线性寻址公式。扩容或缩容可能会破坏连续内存分配。容量信息 length 是对象创建时写死在 JVM 的数组头信息里,不能修改。
1.2 内存布局分析
css
数组在内存中的布局:
[元素0][元素1][元素2][元素3]...
↑ ↑ ↑ ↑
地址A 地址A+4 地址A+8 地址A+12 (假设int类型,每个元素4字节)
访问公式:元素地址 = 基地址 + 索引 × 元素大小
1.3 时间复杂度分析
操作 | 时间复杂度 | 说明 |
---|---|---|
访问 | O(1) | 通过索引直接计算地址 |
搜索 | O(n) | 需要遍历查找 |
插入 | O(n) | 需要移动后续元素 |
删除 | O(n) | 需要移动后续元素 |
1.4 一维数组操作详解
1.4.1 Java 中数组的创建和初始化
java
// 创建方式1:声明后分配空间
int[] arr1 = new int[5]; // 创建长度为5的int数组,默认值为0
// 创建方式2:声明同时初始化,静态初始化
int[] arr2 = {1, 2, 3, 4, 5}; // 等价 int[] arr2 = new int[]{1, 2, 3, 4, 5};
// 创建方式3:使用new关键字初始化
int[] arr3 = new int[]{1, 2, 3, 4, 5}; // 这种简写只能在声明时使用,不能单独赋值给已有数组引用。
// 动态创建
int size = 10;
int[] arr4 = new int[size];
1.4.2 核心操作实现
1.4.2.1 数组遍历
java
/**
* 数组遍历 - 四种方式
*/
public void traverseArray(int[] arr) {
// 方式1:传统for循环
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
// 方式2:增强for循环
for (int element : arr) {
System.out.print(element + " ");
}
// 方式3:while循环
int i = 0;
while (i < arr.length) {
System.out.print(arr[i] + " ");
i++;
}
// 方式4:使用Stream(Java 8+)
Arrays.stream(arr).forEach(System.out::print);
}
1.4.2.2 数组查找
java
/**
* 数组查找 - 线性搜索
* 时间复杂度:O(n)
*/
public int linearSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i; // 返回找到的索引
}
}
return -1; // 未找到返回-1
}
/**
* 数组查找 - 二分搜索(要求数组已排序)
* 时间复杂度:O(log n)
*/
public int binarySearch(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出的写法,反例:mid = (left + right)/2
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
1.4.2.3 数组插入
java
/**
* 数组插入 - 在指定位置插入元素
* 时间复杂度:O(n)
*/
public int[] insertElement(int[] arr, int index, int element) {
if (index < 0 || index > arr.length) {
throw new IndexOutOfBoundsException("Invalid index");
}
// 创建新数组,长度+1
int[] newArr = new int[arr.length + 1];
// 复制插入位置之前的元素
System.arraycopy(arr, 0, newArr, 0, index);
// 插入新元素
newArr[index] = element;
// 复制插入位置之后的元素
System.arraycopy(arr, index, newArr, index + 1, arr.length - index);
return newArr;
}
1.4.2.4 数组删除
java
/**
* 数组删除 - 删除指定位置的元素
* 时间复杂度:O(n)
*/
public int[] deleteElement(int[] arr, int index) {
if (index < 0 || index >= arr.length) {
throw new IndexOutOfBoundsException("Invalid index");
}
// 创建新数组,长度-1
int[] newArr = new int[arr.length - 1];
// 复制删除位置之前的元素,把 arr 数组从下标 0 开始的 index 个元素,复制到 newArr 的开头(从下标 0 开始
System.arraycopy(arr, 0, newArr, 0, index);
// 复制删除位置之后的元素
System.arraycopy(arr, index + 1, newArr, index, arr.length - index - 1);
return newArr;
}
1.4.2.5 数组更新
java
/**
* 数组更新 - 修改指定位置的元素
* 时间复杂度:O(1)
*/
public void updateElement(int[] arr, int index, int newValue) {
if (index < 0 || index >= arr.length) {
throw new IndexOutOfBoundsException("Invalid index");
}
arr[index] = newValue;
}
1.5 多维数组处理
1.5.1 二维数组的创建和初始化
java
// 方式1:指定大小
int[][] matrix1 = new int[3][4]; // 3行4列的矩阵
// 方式2:直接初始化
int[][] matrix2 = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 方式3:不规则数组(锯齿数组)
int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[2]; // 第一行2个元素
jaggedArray[1] = new int[4]; // 第二行4个元素
jaggedArray[2] = new int[3]; // 第三行3个元素
1.5.2 核心操作实现
1.5.2.1 二维数组遍历
java
/**
* 二维数组遍历 - 两种方式
*/
public void traverse2DArray(int[][] matrix) {
// 方式1:传统双重循环
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j] + " ");
}
System.out.println();
}
// 方式2:增强for循环
for (int[] row : matrix) {
for (int element : row) {
System.out.print(element + " ");
}
System.out.println();
}
}
1.5.2.2 矩阵转置
java
/**
* 矩阵转置
* 时间复杂度:O(m*n)
*/
public int[][] transposeMatrix(int[][] matrix) {
int rows = matrix.length;
int cols = matrix[0].length;
int[][] transposed = new int[cols][rows];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
transposed[j][i] = matrix[i][j];
}
}
return transposed;
}
1.5.2.3 矩阵乘法
java
/**
* 矩阵乘法
* 时间复杂度:O(m*n*p)
*/
public int[][] multiplyMatrices(int[][] A, int[][] B) {
int rowsA = A.length; // A 的行数 m(假定 A 非空)
int colsA = A[0].length; // A 的列数 n(假定 A[0] 存在且每行长度一致)
int colsB = B[0].length; // B 的列数 p(假定 B 非空)
if (colsA != B.length) { // 矩阵相乘的必要条件:A 的列数(n)必须等于 B 的行数(也就是 B.length)
throw new IllegalArgumentException("矩阵无法相乘"); // 条件不满足则抛出异常
}
int[][] result = new int[rowsA][colsB]; // 结果矩阵大小为 m x p, Java 数组默认元素为 0
for (int i = 0; i < rowsA; i++) { // 遍历结果矩阵的每一行(i 从 0 到 m-1)
for (int j = 0; j < colsB; j++) { // 遍历结果矩阵的每一列(j 从 0 到 p-1)
for (int k = 0; k < colsA; k++) { // 求和索引 k,从 0 到 n-1
// 对应数学定义:result[i][j] = sum_{k=0..n-1} A[i][k] * B[k][j]
result[i][j] += A[i][k] * B[k][j]; // 累加当前项的乘积
}
}
}
return result; // 返回计算结果矩阵
}
1.5.2.4 螺旋遍历矩阵
java
/**
* 螺旋遍历矩阵
* 时间复杂度:O(m*n)
*/
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) return result;
int top = 0, bottom = matrix.length - 1;
int left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// 从左到右遍历上边界
for (int j = left; j <= right; j++) {
result.add(matrix[top][j]);
}
top++;
// 从上到下遍历右边界
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--;
// 从右到左遍历下边界
if (top <= bottom) {
for (int j = right; j >= left; j--) {
result.add(matrix[bottom][j]);
}
bottom--;
}
// 从下到上遍历左边界
if (left <= right) {
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++;
}
}
return result;
}
2. 动态数组深入分析
2.1 ArrayList 原理分析
ArrayList 是基于动态数组实现的顺序容器,支持按索引随机访问(O(1)),在尾部追加元素摊销为 O(1),中间插入或删除需要移动元素为 O(n);通过几何扩容(默认 1.5 倍)保证空间动态增长,内部用 Object[] 存储元素,允许 null,非线程安全,但结构简单、查询高效,适合频繁读取、追加场景。
默认 ArrayList 只扩容不缩容,扩容代价高,预设容量或自定义优化类可显著提高性能并减少内存浪费。当调用 list.addAll() 时,底层其实是循环调用 add。
java
/**
* 自定义ArrayList实现 - 核心功能
*/
public class MyArrayList<E> {
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认空数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储元素的数组
transient Object[] elementData;
// 实际元素个数
private int size;
// 构造函数
public MyArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
public MyArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 添加元素到末尾
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e;
return true;
}
/**
* 在指定位置插入元素
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);
// 将index及其后面的元素向后移动一位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
/**
* 获取指定位置的元素
*/
@SuppressWarnings("unchecked")
public E get(int index) {
rangeCheck(index);
return (E) elementData[index];
}
/**
* 设置指定位置的元素
*/
public E set(int index, E element) {
rangeCheck(index);
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
/**
* 删除指定位置的元素
*/
public E remove(int index) {
rangeCheck(index);
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null; // 清除引用,帮助GC
return oldValue;
}
/**
* 确保内部容量
*/
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
if (minCapacity - elementData.length > 0) {
grow(minCapacity);
}
}
/**
* 扩容核心方法
*/
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容1.5倍
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
// 复制到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
// 边界检查
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
2.2 LinkedList 实现原理
LinkedList 是基于双向链表实现的顺序容器,支持在任意位置高效插入和删除(O(1)),但随机访问需要遍历链表(O(n));每个节点包含数据和前后指针,动态分配内存,允许 null,非线程安全,适合频繁插入删除、较少随机访问的场景。
3. 面试高频问题与解答
3.1 数组和链表的区别是什么?
答案要点:
特性 | 数组 | 链表 |
---|---|---|
内存布局 | 连续内存空间 | 非连续内存空间 |
访问方式 | 随机访问 O(1) | 顺序访问 O(n) |
插入删除 | O(n) 需要移动元素 | O(1) 只需修改指针 |
内存开销 | 只存储数据 | 额外存储指针 |
缓存友好性 | 高(局部性原理) | 低(内存分散) |
深入分析:
java
// 数组访问:CPU可以直接计算地址
int value = array[index]; // 地址 = base + index * sizeof(type)
// 链表访问:需要遍历节点
Node current = head;
for (int i = 0; i < index; i++) {
current = current.next; // 每次访问都需要解引用
}
int value = current.data;
数组(Array/ArrayList)在内存中连续分布,遍历时能充分利用 CPU 缓存的空间局部性,因此访问速度快;链表(LinkedList)节点分散在堆上,遍历需频繁跳转内存,缓存命中率低,访问效率较低。
3.2 ArrayList 和 LinkedList 的性能差异?
详细对比:
ArrayList 是基于动态数组实现的顺序容器,内存连续,访问元素时可按索引直接读取,随机访问速度非常快(O(1));尾部追加摊销时间复杂度为 O(1),但中间插入或删除需要移动元素,时间复杂度为 O(n)。空间开销较低,仅为元素引用的数组,缓存友好,适合大量读取和尾部追加操作。
LinkedList 是基于双向链表实现的顺序容器,每个节点包含数据和前后指针,内存分散,遍历或随机访问元素需要从头或尾顺序跳转,随机访问时间复杂度为 O(n),中间插入或删除只需修改指针,时间复杂度为 O(1)。由于节点分散且有额外指针,空间开销高,缓存不友好。
选择建议:当操作以读取和尾部追加为主、内存敏感、需要快速随机访问时优先 ArrayList;当操作以中间插入/删除为主、顺序遍历多且对随机访问要求低时可选择 LinkedList。
3.3 ArrayList 的扩容机制详解
核心要点:
- 初始容量: 默认10,延迟初始化
- 扩容时机 : 当
size >= capacity
时触发 - 扩容倍数 : 1.5倍 (
oldCapacity + (oldCapacity >> 1)
) - 最大容量 :
Integer.MAX_VALUE - 8
面试加分点:
- 为什么是1.5倍而不是2倍?(内存利用率vs性能平衡)
- 扩容的时间复杂度分析(摊还分析)。扩容时需要复制元素到新数组,时间复杂度为 O(n),但由于是摊销时间复杂度,平均每次操作时间复杂度为 O(1)。
- 如何避免频繁扩容?(预设初始容量)。如果能预估元素总数,直接分配足够容量 → 避免多次扩容拷贝。
3.4 如何在O(1)时间内删除数组中的元素?
核心思路: 用最后一个元素覆盖要删除的元素,然后删除最后一个元素
java
/**
* O(1)时间删除数组元素(不保持顺序)
*/
public void deleteElement(int[] arr, int index, int size) {
// 边界检查
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
// 用最后一个元素覆盖要删除的元素
arr[index] = arr[size - 1];
// 清除最后一个元素的引用(如果是对象数组)
// arr[size - 1] = null;
// 返回新的有效长度
// return size - 1;
}
适用场景: 不需要保持元素顺序的情况 时间复杂度: O(1) 空间复杂度: O(1)
3.5 如何判断两个数组是否相等?
多种实现方式:
java
/**
* 方法1:使用Arrays.equals()(推荐)
*/
public boolean arraysEqual1(int[] arr1, int[] arr2) {
return Arrays.equals(arr1, arr2);
}
/**
* 方法2:手动实现
*/
public boolean arraysEqual2(int[] arr1, int[] arr2) {
if (arr1 == arr2) return true;
if (arr1 == null || arr2 == null) return false;
if (arr1.length != arr2.length) return false;
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
/**
* 方法3:深度比较(多维数组)
*/
public boolean deepArraysEqual(int[][] arr1, int[][] arr2) {
return Arrays.deepEquals(arr1, arr2);
}
3.6 数组中找到第K大的元素(经典算法题)
解法1:快速选择算法(最优)
java
/**
* 快速选择算法找第K大元素
* 时间复杂度:平均O(n),最坏O(n²)
* 空间复杂度:O(1)
*/
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
}
private int quickSelect(int[] nums, int left, int right, int kSmallest) {
if (left == right) {
return nums[left];
}
// 随机选择pivot避免最坏情况
Random random = new Random();
int pivotIndex = left + random.nextInt(right - left + 1);
pivotIndex = partition(nums, left, right, pivotIndex);
if (kSmallest == pivotIndex) {
return nums[kSmallest];
} else if (kSmallest < pivotIndex) {
return quickSelect(nums, left, pivotIndex - 1, kSmallest);
} else {
return quickSelect(nums, pivotIndex + 1, right, kSmallest);
}
}
private int partition(int[] nums, int left, int right, int pivotIndex) {
int pivot = nums[pivotIndex];
// 将pivot移到末尾
swap(nums, pivotIndex, right);
int storeIndex = left;
for (int i = left; i < right; i++) {
if (nums[i] < pivot) {
swap(nums, storeIndex, i);
storeIndex++;
}
}
// 将pivot移到正确位置
swap(nums, storeIndex, right);
return storeIndex;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
解法2:堆排序
java
/**
* 使用最小堆找第K大元素
* 时间复杂度:O(n log k)
* 空间复杂度:O(k)
*/
public int findKthLargestWithHeap(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int num : nums) {
heap.offer(num);
if (heap.size() > k) {
heap.poll();
}
}
return heap.peek();
}
3.7 如何实现数组的旋转?
问题: 给定数组 [1,2,3,4,5,6,7]
和 k=3
,旋转后得到 [5,6,7,1,2,3,4]
解法1:三次反转(最优)
java
/**
* 数组旋转 - 三次反转法
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
public void rotate(int[] nums, int k) {
int n = nums.length;
k = k % n; // 处理k大于数组长度的情况
// 反转整个数组
reverse(nums, 0, n - 1);
// 反转前k个元素
reverse(nums, 0, k - 1);
// 反转后n-k个元素
reverse(nums, k, n - 1);
}
private void reverse(int[] nums, int start, int end) {
while (start < end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
解法2:环形替换
java
/**
* 数组旋转 - 环形替换
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
public void rotateWithCycle(int[] nums, int k) {
int n = nums.length;
k = k % n;
int count = 0;
for (int start = 0; count < n; start++) {
int current = start;
int prev = nums[start];
do {
int next = (current + k) % n;
int temp = nums[next];
nums[next] = prev;
prev = temp;
current = next;
count++;
} while (start != current);
}
}
3.8 合并两个有序数组(原地合并)
经典题目: 给定两个有序数组,将第二个数组合并到第一个数组中
java
/**
* 合并两个有序数组(原地合并)
* nums1有足够的空间容纳nums2的所有元素
* 时间复杂度:O(m + n)
* 空间复杂度:O(1)
*/
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1; // nums1的最后一个有效元素
int j = n - 1; // nums2的最后一个元素
int k = m + n - 1; // 合并后数组的最后一个位置
// 从后往前合并,避免覆盖未处理的元素
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
// 如果nums2还有剩余元素
while (j >= 0) {
nums1[k--] = nums2[j--];
}
// nums1的剩余元素已经在正确位置,无需处理
}
3.9 数组中的重复元素检测
问题1: 检测数组中是否有重复元素
java
/**
* 方法1:使用HashSet
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
public boolean containsDuplicate(int[] nums) {
Set<Integer> seen = new HashSet<>();
for (int num : nums) {
if (!seen.add(num)) {
return true;
}
}
return false;
}
/**
* 方法2:排序后检查相邻元素
* 时间复杂度:O(n log n)
* 空间复杂度:O(1)
*/
public boolean containsDuplicateSort(int[] nums) {
Arrays.sort(nums);
for (int i = 1; i < nums.length; i++) {
if (nums[i] == nums[i - 1]) {
return true;
}
}
return false;
}
问题2: 找到第一个重复的元素
java
/**
* 找到第一个重复出现的元素
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
public int findFirstDuplicate(int[] nums) {
Set<Integer> seen = new HashSet<>();
for (int num : nums) {
if (seen.contains(num)) {
return num;
}
seen.add(num);
}
return -1; // 没有重复元素
}
3.10 数组的子数组问题
最大子数组和(Kadane算法)
java
/**
* 最大子数组和 - Kadane算法
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
public int maxSubArray(int[] nums) {
int maxSoFar = nums[0];
int maxEndingHere = nums[0];
for (int i = 1; i < nums.length; i++) {
// 要么扩展现有子数组,要么开始新的子数组
maxEndingHere = Math.max(nums[i], maxEndingHere + nums[i]);
maxSoFar = Math.max(maxSoFar, maxEndingHere);
}
return maxSoFar;
}
/**
* 返回最大子数组的起始和结束位置
*/
public int[] maxSubArrayWithIndices(int[] nums) {
int maxSum = nums[0];
int currentSum = nums[0];
int start = 0, end = 0, tempStart = 0;
for (int i = 1; i < nums.length; i++) {
if (currentSum < 0) {
currentSum = nums[i];
tempStart = i;
} else {
currentSum += nums[i];
}
if (currentSum > maxSum) {
maxSum = currentSum;
start = tempStart;
end = i;
}
}
return new int[]{start, end, maxSum};
}
3.11 总结
考点类型 | 核心算法 | 时间复杂度 | 典型题目 |
---|---|---|---|
查找 | 二分查找 | O(log n) | 搜索旋转排序数组 |
排序 | 快排/归并 | O(n log n) | 数组排序、第K大元素 |
双指针 | 左右指针 | O(n) | 两数之和、三数之和 |
滑动窗口 | 快慢指针 | O(n) | 最长无重复子串 |
动态规划 | 状态转移 | O(n) | 最大子数组和 |
贪心算法 | 局部最优 | O(n) | 买卖股票最佳时机 |
-
为什么是1.5倍而不是2倍?(内存利用率vs性能平衡)
-
扩容的时间复杂度分析(摊还分析)。扩容时需要复制元素到新数组,时间复杂度为 O(n),但由于是摊销时间复杂度,平均每次操作时间复杂度为 O(1)。
-
如何避免频繁扩容?(预设初始容量)。如果能预估元素总数,直接分配足够容量 → 避免多次扩容拷贝。
3.12 实现一个动态数组
见 2.1 章节
3.13 数组去重的多种实现方式
3.13.1 使用 HashSet 去重
特点: 时间复杂度 O(n),空间复杂度 O(n),可保持原有顺序(使用 LinkedHashSet 时)
java
/**
* 使用 HashSet 进行数组去重
* 优点:时间复杂度最优,保持元素原有顺序
* 缺点:需要额外的空间存储
*/
public int[] removeDuplicatesWithSet(int[] nums) {
Set<Integer> seen = new HashSet<>();
List<Integer> result = new ArrayList<>();
for (int num : nums) {
if (seen.add(num)) { // add 返回 true 表示之前不存在
result.add(num);
}
}
return result.stream().mapToInt(i -> i).toArray();
}
扩展 :若想严格保持元素插入顺序,可将
HashSet
换成LinkedHashSet
。
3.13.2 使用 Stream.distinct()(Java 8+)
特点: 时间复杂度 O(n),空间复杂度 O(n),保持原有顺序
java
/**
* 使用 Java 8 Stream API 去重
* 优点:简洁高效,保持顺序
* 缺点:需要额外空间存储
*/
public int[] removeDuplicatesWithStream(int[] nums) {
return Arrays.stream(nums).distinct().toArray();
}
3.13.3 排序后双指针去重
特点: 时间复杂度 O(n log n),空间复杂度 O(1),不保持原有顺序
java
/**
* 先排序再使用双指针去重
* 优点:空间复杂度低
* 缺点:改变了元素原有顺序
*/
public int[] removeDuplicatesWithSort(int[] nums) {
if (nums.length <= 1) return nums;
Arrays.sort(nums);
int writeIndex = 1;
for (int readIndex = 1; readIndex < nums.length; readIndex++) {
if (nums[readIndex] != nums[readIndex - 1]) {
nums[writeIndex++] = nums[readIndex];
}
}
return Arrays.copyOf(nums, writeIndex);
}
3.13.4 原地去重(已排序数组)
特点: 时间复杂度 O(n),空间复杂度 O(1),适用于已排序数组
java
/**
* 对已排序数组进行原地去重
* 优点:时间和空间复杂度都很优秀
* 缺点:仅适用于已排序数组
*/
public int removeDuplicatesInPlace(int[] nums) {
if (nums.length <= 1) return nums.length;
int slow = 0;
for (int fast = 1; fast < nums.length; fast++) {
if (nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
}
}
return slow + 1; // 返回新长度
}
3.13.5 双重循环去重(原始方法)
特点: 时间复杂度 O(n²),空间复杂度 O(n),保持原有顺序
java
/**
* 使用双重循环进行数组去重
* 优点:无需额外库或集合
* 缺点:时间复杂度高,适合小数组
*/
public int[] removeDuplicatesBruteForce(int[] nums) {
int n = nums.length;
int[] temp = new int[n];
int j = 0;
for (int i = 0; i < n; i++) {
boolean duplicate = false;
for (int k = 0; k < j; k++) {
if (nums[i] == temp[k]) {
duplicate = true;
break;
}
}
if (!duplicate) {
temp[j++] = nums[i];
}
}
return Arrays.copyOf(temp, j);// 长度可能小于原数组
}
3.13.6 布尔数组去重(适用于小范围整数)
特点: 时间复杂度 O(n),空间复杂度 O(maxValue),保持顺序
java
/**
* 使用布尔数组去重
* 优点:时间复杂度低
* 缺点:只适合整数且范围不大
*/
public int[] removeDuplicatesWithBooleanArray(int[] nums, int maxValue) {
boolean[] seen = new boolean[maxValue + 1];
List<Integer> result = new ArrayList<>();
for (int num : nums) {
if (!seen[num]) {
seen[num] = true;
result.add(num);
}
}
return result.stream().mapToInt(i -> i).toArray();
}
3.13.7 总结对比
方法 | 时间复杂度 | 空间复杂度 | 保序 | 适用场景 |
---|---|---|---|---|
HashSet / LinkedHashSet | O(n) | O(n) | ✅ (LinkedHashSet) | 通用去重,大部分情况 |
Stream.distinct() | O(n) | O(n) | ✅ | Java 8+,简洁风格 |
排序 + 双指针 | O(n log n) | O(1) | ❌ | 可以改变顺序、空间敏感 |
原地去重(已排序) | O(n) | O(1) | ❌ | 已排序数组,高效 |
双重循环 | O(n²) | O(n) | ✅ | 小数组或限制库环境 |
布尔数组 | O(n) | O(maxValue) | ✅ | 小整数范围数组,速度最快 |
3.14 内存优化
3.14.1 选择合适的数据类型
java
// 如果数值范围在-128到127之间,使用byte
byte[] smallNumbers = new byte[1000]; // 1KB
// 而不是
int[] numbers = new int[1000]; // 4KB
3.14.2 避免内存碎片
java
// 好的做法:一次性分配
int[][] matrix = new int[1000][1000];
// 不好的做法:分多次分配
int[][] matrix2 = new int[1000][];
for (int i = 0; i < 1000; i++) {
matrix2[i] = new int[1000]; // 可能造成内存碎片
}