前言
为什么要学数据结构 他又有什么作用?
比如:我塞牙了,那么就要用到牙签这“数据结构”,当然你用指甲也行,只不过“性能”没那么好。
我要拧螺母,肯定用扳手这个“数据结构”,当然你用钳子也行,只不过也没那么好用。
学习数据结构,就是为了了解以后在行业里搬砖需要用到什么工具,这些工具有什么利弊,应用于什么场景。
你会发现这些基础的“工具”也存在着一些缺陷,你不满足于此工具,此时,你就开始自己在这些数据结构的基础上加以改造,这就叫做自定义数据结构
1.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2.顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
静态顺序表:
typedef int SLdataType;
#define N 10
//静态顺序表
struct SeqList
{
SLdataType a[N];//定长数组
int size; //有效数据的个数
};
静态顺序表在实际当中非常不好用,因为他的个数是写死的,比如我们只开了10个数据,但是我们需要存储15个数据,那怎么办呢,开大一点?那我们开1000个,那么问题来了如果要存1001个呢?那我们开100000个,但如果我们开了100000个,我只用存储10个数据呢?
所以静态顺序表存在最大的问题就是:开多了浪费,开少了不够用。
动态顺序表就解决了上面的问题
动态顺序表:
typedef int SLDataType;
#define N 10
//动态顺序表 -- 按需申请
//如果 有效数据个数 == 空间容量 -- 扩容
typedef struct SLList
{
SLDataType* a; //指向动态开辟的数组
int size; //有效数据的个数
int capacity; //空间容量
}SL;
动态顺序表的初始化和销毁
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
#define N 10
#define INIT_CAPACITY 4
//动态顺序表 -- 按需申请
//如果 有效数据个数 == 空间容量 -- 扩容
typedef struct SLList
{
SLDataType* a; //指向动态开辟的数组
int size; //有效数据的个数
int capacity; //空间容量
}SL;
//顺序表销毁
void SLDestroy(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
//顺序表初始化
void SLInit(SL* ps)
{
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
void TestSeqList1()
{
SL s;
SLInit(&s);
SLDestroy(&s);
}
int main()
{
TestSeqList1();
return 0;
}
尾插和尾删
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//扩容
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->size++] = x;
}
//尾删
void SLPopBack(SL* ps)
{
assert(ps);
//检查是否为空
assert(ps->size > 0);
ps->size--;
}
//打印
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
整合:
void TestSeqList1()
{
SL s;
SLInit(&s);
//尾插
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPrint(&s);
//尾删
SLPopBack(&s);
SLPopBack(&s);
SLPrint(&s);
SLDestroy(&s);
}
int main()
{
TestSeqList1();
return 0;
}
以上代码的运行效果:
头插和头删
在头插之前我们应该先检查顺序表当前空间是否已经存满了,如果不做检查就有可能出现越界的情况。
//头插
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[0] = x;
ps->size++;
}
//头删
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
整合:
void TestSeqList1()
{
SL s;
SLInit(&s);
//头插
SLPushFront(&s, 1);
SLPushFront(&s, 2);
SLPushFront(&s, 3);
SLPushFront(&s, 4);
SLPrint(&s);
//头删
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLDestroy(&s);
}
int main()
{
TestSeqList1();
return 0;
}
头插和头删运行效果:
头插/头删的时间复杂度是:O(N^2)
尾插/尾删的时间复杂度是:O(N)
可以看到头插和尾插的差距很大,比如我们要插入10000个数据,头插需要执行100000000次,而尾插只需要执行10000次。
所以我们要尽可能的避免使用头插/头删。
指定位置插入数据
//指定位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
当我们实现了SLInsert函数的功能时,我们之前的头插和尾插就可以套用SLInsert函数。
尾插:
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
//assert(ps);
扩容
//SLCheckCapacity(ps);
//ps->a[ps->size++] = x;
//改写为:
SLInsert(ps, ps->size, x);
}
头插:
//头插
void SLPushFront(SL* ps, SLDataType x)
{
//assert(ps);
//SLCheckCapacity(ps);
//int end = ps->size - 1;
//while (end >= 0)
//{
// ps->a[end + 1] = ps->a[end];
// --end;
//}
//ps->a[0] = x;
//ps->size++;
//改写为:
SLInsert(ps, 0, x);
}
删除指定位置的数据
//删除指定位置的数据
void SLErase(SL* ps, int pos)
{
assert(ps);
//注:要删除的元素不能等于size
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
整合:
void TestSeqList1()
{
SL s;
SLInit(&s);
//尾插
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPushBack(&s, 6);
SLPushBack(&s, 7);
SLPushBack(&s, 8);
SLPrint(&s);
//指定4号位置插入40并打印
SLInsert(&s, 4, 40);
SLPrint(&s);
//指定0号位置插入40并打印
SLInsert(&s, 0, 40);
SLPrint(&s);
//删除4号位置元素并打印
SLErase(&s, 4);
SLPrint(&s);
SLDestroy(&s);
}
int main()
{
TestSeqList1();
return 0;
}
运行效果:
同理头删和尾删也可以复用SLErase函数。
头删:
//头删
void SLPopFront(SL* ps)
{
//assert(ps);
//assert(ps->size > 0);
//int begin = 1;
//while (begin < ps->size)
//{
// ps->a[begin - 1] = ps->a[begin];
// ++begin;
//}
//ps->size--;
//修改为:
SLErase(ps, 0);
}
尾删:
//尾删
void SLPopBack(SL* ps)
{
//assert(ps);
检查是否为空
//assert(ps->size > 0);
//ps->size--;
//修改为:
SLErase(ps, ps->size - 1);
}
查找指定位置的数据
//查找指定位置的数据
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
以上就是本篇文章的全部内容了,希望大家看完能有所收获。