数组(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];  // 可能造成内存碎片
}
相关推荐
Lee川8 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川11 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i13 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有14 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有14 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫15 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫15 小时前
Handler基本概念
面试
Wect15 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼16 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼16 小时前
Next.js 企业级落地
前端·javascript·面试