基础顺序表的实现
1.顺序表的介绍
1.1理解
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。
那么什么是物理地址连续呢?
物理地址连续就是在内存中的存储是连续的,可以理解为地址连续存储。
1.2分类
静态顺序表:使用定长数组存储元素
#define N 100
typedef int SLDataType;
typedef struct SeqList
{
SLDataType arr[N];//定长数组
int size;//有效数据的个数
}SL;
这里解释一下为什么要使用typedef:
typedef可以简单理解为取别名,比如一个人有他自己身份证上的名字,还有朋友给它起的绰号,这两个名字都代表一个人,而typedef就是相当于其中的取绰号的操作。
它在这里的用途有两个:
1.如果我们需要修改数据的类型,比如不使用int而是使用double等其他类型,那么只需要修改typedef这里的int即可,不需要修改下文。
2.可以简化名称,如果名字太长,我们可以通过typedef使其长度减少一点。比如上面的例子:如果我们没有使用typedef,那么下面我们创建这个结构体时,需要手敲出struct SeqList sl,但是我们使用typedef后,就可以使用SL代替struct SeqList,即声明为SL sl。
动态顺序表:使用动态开辟的数组存储元素
静态顺序表有一个缺点:如果N太大,空间浪费,如果N太小,空间不够用,为了解决这个问题,从而实现动态内存申请,如果空间不够则自动扩容,这样就可以很好的解决上述的问题
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;//指向动态开辟的数组
int size;//有效元素的个数
int capacity;//最大容量的大小
}SL;
2.接口实现
2.1初始化
void SLInit(SL* psl)
{
assert(psl);//默认是有效的地址
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
2.2销毁
void SLdestroy(SL* psl)
{
assert(psl);
free(psl->a);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
2.3扩容
为什么要写这个扩容函数呢,因为每一次给顺序表插入元素时,我们需要判断它的空间是否足够让我们插入,而如果每一个插入函数内部都写一个扩容的部分,则会产生许多重复代码,所以将其当作接口实现。
void CheckCapacity(SL* psl)
{
if (psl->size == psl->capacity)
{
int newcapacity = psl->capacity;
newcapacity = newcapacity == 0 ? 4 : psl->capacity * 2;
SLDataType* temp = (SLDataType*)realloc(psl->a, newcapacity * sizeof(SLDataType));
if (temp == NULL)
{
perror("realloc failed");
return;
}
psl->a = temp;
psl->capacity = newcapacity;
}
}
扩容函数的常见问题:
(1)三目运算符的常见错误:
int newcapacity = psl->capacity;
newcapacity = newcapacity == 0 ? 4 : psl->capacity * 2;//正确
newcapacity == 0 ? 4 : psl->capacity * 2;//错误
为什么第二句是错的呢?
这里就牵扯到了三目运算符:第二句的含义是判断newcapacity是否为0,如果为0执行第一个语句4,如果不为0执行第二个语句让它扩容2倍,但是由于它没有赋值,所以说只是执行了判断,并没有赋值,从而扩容错误,导致capacity和newcapacity一直为0。
(2)为什么要用temp临时变量接收返回值?
因为realloc有动态内存申请失败的可能,如果申请失败,则会返回空指针,使之前申请的地址被覆盖,从而无法找到,造成内存泄漏
(3)realloc第一次扩容时,传入的地址是空,此时有没有问题?
realloc函数如果传入的是空指针,则其作用相当于malloc
2.4头插
由于顺序表在物理上是连续的,所以头插时我们需要将所有的元素都往后面移动一位,将头部的位置空出来,再将要插入的数据存放进去。
void SLPushFront(SL* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
int end = psl->size;
while (end > 0)
{
psl->a[end] = psl->a[end - 1];
end--;
}
psl->a[0] = x;
psl->size++;
}
2.5尾插
void SLPushBack(SL* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
psl->a[psl->size] = x;
psl->size++;
}
2.6头删
void SLPopFront(SL* psl)
{
assert(psl);
assert(psl->size > 0);
int begin = 1;
while (begin < psl->size)//遍历挪动元素,实现覆盖
{
psl->a[begin - 1] = psl->a[begin];
begin++;
}
psl->size--;
}
2.7尾删
void SLPopBack(SL* psl)
{
assert(psl);
assert(psl->size > 0);//必须有元素才能删
psl->size--;
}
对于尾删操作的理解:
(1)如何删?
尾删只需要将它的有效个数psl->size–就可以,因为我们在使用时只是使用的有效数据的个数,而且如果在增加数据时,会将这个位置的数据覆盖,所以要尾删位置的数字是什么我们并不关心。
(2)可不可以只将尾部数据的空间释放(free)?
尾删的时候可能会有同学疑惑,尾删是不是要将那里的空间释放。
答案是不能,因为释放申请的空间是要从头开始一次性释放完的,不能从中间或者末尾把一次申请的空间分成多次释放。而且那里的空间我们在增加元素时还会使用,所以不可以直接free。
2.8随机位置插入
图解:
注意:任意位置的插入删除最关键的地方就是循环时的下标判断,这个下标的变化影响着循环的结束条件,所以我们可以通过画图的方式来判断出下标和循环的结束条件,这样比较清晰
void SLInsert(SL* psl, int pos, SLDataType x)
{
assert(psl);
assert(pos <= psl->size && pos >= 0);
CheckCapacity(psl);
int end = psl->size;
while (end > pos)
{
psl->a[end] = psl->a[end - 1];
end--;
}
psl->a[pos] = x;
psl->size++;
}
2.9随机位置删除
图解:
void SLErase(SL* psl, int pos)
{
assert(psl);
assert(pos >= 0 && pos < psl->size);
int begin = pos;
while (begin < psl->size - 1)
{
psl->a[begin] = psl->a[begin + 1];
begin++;
}
psl->size--;
}
2.10打印
void SLPrint(SL* psl)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
2.11按值查找
int SLFind(SL* psl, SLDataType x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;//按值查找返回的是该值的下标
}
}
return -1;
}
3.完整原码(动态顺序表)
SeqList.h
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size;
int capacity;
}SL;
//接口声明
void SLInit(SL* psl);
void SLdestroy(SL* psl);
void SLPushFront(SL* psl, SLDataType x);
void SLPushBack(SL* psl, SLDataType x);
void SLPopFront(SL* psl);
void SLPopBack(SL* psl);
void SLInsert(SL* psl, int pos, SLDataType x);
void SLErase(SL* psl, int pos);
void SLPrint(SL* psl);
int SLFind(SL* psl, SLDataType x);
SeqList.c
#include "SeqList.h"
void SLInit(SL* psl)
{
assert(psl);//默认是有效的地址
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
void SLdestroy(SL* psl)
{
assert(psl);
free(psl->a);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
void CheckCapacity(SL* psl)
{
if (psl->size == psl->capacity)
{
int newcapacity = psl->capacity;
newcapacity = newcapacity == 0 ? 4 : psl->capacity * 2;
SLDataType* temp = (SLDataType*)realloc(psl->a, newcapacity * sizeof(SLDataType));
if (temp == NULL)
{
perror("realloc failed");
return;
}
psl->a = temp;
psl->capacity = newcapacity;
}
}
void SLPushFront(SL* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
int end = psl->size;
while (end > 0)
{
psl->a[end] = psl->a[end - 1];
end--;
}
psl->a[0] = x;
psl->size++;
}
void SLPushBack(SL* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
psl->a[psl->size] = x;
psl->size++;
}
void SLPopFront(SL* psl)
{
assert(psl);
assert(psl->size > 0);
int begin = 1;
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];
begin++;
}
psl->size--;
}
void SLPopBack(SL* psl)
{
assert(psl);
//必须有元素才能删
assert(psl->size > 0);
psl->size--;
}
void SLInsert(SL* psl, int pos, SLDataType x)
{
assert(psl);
assert(pos <= psl->size && pos >= 0);
CheckCapacity(psl);
int end = psl->size;
while (end > pos)
{
psl->a[end] = psl->a[end - 1];
end--;
}
psl->a[pos] = x;
psl->size++;
}
void SLErase(SL* psl, int pos)
{
assert(psl);
assert(pos >= 0 && pos < psl->size);
int begin = pos;
while (begin < psl->size - 1)
{
psl->a[begin] = psl->a[begin + 1];
begin++;
}
psl->size--;
}
void SLPrint(SL* psl)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
int SLFind(SL* psl, SLDataType x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
4.常见问题总结
这里只是将一些可能有疑问的地方汇总,如果前文已经解释过,则会说明在文章哪里,请自行前去观看,谢谢理解。
4.1静态顺序表和动态顺序表的区别
静态顺序表有一个缺点:如果N太大,空间浪费,如果N太小,空间不够用,为了解决这个问题,从而实现动态内存申请,如果空间不够则自动扩容,这样就可以很好的解决上述的问题
4.2为什么扩容2倍而不是其他的倍数,比如3倍之类的
这个是按照情况情景来申请的,并没有明文规定。
举个栗子,假如小明一个月生活费1500,这个月提前一周花光了,再向家里人要这一周的费用时,家里人给多少?
如果小明是一个乖巧的孩子,那么妈妈可能会一次性多给点,将下个月的也一起给了,因为妈妈知道小明不会乱花钱,有多的会留到下一个月继续使用。
如果小明是一个月光族,那么妈妈会让它算出这一周需要多少钱,比如小明算出300,然后妈妈给他300,但是不会提前给他下一周的,因为小明管不住。
所以扩容多少倍就和小明要多少生活费是一个道理。按情况按需求申请,只是2倍可能是一个相对好的处理方式。
4.3realloc函数传入的地址为空时是如何处理的
请看本文2.3扩容处谢谢
4.4三目运算符可能出现的错误
请看本文2.3扩容处谢谢
4.5扩容时临时值temp的作用
请看本文2.3扩容处谢谢
4.6删除数据时是否free
请看本文2.7尾删操作谢谢
4.7释放(free)realloc所申请的空间产生奔溃的原因
(1)指针位置不对:一次所申请的空间必须一次释放,不能分期释放。
(2)空间访问越界:比如申请了4个字节的空间,但是在使用时使用了8个字节空间,申请少了空间,但是还以为空间足够继续使用和释放。
4.8为什么数组的下标是从0开始
注:这个问题是为了让我们回忆一下数组和指针的内容。
这是一种逻辑自洽。
在我们学习数组时学习到了 [ ]相当于解引用的操作
对于arr[3]等价于*(arr+3),而数组名表示首元素的地址,所以arr+3就相当于跳过了三个元素,表示第四个元素的地址,再解引用得到第四个元素。而数组下标从0开始,即arr[0]等价于*(arr+0),表示首元素,0表示从数组首元素地址开始跳过0个元素。
以上就是本文所有内容,如有问题,请多指正,谢谢观看。