news 2026/9/7 6:33:20

Java实现顺序表:从线性表到动态数组扩容的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java实现顺序表:从线性表到动态数组扩容的完整指南

在开发中,我们经常要处理“一组有序数据”的存储问题,比如学生成绩表、商品列表、任务队列。最容易想到的方式就是使用数组,但数组的长度固定、增删元素麻烦,用起来并不顺手。数据结构中的“顺序表”正是在数组基础上封装出来的一种线性表,它既保留了数组随机访问快的优势,又解决了容量固定、插入删除需要手动搬移数据的痛点。本文将以 Java 语言为例,从顺序表的定义出发,完整实现初始化、插入、删除、查找、扩容等核心操作,并给出可直接运行的完整代码和测试用例。无论你是正在学习数据结构课程,还是准备面试、复习算法基础,这篇文章都能帮你把顺序表这一章彻底吃透。

1. 背景与核心概念

1.1 线性表是什么

在系统学习顺序表之前,首先要理解“线性表”这个概念。线性表是具有相同数据类型的 n 个数据元素的有限序列,比如一个班级的学生名单、一串电话号码记录,都可以抽象成线性表。线性表中每个元素都有唯一的前驱和后继,除了第一个元素没有前驱、最后一个元素没有后继之外,其他元素都“手拉手”排成一条直线,这种逻辑结构非常直观。

线性表的存储方式主要有两种。一种是顺序存储,用一组地址连续的内存单元依次存放数据元素,这种实现就是顺序表;另一种是链式存储,通过指针把散落在内存各处的节点串起来,这种实现就是链表。在真实项目中,两种方式都有大量应用,但对于“按位置访问”特别频繁、很少在中间插入删除的场景,顺序表的效率远高于链表。这也是为什么我们要先把顺序表吃透。

1.2 顺序表解决什么问题

顺序表本质上是一段连续的物理存储空间,通常用“数组 + 当前长度”两个属性来描述。它解决的问题可以归纳成三个:

第一,容量的动态管理。静态数组在使用前必须确定大小,如果估计少了,数据存不下;估计多了,又浪费内存。顺序表通过扩容机制解决了这个问题,在空间不足时自动申请更大的数组,并把原数据搬移过去。

第二,数据的有序组织。顺序表让元素按照逻辑顺序紧密排列,通过下标可以直接访问任意位置的元素。比如要访问第 3 个元素,数组下标就是 2,一步到位。

第三,常用操作的标准化。初始化、插入、删除、查找、遍历、清空,这些操作被封装成方法或函数,使用时不需要关心内部细节。这样写业务代码时,可以像使用工具一样操作数据集合,而不用手动管理底层数组。

1.3 顺序表和数组、链表的区别

初学者最容易把数组和顺序表混为一谈。数组是编程语言提供的一种基本语法结构,长度固定,只能由程序员手动处理增删逻辑。顺序表是建立在数组之上的抽象数据结构,它把数组的固定长度和增删逻辑封装起来,形成一个具备“自动扩容、边界检查、标准接口”的数据容器。在学习时可以把顺序表想象成一个“升级版数组”。

和链表相比,顺序表的特点是“内存连续、随机访问快、插入删除慢”。链表正好相反,它不要求内存连续,插入删除只需要修改指针,但随机访问必须从头遍历。实际项目中选型时要根据业务特点决定。如果读多写少,优先考虑顺序表;如果频繁在中间插入删除,链表可能更合适。

2. 环境准备与代码结构

2.1 运行环境说明

本文所有代码使用 Java 语言实现,在本地开发时建议准备以下环境:

  • JDK 8 及以上版本,本文代码使用了泛型和增强 for 循环等基础特性,JDK 8 完全可以运行。
  • 任意 Java IDE,例如 IntelliJ IDEA、Eclipse,或者直接用记事本配合命令行编译运行。
  • 使用命令行时,请确认java -versionjavac -version能正常输出版本信息。

版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路和代码实现。

2.2 项目文件结构

示例工程采用最简单的单类结构,目的是让读者把注意力集中到数据结构本身,而不是被 Maven 或 Gradle 的工程结构干扰。实际项目中使用时,可以把顺序表类放到独立的datastructure包中,方便统一管理。

顺序表演示项目/ ├── src/ │ └── SqList.java (顺序表完整实现) │ └── SqListTest.java (测试入口类)

SqList是顺序表的核心类,包含成员变量和所有操作方法;SqListTest是测试类,用于验证功能。

2.3 使用到的 Java 技术点

在阅读代码前,先了解几个关键的 Java 技术点:

  • 泛型。通过泛型可以让顺序表支持任意数据类型,而不只是int。本文为了降低上手难度,先用int类型演示核心逻辑,最后补充泛型化的改造思路。
  • 可变参数。部分初始化方法可以使用可变参数,便于一次性添加多个元素。
  • 数组拷贝。扩容时需要把原数组的数据复制到新数组,可以使用循环逐位复制,也可以使用System.arraycopy提高效率。

代码中会尽量避免使用过于高级的语法,保证基础薄弱的读者也能看懂。

3. 顺序表的核心设计原理

3.1 顺序表的存储结构

顺序表的逻辑结构是“一对一”的线性关系,物理结构是连续内存空间。它的定义可以概括为:用一组地址连续的存储单元依次存放线性表中的数据元素,元素之间的逻辑关系通过它们在内存中的物理位置自然表达。

在 Java 中,我们使用数组作为底层存储结构,再加上一个size变量记录当前元素个数。这里有个容易混淆的地方:数组的容量capacity和顺序表的长度size是不同的概念。容量是数组最多能放多少个元素,长度是当前实际存放了多少个元素。判断顺序表是否为空,看的是size是否为 0,而不是容量是否为 0。

private int[] data; // 底层数组 private int size; // 当前元素个数

data用来存放元素,size指向下一个要写入的位置,同时也是当前元素个数。初始化时分配一个默认容量的数组,size置为 0。

3.2 为什么需要扩容机制

数组一旦创建,长度就不可变。如果顺序表在元素插入时发现数组已满,继续写入就会导致数组下标越界。扩容机制的思路是:在插入前判断是否还有剩余空间,如果没有,申请一个更大的新数组,把原数组元素全部搬移过去,然后让data指向新数组。

扩容的容量策略在真实项目中很有讲究。最简单的是“每次增加一个固定大小”,比如每次多分配 10 个空间;更通用的是“原容量翻倍”或者“增加原容量的一半”。容量翻倍的好处是均摊下来每次插入的时间复杂度为 O(1),不会出现多次扩容的抖动问题。Java 的ArrayList在扩容时,默认策略是oldCapacity + (oldCapacity >> 1),也就是增加原来容量的一半。本文会采用这个策略作为演示。

3.3 插入操作的核心思想

插入操作比较容易出错的点在于元素搬移。如果要在第index个位置插入元素,那么从indexsize-1的所有元素都必须往后挪动一位,空出第index个位置,再把新元素写入。

这里必须强调一个关键细节:搬移元素时,必须从最后一个元素开始,从后向前依次移动。如果从前往后移动,后面的元素还没有被搬走,就直接被前一个元素覆盖,导致数据丢失。这种“从后往前搬”的规则是顺序表插入操作最常见的考点。

// 从后往前搬移元素 for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; }

插入前还需要判断下标是否合法。合法范围是0size。如果index等于size,相当于在表尾追加元素;如果超出这个范围,应该抛出下标越界异常。

3.4 删除操作的核心思想

删除操作和插入操作方向相反。删除第index个元素后,从index+1size-1的元素都必须往前挪动一位,填补被删除元素留下的空位。

搬移元素时,必须从前往后移动。这正好和插入操作相反:

for (int i = index; i < size - 1; i++) { data[i] = data[i + 1]; }

删除完成后,size减 1。这里还有一个性能优化点:末尾的废弃数据可以置为默认值,帮助垃圾回收。对于基本数据类型int,可以置为 0;对于对象类型,可以置为null

删除操作的时间主要消耗在元素搬移上,平均要移动n/2次,所以时间复杂度是 O(n)。

3.5 查找操作的设计

查找可以分成两种。第一种是按位置查找,给定下标返回元素,顺序表支持随机访问,时间复杂度是 O(1)。第二种是按值查找,需要遍历整个顺序表,找到第一个与目标值相等的元素并返回其下标,时间复杂度是 O(n)。

按值查找在真实项目中非常常见。比如在成绩表里找到 90 分以上的第一个学生,或者在订单表里定位某个订单号。实现时要注意两个点:第一,如果找不到,要有一个统一的返回值约定,比如返回-1表示不存在;第二,在泛型场景下比较元素时要使用equals而不是==,否则比较的是引用地址而不是内容。

4. 顺序表完整 Java 实现

4.1 类的成员与构造方法

先来定义顺序表类的基本结构:

// 文件路径:src/SqList.java public class SqList { private int[] data; // 底层数组 private int size; // 当前元素个数 // 默认构造方法:初始化容量为 10 public SqList() { data = new int[10]; size = 0; } // 指定初始容量构造方法 public SqList(int capacity) { if (capacity < 0) { throw new IllegalArgumentException("容量不能为负数"); } data = new int[capacity]; size = 0; } }

默认容量设置为 10,这样既不会在刚创建时就占用大量内存,又能满足一般的小规模数据需求。第二个构造方法允许调用者按照预估的数据量分配初始空间,减少后续扩容次数。

4.2 基本成员方法

包括获取长度、判断是否为空、按位置访问元素、修改元素:

// 获取当前元素个数 public int size() { return size; } // 判断顺序表是否为空 public boolean isEmpty() { return size == 0; } // 按位置获取元素 public int get(int index) { checkIndexForGet(index); return data[index]; } // 修改指定位置的元素 public void set(int index, int value) { checkIndexForGet(index); data[index] = value; } // 检查按位置访问时下标是否合法 private void checkIndexForGet(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("下标越界: " + index); } }

getset都是 O(1) 的随机访问操作,这也是顺序表相对链表最核心的优势。检查下标的代码被抽取成私有方法,避免重复编写,同时让外部调用时能立即得到清晰的异常信息。

4.3 扩容方法

扩容是顺序表内部的基础能力,被插入操作调用。虽然写成public也不会报错,但合理的设计应该把扩容声明为private,因为扩容属于内部机制,不应该暴露给调用者。

// 确保数组有足够的容量 private void ensureCapacity() { if (size < data.length) { return; } // 扩容策略:原容量 + 原容量的一半 int newCapacity = data.length + (data.length >> 1); if (newCapacity == data.length) { newCapacity = 10; } int[] newData = new int[newCapacity]; // 搬移原数组数据 for (int i = 0; i < size; i++) { newData[i] = data[i]; } data = newData; }

这里的data.length >> 1相当于data.length / 2,用移位运算比除法运算效率更高。扩容后原来的数组失去引用,由 JVM 垃圾回收机制自动回收。工程实践中,如果知道数据量会持续增长,也可以在调用插入时手动触发一次较大容量的扩容,减少多次扩容带来的性能开销。

4.4 插入操作实现

插入操作支持在任意合法位置插入新元素。插入位置可以是表头、表中或者表尾。

// 在指定位置插入元素 public void insert(int index, int value) { if (index < 0 || index > size) { throw new IndexOutOfBoundsException("插入位置非法: " + index); } ensureCapacity(); // 从后往前搬移元素,腾出位置 for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = value; size++; } // 在表尾追加元素 public void add(int value) { ensureCapacity(); data[size] = value; size++; }

在表尾追加元素是插入的一种特殊情况,单独提供add方法会让调用更加简洁。每次插入完都要记得size++,漏掉这一步会导致后续读写错位,也是最常见的低级错误。

为了验证插入逻辑,可以看一个简单例子。假设数组中已有元素[1, 2, 3, 4],要在下标 1 的位置插入9。首先从后往前搬移:下标 3 的4移到下标 4,下标 2 的3移到下标 3,下标 1 的2移到下标 2,然后下标 1 写入9。最终数组变为[1, 9, 2, 3, 4],完全符合预期。

4.5 删除操作实现

删除操作接收一个下标,返回被删除的元素值。返回被删除的元素在某些业务场景中很有用,比如需要记录日志或做撤销操作。

// 删除指定位置的元素,并返回被删除的元素 public int remove(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("删除位置非法: " + index); } int oldValue = data[index]; // 从前往后搬移元素,覆盖被删除位置 for (int i = index; i < size - 1; i++) { data[i] = data[i + 1]; } size--; // 将末尾废弃位置置为 0,帮助释放引用(对基本类型是可选操作) data[size] = 0; return oldValue; }

从前往后搬移是删除操作的正确姿势。如果从后往前搬移,前一个元素会被后一个元素覆盖,导致数据混乱。

4.6 查找与遍历

按值查找返回第一个匹配元素的下标,如果找不到返回-1。判断容器中是否包含某个元素,可以基于查找方法实现:

// 按值查找,返回第一个匹配的下标,找不到返回 -1 public int indexOf(int value) { for (int i = 0; i < size; i++) { if (data[i] == value) { return i; } } return -1; } // 判断是否包含目标值 public boolean contains(int value) { return indexOf(value) != -1; } // 遍历打印所有元素 public void print() { System.out.print("["); for (int i = 0; i < size; i++) { System.out.print(data[i]); if (i != size - 1) { System.out.print(", "); } } System.out.println("]"); }

print方法方便在测试时观察顺序表的内部状态。注意打印遍历时,循环条件是i < size而不是i < data.length。否则会把未使用的扩容空间也打印出来。

4.7 清空与判空

清空操作只重置size,不要求立即释放数组内存。这样即使扩容后的数组被保留,再次添加元素时也能避免立刻扩容,空间可以复用。

// 清空顺序表 public void clear() { size = 0; }

对于对象类型的顺序表,清空时最好把数组中每个引用都置为null,否则会出现“对象无法被垃圾回收”的内存隐患。基本类型数组则没有这个问题。

4.8 完整代码汇总

把上面的方法组合到一起,形成完整的SqList.java类。

// 文件路径:src/SqList.java public class SqList { private int[] data; private int size; public SqList() { data = new int[10]; size = 0; } public SqList(int capacity) { if (capacity < 0) { throw new IllegalArgumentException("容量不能为负数"); } data = new int[capacity]; size = 0; } public int size() { return size; } public boolean isEmpty() { return size == 0; } public int get(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("下标越界: " + index); } return data[index]; } public void set(int index, int value) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("下标越界: " + index); } data[index] = value; } private void ensureCapacity() { if (size < data.length) { return; } int newCapacity = data.length + (data.length >> 1); int[] newData = new int[newCapacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } data = newData; } public void insert(int index, int value) { if (index < 0 || index > size) { throw new IndexOutOfBoundsException("插入位置非法: " + index); } ensureCapacity(); for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = value; size++; } public void add(int value) { ensureCapacity(); data[size] = value; size++; } public int remove(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("删除位置非法: " + index); } int oldValue = data[index]; for (int i = index; i < size - 1; i++) { data[i] = data[i + 1]; } size--; data[size] = 0; return oldValue; } public int indexOf(int value) { for (int i = 0; i < size; i++) { if (data[i] == value) { return i; } } return -1; } public boolean contains(int value) { return indexOf(value) != -1; } public void print() { System.out.print("["); for (int i = 0; i < size; i++) { System.out.print(data[i]); if (i != size - 1) { System.out.print(", "); } } System.out.println("]"); } public void clear() { size = 0; } }

5. 测试与运行验证

5.1 编写测试类

顺序表类写完以后,需要编写测试类验证功能是否正常。测试类覆盖了插入、删除、查找、修改、遍历等核心操作,每个操作之间通过打印结果来检验正确性。

// 文件路径:src/SqListTest.java public class SqListTest { public static void main(String[] args) { // 1. 创建一个顺序表 SqList list = new SqList(); System.out.println("初始是否为空:" + list.isEmpty()); // 2. 尾部追加元素 list.add(10); list.add(20); list.add(30); list.add(40); System.out.print("尾部追加 10,20,30,40 后:"); list.print(); // 3. 指定位置插入 list.insert(1, 15); System.out.print("在下标 1 插入 15 后:"); list.print(); // 4. 按位置查找和修改 System.out.println("下标 2 的元素是:" + list.get(2)); list.set(0, 100); System.out.print("将下标 0 修改为 100 后:"); list.print(); // 5. 按值查找 System.out.println("元素 30 的下标是:" + list.indexOf(30)); System.out.println("是否包含 99:" + list.contains(99)); // 6. 删除操作 int removed = list.remove(1); System.out.println("删除的元素是:" + removed); System.out.print("删除后:"); list.print(); // 7. 清空操作 list.clear(); System.out.println("清空后是否为空:" + list.isEmpty()); } }

5.2 编译运行

在命令行进入src目录,依次执行编译和运行命令:

javac SqList.java SqListTest.java java SqListTest

使用 IDE 开发时,直接运行SqListTest类中的main方法即可。

5.3 预期输出结果

运行程序后,控制台输出如下:

初始是否为空:true 尾部追加 10,20,30,40 后:[10, 20, 30, 40] 在下标 1 插入 15 后:[10, 15, 20, 30, 40] 下标 2 的元素是:20 将下标 0 修改为 100 后:[100, 15, 20, 30, 40] 元素 30 的下标是:3 是否包含 99:false 删除的元素是:15 删除后:[100, 20, 30, 40] 清空后是否为空:true

输出结果和预期完全一致。这里需要特别说明set(0, 100)之后,indexOf(30)的结果仍然是 3。因为删除操作发生在查找之后,此时顺序表为[100, 15, 20, 30, 40],元素 30 位于下标 3。

5.4 扩容能力测试

为了验证扩容机制是否正常工作,可以专门写一个循环添加大量数据的测试:

// 扩容测试:连续添加 100 个元素 SqList bigList = new SqList(5); for (int i = 0; i < 100; i++) { bigList.add(i); } System.out.println("扩容后元素个数:" + bigList.size()); System.out.println("扩容后下标 99 的元素:" + bigList.get(99));

初始容量只有 5,但需要存储 100 个元素,因此会触发多次扩容。只要最后能输出元素个数 100,并且get(99)返回 99,就证明扩容机制正常工作。

6. 复杂度分析与性能边界

6.1 时间复杂度分析

顺序表各操作的时间复杂度如下表所示:

操作平均时间复杂度说明
按下标访问getO(1)直接通过数组下标定位
修改元素setO(1)直接通过数组下标定位
尾部插入addO(1)均摊分析下,扩容次数少
任意位置插入insertO(n)需要移动 n/2 个元素
删除removeO(n)需要移动 n/2 个元素
按值查找indexOfO(n)需要遍历数组
判断包含containsO(n)依赖 indexOf

从表中可以看到,顺序表的强项是随机访问和尾部的增删,弱项是中间位置的插入删除和按值查找。

6.2 空间复杂度分析

顺序表的空间复杂度为 O(n),因为底层数组需要为所有元素分配连续内存。在 real-time 场景中,扩容会瞬间申请一份更大的内存,如果顺序表存储的是大量对象引用,扩容时的内存开销会比较明显。为了避免频繁扩容,创建顺序表时可以根据业务大小给出合理的初始容量,例如预估 1000 条数据就初始化new SqList(1000)

6.3 顺序表适合的场景

从复杂度分析可以看出,顺序表最擅长处理以下场景:

  • 需要频繁按下标读取元素的场景,比如排行榜、缓存队列。
  • 数据量相对稳定、增长不剧烈的场景。
  • 主要在尾部添加和删除数据的场景,比如程序运行日志的暂存,或者栈的实现。

如果业务中需要频繁在头部插入或者中间删除,例如待办事项列表、消息队列等,那么建议使用链表或者其他更合适的数据结构,否则会产生大量元素搬移的性能损耗。

7. 常见问题与排查思路

7.1 插入后数据错乱

问题现象:插入元素后,后面的数据出现重复或丢失。

这个问题的根本原因通常是搬移元素的方向搞反了。插入操作必须从后向前搬移元素,如果从前向后移动,前一个元素会覆盖后一个元素,导致后半部分数据重复。例如数组[1, 2, 3, 4],在下标 1 插入9,如果从前往后搬移,下标 0 的1会覆盖到下标 1,原下标 1 的2被覆盖,后续依次处理会得到[1, 1, 1, 1, 1],数据完全错乱。

排查思路:检查循环起点。插入时循环变量从size-1开始,递减到index结束;删除时循环变量从index开始,递增到size-2结束。这是最容易出错的地方。

7.2 数组下标越界异常

问题现象:调用insertremove等操作时报IndexOutOfBoundsException

常见原因有三个:一是传入的index是负数;二是index大于当前顺序表的长度;三是在遍历时错误地使用了data.length作为循环边界,访问到了未使用的空间。

排查思路:所有公共操作入口都要先做下标检查。遍历顺序表时循环条件必须是i < size。如果底层使用data.length,一旦发生扩容,很多未使用的数组空间也会被遍历到,引发逻辑错误。

问题现象常见原因解决思路
插入后数据重复搬移元素方向错误,从前向后移动改为从后向前搬移元素
下标越界未校验传入下标或遍历越界操作前检查下标,遍历使用 size 作为边界
数据丢失删除后元素未正确前移从前往后搬移元素
扩容后原数据丢失扩容循环边界错误使用 size 作为循环上限,而不是 data.length
尾部追加失败忘记调用扩容方法追加前调用 ensureCapacity

7.3 扩容后原数据丢失

问题现象:插入大量元素后,前面的数据变成 0 或者丢失了。

常见原因是扩容时复制数据的循环边界写错,把i < size写成了i < data.length。在扩容方法被调用时,data还指向旧数组,如果循环上限使用data.length,会访问到新数组未初始化的位置,同时漏掉了旧数组的部分数据。

排查思路:在扩容方法中打印旧数组的长度和size值,确认循环边界。扩容复制的目标是把旧数组中范围内有效的元素全部搬到新数组,循环边界必须使用size

7.4 删除元素后末尾残留 0

问题现象:删除元素后,打印顺序表发现末尾出现了一个多余的 0。

这个现象其实不是错误。删除元素后,size--已经让末尾的“残留值”不再属于逻辑范围,打印遍历时只打印到size-1,所以不会看到这个 0。如果使用的是 IDE 的调试功能直接查看data数组,才会发现末尾残留值。对于基本类型数组,残留值没有影响;对于对象类型数组,为了帮助垃圾回收,最好把data[size]置为null

8. 最佳实践与工程建议

8.1 合理设置初始容量

顺序表的扩容操作涉及数组复制,数据量越大,复制开销越高。如果能预估数据规模,创建顺序表时应该指定初始容量,尽量减少扩容次数。例如在处理一个固定数量学生的成绩时,可以这样写:

SqList scores = new SqList(50); // 预估最多 50 个学生

如果业务中数据是持续增长的,初始容量可以适当设置得偏大一些,用少量内存换取更好的性能,这在嵌入式和高性能计算环境中尤其重要。

8.2 封装边界检查逻辑

每次插入、删除、访问前都要做下标校验,如果每个方法里都写一遍,代码会显得很凌乱。建议把边界检查抽取成私有方法,例如:

private void checkIndex(int index, int upperBound, String message) { if (index < 0 || index >= upperBound) { throw new IndexOutOfBoundsException(message + ": " + index); } }

这样既减少了重复代码,也让公共方法的逻辑更清晰。

8.3 使用位运算提高性能

在扩容计算中,使用移位运算代替除法和乘法是常见优化。例如:

int newCapacity = oldCapacity + (oldCapacity >> 1);

这里的>> 1表示右移一位,相当于除以 2。对于计算机来说,移位运算比乘除法更快。不过这属于微优化,在实际业务中影响不大,更重要的是代码可读性。如果团队其他人不熟悉位运算,也可以写成乘 1.5 的方式,配合注释说明。

8.4 接口设计优于直接暴露数组

初学者喜欢直接操作底层数组,虽然代码看起来简单,但是很容易破坏顺序表的不变量。比如直接设置data[10] = 5,却没有同步修改size,导致顺序表内部状态错误。更合理的做法是把顺序表封装成对外提供标准方法的类,内部实现细节完全隐藏,外部使用者只能通过addinsertremove等方法操作数据。

在大型项目中,可以进一步抽象一个List接口,将顺序表定义为接口的实现类。这样在使用时面向接口编程,后续如果需要换成链表实现,业务代码不需要大改。

8.5 泛型化改造

本文示例使用的是int类型数组。实际项目中,顺序表通常需要存储任意类型的数据,因此应该泛型化。泛型化改造的核心点有两个:

第一,创建泛型数组时不能直接new T[capacity],因为 Java 无法创建泛型数组,需要借助(T[]) new Object[capacity]进行强制转换。

第二,按值查找时不能使用==比较对象,要使用equals方法。

public class SqList<T> { private Object[] data; private int size; public SqList(int capacity) { data = new Object[capacity]; size = 0; } @SuppressWarnings("unchecked") public T get(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("下标越界: " + index); } return (T) data[index]; } public boolean contains(T value) { for (int i = 0; i < size; i++) { if (data[i].equals(value)) { return true; } } return false; } }

泛型化之后,SqList<String>SqList<Student>都能使用同一个类,大大提升了代码复用率。

8.6 与 Java 集合框架的关系

看到这里,很多读者会想到 Java 自带的ArrayList。实际上,ArrayList就是标准库中的顺序表实现,它的底层就是Object[]数组,同样支持动态扩容、随机访问、按值查找。理解了本文的顺序表后,再去看ArrayList的源码会轻松很多。

实际生产项目中,不需要自己造轮子,直接使用ArrayList是更稳妥的选择。自己实现顺序表的价值更多体现在面试、考试和理解数据结构原理上。如果往深处学习,可以阅读ArrayList的源码,看看 Java 官方是如何处理扩容、迭代器和快速失败机制的。

9. 总结与下一步学习方向

本文从线性表和顺序表的概念出发,解释了顺序表“数组 + 长度”的存储结构,并完整实现了初始化、扩容、插入、删除、按位访问、按值查找、清空等核心操作。插入和删除的元素搬移方向是关键,扩容的触发时机和容量策略也直接影响性能。通过测试用例和预期输出,我们验证了整个实现是正确可靠的。

学完顺序表之后,下一步建议学习链表。顺序表和链表是线性表的两种典型实现,它们在内存布局、操作效率、适用场景上形成鲜明对比,只有把两者放在一起对比学习,才能真正理解“数据结构的选择取决于业务需求”。可以继续尝试用 Java 实现单链表,并对比两者在头部插入、尾部插入、随机访问上的效率差异。

在面试中,顺序表常见的进阶问题包括:怎么实现动态扩容才能避免性能抖动、如何在顺序表中实现去重、如何用两个顺序表实现集合的交集和并集、如何基于顺序表实现栈和队列。这些都是在本文基础上的延伸练习。

如果本文对你有帮助,可以收藏备用,动手把代码敲一遍。数据结构没有捷径,自己实现的每个类都会成为后面学习算法和框架源码的坚实基础。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 6:33:16

Docker新手实战:从安装避坑到MySQL与Redis部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 6:32:51

FlaUI与Winform联手:微信自动化操作实战指南

简介&#xff1a;面向希望用C# Winform实现微信自动化的开发者&#xff0c;以FlaUI为自动化核心的完整工程&#xff0c;系统展示了定时任务、自动回复、群聊机器人三大功能的落地思路&#xff0c;覆盖从消息监听、界面元素定位到逻辑判断与模拟发送的完整链路&#xff0c;适合有…

作者头像 李华
网站建设 2026/9/7 6:31:44

Nginx 1.7.11.3 Gryphon定制版实战:反向代理、负载均衡与部署排查

简介&#xff1a;这份压缩包是基于Nginx 1.7.11.3的Gryphon定制版本&#xff0c;专为媒体直播服务优化&#xff0c;与FFmpeg整合后可以支撑RTMP、HLS、DASH等流媒体协议&#xff0c;适合运维人员和流媒体开发者参考学习。包内共有126个文件&#xff0c;以C语言模块源码、头文件…

作者头像 李华
网站建设 2026/9/7 6:30:30

椭球大地测量中贝塞尔法正反解的MATLAB实现与编程避坑指南

简介&#xff1a;基于CGCS2000国家大地坐标系椭球参数&#xff0c;使用MATLAB编写的贝塞尔大地问题正反算程序&#xff0c;面向测绘工程、大地测量学相关课程的本科生及需要实现椭球面解算的编程学习者。程序支持两类计算&#xff1a;已知一点经纬度及至另一点的大地线长和方位…

作者头像 李华
网站建设 2026/9/7 6:30:16

QModbus TCP模式综合操作:从寄存器读写到抓包调试实战

简介&#xff1a;面向 Qt 工业通信开发者&#xff0c;这份 QModbus TCP 模式演示工程源自《QModbus TCP模式综合操作详解(二)》&#xff0c;以 RTUMasterTest 为蓝本&#xff0c;专门展示 Modbus TCP 客户端连接、保持寄存器读写与错误处理等关键场景&#xff0c;适合正在学习 …

作者头像 李华