线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。今天我主要介绍的就是顺序表。
刚开始接触顺序表,可能有许多人觉得会很陌生,但是我们之前只要学习过C语言,对数组这个名词就不会陌生,因为数组其实就是顺序表的一种。后面我来讲的顺序表。你就把它理解为数组就行。
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为两种:
因为静态顺序表的缺点有很多,不是很实用,所以我后来讲的都是动态顺序表。
刚在再概念中提到,“顺序表”只是一个数据存储的一种结构,而之前学到的数组就是这种结构,所以我们现在需要开辟一个数组。
首先我们需要动态开辟一块空间,因为是动态开辟,所以我们需要用malloc或者calloc函数来开辟。开辟好之后我们需要用一个指针来接收开辟好之后空间的地址。
既然开辟好了这块空间,我们肯定要往这块空间存储数据吧,如果数据多了,我们就用realloc函数来进行扩容,但是我们要如何判断需不需要扩容呢?现在我们定义两个变量size,capacity,size代表此时这个空间里存的数据的个数,capacity代表这块空间的最大容量。
现在我们就需要一个指针变量,两个整型变量。为了方便我们后来对这个顺序表的增删查改,我们需要将这三个变量整合在一起也就是放在一个结构体里面
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a; //动态开辟的空间返回的地址
size_t size; //顺序表已写的数据个数
size_t capacity; //数据表中数据的总大小
// unsigned int
}SeqList;
这里我们将int这个类型进行重定义,也就是换一个名字叫SLDataType,这样以后希望我们的顺序表里面的数据是其他类型的时候也好更改。
因为size,capacity只是说明数据的个数和总大小,只能是正数,所以说类型用size_t就行,size_t就是unsigned int二者是一样的。
我们将这些函数统一放在一个.c文件里。
//顺序表的初始化
void SeqListInit(SeqList* ps)
{
assert(ps);
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
我们将顺序表所需的空间的开辟单独放在一个函数里进行。所以这里我们先将用来接收空间地址的指针置空,capacity这个变量也置0,又因为还没存数据,size也置0.
//扩容函数
static void SeqCheckCapacity(SeqList* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
size_t newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("SeqListAdd()");
exit(-1);
}
ps->a = tmp;
ps->capacity = newCapacity;
}
}
走到这一步我们需要真正的开辟一块动态空间了,并且把空间满了的增容问题也一并写了出来。
assert(ps);为了防止传过来的结构体指针是空指针,所以提前用assert做一个断言,如果真不小心传了一个空指针,在运行是系统会直接报错的。
if (ps->size == ps->capacity)
我们函数需要通过if来判断需不需要增加空间,这里分成了两种情况:
1.最开始还没开辟的时候size,capacity都为0,所以也能达到要求
2.在储存了一定的数据之后,capacity相当于此时空间里的最大容量,而size==capacity时说明此时空间已经满了,这时候也能达到要求
size_t newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
newCapacity可以认为我们开辟之后新的空间的元素总数,在这里我们用了三目运算符,如果是第一次开辟空间,此时的ps->capacity肯定是空,也就是0,这时候我们固定开辟一块可以装有四个元素空间的大小,所以newCapacity==4.如果不是第一次开辟,我们就每次把空间乘以一个2倍,因为如果一次空间开辟少了,数据又很多,就会导致一直开辟空间,太繁琐了。一次开辟多了也不好,如果数据没有这么多,又会造成空间的浪费,所以每次*2是个不错的选择。
SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapacity * sizeof(SLDataType));
在这里我们固定用realloc开辟,但是刚开始的时候也要用这个函数吗?不应该用maollc或者calloc来开辟吗?其实这里用到了realloc函数的一个特点:如果第一个参数给的是一个空指针,realloc就相当于malloc函数。
newCapacity是总的元素个数,sizeof(SLDataType)是一个元素的大小,它们相乘就是空间的总大小了。
if (tmp == NULL)这一个判断就没什么好说的了,就是防止开辟空间失败做的一个防御性的措施,因为开辟失败会返回一个空指针。
ps->a = tmp;
ps->capacity = newCapacity;
tmp是新开辟空间的地址,将这个地址传给我们定义的结构体里面。
capacity的大小也要记得更新。
//顺序表的销毁
void SeqListDestroy(SeqList* ps)
{
assert(ps);
if(ps->a)
{
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
因为顺序表是动态开辟的,是在堆区开辟的一块空间,再使用结束后需要我们自己手动释放。
//在顺序表末尾添加一个元素
void SeqListPushBack(SeqList* ps, SLDataType x)
{
assert(ps);
//判断是否要扩容
SeqCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
尾插就是在顺序表末尾添加一个元素,这个很简单,但是一定要检查是否要扩容。检查完之后将我们的第size的位置上加入一个元素x.插完之后更新一下size的大小,因为我们顺序表里又多了一个数据嘛。
但是可能有人不明白为什么是在数组第size的位置上。
刚开始就说,顺序表其实就是一个数组,假设我们现在有4个元素,也就是size==4,我们插入第五个元素的时候不就是在数组[4]的位置上填一个元素吗,而这个4不就是相当于size的大小吗?
//在顺序表结尾删除一个元素
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
ps->size--;
}
尾删就更简单了,这里我们不是直接把末尾那个元素删掉,而是将现有的元素个数减去一个1,有的元素它还活着,其实它已经死了~。
这里我们多了一个断言assert(ps->size > 0);因为我们一直删一直删,如果删到没有元素了,那肯定是不能删了,所以我们加这个断言,防止已经没元素还在那删。
//顺序表的打印
void SeqListPrint(SeqList* ps)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
这就是一个正常打印数组的步骤,这里也可以看到为什么在尾删时只将size减1就行。即使你没有完全删掉,但是你可以不打印出来。
//在顺序表开头添加一个元素
void SeqListPushFront(SeqList* ps, SLDataType x)
{
assert(ps);
//判断是否要扩容
SeqCheckCapacity(ps);
for (int i = 0; i < ps->size; i++)
{
ps->a[ps->size - i] = ps->a[ps->size - 1 - i];
}
ps->a[0] = x;
ps->size++;
}
顺序表头插比尾插就稍微复杂一些了,如果想在开始的地方加入一个元素,首先是将此时数组里的元素整体向后移一个位置,开头空出来一个位置来放置你需要插入的元素。
移动的方法是从数组的末尾元素开始,一个一个向后挪动:
但是可不可以从前向后挪呢?就是第一个元素一到第二个元素上面,第二个移到第三个元素上面…通过画图不难理解,这当然是不行的,第一个元素移到第二个元素的时候,第二个元素就被第一个元素覆盖而丢失了,后面也是一样。所以在这里我们只能从最后开始。
通过上面图可以看到,如果顺序表有四个元素,则总共需要移动4次,所以for循环里的条件是i < ps->size。
我们当size==4的时候移动的步骤:
a[3] = a[4]
a[2] = a[3]
…
a[0] = a[1]
这样循环的规律也能找出来了,因为一次循环只有i在增加,两边都同时减去i,然后左边要比右边多减去一个1.
//在顺序表开头删除一个元素
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
for (int i = 0; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
头删也是类似的步骤,这里不需要直接把首元素删掉,而是通过移动后面的元素,把第一个元素覆盖掉,这样就达到删除的效果了。
但是有一点要注意,这里的移动是需要从前面开始移动,相同的道理,如果从最后一个元素开始向前移,这样会造成每移动一次后面的元素都会把前面的元素覆盖掉。
通过图可以看到,这里我们只需要移动size-1次就行,因为最开始的那个元素不用移动。
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, size_t pos, SLDataType x)
{
assert(ps);
assert(pos >= 0);
assert(pos <= ps->size);
SeqCheckCapacity(ps);//是否增容
for (int i = 0; i < ps->size - pos; i++)
{
ps->a[ps->size - i] = ps->a[ps->size - i - 1];
}
ps->a[pos] = x;
ps->size++;
}
头删头插和尾删尾插只能在头和尾部进行操作,现在我想在顺序表中间操作可以吗?当然可以,只要找到了规律,轻轻松松写出来。
在写之前需要判断一下需要插入的位置也就是pos位(pos是需要插入位置的下标)合不合理,用assert进行断言,只要0 <= pos < size的时候才能继续向下进行。当然如果你想用if语句也没问题,这里的方法就是强硬了一点
现在我们需要从pos为3的位置上插入一个10.
这里的思路大概就是前pos+1个位置的元素不动,后面的元素往后移动一格,空出的位置就可以插入需要插得元素了。
如果是循环,我们应先确定好循环所需的次数。总共7个元素。在pos为3时只要移动最后3个元素即可,pos为4是移动后两位。这样不难算出pos + 循环次数 + 1 == 总元素个数。这样循环的次数就是size-pos-1。
现在再来退循环里的内容:
a[7] = a[6]
a[6] = a[5]
a[5] = a[4]
通过上面规律可以看到左边是size-i,右边是size-i-1.
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, size_t pos)
{
assert(ps);
assert(pos >= 0);
assert(pos < ps->size);
for (int i = 0; i < ps->size - pos - 1; i++)
{
ps->a[pos + i] = ps->a[pos + i + 1];
}
ps->size--;
}
现在我们想把pos==3位置上的元素也就是4删掉。思路可以是在4后面的所有元素都向前挪动一个位置即可,挪动的次数也很容易就可以看出:size-pos-1.
这次移动的顺序是从4那个位置后面开始向前挪。
a[3] = a[4]
a[4] = a[5]
a[5] = a[6]
因为每次循环i都会加1,所以左边就可以写pos+i,右边每次比左边多一个,右边就可以写成pos+i+1.
// 顺序表查找
int SeqListFind(SeqList* ps, SLDataType x)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (x == ps->a[i])
return i;
}
printf("找不到数据\n");
return -1;
}
查找的大概思路就是,遍历一遍数组,如果此时的元素等于想找的元素,就返回1。如果全部遍历完了,还没有找到说明要找的元素不在里面就返回-1.
现在在看看刚才写的两个函数,在任意位置删除和插入:
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, size_t pos, SLDataType x)
{
assert(ps);
assert(pos >= 0);
assert(pos <= ps->size);
SeqCheckCapacity(ps);//是否增容
for (int i = 0; i < ps->size - pos; i++)
{
ps->a[ps->size - i] = ps->a[ps->size - i - 1];
}
ps->a[pos] = x;
ps->size++;
}
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, size_t pos)
{
assert(ps);
assert(pos >= 0);
assert(pos < ps->size);
for (int i = 0; i < ps->size - pos - 1; i++)
{
ps->a[pos + i] = ps->a[pos + i + 1];
}
ps->size--;
}
我那第一个函数做说明,不难看到当pos取边缘的两个位置时,是不是就相当于头插和尾插了。这样的话,我们之前写的头插,尾插其实可以抛弃不要了。但是为了让其他人用我们写的函数有更多选择,留着也可以,但是我们可以这样改:
//在顺序表末尾添加一个元素
void SeqListPushBack(SeqList* ps, SLDataType x)
{
//assert(ps);
判断是否要扩容
//SeqCheckCapacity(ps);
//ps->a[ps->size] = x;
//ps->size++;
SeqListInsert(ps, ps->size, x);
}
//在顺序表开头添加一个元素
void SeqListPushFront(SeqList* ps, SLDataType x)
{
//assert(ps);
判断是否要扩容
//SeqCheckCapacity(ps);
//for (int i = 0; i < ps->size; i++)
//{
// ps->a[ps->size - i] = ps->a[ps->size - 1 - i];
//}
//ps->a[0] = x;
//ps->size++;
SeqListInsert(ps, 0, x);
}
头删和尾删也是一个道理:
//在顺序表开头删除一个元素
void SeqListPopFront(SeqList* ps)
{
//assert(ps);
//assert(ps->size > 0);
//for (int i = 0; i < ps->size - 1; i++)
//{
// ps->a[i] = ps->a[i + 1];
//}
//ps->size--;
SeqListErase(ps, 0);
}
//在顺序表结尾删除一个元素
void SeqListPopBack(SeqList* ps)
{
//assert(ps);
//assert(ps->size > 0);
//ps->size--;
SeqListErase(ps, ps->size - 1);
}
讲了这么多函数,其实我们就是对一个动态开辟的数组做了一些增删查改的功能,非常的简单。