数据结构有三种结构:线性结构,树形结构以及图形结构,今天来详细介绍下线性结构——线性表;
线性表粗略分为顺序表和链表,根据难易不同,我们先来学习顺序表;
顺序表
任何线性结构都分为静态和动态的,我们首先简单了解静态的顺序表;
静态顺序表
静态顺序表是数据结构中比较挫的那种,它实际上和数组差不多,只不过是用结构体变量实现而已
struct SeqList
{
int data[N];
int size;
}
上面就是一个普通的静态顺序表;
动态顺序表
静态的顺序表很简单,而它的孪生兄弟可不简单了,接下来介绍下动态顺序表;
struct SeqList
{
int *data;
int size;
int capacity;
}
动态顺序表利用结构体变量创建,需要一个指针变量用来存储数据,size和capacity用来计算容量;
动态顺序表的优点就是随用随扩,每次扩容最好扩容两倍,因为扩了两倍既不会扩多导致空间浪费,也不会频繁扩容导致效率下降;
看到了顺序表的基本语法后,我们来了解动态顺序表的增删查改;
在使用顺序表之前,我们需要对顺序表进行初始化;
void SLinit(SL* psl)//顺序表的初始化
{
psl->data = NULL;
psl->capacity = psl->size = 0;
}
将指针先置为NULL,将容量和大小全部置为0,这样一个顺序表就做好了;
但是我们在对这个顺序表进行增删查改等操作的时候,我们需要检查顺序表的容量是否到达最大值,而且每一次使用都需要检查,因此我们可以自行写一个函数用来检查容量;
void SLcheckcapasity(SL* psl)//链表的扩容,每次都翻倍
{
assert(psl);
if (psl->capacity == psl->size)
{
int Newcapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
int* tmp = realloc(psl->data, sizeof(int) * Newcapacity);
if (tmp == NULL)
{
perror("realloc");
return;
}
psl->capacity = Newcapacity;
psl->data = tmp;
}
}
在容量检查这个函数中,我们发现每次扩容都是用realloc函数给结构体内部的指针扩容,而非单纯的将容量改变,这是因为我们的结构体内部指针才是存储数据的关键,而不是一个单纯的容量改变就可以更改顺序表的大小;
顺序表的头插和尾插
接下来就是顺序表的各种操作了,首先是增,增这个操作其实就是插入数据,分为头插和尾插,接下来请分别看看两个操作的不同;
void SLpushfront(SL* psl,SLData x)//从头部插入数据
{
assert(psl);
SLcheckcapasity(psl);//先检查
int end = psl->size;
while (end > 0)
{
psl->data[end] = psl->data[end - 1];
end--;
}
psl->data[0] = x;
psl->size++;
}
void SLpushBack(SL* psl, int x)//从尾部插入数据
{
assert(psl);
SLcheckcapasity(psl);//先检查
psl->data[psl->size] = x;
psl->size++;
}
看到这里大家会发现,顺序表的头插和尾插的代码长度不在一个级别,这是为什么呢?
这就要看清楚顺序表的特性,顺序表的特性就是存储的信息不论是物理位置还是逻辑上的位置都是相邻的,这里我们可以看几张图来加深理解;
根据顺序表的特性,所有信息都存储在内存中以psl中的data指针开辟出来的一连串的内存空间,而若是我们想要在头部插入一个数据那么我们需要将所有数据全部相后移动一位,然后才能头插;
首先将所有数字往后移动;
然后将想头插的数据覆盖了第一个数据并且size++就完成了头插;
顺序表的头删和尾删
之前的头插和尾插一样,顺序表的头删和尾删的代码差距也很大,我们先看看代码感受下;
void SLpopfront(SL* psl)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size - 1; i++)
{
psl->data[i] = psl->data[i + 1];
}
psl->size--;
}
首先是头删,头删需要将后面的数据全部往前挪动,然后将size--就行了;
这里我们用图来加深理解;
这是一个普通的顺序表,我们需要将头部的1给删除掉,我们需要将后面的2向前挪动一步,然后将3向前挪动一步。。。以此类推,变成功将1覆盖了 ;
最后--size,就完成了头删了;
void SLpopback(SL* psl)
{
assert(psl);
psl->size--;
}
而和头删形成鲜明对比的尾删则简单到只有一句代码就完成了;
这里也用图简单的表示一下;
刚开始只是一个普通的顺序表,然后--size;
按照之前我所写的头插尾插代码逻辑来说,每次都会在size位插入数据,然后才size++,因此尾删--size后,当下次尾插会将size上的数据给覆盖掉,然后size再++;
插入/修改/查找/删除
接下来的都比较简单,只不过是在之前的头插头删,尾插尾删的基础上增加了变量而已;
接下来给大家看看代码;
int SLsearch(SL* psl, SLData x)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (psl->data[i] == x)
{
return i;
}
}
return -1;
}
查找操作就是在顺序表中一个一个查找看是否有该数据,有就返回下标,无则返回-1;
void SLInsert(SL* psl, size_t pos, SLData x)
{
assert(psl);
assert(psl->size >= pos);
psl->size++;
SLcheckcapasity(psl);
int end = psl->size;
while (end > pos)
{
psl->data[end] = psl->data[end - 1];
end--;
}
psl->data[pos] = x;
}
此处插入操作实际上是将pos之前的数据全部往后移动一位,然后在pos位置的前一位插入数据;
但是有一点需要记住的是,while循环中的条件不能写>=,而只能写>,因为若是当pos为0的时候,>=会导致指针越界,需要注意;
void SLErase(SL* psl, size_t pos)
{
assert(psl);
assert(psl->size >= pos);
size_t end = pos;
for (; end < psl->size-1; end++)
{
psl->data[end] = psl->data[end + 1];
}
psl->size--;
}
删除操作实际上也就是将pos之后的位置全部往前移动一位,覆盖pos上的数据并且将size--就ok;
void SLmodify(SL* psl, size_t pos,SLData x)
{
assert(psl);
assert(psl->size >= pos);
psl->data[pos] = x;
}
更改操作更不用说,十分简单;