数据结构——顺序表
一、线性表
1.概念:
线性表是具有一类相同特性的数据结构的集合。线性表是一种在实际中广泛使用的数据结构,常见的线性表有顺序表、链表、栈、队列、字符串…
相同特性从两个方面来看:
逻辑结构:人为想象出来的数据的组织形式;
物理结构:数据在内存上的存储形式。
2.结构:
逻辑结构:都是线性的;
物理结构:不一定是线性的。
二、顺序表
1.概念:
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。
2.结构:
逻辑结构:是线性的;
物理结构:是线性的。
3.顺序表与数组的关系:
顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接口。
顺序表用结构体对数据进行封装。
即顺序表=(数组+增加数据+删除数据+修改数据+查找数据+…)
4.顺序表的结构体如何定义:
4.1.已知顺序表的大小(静态顺序表):
struct SeqList
{
int arr[1000];
int size;//顺序表中有效数据的个数
}
静态顺序表缺点:空间给小了不够用,给大了造成空间的浪费。
4.2.不知道顺序表的大小(动态顺序表):动态内存管理
struct SeqList
{
int* arr;
int capacity;//顺序表空间大小,可变
int size;//有效数据个数
}
实现动态顺序表:
//头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义动态顺序表结构
typedef int SLDatatype;
typedef struct SeqList {
SLDatatype* arr;
int capacity;//空间大小
int size;//有效数据个数
}SL;
//初始化
void SLInit(SL* ps);
//销毁
void SLDestroy(SL* ps);
//打印
void SLPrint(SL* ps);
//插入数据
void SLPushBack(SL* ps, SLDatatype x);
void SLPushFront(SL* ps, SLDatatype x);
//删除
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);
//在指定位置之前插入数据
void SLInsert(SL* ps, SLDatatype x, int pos);
//删除指定位置的数据
void SLErase(SL* ps, int pos);
//查找
int SLFind(SL* ps, SLDatatype x);
4.2.1初始化
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
4.2.2销毁
//销毁
void SLDestroy(SL* ps)
{
if (ps->arr)//相当于ps->arr!=NULL
{
free(ps->arr);
}
ps->arr = NULL;//防止野指针
ps->size = ps->capacity = 0;
}
4.2.3顺序表元素的打印
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
4.2.4插入
在插入数据时,我们要考虑空间是否足够,若是插满数据了,就要进行增容
增容分两种情况:
1.连续空间足够,直接扩容;
2.连续空间不够:
(1).重新找一块地址,分配足够的内存;
(2).拷贝数据到新的地址;
(3).销毁旧地址.
代码如下:
void SLCheckCapacity(SL* ps)
{
//判断空间是否充足
if (ps->size == ps->capacity)
{
//若capacity为0,给个默认值,否则×2倍
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, newCapacity * sizeof(SLDatatype));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
增容是成倍增加的,为什么不一个一个加呢?
因为增容的操作本身就有一定的程序性能的消耗,若频繁的增容会导致程序效率低下。
-
尾插
void SLPushBack(SL* ps, SLDatatype x) { assert(ps); /*if (ps == NULL) { return; }*/ SLCheckCapacity(ps); ps->arr[ps->size++] = x; }
尾插的时间复杂度为O(1).
-
头插
void SLPushFront(SL* ps, SLDatatype x) { assert(ps); //判断空间是否足够 SLCheckCapacity(ps); //数据整体后移一位 for (int i = ps->size; i > 0; i--) { ps->arr[i] = ps->arr[i - 1]; } //下标为0的位置空出来 ps->arr[0] = x; ps->size++; }
头插的时间复杂度为O(N).
4.2.5删除
-
尾删
void SLPopBack(SL* ps) { assert(ps); assert(ps->size); //ps->arr[ps->size - 1] = -1;可要可不要 ps->size--; }
尾删的时间复杂度为O(1).
-
头删
void SLPopFront(SL* ps) { assert(ps); assert(ps->size); //数据整体向前移动一位 for (int i = 0; i < ps->size - 1; i++) { ps->arr[i] = ps->arr[i + 1];//i=size-2 } ps->size--; }
头删的时间复杂度是O(N).
4.2.6在指定位置之前插入数据
//在指定位置之前插入数据(空间足够才能直接插入数据)
void SLInsert(SL* ps, SLDatatype x, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
//pos及之后的数据整体向后移动一位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];//pos+1——>pos
}
ps->arr[pos] = x;
ps->size++;
}
中间插入的时间复杂度是O(N - pos).
4.2.7删除指定位置的数据
//删除指定位置的数据
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
//还有更多的限制:如顺序表不能为空...
//pos之后的数据整体向前挪动一位
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];//size-2<- size-1
}
ps->size--;
}
4.2.8查找
int SLFind(SL* ps, SLDatatype x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
//没有找到:返回一个无效的下标
return -1;
}
三、总结
- 尾插和尾删的时间复杂度都是O(1),因为操作只涉及数组的最后一个位置。
- 头插和头删的时间复杂度都是O(N),因为操作涉及数组中所有元素的移动。
- 中间插入的时间复杂度是O(N- pos)。
- 虽然头插和尾插可以视为中间插入的特例,但它们在时间复杂度上的表现是不同的,因此,在实际讨论和实现中,通常会分开考虑这三种插入操作,以便更清晰地理解和分析它们的时间复杂度。