顺序表知识点梳理
一、基础概念与结构特性
1. 逻辑与物理结构
-
逻辑结构 :顺序表属于线性表,元素呈线性排列(如
[1,2,3,4],元素间存在 "一对一" 相邻关系)。 -
物理结构 :采用顺序存储,即逻辑上相邻的元素,在物理内存中地址也连续(Java 中通过数组实现,数组元素占用连续内存空间)。
-
对比链表:链表采用离散存储,逻辑相邻元素物理地址可不连续,需通过指针 / 引用关联。
2. 核心特性:随机存取
-
定义:可通过 "起始地址 + 元素索引" 直接定位任意元素,无需遍历。
-
Java 映射 :数组的索引访问(如
arr[i]),本质就是顺序表的随机存取。- 地址公式(逻辑映射):若数组首元素地址为
LOC(arr[0]),元素大小为size,则第i个元素地址为LOC(arr[i]) = LOC(arr[0]) + i * size。
- 地址公式(逻辑映射):若数组首元素地址为
-
时间复杂度 :
O(1)(直接定位,无循环 / 递归开销),远高于链表的顺序访问(O(n))。
二、声明与初始化方式(Java 实现)
Java 中无 "静态 / 动态数组" 的严格语法区分,但可通过固定长度数组 (模拟静态)和动态扩容数组 (模拟动态,如 ArrayList)实现两种模式。
1. 静态数组实现(固定长度)
特点
-
初始化时确定数组大小,后续不可扩展;
-
内存由 JVM 自动分配(基本类型数组在栈 / 堆,对象数组在堆),无需手动释放。
Java 示例代码
java
public class StaticSequenceList {
// 存储元素的数组
private int[] data;
// 当前元素个数(注:区别于数组长度,数组长度是"最大容量")
private int length;
// 构造
public StaticSequenceList(int maxSize) {
this.data = new int[maxSize];
this.length = 0;
}
/** 获取当前元素个数 */
public int getLength() {
return this.length;
}
/** 获取最大容量 */
public int getMaxSize() {
return this.data.length;
}
public boolean add(int value) {
if (this.length == this.data.length) {
System.out.println("静态顺序表已满,无法添加元素:" + value);
return false;
}
this.data[this.length++] = value; // 末尾赋值,元素个数+1
return true;
}
public static void main(String[] args) {
// 创建容量为5的静态顺序表
StaticSequenceList list = new StaticSequenceList(5);
// 初始状态
System.out.println("初始状态:");
System.out.println("元素个数: " + list.getLength());
System.out.println("最大容量: " + list.getMaxSize());
// 添加元素
System.out.println("\n添加元素测试:");
list.add(10);
list.add(20);
list.add(30);
System.out.println("添加3个元素后,元素个数: " + list.getLength());
// 再次添加
list.add(40);
list.add(50);
System.out.println("添加5个元素后,元素个数: " + list.getLength());
// 继续添加
System.out.println("\n测试超出容量:");
boolean result = list.add(60);
System.out.println("添加第6个元素结果: " + result);
System.out.println("最终元素个数: " + list.getLength());
}
}
2. 动态数组实现(可扩容)
特点
-
基于数组实现,但支持自动扩容(容量不足时扩大数组大小);
-
底层逻辑与 Java
ArrayList一致,需手动实现扩容、元素复制等核心逻辑; -
内存由 JVM 自动回收(无需
free(),GC 会处理无用数组)。
Java 示例代码(模拟 ArrayList 核心逻辑)
java
public class DynamicSequenceList {
private int[] data;
private int length;
private int maxSize;
/** 无参构造 */
public DynamicSequenceList() {
this.maxSize = 10;
this.data = new int[this.maxSize];
this.length = 0;
}
/** 有参构造 */
public DynamicSequenceList(int initialCapacity) {
if (initialCapacity < 1) {
throw new IllegalArgumentException("初始容量非法:" + initialCapacity);
}
this.maxSize = initialCapacity;
this.data = new int[this.maxSize];
this.length = 0;
}
//get方法
public int getLength() {
return this.length;
}
public int getMaxSize() {
return this.maxSize;
}
/**
* 扩容方法:当元素个数达到最大容量时,扩容为原容量的1.5倍
* 采用位运算(maxSize >> 1)计算半容量,比除法(maxSize / 2)更高效
*/
private void expandCapacity() {
int newMaxSize = this.maxSize + (this.maxSize >> 1);
int[] newData = new int[newMaxSize];
System.arraycopy(this.data, 0, newData, 0, this.length);
this.data = newData;
this.maxSize = newMaxSize;
System.out.println("动态顺序表扩容成功,新容量:" + this.maxSize);
}
/** 向末尾添加元素(自动扩容) */
public void add(int value) {
if (this.length == this.maxSize) {
expandCapacity();
}
this.data[this.length++] = value;
}
public Integer get(int index) {
if (index < 0 || index >= this.length) {
throw new IndexOutOfBoundsException("索引越界:" + index + ",当前长度:" + this.length);
}
return this.data[index];
}
public static void main(String[] args) {
DynamicSequenceList list = new DynamicSequenceList(2);
System.out.println("初始状态:");
System.out.println("元素个数: " + list.getLength());
System.out.println("最大容量: " + list.getMaxSize());
System.out.println("\n添加元素测试:");
for(int i = 0;i<12;i++){
list.add(i);
}
System.out.println("添加12个元素后,元素个数: " + list.getLength());
System.out.println("添加12个元素后,最大容量: " + list.getMaxSize());
System.out.println("添加12个元素后,索引为3的元素: " + list.get(3));
}
}
说明
-
>>是右移运算符,属于二元操作符,需要两个操作数。原代码中的 this.maxSize >> 1 表示将maxSize的值右移1位,相当于整数除法除以2(向下取整); -
实际开发中无需自定义动态顺序表,直接使用
java.util.ArrayList即可(如List<Integer> list = new ArrayList<>()),其内部已封装扩容、增删改查等逻辑; -
ArrayList扩容倍数:默认初始容量为10,当元素数量达到容量时,扩容为原来的1.5倍
三、基本操作及复杂度(Java 实现)
以 "动态顺序表(DynamicSequenceList)" 为例,实现插入、删除、查找操作,并分析时间复杂度(核心衡量指标:操作与数据规模 n 的关系)。
1. 插入操作
操作逻辑
-
合法性校验 :判断插入索引是否在
[0, length]范围内(0 可插首,length 可插尾); -
扩容判断:若元素个数达到最大容量,触发扩容;
-
元素后移:从插入位置的 "下一个元素" 到 "最后一个元素",依次后移 1 位(空出插入位置);
-
插入元素:将新元素赋值到空出的位置,元素个数 + 1。
Java 示例代码
java
public boolean insert(int index, int value) {
if (index < 0 || index > this.length) {
throw new IndexOutOfBoundsException("插入索引非法:" + index + ",当前长度:" + this.length);
}
// 2. 扩容(若需)
if (this.length == this.maxSize) {
expandCapacity();
}
// 3. 元素后移
// 例:index=2,length=5 → 元素4→5、3→4、2→3,空出index=2
for (int i = this.length; i > index; i--) {
this.data[i] = this.data[i - 1];
}
// 4. 插入并更新长度
this.data[index] = value;
this.length++;
return true;
}
时间复杂度分析
-
最好情况 :插入到末尾(
index = length),无需移动元素 →O(1)(如ArrayList.add(value)); -
最坏情况 :插入到首位(
index = 0),需移动所有n个元素 →O(n); -
平均情况 :移动元素的平均次数为
n/2→O(n)。
2. 删除操作
操作逻辑
-
合法性校验 :判断删除索引是否在
[0, length-1]范围内(无元素时无法删除); -
记录待删元素:保存删除位置的元素值(用于返回);
-
元素前移:从删除位置的 "下一个元素" 到 "最后一个元素",依次前移 1 位(覆盖待删元素);
-
更新长度:元素个数 - 1(无需手动清空最后一个元素,后续添加会覆盖)。
Java 示例代码
java
public Integer remove(int index) {
if (index < 0 || index >= this.length) {
throw new IndexOutOfBoundsException("删除索引非法:" + index + ",当前长度:" + this.length);
}
// 2. 记录待删元素
int deletedValue = this.data[index];
// 3. 元素前移(从前往后移,覆盖待删元素)
// 例:index=2,length=5 → 元素3→2、4→3、5→4,覆盖index=2的元素
for (int i = index; i < this.length - 1; i++) {
this.data[i] = this.data[i + 1];
}
// 4. 更新长度(最后一个元素无需清空,后续添加会覆盖)
this.length--;
return deletedValue;
}
时间复杂度分析
-
与插入操作逻辑一致:
-
最好情况(删除末尾):
O(1); -
最坏情况(删除首位):
O(n); -
平均情况:
O(n)。
-
3. 查找操作
顺序表的查找分为 "按位序查找"(按索引)和 "按键值查找"(按元素内容),两者复杂度差异显著。
(1)按位序查找(按索引)
-
逻辑:利用随机存取特性,直接通过索引定位元素。
-
Java 示例代码 (复用动态顺序表的
get方法):
java
public Integer getByIndex(int index) {
if (index < 0 || index >= this.length) {
System.out.println("查找索引越界:" + index);
return null;
}
return this.data[index];
}
- 时间复杂度 :
O(1)(无循环,一步到位)。
(2)按键值查找(按元素内容)
需根据数组是否有序,选择不同查找方式:
| 数组类型 | 查找方式 | 核心逻辑 | 时间复杂度 |
|---|---|---|---|
| 无序数组 | 顺序查找 | 遍历所有元素,逐个比较 | O(n) |
| 有序数组 | 折半查找(二分查找) | 利用有序性,每次缩小一半查找范围 | O(log₂n) |
① 无序数组:顺序查找(Java 示例)
java
public int findByValueUnordered(int value) {
for (int i = 0; i < this.length; i++) {
if (this.data[i] == value) {
return i; // 找到第一个匹配元素,返回索引
}
}
return -1; // 遍历结束未找到
}
- 平均比较次数 :
(n+1)/2(最好 1 次,最坏 n 次)。
② 有序数组:折半查找(Java 示例)
java
public int binarySearch(int value) {
int left = 0; // 左边界
int right = this.length - 1; // 右边界
while (left <= right) {
// 计算中间索引:用left + (right-left)/2 避免left+right溢出(同 (left+right)/2)
int mid = left + (right - left) / 2;
if (this.data[mid] == value) {
return mid; // 找到目标值,返回索引
} else if (this.data[mid] < value) {
left = mid + 1; // 目标值在右半区,调整左边界
} else {
right = mid - 1; // 目标值在左半区,调整右边界
}
}
return -1;
}
当 left 和 right 都接近 Integer.MAX_VALUE (2^31-1) 时,它们的和可能会超过 int 的表示范围,导致溢出,结果变成负数
-
关键前提:数组必须有序(升序 / 降序,需与比较逻辑匹配);
-
复杂度优势 :
log₂n增长极慢(如n=1000时,仅需 10 次比较)。
四、关联算法
1. 核心算法基础
顺序表是数组类算法的载体,常用核心算法包括排序和查找:
(1)排序算法(适用于顺序表)
| 算法名称 | 时间复杂度 | Java 实现(工具类) | 适用场景 |
|---|---|---|---|
| 快速排序 | O(n log n)(平均) |
Arrays.sort(int[])(基本类型) |
大规模数据,对效率要求高 |
| 归并排序 | O(n log n)(稳定) |
Arrays.sort(Object[])(对象类型) |
需稳定排序(如自定义对象) |
| 插入排序 | O(n²)(最坏) |
手动实现(小数据量) | 数据量小或接近有序 |
- 说明 :Java
Arrays.sort()是优化后的排序方法,底层根据元素类型选择不同算法(基本类型用双轴快速排序,对象类型用 TimSort,一种归并排序变体),实际开发中优先使用。
(2)查找算法
- 无序表:顺序查找(
O(n)); - 有序表:折半查找(
O(log₂n)),或Collections.binarySearch(List, T)(针对List集合)。
个人观点,仅供参考
如有错误还望指正,共同进步!
