在校大一小生一枚~~~水平有限,希望大神们看到后批评指正,同时如果在一些方面对大家有所帮助的话别忘了关注一下小生哦,废话不多说,直接上干货:冲冲冲!!!!
线性表是N个具有相同特性的数据元素的有限序列。线性表不等同于顺序表,常见的线性表有:顺序表,链表,字符串,队列,栈等。都是属于线性结构。
后面小生也会更新非线性结构,例如树、图等。在这里就不做过多阐述
顺序表是用一段连续的存储单元依次存储数据元素的线性结构,一般采用数组存储,进行数组的增删改查。
既然顺序表在某种程度上可以认为是数组,那为什么还要学习顺序表呢?这就涉及到动态顺序表和静态顺序表的相关内容了
特点:使用定长数组储存元素
我们用代码实现一下静态顺序表
//初始化静态顺序表
#define N 10 //长度根据需求定义,宏定义的目的是发现数组内存不够时可以只调整N的大小不必要再到后面的程序做更改,更为方便。
typedef int SLDataType; //这里可以不重命名,小生这里是提升代码的规范性方便读者阅读。
typedef struct SeqList
{
SLDataType arry[N]; //定长数组
int size; //代表有效数据的个数
}SeqList;
但是静态顺序表储存容量是固定的,可变性不强,如果在实际开发中内存超出了所设定的最大内存只能不断改变N的大小,从而实现对静态顺序表的扩容,但这种操作是很不方便的。
因此我们在实际的应用中使用动态顺序表更多
使用动态开辟的顺序表存储数据,在C语言中常用malloc和realloc函数开辟动态内存
//顺序表的动态存储
typedef struct SeqList
{
SLDataType *arry; //指向动态开辟的数组
int size; //用于记录有效数据的个数
int capicity; //用于记录容量空间的大小
}
小生在此就不做阐明了,相信大神们应该都知道吧,如果想深入了解的话详见: 深入了解realloc、malloc、以及calloc函数的区别.
相信大家看到这里已经大概认识了顺序表,那我们接下来就对动态顺序表进行一些基础操作。
初始化操作是比较简单的,只需将指向数组的指针置为空,并将容量和有效数据个数置为空即可。
void SeqListInit(SeqList s)
{
s.a = NULL; //置为空
s.size = 0; //初始化为0
s.capacity = 0;//初始化为0
}
这种方式行不行呢?让我们来测试一下:
#include
typedef int SLDataType;
typedef struct SeqList
{
int* a;
int size;
int capacity;
}SeqList;
void SeqListInit(SeqList sl)
{
sl.a = NULL;
sl.size = 0;
sl.capacity = 0;
}
int main()
{
SeqList s;
SeqListInit(s);
return 0;
}
通过调用该函数并未对该顺序表完成初始化,这是由于形参和实参的问题所致,s和sl是不同两个结构体变量,传值的过程实际上就是拷贝的过程,形参是实参的拷贝,形参的改变不影响实参。
但是我们可以指针存储地址,再对指针进行解引用的方式改变实参,由此我们可以通过传址实现初始化顺序表
//顺序表的初始化
void SeqListInit(SeqList* psl) //用一个结构体指针psl保存顺序表的地址
{
psl->a = NULL;
psl->capacity = 0;
psl->size = 0;
}
由此我们可以自行测试一下
#include
typedef int SLDataType;
typedef struct SeqList
{
int* a;
int size;
int capacity;
}SeqList;
void SeqListInit(SeqList* psl) //用一个结构体指针psl保存顺序表的地址
{
psl->a = NULL;
psl->capacity = 0;
psl->size = 0;
}
int main()
{
SeqList s;
SeqListInit(&s); //传送顺序表地址
return 0;
}
实现顺序表初始化的小结:要传送顺序表的地址而非结构体,通过指针解引用的方式实现改变实参的值,最终完成初始化。
顺序表的尾插就是在最后一个有效元素后面增加新的元素,我们通过一组图片感受一下
我们将41尾插到顺序表的后面覆盖了随机值,顺序表的有效元素增加了1。但是我们又不得不考虑下面这种情况。
此时顺序表的最大容量和其存储的有效元素个数相等,如果要进行尾插则需对该顺序表进行扩容。
//顺序表的尾插
void SeqListPushBack(SeqList* psl, SLDataType val)
{
if (psl->capacity == psl->size) //判断该顺序表是否已满
{
int* tmp = realloc(psl->a, sizeof(int) * psl->capacity); //对顺序表进行扩容,在这里我们进行一倍的扩容
if (tmp == NULL) //判断内存是否分配成功
{
printf("Realloc fail/n");
exit(-1);
}
else
{
psl->a = tmp;
psl->capacity *= 2;
}
}
else
{
psl->a[psl->size] = val;
psl->size++; //元素的有效个数加1
}
}
在这里也可以进行多倍扩容如果进行N倍扩容只需执行int* tmp = relloc(psl->a, sizeof(int) * psl->capacity*N)即可,但是在实际的情况中为了放置一次性扩容太多从而导致内存浪费,我们一般只将其扩容为原来的两倍
大家仔细看看上述的程序看能否找出bug,如果当前的顺序表为空容量和有效元素都为0呢?
那用这种方式无论对它如何扩容最后得出的结果永远都是扩容后的容量还是0
那我们需要对其初始容量赋予一个确定的值,可以用如下方式实现,而且我们发现在很多操作中都需要进行一空间的判断,那我们便用一个接口实现检查顺序表的空间的检查与扩容。
为解决上述矛盾,我们可以通过一个函数实现对顺序表是否已满的检查与适当扩容。
void CheckCapacity(SeqList* psl)
{
if (psl->capacity == psl->size) //判断顺序表空间是否已满
{
int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2; //如果容量为空则将其赋值为4,否则就对他扩容一倍
SLDataType* tmp = realloc(psl->a, sizeof(SLDataType) * newCapacity);
if (tmp == NULL)
{
printf("Relloc fail\n");
exit(-1);
}
else
{
psl->a = tmp;
psl->capacity = newCapacity;
}
}
}
这里又牵扯到新的问题,realloc的扩容问题,使用realloc进行扩容时分为两种扩容:原地扩容和异地扩容
我们来看看这两种扩容方式
原地扩容就是保证首地址不变,在后面开辟新的地址。但是如果我们所需要的内存过大,后面的内存无法满足我们的需求呢?
这时候我们便需要用到异地扩容了
异地扩容就是开辟一块新的空间并释放原来的空间,将顺序表存放在新的空间中。
方便大家理解,我们可以通过代码实现一下:
当所需要的空间不大时,relloc采用原地扩容,此时首地址不变相当于malloc,但如果我们将10变成100呢?
由打印的首地址可以看到,首地址改变,此时relloc采用的是异地扩容。这里小生推荐大家使用一个查资料的网站: cplusplus.com.
可以用来查有关C语言和C++的资料。(尽量用英文直接看)
弄明白realloc实现扩容的原理,我们通过接口将之前的尾插函数做修改。
//尾插
void SeqListPushBack(SeqList* psl, SLDataType val)
{
CheckCapacity(psl); //调用检查函数并相应进行扩容
psl->a[psl->size] = val;
psl->size++;
}
void SeqListPopBack(SeqList* psl)
{
assert(psl); //断言处理
psl->size--; //有效数据减1
}
顺序表有有效区域和无效区域,有效区域储存有效数据,而其他部分可以视为是分配空间时多出的空间,存储的是系统的随机值,可以认为是无效数据。我们通过一个图来认识
通过移动插入val的数据
头插的过程就是先将顺序表全部往后移为头部预留空间,挪动的过程就是从末尾开始前面的元素不断覆盖后面的元素,但是此时要考虑空间是否已满的情况。用CheckCapacity函数进行检测与扩容。
//头插
void SeqListPushFront(SeqList* psl,int val)
{
assert(psl); //防止传入的是空指针
CheckCapacity(psl);
//挪动数据,腾出头部空间
int end = psl->size - 1;
while (end >= 0)
{
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[0] = val;
psl->size++; //有效元素个数加1
}
头删比头插更加简单,因为头删的时候不需要考虑顺序表内存空间已满的情况。头删的过程就是从第二个元素开始将后面的元素依次覆盖前面的元素。
void SeqListPopFront(SeqList* psl)
{
assert(psl);
if (psl->size > 0)
{
//挪动数据覆盖删除
int begin = 1;
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];
begin++;
}
psl->size--;
}
}
//在pos位置插入val
void SeqListInsert(SeqList* psl, int pos, int val)
{
assert(psl);
CheckCapacity(psl);
int end = psl->size - 1; //end代表最后一个有效元素的下标
while (end > pos)
{
psl->a[end + 1] = psl->a[end];
end--;
}
psl->a[pos] = val;
psl->size++;
}
在下标为pos处插入一个数据val,应该让pos左边的数据不变让其右边的数据(包括原来pos处存的数据)向右挪动一个单位。但是别忘了要对内存进行检查和扩容,调用函数即可。
从而变成:
我们用代码来实现一下吧:
//在pos位置插入val
void SeqListInsert(SeqList* psl, int pos, int val)
{
assert(psl);
CheckCapacity(psl);
int end = psl->size - 1; //end代表最后一个有效元素的下标
while (end > pos)
{
psl->a[end + 1] = psl->a[end]; //
end--;
}
psl->a[pos] = val;
psl->size++;
}
用代码实现一下整个过程:
void SeqListErase(SeqList* psl, int pos)
{
assert(psl); //断言处理
assert(pos < psl->size);
int begin = pos + 1; //begin的位置是pos的后一个位置
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin]; //pos后面的数据从前开始依次覆盖前面位置的数据
begin++;
}
psl->size--;
}
销毁顺序表就是将存有数据的顺序表强制初始化。将顺序表的容量和有效数据的个数都初始化为0,将指针a置为空
void SeqListDestroy(SeqList* psl)
{
psl->a = NULL;
psl->capacity = 0;
psl->size = 0;
}
打印顺序表我们选择从前往后依次打印。打印的时候不改变顺序表的结构和各个位置的值,因此我们有两种打印方法,第一种直接传参,但是用形参拷贝该实参的时候为新参分配了一块新的内存,占用内存较多,但是传址后用指针接收的时候只为该指针分配了四个字节的内存,节省了空间。从而在大多数的情况下我们直接传址就可以了。
void SeqListPrint(SeqList* psl)
{
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
大佬们,小生顺序表初阶的总结就到此结束了,码字不易,原创不易(写了好久,呜呜呜~~~)希望能帮到大家,同时有什么不对的地方希望大佬们批评指正,关注小生,小生后续会不断更新数据结构初阶进阶和C++相关的知识,凌晨一点了,冲冲冲!!!!!