数据结构 时间复杂度和空间复杂度
目录
1.线性表
2.顺序表
2.1概念及结构
2.2 接口实现
SeqList.h
SeqList.c
2.2.1初始化链表以及销毁链表的实现
初始化顺序表
销毁顺序表
2.2.2查找元素前驱或后继的实现
查找前驱
查找后继
2.2.3查找元素和获取元素的实现
查找元素
2.2.4删除元素的实现
尾删
头删
任意位置删除
2.2.4增加元素的实现
尾加
malloc / calloc / realloc 区别
扩容
头加
任意位置添加
概念:线性表是n个具有相同特征性的数据元素的有限序列,线性表是一种在实际中广泛使用的数据结构,每个元素都有唯一的前去和唯一的后继,前一个元素没有前驱只有后继,最后一个元素没有后继只有前驱,常见的线性表:顺序表、链表、栈、队列、字符串。
数组不等于线性表,数组只是把元素存储起来了!可以增加一些增删改的方法!
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
1. 静态顺序表:使用定长数组存储元素。
2. 动态顺序表:使用动态开辟的数组存储。
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
也可以用来实现严薇敏版数据结构的算法2.1~2.7
#pragma once
#include
#include
#include
#include
//可以改变你给顺序表里面放的数据类型
typedef int Datatype;
//静态顺序表
//你数组开辟空间时候必须要知道开辟的大小
//typedef struct SeqList {
// Datatype arr[NUM];
// int capacity;
// int size;
//}SeqList;
//动态顺序表
typedef struct SeqList {
Datatype* arr;
int capacity;
int size;
}SeqList;
//对顺序表进行打印
void PrintSeqList(SeqList* ps);
//显示顺序表信息
void PrintSeqListData(SeqList* ps);
//初始化顺序表
void SeqListInit(SeqList* ps, int initcapacity);
//销毁顺序表
void SeqListDestory(SeqList* ps);
//获取顺序表有效元素的个数
int SeqListSize(SeqList* ps);
//获取顺序表的容量大小
int SeqListCapacity(SeqList* ps);
//检测顺序表是否为空
int SeqListEmpty(SeqList* ps);
//将顺序表置为空表
void SeqListClear(SeqList* ps);
//查找顺序表中元素
int SeqListFind(SeqList* ps, Datatype data);
//获取顺序表中对应位置的值
Datatype SeqListGet(SeqList* ps, int pos);
//查找顺序表中一个元素的前驱
Datatype SeqListGetPrior(SeqList* ps, int pos);
//查找顺序表中一个元素的后继
Datatype SeqListGetNext(SeqList* ps, int pos);
// 将顺序表中最后一个元素删除掉
void SeqListPopBack(SeqList* ps);
// 将顺序表中第一个元素删除掉
// 时间复杂度:O(N)
void SeqListPopFront(SeqList* ps);
// 往顺序表中尾插一个值为data的元素
// 时间复杂度:O(1)
void SeqListPushBack(SeqList* ps, Datatype data);
// 往顺序表中头插一个值为data的元素
// 时间复杂度是多少?O(N)
void SeqListPushFront(SeqList* ps, Datatype data);
// 删除顺序表任意位置的元素
// 时间复杂度O(N)
void SeqListErase(SeqList* ps, int pos);
// 在顺序表的任意pos位置插入元素data
// 时间复杂度O(N)
void SeqListInsert(SeqList* ps, int pos, Datatype data);
//写到这块了其实咱们顺序表存在一个巨大的问题
//你的size == capacity 在增加还行吗?
//还需要对你的arr进行扩容
//1.申请新的空间
//2.将原有数据进行拷贝
//3.旧空间的释放
//4.返回新空间的地址
void CheckCapacityMode1(SeqList* ps);
//1.申请新的空间
//2.将原有数据进行拷贝
//3.旧空间的释放
//4.返回新空间的地址
//这不和realloc一样吗
void CheckCapacityMode2(SeqList* ps);
#include "SeqList.h"
void PrintSeqList(SeqList* ps)
{
//对传入的指针进行断言
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
//显示顺序表信息
void PrintSeqListData(SeqList* ps)
{
assert(ps);
PrintSeqList(ps);
printf("有效元素个数为: %d \n", ps->size);
printf("开辟容量大小为: %d \n", ps->capacity);
}
//初始化顺序表
void SeqListInit(SeqList* ps, int initcapacity)
{
//对传入的指针进行断言
assert(ps);
//传入负数的体现
initcapacity = initcapacity <= 0 ? 3 : initcapacity;
//对顺序表开始动态申请内存
ps->arr = (Datatype*)malloc(initcapacity * sizeof(Datatype));
if (NULL == ps->arr) {
assert(0);
return;
}
ps->capacity = initcapacity;
ps->size = 0;
}
//销毁顺序表
void SeqListDestory(SeqList* ps)
{
//已经为空直接返回
if (NULL == ps->arr) {
printf("你的顺序表已经为空!");
return;
}
//free();掉你在堆上申请的内存空间
if (ps->arr) {
free(ps->arr);
ps->arr = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
//获取顺序表有效元素的个数
int SeqListSize(SeqList* ps)
{
assert(ps);
//直接返回size大小即可
return ps->size;
}
//获取顺序表的容量大小
int SeqListCapacity(SeqList* ps)
{
assert(ps);
//直接返回capacity即可
return ps->capacity;
}
//检测顺序表是否为空
int SeqListEmpty(SeqList* ps)
{
//你的有效元素要是为0则顺序表为空
if (0 == ps->size) {
printf("你的顺序表为空!");
return 1;
}
return 0;
}
//将顺序表置为空表
void SeqListClear(SeqList* ps)
{
assert(ps);
//你的有效元素要是为0则顺序表为空
ps->size = 0;
}
//查找顺序表中元素
int SeqListFind(SeqList* ps, Datatype data)
{
assert(ps);
//遍历顺序表找到你的目标数字
for (int i = 0; i < ps->size; i++) {
if (data == ps->arr[i]) {
printf("找到了%d!他的下标是%d", ps->arr[i], i);
return 1;
}
}
return -1;
}
//获取顺序表中对应位置的值
Datatype SeqListGet(SeqList* ps, int pos)
{
assert(ps);
//对pos合法性判断
if (pos<0 || pos>ps->size - 1) {
printf("你的pos输入有误!");
return 0;
}
return (Datatype)ps->arr[pos];
}
//查找顺序表中一个元素的前驱
Datatype SeqListGetPrior(SeqList* ps, int pos)
{
assert(ps);
//对pos合法性判断
if ((pos<0 || pos>ps->size - 1) && pos == 0) {
printf("你的pos输入有误!");
return 0;
}
return (Datatype)ps->arr[--pos];
}
Datatype SeqListGetNext(SeqList* ps, int pos)
{
assert(ps);
//对pos合法性判断
if ((pos<0 || pos>ps->size - 1) && pos == ps->size - 1) {
printf("你的pos输入有误!");
return 0;
}
return (Datatype)ps->arr[++pos];
}
// 将顺序表中最后一个元素删除掉
//我们的size是有效元素的个数,因此即使删除,数据还在capacity,即使删除后没有被覆盖,但不会访问!
void SeqListPopBack(SeqList* ps)
{
//顺序是空我们就不操作了
if (SeqListEmpty(ps)) {
return;
}
if (ps->size > 0) {
ps->size--;
}
}
// 将顺序表中第一个元素删除掉
// 时间复杂度:O(N)
void SeqListPopFront(SeqList* ps)
{
//顺序是空我们就不操作了
if (SeqListEmpty(ps)) {
return;
}
//删除第一个元素之后应该要把原有数据整体向前搬运
//如果是从后往前搬运那这样就会造成内存覆盖
//应该从前面开始搬运
for (int i = 1; i < ps->size; i++) {
ps->arr[i - 1] = ps->arr[i];
}
//搬运结束有效元素减一
ps->size--;
}
// 往顺序表中尾插一个值为data的元素
// 时间复杂度:O(1)
void SeqListPushBack(SeqList* ps, Datatype data)
{
assert(ps);
CheckCapacityMode2(ps);
ps->arr[ps->size] = data;
//有效元素加一
ps->size++;
}
// 往顺序表中头插一个值为data的元素
// 时间复杂度是多少?O(N)
void SeqListPushFront(SeqList* ps, Datatype data)
{
CheckCapacityMode2(ps);
//在第一个位置插入,那也就是说还得进行搬运
//这次怎么搬运呢
//应该是从后往前搬运要是从前往后搬运就会造成内存覆盖
for (int i = ps->size - 1; i >= 0; --i) {
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[0] = data;
ps->size++;
}
// 删除顺序表任意位置的元素
// 时间复杂度O(N)
void SeqListErase(SeqList* ps, int pos)
{
//顺序是空我们就不操作了
if (SeqListEmpty(ps)) {
return;
}
//对pos合法性判断
if (pos < 0 || pos > ps->size - 1) {
printf("你的pos输入有误!");
return;
}
for (int i = pos; i < ps->size; i++) {
ps->arr[pos] = ps->arr[pos + 1];
}
ps->size--;
}
// 在顺序表的任意pos位置插入元素data
// 时间复杂度O(N)
void SeqListInsert(SeqList* ps, int pos, Datatype data)
{
CheckCapacityMode2(ps);
//顺序是空我们就不操作了
if (SeqListEmpty(ps)) {
return;
}
//对pos合法性判断
if (pos < 0 || pos > ps->size - 1) {
printf("你的pos输入有误!");
return;
}
for (int i = ps->size - 1; i >= pos; i--)
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[pos] = data;
ps->size++;
}
//写到这块了其实咱们顺序表存在一个巨大的问题
//你的size == capacity 在增加还行吗?
//还需要对你的arr进行扩容
//1.申请新的空间
//2.将原有数据进行拷贝
//3.旧空间的释放
//4.返回新空间的地址
void CheckCapacityMode1(SeqList* ps)
{
if (ps->size == ps->capacity) {
//1.申请新的空间
int newcapacity = ps->capacity * 2;
Datatype* temp = (Datatype*)malloc(newcapacity * sizeof(Datatype));
if (NULL == temp)
{
assert(0);
return;
}
//2.将原有数据进行拷贝
memcpy(temp, ps->arr, ps->size * sizeof(Datatype));
//3.旧空间的释放
free(ps->arr);
ps->arr = temp;
ps->capacity = newcapacity;
}
}
//
void CheckCapacityMode2(SeqList* ps)
{
int newcapacity = ps->capacity * 2;
ps->arr = (int*)malloc(sizeof(Datatype));
if (NULL != ps->arr)
{
Datatype* temp = (Datatype*)realloc(ps->arr, sizeof(Datatype) * newcapacity);
if (NULL != temp)
{
ps->arr = temp;
}
ps->capacity = newcapacity;
}
/*
ps->array = (int*)realloc(ps->array, sizeof(DataType)*newCapacity);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = newCapacity;
*/
//会有告警"realloc"可能会返回 null 指针:将空<>指针分配给变量(作为参数传递给"realloc")将导致原始内存块泄漏
//此警告指示内存泄漏,该泄漏是不正确使用重新分配函数的结果。 如果重新分配不成功
//堆重新分配函数不会释放传递的缓冲区
//若要更正缺陷,请将重新分配函数的结果分配给临时 ,然后在成功重新分配后替换原始指针。
}
int main() {
}
void SeqListInit(SeqList* ps, int initcapacity)
{
//对传入的指针进行断言
assert(ps);
//传入负数的体现
initcapacity = initcapacity <= 0 ? 3 : initcapacity;
//对顺序表开始动态申请内存
ps->arr = (Datatype*)malloc(initcapacity * sizeof(Datatype));
if (NULL == ps->arr) {
assert(0);
return;
}
ps->capacity = initcapacity;
ps->size = 0;
}
动态链表吗,那就用动态开辟内存的方法从堆上申请空间呗,对于这个顺序表尽可能完善他的功能,当capacity为负数的时候也要检验一下,当用malloc的时候还要判断是否开辟成功(是否为空)调用完Init这个方法一定要调用destory这个方法将我们动态开辟的内存空间释放防止内存泄漏
测试
//销毁顺序表
void SeqListDestory(SeqList* ps)
{
//已经为空直接返回
if (NULL == ps->arr) {
printf("你的顺序表已经为空!");
return;
}
//free();掉你在堆上申请的内存空间
if (ps->arr) {
free(ps->arr);
ps->arr = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
顺序表为空的话就直接返回了不用在销毁了。然后在free掉你在堆上申请的空间,让后将capacity和size置零。
测试
Datatype SeqListGetPrior(SeqList* ps, int pos)
{
assert(ps);
//对pos合法性判断
if ((pos<0 || pos>ps->size - 1) && pos == 0) {
printf("你的pos输入有误!");
return 0;
}
return (Datatype)ps->arr[--pos];
}
我们这里的pos是数组下标 先对传入的pos进行合法性校验,根据顺序表的概念我们可知,顺序表的每一个元素都有唯一的前驱和后继,第一个元素没有前驱,最后一个元素没有后继,而且pos是数组下标不能越界!
//查找顺序表一个元素的后继
Datatype SeqListGetNext(SeqList* ps, int pos)
{
assert(ps);
//对pos合法性判断
if ((pos<0 || pos>ps->size - 1) && pos == ps->size - 1) {
printf("你的pos输入有误!");
return 0;
}
return (Datatype)ps->arr[++pos];
}
测试
//查找顺序表中元素
int SeqListFind(SeqList* ps, Datatype data)
{
assert(ps);
//遍历顺序表找到你的目标数字
for (int i = 0; i < ps->size; i++) {
if (data == ps->arr[i]) {
printf("找到了%d!他的下标是%d", ps->arr[i], i);
return 1;
}
}
return -1;
}
遍历顺序表找到你的目标数字找到了返回一找不到返回-1
Datatype SeqListGet(SeqList* ps, int pos)
{
assert(ps);
//对pos合法性判断
if (pos<0 || pos>ps->size - 1) {
printf("你的pos输入有误!");
return 0;
}
return (Datatype)ps->arr[pos];
}
我们这里的pos是数组下标 先对传入的pos进行合法性校验,返回pos位置的元素
// 将顺序表中最后一个元素删除掉
//我们的size是有效元素的个数,因此即使删除,数据还在capacity,即使删除后没有被覆盖,但不会访问!
void SeqListPopBack(SeqList* ps)
{
//顺序是空我们就不操作了
if (SeqListEmpty(ps)) {
return;
}
if (ps->size > 0) {
ps->size--;
}
}
将顺序表中最后一个元素删除掉
我们的size是有效元素的个数,因此即使删除,数据还在capacity,即使删除后没有被覆盖,但不会访问!
测试
就很清晰的看出来数据还在capacity,即使删除后没有被覆盖
// 将顺序表中第一个元素删除掉
// 时间复杂度:O(N)
void SeqListPopFront(SeqList* ps)
{
//顺序是空我们就不操作了
if (SeqListEmpty(ps)) {
return;
}
//删除第一个元素之后应该要把原有数据整体向前搬运
//如果是从后往前搬运那这样就会造成内存覆盖
//应该从前面开始搬运
for (int i = 1; i < ps->size; i++) {
ps->arr[i - 1] = ps->arr[i];
}
//搬运结束有效元素减一
ps->size--;
}
先检验顺序表是否为空,要是为空的话就不操作了,直接返回,然后就是剩下的算法,怎么样子将顺序表元素一个个置位呢?
测试
// 删除顺序表任意位置的元素
// 时间复杂度O(N)
void SeqListErase(SeqList* ps, int pos)
{
//顺序是空我们就不操作了
if (SeqListEmpty(ps)) {
return;
}
//对pos合法性判断
if (pos < 0 || pos > ps->size - 1) {
printf("你的pos输入有误!");
return;
}
for (int i = pos; i < ps->size; i++) {
ps->arr[pos] = ps->arr[pos + 1];
}
ps->size--;
}
删除吗,还是那句话顺序表是空的删除啥?直接先判断是否为空,是空直接就返回了,第二个参数是我们的位置参数,换而言之就是数组下标,下标传进来首先就是要进行参数的合法性校验,看是不是在我们的操作范围之内,不在返回,在范围内进行操作,还是那一套搬运从前往后搬运防止内存覆盖,最后有效元素的个数--;
测试
// 往顺序表中尾插一个值为data的元素
// 时间复杂度:O(1)
void SeqListPushBack(SeqList* ps, Datatype data)
{
assert(ps);
ps->arr[ps->size] = data;
//有效元素加一
ps->size++;
}
我们的size是有效元素的大小,做数组下标的时候那就是第size + 1个元素我们直接放进去就好了,最后有效元素的个数++就好了,
请问这样真的就好了吗?这个尾插的方法就没有缺陷吗?
如果尾插完之后有效元素的个数大于你所开辟的capacity呢会怎么样呢?
我们用以下测试样例
void TestSeqList()
{
SeqList s;
SeqListInit(&s, 3);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPushBack(&s, 6);
SeqListPushBack(&s, 7);
PrintSeqList(&s);
SeqListDestory(&s);
}
heap corruption detected检测到堆溢出了,你动态开辟就申请了三个数据类型大小的空间你一下尾插就加入了7个直接就溢出了,这个报错在debug版本会弹框release版本下不会,但是还是很危险的,你的内存泄露了!
那应该怎么办呢?
扩大我capacity容量呗!
那我们先再次复习一下动态开辟内存函数
相同点:
不同点:
malloc :只负责从堆上将空间申请成功,并不会将其初始化
calloc :有两个参数 一个是元素个数一个是元素的的大小,从堆上申请空间,并且会将内存空间中的每个元素初始话为0
realloc :函数原型
void* relloc (void* ptr,size_t size);
size_t 表示的是unsigned int 类型将ptr中的字节调整到size个
ptr表示的是是调整内存的地址当ptr == NULL 则等于是malloc了
一般relloc在调整内存是存在两种情况
1.原有空间后面有足够大的空间
直接向后扩充就好了
2.原有空间后面没有足够大的空间
所以说ptr也就是堆空间的起始地址有可能是变化的!
扩容的时机:当顺序表中有效元素已经将空间填充满则需要扩容!
扩容大小:如果每次扩容一个单位,每次就调用一次malloc这样会使效率十分低下,但是一次性申请过多内存会导致内存空间的浪费!我们一般申请capacity的整数倍。
如何扩容:
1.申请新的空间
2.将原有数据进行拷贝
3.旧空间的释放
4.返回新空间的地址
方法实现
普通版本
//写到这块了其实咱们顺序表存在一个巨大的问题
//你的size == capacity 在增加还行吗?
//还需要对你的arr进行扩容
//1.申请新的空间
//2.将原有数据进行拷贝
//3.旧空间的释放
//4.返回新空间的地址
void CheckCapacityMode1(SeqList* ps)
{
if (ps->size == ps->capacity) {
//1.申请新的空间
int newcapacity = ps->capacity * 2;
Datatype* temp = (Datatype*)malloc(newcapacity * sizeof(Datatype));
if (NULL == temp)
{
assert(0);
return;
}
//2.将原有数据进行拷贝
memcpy(temp, ps->arr, ps->size * sizeof(Datatype));
//3.旧空间的释放
free(ps->arr);
ps->arr = temp;
ps->capacity = newcapacity;
}
}
realloc版本
//
void CheckCapacityMode2(SeqList* ps)
{
int newcapacity = ps->capacity * 2;
Datatype* temp1 = (int*)malloc(sizeof(Datatype) * ps ->capacity);
assert(temp1);
memcpy(temp1, ps->arr, ps->size * sizeof(Datatype));
ps->arr = temp1;
if (NULL != ps->arr)
{
Datatype* temp2 = (Datatype*)realloc(ps->arr, sizeof(Datatype) * newcapacity);
if (NULL != temp2)
{
ps->arr = temp2;
}
ps->capacity = newcapacity;
}
/*ps->arr = (Datatype*)realloc(ps->arr, sizeof(Datatype)*newcapacity);
if (NULL == ps->arr)
{
assert(0);
return;
}
ps->capacity = newcapacity;*/
//会有告警"realloc"可能会返回 null 指针:将空<>指针分配给变量(作为参数传递给"realloc")将导致原始内存块泄漏
//此警告指示内存泄漏,该泄漏是不正确使用重新分配函数的结果。 如果重新分配不成功
//堆重新分配函数不会释放传递的缓冲区
//若要更正缺陷,请将重新分配函数的结果分配给临时 ,然后在成功重新分配后替换原始指针。
}
会有告警"realloc"可能会返回 null 指针:将空<>指针分配给变量(作为参数传递给"realloc")将导致原始内存块泄漏此警告指示内存泄漏,该泄漏是不正确使用重新分配函数的结果。 如果重新分配不成功堆重新分配函数不会释放传递的缓冲区若要更正缺陷,请将重新分配函数的结果分配给临时 ,然后在成功重新分配后替换原始指针,详细参考 C6308
为啥用的是memcpy呢?不用strcpy?
还记刚刚的堆溢出吗 heap corruption detected
我们的字符串函数是以 \0 结尾的 假设你开辟了五个空间strcpy之后就是6个因为strcpy的本身属性:即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符!多了一个不久造成堆溢出吗 !heap corruption detected
现在有了这个方法之后再看我们的尾插
测试
// 往顺序表中头插一个值为data的元素
// 时间复杂度是多少?O(N)
void SeqListPushFront(SeqList* ps, Datatype data)
{
CheckCapacityMode2(ps);
//在第一个位置插入,那也就是说还得进行搬运
//这次怎么搬运呢
//应该是从后往前搬运要是从前往后搬运就会造成内存覆盖
for (int i = ps->size - 1; i >= 0; --i) {
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[0] = data;
ps->size++;
}
在第一个位置插入,那也就是说还得进行搬运这次怎么搬运呢
应该是从后往前搬运要是从前往后搬运就会造成内存覆盖
// 删除顺序表任意位置的元素
// 时间复杂度O(N)
void SeqListErase(SeqList* ps, int pos)
{
//顺序是空我们就不操作了
if (SeqListEmpty(ps)) {
return;
}
//对pos合法性判断
if (pos < 0 || pos > ps->size - 1) {
printf("你的pos输入有误!");
return;
}
for (int i = pos; i < ps->size; i++) {
ps->arr[pos] = ps->arr[pos + 1];
}
ps->size--;
}
还是先检测顺序表是否为空,为空返回,对操作数合法性检验,最后在搬运。
希望本篇 文章对你有帮助 !