快速掌握Java线性数据结构:数组、链表、栈与队列【算法必备】

快速掌握Java线性数据结构:数组、链表、栈与队列


非线性数据结构:快速掌握Java非线性数据结构:树(二叉树、平衡二叉树、多路平衡树)、堆、图

概率型数据结构:快速掌握Java概率型数据结构:布隆过滤器


一、引子

数据结构是算法的基石,更是高效解决实际问题的关键工具。本文详解Java中五大核心线性数据结构:数组(Array)、链表(Linked List)、栈(Stack)、队列(Queue)和双端队列(Deque)。通过代码实例、时间复杂度分析和应用场景对比,助你掌握不同场景下的最优选择策略。

二、 数组 (Array)

概念: 数组是最基础且重要的线性数据结构。它在内存中分配连续的存储空间,用于存储相同类型元素的集合。数组通过从0开始的整数索引直接访问任意元素。

Java实现核心: Java 提供了内置的数组语法(int[] arr)以及功能更强大的 java.util.ArrayList 类(基于数组的动态扩容实现)。

  1. 基本数组声明与初始化:

    // 声明并初始化一个固定大小的整型数组
    int[] staticArray = new int[5]; // 初始化为默认值0
    staticArray[0] = 10; // 赋值
    staticArray[1] = 20;
    
    // 声明并直接初始化
    String[] names = {"Alice", "Bob", "Charlie"};
    
  2. ArrayList (动态数组):

    import java.util.ArrayList;
    
    // 创建一个存储整数的动态数组
    ArrayList<Integer> dynamicArray = new ArrayList<>();
    
    // 添加元素 (自动扩容)
    dynamicArray.add(10); // O(1) 摊销时间复杂度
    dynamicArray.add(20);
    dynamicArray.add(1, 15); // 在索引1处插入15, 后续元素后移 O(n)
    
    // 访问元素
    int firstElement = dynamicArray.get(0); // O(1)
    
    // 修改元素
    dynamicArray.set(1, 25); // O(1)
    
    // 删除元素 (移除索引1处的元素)
    dynamicArray.remove(1); // 后续元素前移 O(n)
    
    // 获取大小
    int size = dynamicArray.size(); // O(1)
    

核心操作时间复杂度:

  • 访问 (Access by Index): O(1) - 直接计算内存地址偏移。
  • 搜索 (Search): O(n) - 最坏情况下需要遍历整个数组(无序时)。
  • 插入 (Insertion):
    • 末尾插入 (add(elem)): O(1) 摊销 (Amortized) - 大多数情况下很快,扩容时O(n)
    • 指定索引插入 (add(index, elem)): O(n) - 需要移动后续元素。
  • 删除 (Deletion):
    • 删除末尾元素: O(1)
    • 删除指定索引元素 (remove(index)): O(n) - 需要移动后续元素。

特点与适用场景:

  • 优点: 随机访问极快;内存连续,缓存友好(Cache Locality);实现简单。
  • 缺点: 大小固定(基本数组)或扩容有成本(ArrayList);在非末尾位置插入/删除效率低(需要移动元素)。
  • 典型应用:
    • 存储已知大小或变化不大的数据集。
    • 需要频繁随机访问元素的场景(如通过下标获取)。
    • 作为其他数据结构(如堆、哈希表)的基础实现。
    • 多维数组表示矩阵、图像像素等。

三、 链表 (Linked List)

概念: 链表由一系列结点 (Node) 组成,每个结点包含数据域 (data) 和指向下一个结点地址的指针域 (next)。结点在内存中不必连续存储。链表主要有两种基本形式:单向链表 (Singly Linked List)双向链表 (Doubly Linked List)

Java实现核心: Java 标准库提供了 java.util.LinkedList(基于双向链表)。理解链表原理通常需要手动实现结点类。

  1. 结点类定义 (单向链表):

    class ListNode<T> {
        T data;           // 存储的数据
        ListNode<T> next; // 指向下一个结点的引用
    
        ListNode(T data) {
            this.data = data;
            this.next = null;
        }
    }
    
  2. 手动实现链表基本操作示例:

    public class SinglyLinkedList<T> {
        private ListNode<T> head; // 头结点引用
        private int size;
    
        // 在链表头部插入
        public void addFirst(T data) {
            ListNode<T> newNode = new ListNode<>(data);
            newNode.next = head;
            head = newNode;
            size++;
        } // O(1)
    
        // 在链表尾部插入 (需遍历找到尾结点)
        public void addLast(T data) {
            ListNode<T> newNode = new ListNode<>(data);
            if (head == null) {
                head = newNode;
            } else {
                ListNode<T> current = head;
                while (current.next != null) {
                    current = current.next;
                }
                current.next = newNode;
            }
            size++;
        } // O(n)
    
        // 查找元素 (按值)
        public boolean contains(T data) {
            ListNode<T> current = head;
            while (current != null) {
                if (current.data.equals(data)) {
                    return true;
                }
                current = current.next;
            }
            return false;
        } // O(n)
    
        // 删除第一个匹配项 (按值)
        public boolean remove(T data) {
            if (head == null) return false;
            if (head.data.equals(data)) {
                head = head.next;
                size--;
                return true;
            }
            ListNode<T> prev = head;
            ListNode<T> current = head.next;
            while (current != null) {
                if (current.data.equals(data)) {
                    prev.next = current.next;
                    size--;
                    return true;
                }
                prev = current;
                current = current.next;
            }
            return false;
        } // O(n)
    }
    
  3. 使用 LinkedList

    import java.util.LinkedList;
    
    LinkedList<String> list = new LinkedList<>();
    
    // 添加元素
    list.add("Apple"); // 尾部添加 O(1)
    list.addFirst("Banana"); // 头部添加 O(1)
    list.addLast("Cherry"); // 尾部添加 O(1)
    list.add(1, "Orange"); // 指定索引插入 O(n)
    
    // 访问元素
    String first = list.getFirst(); // O(1)
    String last = list.getLast();   // O(1)
    String elem = list.get(2);      // O(n) - 需要遍历
    
    // 删除元素
    list.removeFirst(); // O(1)
    list.removeLast();  // O(1)
    list.remove("Orange"); // O(n) - 按值查找
    list.remove(0);        // O(1) - 移除头/尾是O(1), 中间是O(n)
    

核心操作时间复杂度 (双向链表 LinkedList):

  • 访问 (Access by Index): O(n) - 需要从头或尾(取决于哪个更近)遍历。
  • 搜索 (Search): O(n) - 需要遍历。
  • 插入 (Insertion):
    • 头部 (addFirst, push): O(1)
    • 尾部 (addLast, add): O(1)
    • 指定索引 (add(index, elem)): O(n) - 需要遍历到该位置。
  • 删除 (Deletion):
    • 删除头部 (removeFirst, pop, poll): O(1)
    • 删除尾部 (removeLast): O(1)
    • 删除指定索引 (remove(index)): O(n) - 需要遍历到该位置。
    • 删除指定元素 (remove(Object)): O(n) - 需要查找。

特点与适用场景:

  • 优点: 动态大小,插入/删除操作(尤其在头尾)效率高(O(1));不需要连续内存空间。
  • 缺点: 随机访问慢(O(n));需要额外空间存储指针;缓存局部性较差。
  • 典型应用:
    • 实现栈、队列、双端队列等抽象数据类型。
    • 需要频繁在列表头部或尾部进行插入/删除的场景。
    • 内存分配管理(如操作系统内存池)。
    • 实现图结构的邻接表表示法。
    • LinkedList 在需要频繁操作头尾或作为队列/栈使用时效率很高。

四、 栈 (Stack)

概念: 栈是一种遵循 后进先出 (Last-In-First-Out, LIFO) 原则的线性数据结构。元素的添加(压栈,Push)和移除(弹栈,Pop)操作都发生在同一端,称为栈顶 (Top)。另一端称为栈底 (Bottom)

Java实现核心: Java 提供了 java.util.Stack 类(继承自 Vector,线程安全但通常不推荐),更常用的是 java.util.Deque 接口的实现类(如 ArrayDeque, LinkedList)来模拟栈,因为它们提供了更高效和一致的 API。

  1. 使用 Deque 作为栈 (推荐):

    import java.util.ArrayDeque;
    import java.util.Deque;
    
    Deque<Integer> stack = new ArrayDeque<>(); // 通常用ArrayDeque, 效率高
    
    // 压栈 (Push)
    stack.push(10); // 或 stack.addFirst(10)
    stack.push(20);
    stack.push(30);
    
    // 查看栈顶元素 (Peek) - 不移除
    int top = stack.peek(); // 返回30, 栈不变
    System.out.println