Hello,大家好,欢迎来到肖肖的数据结构讲堂,今天我们要来学习的是,线性表中的顺序表。
一、理论知识:
1.你需要知道的知识:
- 结构体
- 动态分配内存(malloc)
- 指针
- typedef
- 宏定义
2.什么是顺序表?
顺序表是线性表的一种,但它里面元素的存储时连续的,也就是说在内存中顺序表中的元素的地址是挨在一起的,这有点类似于一维数组,但是两者是有区别的。在C语言中,数组的大小不能动态改变,而顺序表的大小是可以”改变“的。
顺序表的创建代码如下:
typedef struct {
ElementType *array; //存放数据元素的数组.
int length; //顺序表的实际大小.
int capacity; //顺序表的最大容量.
}SeqList;
其中,ElementType是一个新的数据类型,它可以是int,float,char等等,还可以是自定义类型。下面是创建一个自定义类型(结构类型)的实例,为了后续的操作,我这里只在结构体里面只弄了一个整型变量,实际情况可以弄多个,而且还是不同类型的。也可以直接把基本数据类型的名字改成ElementType(typedef int ElementType;)。至于为什么弄成ElementType,这是为了规范,统一,方便交流。
typedef struct {
int a;
/*
...
*/
}ElementType;
定义好之后,我们就可以使用代码:
SeqList *L;
来定义一个顺序表,因为我们的顺序表是一个SeqList类型的指针,所以我们可以用 -> 来访问顺序表(结构体)中的元素:
L->array[i];
L->length;
我们也可以使用:
SeqList L;
来定义。如果是这样定义的话,就要用 . 运算符来访问顺序表中的数据(元素)。
L.array[i];
L.length;
在这里,还是建议使用第一种方法来定义,因为本文采取的是第一种方式,第二种方式,读者可以阅读本文后自行尝试。
3.顺序表的API设计
工作的时候,做一个项目前,都是先商量定义好API,再进行其它操作的,这里我们定义一些基本的顺序表的操作。
SeqList *cleateList(int capacity): 创建线性表。
int get(SeqList *L, int i, ElementType *x): 获取线性表中第i个数据元素的值。
int find(SeqList *L, ElementType x): 查找线性表中与给定值 x 相等的数据元素。
int insertList(SeqList *L, int i, ElementType x): 在第i个位置插入数据元素x。
int addList(SeqList *L, ElementType x): 在顺序表的最后增加一个数据元素。
int deleteList(SeqList *L, int i, ElementType x): 删除第i个位置的数据元素,并将它保存在x中。
int DestoryList(SeqList *L): 销毁一个线性表。
int ClearList(SeqList *L): 清空一个线性表。
void mergeList(SeqList *LA, SeqList *LB, SeqList *LC): 合并连个顺序表到第三个顺序表中。
void outPutList(SeqList *L): 输出线性表中的内容。
二.顺序表基本操作的实现
1.创建顺序表
顺序表的创建,其实就是创建一个空的顺序表,我们在给顺序表分配内存空间后,需要把length的值置为。声明如下:
SeqList *cleateList(int capacity);
其中,capacity是这个顺序表的最大容量。我们可以使用语句:
SeqList *L = cleateList(5);
来创建一个最大容量为5的顺序表。它的示意图,如图所示:
完整代码如下:
SeqList *cleateList(int capacity)
{
SeqList *L;
L = (SeqList *)malloc(sizeof(SeqList));
L->length = 0;
L->capacity = capacity;
L->array = (ElementType *)malloc(capacity * sizeof(ElementType));
return L;
}
这样,我们就可以得到一个空的顺序表了,下面我们来讲如何在这个空的顺序表中增添数据元素。
2.增加(插入)数据元素
我们增加数据元素的方式有两种,一种是直接在当前顺序表的最后增加一个数据元素,另一种则是在指定位置增加数据元素。
对于第一种增加数据元素,我们只需要判断当前的顺序表是否满了就可以了,而另一种还需要找到第i个位置,移动后面的数据元素。具体算法如下:
(1) 判断顺序表的存储空间是否为满,若满,返回0;
(2)判断要增加(插入)的第i个位置是否是合法的(1≤ i ≤ n+1),若不合法,返回0;
(3)因为顺序表内数据元素的存储空间在内存中是挨在一起的,因此要将i以后的元素全部往后移一下,即腾出第i个位置。
(4)将带添加的数据元素x存储到第i个位置中;
(5)修改顺序表中length的值,让其+1。
int insertList(SeqList *L, int i, ElementType x)
{
int k;
if (L->length == L->capacity) return 0; //判断顺序表是否为满。
if (i < 1 || i > L->length+1) return 0; //判断顺序表带插入的数据元素的位置是否合法。
for (k = L->length-1; k >= i-1; k--) { //将第i个元素以后的所有元素都往后挪一个位置。
L->array[k+1] = L->array[k];
}
L->array[i-1] = x; //注意第i个元素它的下标其实是i-1。
L->length++; //顺序表的长度增加1。
}
对于直接在顺序表后面增加元素就相对简单了,只要先判断顺序表是否满了,再把这个数据元素加到顺序表的最后,然后length+1即可实现。
int addList(SeqList *L, ElementType x)
{
if (L->length == L->capacity) return 0; //判断顺序表是否满了。
L->array[L->length] = x; //把这个数据元素添加到顺序表的末尾。
L->length++; //顺序表长度+1。
return 1;
}
现在,我们学会了如何增加数据元素,那么当我们想输出这些数据元素,要怎么做呢?
3.输出顺序表中的数据元素
具体代码 :
void outPutList(SeqList *L)
{
int i;
for (i = 0; i < L->length; i++) {
printf("%d\n", L->array[i].a);
}
}
注意,这里把它当成输出数组元素,我前面定义的结构变量里面是一个整型的a,使用这里我用了 array[i].a 来访问数据元素的内容。
4.测试1!
我们在前面学习了在顺序表中增加数据元素和输出数据元素的函数,但是我们不知道自己写的函数是否是正确的,所以,现在就让肖肖来带着你来测试一把吧!下面是含增加数据元素、输出顺序表内容、主函数的完整程序:
#include <stdio.h>
typedef struct
{
int a;
}ElementType;
typedef struct
{
ElementType *array;
int length;
int capacity;
}SeqList;
SeqList *cleateList(int capacity);
int addList(SeqList *L, ElementType x);
int insertList(SeqList *L, int i, ElementType x);
void outPutList(SeqList *L);
int main(void)
{
SeqList *L;
ElementType x;
int i;
L = cleateList(5);
scanf("%d", &x.a);
insertList(L, 1, x);
scanf("%d", &x.a);
addList(L, x);
outPutList(L);
return 0;
}
SeqList *cleateList(int capacity)
{
SeqList *L;
L = (SeqList *)malloc(sizeof(SeqList));
L->length = 0;
L->capacity = capacity;
L->array = (ElementType *)malloc(capacity * sizeof(ElementType));
return L;
}
int addList(SeqList *L, ElementType x)
{
int i;
if (L->length == L->capacity) return 0;
L->array[L->length] = x;
L->length++;
return 1;
}
int insertList(SeqList *L, int i, ElementType x)
{
int k;
if (L->length == L->capacity) return 0;
if (i < 1 || i > L->length+1) return 0;
for (k = L->length-1; k >= i-1; k--) {
L->array[k+1] = L->array[k];
}
L->array[i-1] = x;
L->length++;
return 1;
}
void outPutList(SeqList *L)
{
int i;
for (i = 0; i < L->length; i++) {
printf("%d\n", L->array[i].a);
}
}
控制台输入: 1 2
输出: 1 2
由此可见,函数正确!然而,如果我们只是往这个表中添加数据元素,而无法得到里面的数据元素,我们就无法对这个表中数据进行一系列的操作,那么我们该如何得到(查找)顺序表中的数据元素呢?
5.得到、查找数据元素
既然我们学会了如何添加元素到顺序表,我们理所当然的要知道如何得到顺序表中的数据元素。顺序表的查找,获取数据元素其实和简单,基本上与一维数组的类似,获得数据元素的代码如下:
int get(SeqList *L, int i, ElementTypr *x)
{
if (i < 1 || i > L->length) {
return 0;
}
*x = L->array[i-1];
return 1;
}
值得注意的是,顺序表中的第i个元素,它的下标其实是i-1。我们最先判断要取的位置的是否合理,如果是不合理的,我们为什么还要去返回数据呢?查找的操作是查找我们给定的数据是否在顺序表中,然后返回它的位置,如果没有找到,那么返回-1,代码如下:
int find(SeqList *L, ElementType x)
{
int i = L->length - 1;
while (i >= 0 && L->array[i] != x) {
i--;
}
return i;
}
因为顺序表是一个带有数组的结构,如果我们把它当成函数的参数传入会浪费空间,所以我们这里传入的是指向这个顺序表的指针。
若是我不想要一些数据元素要怎么办,或者,我干脆不要这个顺序表了,我们该如何实现呢?别急,带我慢慢述说。
6.删除,清空,销毁!
1> 删除第i个数据元素。
首先要说一下,删除的原理:
(1)判断删除位置i是否合法(1≤ i ≤ n),若不合法,则返回0。
(2)将i-1到n位置的数据元素依次往前移动一位。当 i == n 时,无需移动。
(3)修改length,使得表长减1。
具体代码:
int deleteList(SeqList *L, int i, ElementType x)
{
int k;
if (i < 1 || i > L->length) return 0;
*x = L->array[i-1];
for (k = i; k < L->length; k++) {
L->array[k-1] = L->array[k];
}
L->length--;
return 1;
}
我们怎么操作,是把前面的数据元素被后面的覆盖掉,其实之前的 L->length 位置的那个数据元素还是存在的,我们可能通过找到它的地址,通过它的地址来访问它里面的数据,不过我们的查找,得到数据元素的函数中,有如下判断:
if (i < 1 || i > L->length) return 0;
我们的查找、获取函数都只能去找这个范围内的,其他地方呢,我们是访问不到的,所以可以“认为”那个数据元素不见了。
2> 清空顺序表
我们要理解清空顺序表,我们应该先明白,计算机删除数据的原理,我们想想,当我们创建了一个包含“123”的txt文件,然后右键把它删除后,计算机是如何删除的,是像现实生活中的那样,扔掉?不不不,如果计算机是这样,那么你的硬盘和软盘岂不是要年年更换?计算机删除数据的原理就是把另外一些数据覆盖掉你写的数据上面,或者干脆不是它自己覆盖的,而是做上标记,让你不能读取数据,但是可以修改数据,待你下一次写入数据时,再把原来的数据覆盖掉。(非专业解释,想了解清楚,请自行百度)
我们的清空顺序表的操作也是如此,只是对访问做出限制,然后等待被新的数据元素覆盖掉。代码实现:
int ClearList(SeqList *L)
{
if (L->array) {
L->length = 0;
return 1;
}
else {
return 0;
}
}
L->array 用来判断顺序表中是否有数据元素,如果没有,我们就没有必要对它进行清空操作了。随后我们只需要把顺序表的实际长度length置为0,就可以认为这个顺序表已经被我们清空了。
3> 销毁顺序表
销毁?清空?听起来相同的两个词汇,其实差距挺大的。销毁顺序表,就是说,我不要这个顺序表了,这个顺序表从内存中消失了,就算通过指针来访问,访问到的也是无意义的数据。而清空顺序表呢?仅仅把顺序表里面的旧数据元素不要了,这个顺序表还是存在的,我们可以对这个顺序表进行增加数据元素的操作。
所以,当我们进行销毁顺序表的操作时,我们要注意释放内存(因为我们之前给顺序表、存放数据元素的数组分配了内存),同时把顺序表的实际大小,最大容量都改为0。具体代码如下:
int DestoryList(SeqList *L)
{
if (L->array) {
free(L->array);
L->length = 0;
L->capacity = 0;
printf("顺序表销毁成功!\n");
return 1;
}
else {
printf("顺序表销毁失败!\n");
return 0;
}
}
7.合并顺序表
前面我们把顺序表的基础操作学完了,现在我们来学习一个难一点的,合并两个有序顺序表,要求合并后的顺序表还是有序的。这个还是挺简单的,我们先设置三个指针分别指向每一个顺序表的元素,然后比较需要合并的两个顺序表中指针指向的数据元素的大小,然后把其插入到新表即可,具体算法:
(1)设表LC是一个空表,为使LC也是一个有序的顺序表,设置三个指针i,j,k分别指向三个顺序表中的数据元素。
(2)若LA->array[i] <= LB->array[j],则将LA->array[i] 插入到表LC中。
(3)重复操作步骤(2),直到一个表被扫描完毕为止。
(4)再将为扫描完的表中的剩余的所有元素放到表LC中。
具体代码:
void mergeList(SeqList *LA, SeqList *LB, SeqList *LC)
{
int i, j, k;
i = j = k = 0;
if (LC->capacity < LA->length + LB->length) {
printf("表LC容量不够!\n");
return;
}
while (i < LA->length && j < LB->length) {
if (LA->array[i] <= LB->array[j]) {
LC->array[k++] = LA->array[i++];
}
else {
LC->array[k++] = LB->array[j++];
}
}
while (i < LA->length) {
LC->array[k++] = LA->array[i++];
}
while (j < LB->length) {
LC->array[k++] = LB->array[j++];
}
LC->length = k;
}
三、作者的话
到这里,我们的顺序表已经学完了。可以看到,顺序表并不是很难,只要原理记清楚,多敲几次就可以记住。下一节的链表,难度大一点,因为指针用得多,容易乱吧!