【数据结构】Java集合核心:线性表、List接口、ArrayList与LinkedList深度解析

目录

前言

一、线性表:数据结构的基础

[1.1 什么是线性表?](#1.1 什么是线性表?)

[1.2 顺序表 vs 链表](#1.2 顺序表 vs 链表)

二、List接口:集合框架的核心

[2.1 List接口概述](#2.1 List接口概述)

[2.2 为什么要用List接口?](#2.2 为什么要用List接口?)

三、ArrayList:基于数组的动态顺序表

[3.1 ArrayList核心特性](#3.1 ArrayList核心特性)

[3.2 ArrayList扩容机制(面试高频!)](#3.2 ArrayList扩容机制(面试高频!))

[3.3 ArrayList常用操作时间复杂度](#3.3 ArrayList常用操作时间复杂度)

[3.4 ArrayList遍历方式](#3.4 ArrayList遍历方式)

四、LinkedList:基于双向链表的实现

[4.1 LinkedList核心结构](#4.1 LinkedList核心结构)

[4.2 LinkedList时间复杂度分析](#4.2 LinkedList时间复杂度分析)

[4.3 LinkedList特有的双端队列操作](#4.3 LinkedList特有的双端队列操作)

[五、ArrayList vs LinkedList:如何选择?](#五、ArrayList vs LinkedList:如何选择?)

[5.1 性能对比总结](#5.1 性能对比总结)

[5.2 选择指南](#5.2 选择指南)

六、实战应用:扑克牌游戏

[6.1 完整扑克牌示例](#6.1 完整扑克牌示例)

七、性能优化建议

[7.1 ArrayList优化](#7.1 ArrayList优化)

[7.2 LinkedList优化](#7.2 LinkedList优化)

总结


一、线性表:数据结构的基础

1.1 什么是线性表?

线性表是n个具有相同特性数据元素的有限序列。它在逻辑上是连续的一条直线,但物理存储上并不一定连续。

常见线性表实现

  • 顺序表(数组实现)

  • 链表(链式存储)

  • 栈、队列(特殊线性表)

1.2 顺序表 vs 链表

特性 顺序表(数组) 链表
存储方式 物理地址连续 节点通过引用链接
访问速度 O(1)随机访问 O(n)顺序访问
插入删除 O(n)需要搬移元素 O(1)修改指针
空间利用 可能浪费或需要扩容 按需申请,无容量概念

二、List接口:集合框架的核心

2.1 List接口概述

List是Java集合框架中的重要接口,继承自Collection接口,代表一个有序、可重复的元素序列。

java 复制代码
public interface List<E> extends Collection<E> {
    // 核心方法
    boolean add(E e);
    void add(int index, E element);
    E remove(int index);
    E get(int index);
    E set(int index, E element);
    int indexOf(Object o);
    List<E> subList(int fromIndex, int toIndex);
    // ...
}

2.2 为什么要用List接口?

java 复制代码
// 良好的编程实践:面向接口编程
List<String> list = new ArrayList<>();  // √
ArrayList<String> list = new ArrayList<>();  // ×(不够灵活)

// 可以轻松切换实现
List<String> list = new LinkedList<>();  // 只需改一行代码

三、ArrayList:基于数组的动态顺序表

3.1 ArrayList核心特性

java 复制代码
public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, Serializable {
    
    // 底层存储数组
    transient Object[] elementData;
    // 元素个数
    private int size;
}

关键特点

  • 实现RandomAccess接口,支持快速随机访问

  • 线程不安全(多线程环境需同步)

  • 动态扩容,初始容量10

3.2 ArrayList扩容机制(面试高频!)

java 复制代码
// 简化版扩容流程
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 1. 检查容量
    elementData[size++] = e;           // 2. 添加元素
    return true;
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);  // 1.5倍扩容
    
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容规则总结

  1. 首次添加元素时,容量从0扩容到10

  2. 后续按1.5倍(oldCapacity + oldCapacity >> 1)扩容

  3. 若预估容量超过1.5倍,按实际需求扩容

  4. 最大容量为Integer.MAX_VALUE - 8

3.3 ArrayList常用操作时间复杂度

操作 时间复杂度 说明
get(index) O(1) 数组随机访问
add(E e) 平摊O(1) 尾部添加,可能触发扩容
add(index, E) O(n) 需要搬移元素
remove(index) O(n) 需要搬移元素
contains(Object) O(n) 需要遍历查找

3.4 ArrayList遍历方式

java 复制代码
List<Integer> list = new ArrayList<>();

// 1. for循环 + 下标(最高效)
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

// 2. foreach语法糖
for (Integer num : list) {
    System.out.println(num);
}

// 3. 迭代器
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

四、LinkedList:基于双向链表的实现

4.1 LinkedList核心结构

java 复制代码
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, Serializable {
    
    // 双向链表节点
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
    }
    
    // 头尾指针
    transient Node<E> first;
    transient Node<E> last;
    transient int size = 0;
}

关键特点

  • 实现双向链表,同时实现List和Deque接口

  • 不支持RandomAccess,顺序访问

  • 插入删除高效,特别适合头尾操作

4.2 LinkedList时间复杂度分析

操作 时间复杂度 说明
addFirst/addLast O(1) 直接修改头尾指针
removeFirst/removeLast O(1) 直接修改头尾指针
get(index) O(n) 需要从头或尾遍历
add(index, E) O(n) 需要找到插入位置
remove(Object) O(n) 需要遍历查找

4.3 LinkedList特有的双端队列操作

java 复制代码
LinkedList<Integer> deque = new LinkedList<>();

// 作为队列使用(FIFO)
deque.offer(1);      // 入队
deque.poll();        // 出队

// 作为栈使用(LIFO)
deque.push(1);       // 入栈
deque.pop();         // 出栈

// 双端队列操作
deque.addFirst(1);   // 头部添加
deque.addLast(2);    // 尾部添加
deque.removeFirst(); // 头部移除
deque.removeLast();  // 尾部移除

五、ArrayList vs LinkedList:如何选择?

5.1 性能对比总结

考量维度 ArrayList LinkedList
随机访问 ⭐⭐⭐⭐⭐ O(1) ⭐⭐ O(n)
头部插入 ⭐ O(n) ⭐⭐⭐⭐⭐ O(1)
尾部插入 ⭐⭐⭐⭐ O(1)平摊 ⭐⭐⭐⭐⭐ O(1)
内存占用 较少(仅存储数据) 较多(额外存储指针)
缓存友好性 ⭐⭐⭐⭐⭐(连续内存) ⭐(内存碎片)
中间插入 ⭐ O(n) ⭐⭐ O(n)找位置+O(1)插入

5.2 选择指南

选择ArrayList当

  • 需要频繁随机访问元素

  • 数据量相对稳定,少在中间插入删除

  • 内存空间有限

选择LinkedList当

  • 需要频繁在头部或尾部插入删除

  • 实现队列、栈或双端队列

  • 数据量变化大,频繁增删

java 复制代码
// 实际应用示例
// 场景1:需要快速随机访问
List<Student> studentList = new ArrayList<>();  // 按学号快速查找

// 场景2:实现最近浏览记录(LRU Cache)
List<PageView> recentViews = new LinkedList<>();  // 频繁在头部插入删除

六、实战应用:扑克牌游戏

6.1 完整扑克牌示例

java 复制代码
// 扑克牌类
class Card {
    public int rank;     // 牌面值
    public String suit;  // 花色
    
    @Override
    public String toString() {
        return String.format("[%s %d]", suit, rank);
    }
}

// 扑克牌操作
public class CardDemo {
    private static final String[] SUITS = {"♥", "♠", "♦", "♣"};
    
    // 买一副牌
    private static List<Card> buyDeck() {
        List<Card> deck = new ArrayList<>(52);
        for (int i = 0; i < 4; i++) {
            for (int j = 1; j <= 13; j++) {
                Card card = new Card();
                card.suit = SUITS[i];
                card.rank = j;
                deck.add(card);
            }
        }
        return deck;
    }
    
    // 洗牌(Fisher-Yates算法)
    private static void shuffle(List<Card> deck) {
        Random random = new Random();
        for (int i = deck.size() - 1; i > 0; i--) {
            int j = random.nextInt(i + 1);
            // 交换位置
            Card temp = deck.get(i);
            deck.set(i, deck.get(j));
            deck.set(j, temp);
        }
    }
    
    // 发牌
    public static void main(String[] args) {
        List<Card> deck = buyDeck();
        System.out.println("新牌:" + deck);
        
        shuffle(deck);
        System.out.println("洗牌后:" + deck);
        
        // 三人玩牌,每人5张
        List<List<Card>> hands = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            hands.add(new ArrayList<>());
        }
        
        for (int i = 0; i < 5; i++) {
            for (int j = 0; j < 3; j++) {
                // 从牌堆顶部取牌
                Card card = deck.remove(0);
                hands.get(j).add(card);
            }
        }
        
        System.out.println("玩家A手牌:" + hands.get(0));
        System.out.println("剩余牌数:" + deck.size());
    }
}

七、性能优化建议

7.1 ArrayList优化

java 复制代码
// 1. 预估容量,避免多次扩容
List<Integer> list = new ArrayList<>(1000);  // 指定初始容量

// 2. 批量添加使用addAll
list.addAll(anotherList);  // 比循环add高效

// 3. 适时trimToSize释放多余空间
list.trimToSize();

7.2 LinkedList优化

java 复制代码
// 1. 使用合适的遍历方式
// 从头开始遍历
for (int i = 0; i < list.size(); i++) {
    list.get(i);  // 效率低!每次都要从头遍历
}

// 使用迭代器
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    it.next();  // 效率高
}

// 2. 利用头尾操作优势
linkedList.addFirst(item);  // O(1)
linkedList.removeLast();    // O(1)

总结

ArrayList和LinkedList是Java集合框架中最基础、最常用的两个List实现:

  • ArrayList :数组实现,空间连续 ,适合读多写少随机访问频繁的场景

  • LinkedList :双向链表实现,空间分散 ,适合增删频繁 、特别是头尾操作的场景

记住黄金法则

  • 当你不知道选什么时,先用ArrayList

  • 当需要频繁在头部插入删除时,考虑LinkedList

  • 当需要实现队列、栈或双端队列时,选择LinkedList

作为Java后端开发者,深入理解这些集合类的实现原理,不仅能帮助你在面试中脱颖而出,更能让你在实际开发中写出更高效、更健壮的代码。

动手实践建议:尝试自己实现一个简化版的ArrayList和LinkedList,亲自体验数组扩容和链表指针操作的过程,这会让你的理解更加深刻!

相关推荐
小旭95272 小时前
【Java 面试高频考点】finally 与 return 执行顺序 解析
java·开发语言·jvm·面试·intellij-idea
hixiong1232 小时前
C# OpenVinoSharp部署Yolo26模型进行推理
开发语言·c#·openvino·yolo26
甄心爱学习2 小时前
leetcode打卡
算法·leetcode·职场和发展
dragoooon342 小时前
[hot100 NO.62~67]
算法
不会c嘎嘎2 小时前
QT中的各种对话框
开发语言·qt
陌路202 小时前
RPC分布式通信(2)---四种典型式线程池(1)
java·开发语言·c++
微露清风2 小时前
系统性学习C++-第二十四讲-智能指针的使用及其原理
java·c++·学习
你撅嘴真丑2 小时前
求矩阵的两对角线上的元素之和 与 sizeof的大作用
线性代数·算法·矩阵
我是一只小青蛙8882 小时前
手撕C++STL的list实现
开发语言·c++·list