文章目录
- 顺序表
- 1. 线性表
- 2. 顺序表
- 3. 顺序表基本概念
- 4. 顺序表实现
- 顺序表初始化
- 顺序表的扩容
- 顺序表的插入
- 顺序表的删除
- 顺序表的查找
- 顺序表的修改
- 顺序表的销毁
- 5. 顺序表总结
顺序表
1. 线性表
线性表:线性表是由n个具有相同特性的数据元素组成的序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串。
数据结构中报考逻辑结构和物理结构两个层次
-
逻辑结构
数据的逻辑结构是从逻辑关系上描述数据,它与数据的存储无关,是独立与计算机的。所以,数据的逻辑结构可以看作是从具体问题抽象出来的数学模型。
数据的逻辑结构有两个要素:一是数据元素,二是关系。数据元素的含义如前所述,关系是指数据元素之间的逻辑关系。它根据元素之间关系的不同特性,通常有4类基本结构。
-
集合结构
数据元素之间除了属于同一个集合的关系外,没有其它关系。
-
线性结构
数据元素之间存在这一对一的关系,比如将一些学生的信息按照它的学号按照先后顺序排序,组成一个线性结构
-
树结构
数据元素之间存在着一对多的关系,比如在班级管理体系中,班长管理多个组长,每个组长管理多名组员
-
图结构
数据元素之间存在着多对多的关系,比如多位同学之间的朋友关系,任何两个同学都可以是朋友,从何构成图结构。
-
物理结构
数据对象在计算机中的存储表示称为存储结构,也称为物理结构。把数据对象存储到计算机时,通常要求既要存储数据元素的数据,又要存储数据元素之间的逻辑关系,数据元素在计算机内用一个节点来表示。
物理结构又可以分为顺序存储和链式存储。顺序表和链表就是典型代表。
2. 顺序表
3. 顺序表基本概念
顺序表使用一段连续物理地址的存储单元依次存储数据元素的一种线性结构,简单来说就是一个数组,一个可以动态增长的数组,并且要求里面存储的数据必须是从左往右是连续的。然后在这个数组上完成增删改查。顺序表的特点是逻辑上相邻的数据元素,物理上也是相邻的
顺序表的缺陷
- 动态扩容有性能消耗
- 如果在头部插入数据,需要挪动元素
顺序表的存储结构
typedef int SeqDataType;
typedef struct SeqList
{
SeqDataType* arr;//指向动态开辟数组首元素
size_t size;//存放元素个数
size_t capacity;//容量
}SeqList;
4. 顺序表实现
静态的顺序表只适合用于知道需要存储多少数据的场景,如果静态的顺序表定义的数组太大就会出现浪费,而定义的太小就会不够用。所以使用动态的顺序表会更加合理,通过动态分配空间来实现顺序表。
typedef int SeqDataType;
typedef struct SeqList
{
SeqDataType* arr;
size_t size;//存放元素个数
size_t capacity;//容量
}SeqList;
//初始化顺序表
void SeqListInit(SeqList* pq);
//销毁顺序表
void SeqListDestory(SeqList* pq);
//打印顺序表元素
void SeqListPrint(SeqList* pq);
//判断顺序表是否为空
int SeqListEmpty(SeqList* pq);
//获取顺序表元素个数
size_t SeqListSize(SeqList* pq);
//判断扩容顺序表
int CheckCapacity(SeqList* pq);
//尾插
void SeqListPushBack(SeqList* pq, SeqDataType data);
//头插
void SeqListPushFront(SeqList* pq, SeqDataType data);
//删除末尾元素
void SeqListPopBack(SeqList* pq);
//删除头部元素
void SeqListPopFront(SeqList* pq);
//在顺序表中查找元素
SeqDataType SeqListFind(SeqList* pq, SeqDataType data);
//获取指定位置的数据元素
SeqDataType SeqListGet(SeqList* pq, size_t pos);
//在顺序表pos位置插入数据
void SeqListInsert(SeqList* pq, size_t pos, SeqDataType data);
//删除顺序表中pos位置的元素
void SeqListErase(SeqList* pq, size_t pos);
//修该顺序表pos位置的元素
void SeqListModify(SeqList* pq, size_t pos, SeqDataType target);
这里我只实现比价关键的接口
顺序表初始化
顺序表初始化比较简单,只需要通过malloc开辟一块合适的空间就好.
- 判断结构体是否为空
- 初始化容量为10
- 使用malloc函数开辟空间
//初始化顺序表
void SeqListInit(SeqList* pq)
{
assert(pq);
pq->capacity = 10;
pq->arr = (SeqDataType*)malloc(sizeof(int) * pq->capacity);
if (pq->arr == NULL)
{
printf("初始化失败\n");
exit(1);
}
memset(pq->arr, 0, sizeof(int) * pq->capacity);//给顺序表初始化
pq->size = 0;
}
顺序表的扩容
如果在插入数据元素的时候发现满了就要进行扩容,通过realloc来对数组进行扩容,需要进行扩容的数组进行判空。
- 如果当前元素个数等于数组容量就进行扩容
- 使用realloc对数组进行二倍扩容
- 如果扩容失败,则什么都不做
- 扩容成功就把结构体数组指向扩容空间的首地址
//判断扩容顺序表
int CheckCapacity(SeqList* pq)
{
if (pq->size == pq->capacity)
{
//二倍扩容
pq->capacity = pq->capacity * 2;
SeqDataType* ptr = (SeqDataType*)(realloc(pq->arr, sizeof(int) * pq->capacity));
if (ptr == NULL)
{
printf("扩容失败\n");
pq->capacity = pq->capacity / 2;
return 1;
}
pq->arr = ptr;
printf("扩容成功\n");
}
return 0;
}
顺序表的插入
顺序表元素插入分为3种,从头部插入、从末尾插入、从指定位置插入。其实只要实现了从指定位置插入,头插和尾插都可以调用指定位置插入的接口。指定位置插入思路。
- 首先得判NULL和是否越界
- 判断是否需要扩容
- 从最后一个元素位置开始,将元素从前往后移动
//在顺序表pos位置插入数据
void SeqListInsert(SeqList* pq, size_t pos, SeqDataType data)
{
assert(pq);
assert(pos <= pq->size);//越界判断
CheckCapacity(pq);
size_t index = pq->size;
while (index > pos)
{
pq->arr[index] = pq->arr[index - 1];
index--;
}
pq->arr[pos] = data;
pq->size++;
}
比如要在下标为2的位置插入元素
实现了指定位置插入之后,头插和尾插就比较简单了
头插
//头插
void SeqListPushFront(SeqList* pq, SeqDataType data)
{
assert(pq);
SeqListInsert(pq, 0, data);
}
尾插
//尾插
void SeqListPushBack(SeqList* pq, SeqDataType data)
{
assert(pq);
SeqListInsert(pq, pq->size, data);
}
顺序表插入元素的时间复杂度是 O ( N ) O(N) O(N)
顺序表的删除
顺序表的删除分为头删、尾删和指定位置删除,和插入类似,只需要从后往前挪动元素将要删除的元素覆盖即可。
- 判NULL和是否越界
- 将pos位置的元素全部往前挪动
- 元素个数减1
//删除顺序表中pos位置的元素
void SeqListErase(SeqList* pq, size_t pos)
{
assert(pq);
assert(pos < pq->size);
size_t index = pos;
while (index < pq->size - 1)
{
pq->arr[index] = pq->arr[index + 1];
index++;
}
pq->size--;
}
完成了指定位置删除就可以,调用它完成头删和尾删了
头删
//删除头部元素
void SeqListPopFront(SeqList* pq)
{
assert(pq);
SeqListErase(pq, 0);
}
尾删
//删除末尾元素
void SeqListPopBack(SeqList* pq)
{
assert(pq);
assert(pq->size != 0);
SeqListErase(pq, pq->size-1);
}
顺序表删除的时间复杂为 O ( N ) O(N) O(N)
顺序表的查找
顺序表的查找分两种,一种是查找指定数据元素返回下标,另外一种是直接获取某个下标的元素
查找元素
//在顺序表中查找元素
SeqDataType SeqListFind(SeqList* pq, SeqDataType data)
{
assert(pq);
size_t i = 0;
for (i = 0; i < pq->size; i++)
{
if (pq->arr[i] == data)
{
//返回第一个找到的元素的下标
return i;
}
}
return -1;
}
获取指定位置的元素
//获取指定位置的数据元素
SeqDataType SeqListGet(SeqList* pq, size_t pos)
{
assert(pq);
assert(pos < pq->size);
return pq->arr[pos];
}
顺序表查找的时间复杂度为 O ( N ) O(N) O(N),但如果是给定下标获取元素则是 O ( 1 ) O(1) O(1)
顺序表的修改
顺序表的修改指定下标的元素,也是非常简单的
//修该顺序表pos位置的元素
void SeqListModify(SeqList* pq, size_t pos, SeqDataType target)
{
assert(pq);
assert(pos < pq->size);
pq->arr[pos] = target;
}
顺序表的修改时间复杂度为 O ( N ) O(N) O(N)
顺序表的销毁
顺序表的销毁只需要
-
把元素个数容量赋值成0
-
将开辟的内存空间释放掉
-
再将数组指向NULL
//销毁顺序表
void SeqListDestory(SeqList* pq)
{
assert(pq);
pq->size = 0;
pq->capacity = 0;
free(pq->arr);
pq->arr = NULL;
}
5. 顺序表总结
- 顺序表的插入和删除时间复杂度为 O ( N ) O(N) O(N)
- 顺序表的扩容使用的是realloc,如果后面的空间不够就会重新开辟新的空间,再把数据拷贝到新的空间,这样就会有一定的额性能消耗
- 顺序表存在着一定的资源浪费,比如当前顺序表的容量为200,当放满之后再插入10个元素,就会进行2倍扩容到400,此时就浪费了190个数据空间。
我们发现顺序表存在着一定缺陷,如果想不那么浪费空间就想着扩容扩少一点,但扩容小了如果有大量数据插入又会存着不断扩容的情况,那么又会有着性能的消耗。
针对以上问题,就可以引入链表。
下一篇链表博客马上发布。