数组(Array)

下列代码示例大部分由 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 的扩容机制详解

核心要点:

  1. 初始容量: 默认10,延迟初始化
  2. 扩容时机 : 当 size >= capacity 时触发
  3. 扩容倍数 : 1.5倍 (oldCapacity + (oldCapacity >> 1))
  4. 最大容量 : 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];  // 可能造成内存碎片
}
相关推荐
前端的日常1 分钟前
不会写Mcp server,那就让Trae写吧
trae
Goboy7 分钟前
消消乐游戏:Trae 轻松实现色彩缤纷的消除乐趣
ai编程·trae
Goboy9 分钟前
纸牌接龙:Trae 轻松实现经典纸牌挑战
ai编程·trae
慧翔天地人才发展学苑1 小时前
大厂 | 华为半导体业务部2026届秋招启动
华为·面试·职场和发展·跳槽·求职招聘·职场晋升
天天摸鱼的java工程师2 小时前
Java 解析 JSON 文件:八年老开发的实战总结(从业务到代码)
java·后端·面试
小高0072 小时前
📈前端图片压缩实战:体积直降 80%,LCP 提升 2 倍
前端·javascript·面试
普罗米拉稀3 小时前
Flutter 复用艺术:Mixin 与 Abstract 的架构哲学与线性化解密
flutter·ios·面试
要做朋鱼燕3 小时前
【数据结构】用堆解决TOPK问题
数据结构·算法
CF14年老兵3 小时前
「Vue 3 + View Transition 实现炫酷圆形缩放换肤动画」
前端·css·trae