顺序表是线性表的一种物理存储结构,它采用一段地址连续的存储单元依次存储线性表中的数据元素。静态分配是指在编译时就确定存储空间大小的分配方式,与之相对的是动态分配。
静态分配顺序表的特点:
静态分配顺序表使用固定大小的数组存储元素,而动态分配顺序表可以根据需要扩容。静态分配的优势在于:
但静态分配的局限性也很明显:
在实际工程中,静态分配适合元素数量确定且不变的场景,比如配置参数、固定大小的查找表等。
静态分配顺序表的核心数据结构包含两个部分:
cpp复制#define MaxSize 15 // 顺序表的最大长度
struct SqList {
int data[MaxSize]; // 静态分配的数组
int length; // 当前长度
};
这里MaxSize定义了顺序表的最大容量,实际使用中应根据具体需求调整这个值。data数组用于存储元素,length记录当前存储的元素数量。
初始化顺序表需要做两件事:
cpp复制void InitList(SqList &L) {
for (int i = 0; i < MaxSize; i++) {
L.data[i] = 0;
}
L.length = 0;
}
注意:在实际项目中,初始化值的选择很重要。如果使用0作为初始值,要确保0不是有效的业务数据,否则可能引起混淆。
为顺序表赋初值的实现:
cpp复制void AssginList(SqList &L) {
for (int i = 0; i < MaxSize - 5; i++) {
L.data[i] = i;
L.length++;
}
}
这里有一个细节:循环条件是i < MaxSize - 5,这意味着只使用了部分容量。在实际应用中,这种硬编码方式不够灵活,更好的做法是传入要赋值的元素数量作为参数。
顺序表支持两种查找方式:
cpp复制bool GetElem(SqList &L, int i) {
if (i <= 0 || i > L.length) {
return false;
}
cout << "查找数据:" << L.data[i - 1] << endl;
return true;
}
时间复杂度:O(1),因为数组支持随机访问。
cpp复制int LocateElem(SqList &L, int i) {
for (int j = 0; j < L.length; j++) {
if (L.data[j] == i) {
return j + 1; // 返回位置(从1开始计数)
}
}
return 0; // 查找失败
}
时间复杂度:O(n),最坏情况下需要遍历整个表。
插入操作需要三个步骤:
cpp复制bool ListInserst(SqList &L, int i, int e) {
if (i > L.length + 1 || i < 0 || L.length >= MaxSize) {
return false;
}
for (int j = L.length; j >= i; j--) {
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e;
L.length++;
return true;
}
时间复杂度:O(n),因为可能需要移动n个元素。
关键点:移动元素必须从后向前进行,如果从前向后会导致数据覆盖。
删除操作的步骤:
cpp复制bool ListDelete(SqList &L, int i, int &e) {
if (i < 0 || i > L.length) {
return false;
}
e = L.data[i - 1];
for (int j = i; j < L.length; j++) {
L.data[j - 1] = L.data[j];
}
L.length--;
return true;
}
时间复杂度:O(n),因为可能需要移动n个元素。
注意:与插入不同,删除操作移动元素是从前向后进行的。
cpp复制int Length(SqList &L) {
return L.length;
}
cpp复制bool Empty(SqList &L) {
return L.length == 0;
}
cpp复制void PrintList(SqList &L) {
for (int i = 0; i < L.length; i++) {
cout << L.data[i] << " ";
}
cout << endl;
}
cpp复制int main() {
struct SqList L;
InitList(L);
cout << "是否为空:" << Empty(L) << endl;
AssginList(L);
cout << "是否为空:" << Empty(L) << endl;
PrintList(L);
int len = Length(L);
cout << "表长:" << len << endl;
if (ListInserst(L, 5, 44)) {
PrintList(L);
} else {
cout << "插入失败" << endl;
}
int e = -1;
if (ListDelete(L, 5, e)) {
cout << "被删除的值:" << e << endl;
PrintList(L);
} else {
cout << "删除失败" << endl;
}
cout << "元素3的位置:" << LocateElem(L, 3) << endl;
return 0;
}
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 访问 | O(1) | 直接通过下标访问 |
| 按值查找 | O(n) | 需要顺序查找 |
| 插入 | O(n) | 需要移动元素 |
| 删除 | O(n) | 需要移动元素 |
静态分配顺序表最适合以下场景:
批量操作优化:当需要插入或删除多个元素时,可以先计算最终位置,然后一次性移动元素,而不是逐个操作。
缓存友好:顺序表的内存连续性对CPU缓存友好,可以利用这一点优化访问模式。
边界检查:在实际项目中,应该添加更严格的边界检查,防止数组越界。
错误处理:当前实现使用bool返回值表示操作成功与否,更完善的实现可以定义错误码或异常机制。
数组越界:最容易出现的错误是访问超过length的位置,或者忘记检查MaxSize限制。
位置混淆:注意用户通常认为位置是从1开始的,而数组下标是从0开始的。
长度更新遗漏:在插入和删除操作中容易忘记更新length变量。
打印状态:在关键操作前后打印顺序表的状态,帮助定位问题。
断言检查:使用assert验证不变式,比如assert(L.length >= 0 && L.length <= MaxSize)。
边界测试:专门测试边界情况,如表满时插入、空表删除等。
内存检查工具:可以使用valgrind等工具检查内存访问错误。
好的测试应该包含:
例如:
cpp复制void test() {
SqList L;
InitList(L);
// 测试空表操作
assert(Empty(L));
assert(Length(L) == 0);
// 测试插入
for (int i = 1; i <= MaxSize; i++) {
assert(ListInserst(L, i, i*10));
}
assert(!ListInserst(L, MaxSize+1, 100)); // 表满应失败
// 测试删除
int e;
assert(ListDelete(L, 5, e));
assert(e == 50);
// 测试查找
assert(LocateElem(L, 30) == 3);
assert(LocateElem(L, 999) == 0); // 不存在的元素
}
静态分配顺序表是理解更复杂数据结构的基础,掌握它的实现细节和特性对学习数据结构非常重要。在实际项目中,需要根据具体需求权衡选择静态分配还是动态分配的实现方式。