一、前言
顺序表是数据结构的一种。数据结构是计算机存储、组织数据的一种方式。
数据结构中一种称为线性结构的逻辑结构主要是对线性表的学习,线性表包含顺序表。线性表是对一类事物的集合的统称;
线性表不一定有物理结构,但是一定有逻辑结构;顺序表含有物理结构,也含有逻辑结构;
含有物理结构就是那一类事物实际上是按照一种特定的有序的方式排列,而含有逻辑结构是人为可以想象成这类事物是按照一定顺序排列的。举例(一类事物):有几种水果,它们可以被称为线性表。为什么呢?最开始是杂乱无章地摆放在桌子上面,可知是没有物理结构,但是我们想着这一类事物都是水果,(香蕉、苹果、菠萝、橙子······)我们自然就把他们归为一类并且常常在脑海中把这几种水果按照先后顺序给想象出来,这种可以想象出来的可被赋予一种排列方式的事物的集合我们就说它们具有逻辑结构。
而顺序表是线性表的一种,它这两种结构都具有,是因为顺序表底层是数组。我们知道,数组在内存上面的开辟方式以及地址都是连续的,它本身就是一种物理结构。
顺序表相比于数组,可以更简单地对数据进行增删查改,顺序表分为静态顺序表和动态顺序表,这里介绍动态顺序表。
二、顺序表实现对数据增删查改
首先对注意事项和实现功能的函数有个基本认识
//顺序表#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int SeqDatatype;typedef struct SeqList
{SeqDatatype* arr;int size;int capacity;
}SL;//初始化顺序表
void SeqInit(SL* ps);//销毁顺序表
void SeqDestroy(SL* ps);//顺序表尾插
void SeqPushBack(SL* ps, SeqDatatype x);//申请空间
void SeqCheckSpace(SL* ps);//打印顺序表的元素
void SeqPrint(SL* ps);//顺序表头插
void SeqPushFront(SL* ps,SeqDatatype x);//顺序表尾删
void SeqPopBack(SL* ps);//顺序表头删
void SeqPopFront(SL* ps);//指定位置前插入数据
void SeqInsert(SL* ps, int pos, SeqDatatype x);//删除指定位置的数据
void SeqErase(SL* ps, int pos);//寻找数据(以下标的形式返回)
int SeqFind(SL s,SeqDatatype x);
对上述代码逐行解释
typedef int SeqDatatype;
这里将int重定义起了一个别名SeqDatatype,表示这是顺序表里面的数据类型;这样做的原因是简化修改顺序表数据类型的步骤,(例如要改成char类型,这样写就不用在后面的代码里面逐行修改,只用在此行代码将int改为char)
typedef struct SeqList
{SeqDatatype* arr;int size;int capacity;
}SL;
这里是定义顺序表的结构,首先它是动态的,那么就只需要定义SeqDatatype* arr,这样可以在后面需要空间时利用动态开辟函数进行动态开辟(倘若是静态的,那么就要定义为SeqDatatype arr[N])N是一个具体数值,代表你定义的顺序表能存储的数据个数,也就是顺序表的长度,这样写将顺序表长度写死了)
size表示的是当前顺序表的含数据的个数;capacity表示顺序表能够容纳的最大数据个数。
typedef只是将顺序表这个定义基于结构体的数据结构起了一个别名 SL。
1、 初始化顺序表
//初始化
void SeqInit(SL* ps)
{ps->arr = NULL;ps->size = ps->capacity = 0;
}
对顺序表的初始化很简单,将要进行动态开辟的地址先置为NULL ,将顺序表的大小和容量置为0;
2、顺序表的尾插
//尾插
void SeqPushBack(SL* ps, SeqDatatype x)
{assert(ps);SeqCheckSpace(ps);ps->arr[ps->size++] = x;
}
尾插是在顺序表的末尾插入数据,这涉及到顺序表的修改,故传参时需要传地址,x表示要尾插的数据的数值;assert是对传过来的顺序表的地址进行断言,如果传过来的地址是空的,那么就会报错;SeqCheckSpace是一个函数,这个函数的功能就是顺序表容量不够时进行动态开辟的,下面会具体介绍;最后一行代码是先将下标为size处的置为要尾插的数值,我们知道,size相当于是一个数组的大小,但是它同时可以表示为数组最后一个元素之后假象的一个元素的下标(例如arr[10]表示有10个数据,但是第十个数据的下标是9,在尾部添加数据时就要在下标为时的地方添加数据)而后置++表示这个操作结束之后把size的数值加一,这样就可以实时跟进顺序表的大小了;
可是这样空间不够就越界了吗?下面的SeqCheckSpace就是来解决这个问题的
3、容量不够时动态开辟空间
//申请空间
void SeqCheckSpace(SL* ps)
{if (ps->size == ps->capacity){SeqDatatype Newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;SeqDatatype* tmp = (SeqDatatype*)realloc(ps->arr, Newcapacity * sizeof(SeqDatatype));if (tmp == NULL){perror("realloc failed");exit(1);}//走到这里说明开辟成功ps->arr = tmp;ps->capacity = Newcapacity;}
}
判断条件是顺序表容量等于大小;这里的动态开辟函数为realloc,但是要创建一个临时变量去接收,这是因为realloc开辟可能会存在失败的情况(后面空间不够,返回NULL) 这个时候若用arr去接收则会使得原顺序表的的存储数据的数组地址被破坏;此时在这里使用一个if条件的判断,如果开辟失败那么就终止,还不会使得顺序表被破坏;如果开辟成功那么就让pa->arr=tmp,最后让链表的容量赋值为Newcapacity;
4、顺序表的头插
//头插
void SeqPushFront(SL* ps, SeqDatatype x)
{assert(ps);SeqCheckSpace(ps);for (int i = ps->size; i >= 1; i--){ps->arr[i] = ps->arr[i - 1];}ps->arr[0] = x;ps->size++;
}
顾名思义,头插就是在顺序表的头部插入数据,但是顺序表底层是数组,我们只能直接做像尾插一样在数组后面增加数据的操作,头插却不能;那么怎么做呢?同样的先判断顺序表的大小和容量是否一样,一样则说明要开辟;之后可以依次把数据向后移动一个位置,使得下表为0的位置可以被赋值为x,并且此时还不会造成原数据被覆盖的问题;值得注意的是往后移动是从后面开始移起,若是先移动下标为0的数据,那么下标为1的数据就被覆盖了,此时,再想做后移的操作就无法得到原来数据了。同理,移动数据并且给arr[0]赋值之后让size++,实时跟进;
5、顺序表的尾删
//尾删
void SeqPopBack(SL* ps)
{assert(ps && ps->size);ps->size--;
}
尾删很简单,只需要将size--,这时顺序表的大小就减了1,虽然没有对顺序表的数据有什么直接的操作,但是间接地使得顺序表地最后一个数据无法被访问,相当于在尾部删除了一个数据;值得注意的是,此时的assert断言还要加上ps->size,因为如果顺序表没有数据,就无法进行删除数据的操作。
6、顺序表的头删
//头删
void SeqPopFront(SL* ps)
{assert(ps && ps->size);for (int i = 0; i < ps->size-1; i++){ ps->arr[i] = ps->arr[i + 1];}ps->size--;
}
头删也是采用移动的方式,从第一个数据开始,往前移动,这样使得数据不会被覆盖,最后一个数据虽然还保留着,只需要将size--,这个时候同尾删的道理一样,最后就实现了头删的操作;
7、指定位置前插入数据
//指定位置前插入数据
void SeqInsert(SL* ps, int pos, SeqDatatype x)
{assert(ps);SeqCheckSpace(ps);for (int i = ps->size ; i>pos ; i--){ps->arr[i] = ps->arr[i - 1];}ps->arr[pos] = x;ps->size++;
}
pos表示下标,我们要在这个下标前的一个下标的位置插入数据,将从pos起以后的数据都向后移动一个位置,使得pos的位置被空出来,注意是从最后一个数据开始移动起,保证数据不会被覆盖丢失,最后在下标为pos的位置赋值为x,再将size++ ;
8、删除指定位置的数据
//删除指定位置的数据
void SeqErase(SL* ps, int pos)
{assert(ps && ps->arr);assert(pos >= 0 && pos < ps->size);for (int i = pos;i<ps->size-1; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;
}
和头删同理,只不过是改变了移动的起点,但是要注意的是,此时的第二个断言表示删除的数据要在有效下标之间,从pos之后的一个位置起,并且从前往后向前移动一个位置,最后size--;
9、查找顺序表中的数据
//寻找数据
int SeqFind(SL s,SeqDatatype x)
{for (int i = 0; i < s.size; i++){if (x == s.arr[i]){return i;}}return -1;
}
查找数据是通过返回该数据下标是否成功的方式实现的,如果找得到那么就返回这个数据的下标,如果找不到就返回一个非下标值(负数)-1,实现方式是遍历顺序表里面的数据,看有没有等于x的;
完。