什么是顺序表?
将表中元素一个接一个的存入一组连续的存储单元中,这种存储结构是顺序结构。
采用顺序存储结构的线性表简称为“ 顺序表”。
顺序表是在计算机内存中以
数组
的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元依次存储线性表中的各个元素、使得线性表中在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系,采用顺序存储结构的线性表通常称为顺序表。顺序表是将表中的结点依次存放在计算机内存中一组地址连续的存储单元中。
【优点】:因为逻辑顺序与物理顺序一致的特点,当查找某个元素,使用O(1)
的时间复杂度就可以找到。
【缺点】:
- 中间/头部的插入删除,时间复杂度为
O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的开销。
- 增容一般是呈
2
倍的增长,势必会有一定的空间浪费。例如当前容量为100
,满了以后增容到200
,再继续插入了5
个数据,后面没有数据插入了,那么就浪费了95
个数据空间。
实现
定义表结构
typedef int SLDataType; //表元素定义统一定义
typedef struct SeqList{
SLDataType* array; // 指向动态开辟的数组
size_t size; // 有效数据个数
size_t capacity; // 容量空间的大小
}SeqList;
初始化
void SeqListInit(SeqList *psl, size_t capacity) {
assert(psl != NULL); //判定是否为空表
psl->capacity = capacity;
psl->size = 0;
psl->array = (SLDataType*)malloc(capacity * sizeof(SLDataType));
//再次进行无错误检验
assert(psl->array != NULL);
}
校验
- 思路:如果容量未满,不进行操作。如果容量充满,进行扩容操作。
//检查:size和capacity,是否需要进行扩容
void CheckCapacity(SeqList *psl) {
//是否扩容
if (psl->size < psl->capacity) { //如果容量没有充满,即不进行扩充
return;
}
//if条件不满足,即容量充满,进行扩充操作:
//1. 申请新空间
int NewCapacity = psl->capacity * 2;
SLDataType *NewArray = (SLDataType*)malloc(sizeof(SLDataType)*NewCapacity);
assert(NewArray);
//2. 搬移数据
for (int i = 0; i < psl->size; ++i) {
NewArray[i] = psl->array[i];
}
//3. 释放旧空间,否则内存泄漏
free(psl->array);
//4. 保存新空间
psl->array = NewArray;
psl->capacity = NewCapacity;
}
输出
void SeqListPrint(SeqList *psl) {
for (int i = 0; i < psl->size; ++i) { //遍历,逐元素输出
printf("%d ", psl->array[i]);
}
printf("\n");
}
查找
- 思路:查找从
0
号下标开始的第一次出现,遍历表中元素,如果到了返回数据所在的下标,如果没找到返回-1
int SeqListFind(SeqList* psl, SLDataType data) {
assert(psl);
for (int i = 0; i < psl->size; ++i) {
if (psl->array[i] == data) {
return i;
}
}
//没有找到
return -1;
}
修改
//修改:修改pos所在的下标的内容
void SeqListModify(SeqList* psl, size_t pos, SLDataType data) {
assert(pos >= 0 && pos < psl->size);
psl->array[pos] = data;
}
头删
- 思路:将全部元素向前挪一个单位,将有效位数减一
//删除首元素
void SeqListPopFront(SeqList *psl) {
assert(psl);
assert(psl->size > 0);
//i:表示数据所在的下标
for (int i = 0; i < psl->size; ++i) {
psl->array[i - 1] = psl->array[i];
}
psl->size--;
}
尾删
- 思路:有效位数减一即可
//删除尾元素
void SeqListPopBack(SeqList *psl) {
assert(psl);
assert(psl->size > 0);
psl->size--;
}
删除所在下标内容
- 思路:从
pos
下标之后开始,将表中元素逐个向前挪移一个单位,有效位数减一
//删除:删掉pos所在的下标数据
void SeqListErase(SeqList* psl, size_t pos) {
assert(psl);
assert(psl->size > 0);
assert(pos >= 0 && pos < psl->size);
for (int i = pos + 1; i < psl->size; ++i) {
psl->array[i - 1] = psl->array[i];
}
psl->size--;
}
删除首次出现的元素
- 思路:使用定义好的
SeqListInsert
函数返回元素下标,如果查找到了直接进行删除,否则无事发生。
//删除:删除第一次出现的data元素
void SeqListRemove(SeqList* psl, SLDataType data) {
assert(psl != NULL);
int pos = SeqListFind(psl, data);
if (pos != -1) { //找到了
SeqListErase(psl, pos);
}
}
删除所有某一数值元素
-
思路1:使用
SeqListFind
函数进行查找,找到即进行删除元素。
(时间复杂度为O(n^2)
,空间复杂度为O(n)
) -
思路2:动态申请一段内存空间进行过渡,将不是指定的此数值的所有元素顺序放入申请的内存空间,然后将此空间中的元素再逐个放回原空间。有效个数也更改为申请的内存中的元素个数。
(时间复杂度为O(n)
,空间复杂度为O(n)
) -
思路3:进行表内元素遍历,如果是指定的此数值就进行跳过操作,不是此数值就顺序排列,最后会筛选掉所有指定数值的元素。
(时间复杂度为O(n)
,空间复杂度为O(1)
)
//删除:删除表中某一个固定数值的所有元素
void SeqListRemoveAll(SeqList *psl, SLDataType data) {
/*
*方案一:
* 时间复杂度为O(n^2):执行了 2 次元素遍历,即为O(N*N)
* 空间复杂度为O(n):每次开辟 N 个内存空间
*/
int pos;
while (pos == SeqListFind(psl, data) != -1) { //时间复杂度O(n)
SeqListErase(psl, pos); //时间复杂度O(n)
}
/*
*方案二:
* 时间复杂度为O(n):进行了 1 次元素遍历
* 空间复杂度为O(n):每次开辟 N 个内存空间
*/
SLDataType *temparray = (SLDataType*)malloc(sizeof(SLDataType*)*psl->size);
assert(temparray); //判定是否申请失败
int j = 0;
for (int i = 0; i < psl->size; ++i) { //主要是这个循环占主要复杂度
if (psl->array[i] != data) {
temparray[j++] = psl->array[i];
}
}
for (int k = 0; k < j; k++) {
psl->array[k] = temparray[k];
}
psl->size = j;
}
/*
*方案三:
* 时间复杂度为O(n):进行了 1 次元素遍历
* 空间复杂度为O(1):每次开辟 常数 个内存空间
*/
int j = 0;
for (int i = 0; i < psl->size; ++i) {
if (psl->array[i] != data) {
psl->array[j++] = psl->array[i];
}
}
psl->size = j;
}
插入
- 思路:检查下标参数是否有效,是则从表尾元素进行遍历挪移一位,直至到
pos
下标位停止挪移,将数据放在此下标,有效位数加一。
//插入:在pos所在的下标做数据插入
void SeqListInsert(SeqList* psl, size_t pos, SLDataType data) {
//1. 有效性检验
assert(psl);
//2. pos的有效范围[0,size]
assert(pos >= 0 && pos <= psl->size);
for (int i = psl->size - 1; i >= (int)pos; --i) {
psl->array[i + 1] = psl->array[i];
}
psl->array[pos] = data;
psl->size++;
}
头插
- 思路1:插入前先检查是否充满容量,是则扩容再进行使用。然后将表元素逐个向后挪一个单位,完成后将输入的数据放在首元素位置上。
- 思路2:使用封装的思想,使用前面
SeqListInsert
插入函数,直接进行插入。
//在表头插入data元素
void SeqListPushFront(SeqList *psl, SLDataType data) {
assert(psl != NULL);
/*
*方案一:
*/
CheckCapacity(psl);
//从头到尾进行数据挪移
for (int i = psl->size; i >= 1; --i) {
psl->array[i] = psl->array[i - 1];
}
psl->array[0] = data;
psl->size--;
/*
*方案二:
*/
SeqListInsert(psl, 0, data);
}
尾插
- 思路:在当前有效位后加上元素,之后有效位数加一
//在表尾插入data元素
void SeqListPushBack(SeqList *psl, SLDataType data) {
assert(psl != NULL);
CheckCapacity(psl); //校验十分必要,如果容量已满无法进行尾插
psl->array[psl->size] = data;
psl->size++;
}
销毁申请的内存
- 思路:释放掉申请的内存,将有效位数与容量都置为
0
,保险起见将释放内存后的指针置为空指针。
void SeqListDestroy(SeqList *psl) {
assert(psl != NULL);
free(psl->array);
psl->array = NULL; //防御性代码
psl->capacity = psl->size = 0;
}
冒泡排序
void SeqListBubbleSort(SeqList *psl) {
assert(psl != NULL);
bool isSort = true; //需要包含头文件 <stdbool.h>
//如果不适用bool类型可以使用:int isSort = 1;
for (int i = 0; i < psl->size; i++) {
for (int j = psl->size - 1; j > i; --j) {
if (psl->array[i - 1] > psl->array[i]) {
int temp = psl->array[i - 1];
psl->array[i - 1] = psl->array[i];
psl->array[i] = temp;
isSort = false; //发生过交换就处理为false
}
}
}
if (isSort == true) { //如果元素本就有序
return;
}
}
二分查找
//===========(前提是数据有序)===========
//如果找到,返回下标,如果没找到,返回-1
int SeqListBinarySearch(SeqList *psl, SLDataType data) {
assert(psl != NULL);
//left 、 right代表下标
int left = 0;
int right = psl->size - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (psl->array[mid] == data) {
return mid;
}
else if (data <= psl->array[left]) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
return -1;
}
- 注:以下两种传参方式的区别:
//定义
SeqList a;
SeqList *b;
SeqListInit(&a);
:
传a
的地址,因为a
是栈上的空间,是用户自己的,所以可以直接传参、访问。SeqListInit(b);
传指针b
,但函数不可以直接传指针b
,因为b
指向的空间未知,指针无效。所以必须先开辟一段内存空间给自己用b = malloc();
才保证有效。
最后附上链表的接口实现:【https://blog.csdn.net/qq_42351880/article/details/87971991 】