线性表(linear list)是 n 个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串 …
线性表在逻辑上是线性结构,也就说是一条连续的直线。
但在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表是用一段 物理地址连续 的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删查改。
顺序表要求存储的数据从下标 0 开始,依次连续存储,中间不能用空位。
顺序表一般可以分为:
1、静态顺序表:使用定长数组存储。
静态顺序表的缺点是存储空间大小被固定了,只适用于知道存储数据数量的情况下。
而实际中,往往并不知道这个数目。
因此一般使用的是动态顺序表,按照需求动态分配空间。
#define N 100
typedef int SLDataType;
// 顺序表的静态存储
typedef struct SeqList{
SLDataType arr[N]; // 定长数组
int size; // 表示数组中存储了多少个有效数据
}SeqList;
2、动态顺序表:使用动态开辟的数组存储。
typedef int SLDataType;
// 顺序表的动态存储
typedef struct SeqList{
SLDataType* arr; // 指向动态开辟的数组
int size; // 有效数据的个数
int capacity; // 容量空间的个数
}SeqList;
2.2 顺序表的实现
静态顺序表只适用于确定知道需要存多少数据的场景。
在实际中基本使用动态顺序表,根据需要,动态地分配空间大小,所以下面我们来实现动态顺序表。
顺序表代码的实现将放在三个文件中,分别是 SeqList.h(用于声明)、SeqList.c(用于定义)、Test.c(用于测试)。
2.2.2 检查顺序表是否需要扩容SeqList.h 文件
#include
#include #include // 确保储存其它数据类型时方便改动,本文以存放整型数据为例 typedef int SLDataType; // 创建动态顺序表 typedef struct SeqList{ SLDataType* a; // 指向动态开辟的数组 int size; // 有效数据的个数 int capacity; // 容量空间的个数 }SeqList; // 初始化顺序表 void SeqListInit(SeqList* ps); SeqList.c 文件
#include "SeqList.h" // 初始化顺序表 void SeqListInit(SeqList* ps){ // 进行断言,当 ps 为 NULL 时,下面的 *** 作将无法进行,因为空指针是无法进行解引用 *** 作的 assert(ps); ps->a = NULL; ps->size =0; ps->capacity = 0; };
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; SeqListInit(&sl); } int main(){ TestSeqList1(); return 0; }
检查是否达到最大容量,检查最大容量是否为 0。
如果为 0 则给一个整形空间,如果不为 0 则将原来空间乘 2,同时重新创立一个指针开辟新空间来存放重新开辟的空间,如果在原来的指针里开辟失败,原指针会丢失,所以重新建立一个指针,而且开辟失败结束进程。
SeqList.h 文件
// 检测顺序表是否需要扩容 void SeqListCheckCapacity(SeqList* ps);
SeqList.c 文件
#include "SeqList.h" // 检测顺序表是否需要扩容 void SeqListCheckCapacity(SeqList* ps) { assert(ps); // size 和 capacity 都为 0 或 size 与 capacity 相等时需要扩容 if (ps->size == ps->capacity){ // capacity 的初始容量为 0,则赋值为 4;不为 0,则扩容为 2 倍。
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; SLDataType* tmp = realloc(ps->a, sizeof(SLDataType) * newCapacity); // 如果 ps->a 为空指针,此时 realloc 相当于 malloc if (tmp == NULL){ printf("realloc fail\n"); exit(-1); }else{ ps->a = tmp; ps->capacity = (int)newCapacity; } } }
realloc 扩容时有两种情况:
(1)如果该段内存空间后面的空余空间够用,则直接原地扩容。
(2)如果后面空余空间不够用,会在内存中重新开辟一段空间,原来位置的数据会被迁移到这段新开辟的内存中,且会将原来的空间释放掉。
迁移的过程会有时间的消耗,带来效率上的降低。
2.2.4 打印SeqList.h 文件
// 尾插 void SeqListPushBack(SeqList* ps, SLDataType x);
SeqList.c 文件
#include "SeqList.h" // 尾插 void SeqListPushBack(SeqList* ps, SLDataType x){ assert(ps); // 检测是否需要扩容 SeqListCheckCapacity(ps); ps->a[ps->size] = x; ps->size++; }
2.2.5 销毁SeqList.h 文件
// 打印 void SeqListPrint(SeqList* ps);
SeqList.c 文件
#include "SeqList.h" // 打印 void SeqListPrint(SeqList* ps){ assert(ps); for (int i = 0; i < ps->size; ++i){ printf("%d ",ps->a[i]); } printf("\n"); }
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; // 初始化顺序表 SeqListInit(&sl); // 尾插数据 SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPushBack(&sl, 4); SeqListPushBack(&sl, 5); // 打印 SeqListPrint(&sl); } int main(){ TestSeqList1(); return 0; }
运行结果:
2.2.6 尾删SeqList.h 文件
// 销毁 void SeqListDestory(SeqList* ps);
SeqList.c 文件
#include "SeqList.h" // 销毁 void SeqListDestory(SeqList* ps){ assert(ps); free(ps->a); ps->a = NULL; ps->capacity = 0; ps->size = 0; }
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; // 初始化顺序表 SeqListInit(&sl); // 尾插数据 SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPushBack(&sl, 4); SeqListPushBack(&sl, 5); // 打印 SeqListPrint(&sl); // 销毁 SeqListDestory(&sl); } int main(){ TestSeqList1(); return 0; }
2.2.7 头插SeqList.h 文件
// 尾删 void SeqListPopBack(SeqList* ps);
SeqList.c 文件
#include "SeqList.h" // 尾删 void SeqListPopBack(SeqList* ps){ assert(ps); if (ps->size > 0){ ps->size--; } }
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; // 初始化顺序表 SeqListInit(&sl); // 尾插数据 SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPushBack(&sl, 4); SeqListPushBack(&sl, 5); // 打印 SeqListPrint(&sl); // 尾删 SeqListPopBack(&sl); SeqListPopBack(&sl); // 打印 SeqListPrint(&sl); // 销毁 SeqListDestory(&sl); } int main(){ TestSeqList1(); return 0; }
运行结果:
2.2.8 头删思路: 头插就是在下标为 0 的位置插入一个元素,同时把原来的元素往后挪,将第一个位置空出来,同时也要确保空间足够,空间不足需要进行扩容。
SeqList.h 文件
// 头插 void SeqListPushFront(SeqList* ps, SLDataType x);
SeqList.c 文件
#include "SeqList.h" // 头插 void SeqListPushFront(SeqList* ps, SLDataType x){ assert(ps); // 检测是否需要扩容 SeqListCheckCapacity(ps); // 挪动数据 int end = ps->size - 1; while (end >= 0){ ps->a[end+1] = ps->a[end]; --end; } ps->a[0] = x; ps->size++; }
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; // 初始化顺序表 SeqListInit(&sl); // 尾插数据 SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPushBack(&sl, 4); // 打印 SeqListPrint(&sl); // 头插 SeqListPushFront(&sl, 10); SeqListPushFront(&sl, 20); SeqListPushFront(&sl, 30); SeqListPushFront(&sl, 40); SeqListPushFront(&sl, 50); // 打印 SeqListPrint(&sl); // 销毁 SeqListDestory(&sl); } int main(){ TestSeqList1(); return 0; }
运行结果:
2.2.9 查找思路: 头删是删除下标为 0 的元素,需要将下标为 [1, size-1] 的元素都依次向前挪动一位,同时要确保size >= 0。
SeqList.h 文件
// 头删 void SeqListPopFront(SeqList* ps);
SeqList.c 文件
#include "SeqList.h" // 头删 void SeqListPopFront(SeqList* ps){ assert(ps->size > 0); // 挪动数据 int begin = 1; while (begin < ps->size){ ps->a[begin - 1] = ps->a[begin]; ++begin; } ps->size--; }
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; // 初始化顺序表 SeqListInit(&sl); // 尾插数据 SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPushBack(&sl, 4); // 打印 SeqListPrint(&sl); // 头删 SeqListPopFront(&sl); SeqListPopFront(&sl); // 打印 SeqListPrint(&sl); // 销毁 SeqListDestory(&sl); } int main(){ TestSeqList1(); return 0; }
运行结果:
2.2.10 指定 pos 位置插入 x找到了返回 X 位置的下标,没有找到返回 -1。
思路: 遍历数组即可。
SeqList.h 文件
// 查找 int SeqListFind(SeqList* ps, SLDataType x);
SeqList.c 文件
#include "SeqList.h" // 查找 int SeqListFind(SeqList* ps, SLDataType x){ assert(ps); for (int i = 0; i < ps->size; i++){ if (ps->a[i] == x){ return i; } } return -1; }
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; // 初始化顺序表 SeqListInit(&sl); // 尾插数据 SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPushBack(&sl, 4); // 打印 SeqListPrint(&sl); // 查找 int pos = SeqListFind(&sl, 2); if (pos != -1){ printf("找到了,下标是:%d\n", pos); } else{ printf("找不到\n"); } // 销毁 SeqListDestory(&sl); } int main(){ TestSeqList1(); return 0; }
运行结果:
2.2.11 删除指定 pos 位置的值思路: 在有效数据 size 范围内,指定下标插入数据,并将该下标的元素及该元素之后的元素都依次向后挪动一位,同时检测是否需要扩容。
SeqList.h 文件
// 在 pos 位置插入 x void SeqListInsert(SeqList* ps, int pos, SLDataType x);
SeqList.c 文件
#include "SeqList.h" // 在 pos 位置插入 x void SeqListInsert(SeqList* ps, int pos, SLDataType x){ assert(ps); // 温柔的检查 /* if (pos > ps->size || pos < 0){ printf("pos invalid:%d\n", pos); // 不会结束程序 return; } */ // 暴力的检查 assert(pos >= 0 && pos <= ps->size); // 检测是否需要扩容 SeqListCheckCapacity(ps); // 挪动数据 int end = ps->size - 1; while (end >= pos){ ps->a[end + 1] = ps->a[end]; --end; } ps->a[pos] = x; ps->size++; }
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; // 初始化顺序表 SeqListInit(&sl); // 尾插数据 SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPushBack(&sl, 4); SeqListPushBack(&sl, 5); // 打印 SeqListPrint(&sl); // 在 pos 位置插入 x SeqListInsert(&sl, 3, 80); // 打印 SeqListPrint(&sl); // 销毁 SeqListDestory(&sl); } int main(){ TestSeqList1(); return 0; }
运行结果:
2.2.12 修改思路: 和头删的思路大致相同,同时确保指定的 pos < size,如果 pos = size,则是删除了一个无意义的数。
SeqList.h 文件
// 删除 pos 位置的值 void SeqListErase(SeqList* ps, int pos);
SeqList.c 文件
#include "SeqList.h" // 删除 pos 位置的值 void SeqListErase(SeqList* ps, int pos){ assert(ps); // 断言判断删除的数据必须在 [0, size-1] 的合法范围内 assert(pos >= 0 && pos < ps->size); int begin = pos + 1; while (begin < ps->size){ ps->a[begin-1] = ps->a[begin]; ++begin; } ps->size--; }
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; // 初始化顺序表 SeqListInit(&sl); // 尾插数据 SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPushBack(&sl, 4); SeqListPushBack(&sl, 5); // 打印 SeqListPrint(&sl); // 删除 pos 位置的值 SeqListErase(&sl, 2); // 打印 SeqListPrint(&sl); // 销毁 SeqListDestory(&sl); } int main(){ TestSeqList1(); return 0; }
运行结果:
2.3 顺序表的问题及思考思路: 在有效数据范围内将指定下标的元素进行修改即可。
SeqList.h 文件
// 修改指定下标的值 void SeqListModify(SeqList* ps, int pos, SLDataType x);
SeqList.c 文件
#include "SeqList.h" // 修改指定下标的值 void SeqListModify(SeqList* ps, int pos, SLDataType x){ assert(ps); assert(pos < ps->size && pos >= 0); ps->a[pos] = x; }
Test.c 文件
#include "SeqList.h" void TestSeqList1(){ SeqList sl; // 初始化顺序表 SeqListInit(&sl); // 尾插数据 SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPushBack(&sl, 4); SeqListPushBack(&sl, 5); // 打印 SeqListPrint(&sl); // 修改指定下标的值 SeqListModify(&sl, 3, 80); // 打印 SeqListPrint(&sl); // 销毁 SeqListDestory(&sl); } int main(){ TestSeqList1(); return 0; }
运行结果:
问题:
1、顺序表要求数据从开始位置连续存储,那么在中间或头部位置插入及删除数据,需要挪动数据,效率不高,时间复杂度为O(N)。
2、扩容需要申请新空间,拷贝数据,释放旧空间。
会有不小的消耗。
3、为了避免频繁扩容,扩容一般是扩大为原来 2 倍的空间,势必会有一定的空间浪费。
例如当前容量为 100,空间满了以后扩容到 200,继续插入 5 个数据,后面没有数据插入,那么就浪费了 95 个数据空间。
针对顺序表的缺陷,就设计出了链表。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)