在C语言学习中,我们经常会遇见增删查改等一系列操作,而这些操作全都与线性表关联,没有线性表将会对这些操作完成的十分艰难!那今天就让我们来了解一下顺序表如何增删查改!!!
1.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储
2.顺序表
2.1概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存 储。在数组上完成数据的增删查改。
顺序表一般可以分为:
1. 静态顺序表:使用定长数组存储元素:
确定的数组是不能改变的
2.动态顺序表:使用动态开辟的数组存储:
通过存放一个结构体指针,使用动态内存开辟malloc函数开辟空间 ,如果空间不够用可以使用realloc函数进行扩容!
3.对顺序表的实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空 间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间 大小,所以下面我们实现动态顺序表。
3.1创建动态内容结构体
typedef int SLDataType; // 顺序表的动态存储 typedef struct SeqList { SLDataType* array; // 指向动态开辟的数组 size_t size ; // 有效数据个数 size_t capicity ; // 容量空间的大小 }SL;
array是动态内存开辟的数组指针,指向开辟的首元素地址。
size是记录存储有效内容的数据个数,方便统计。
capicity是记录容量大小,方便与size进行比较,用来判断是否需要扩容。
我们使用typedef将int重命名为SLDataType,将结构体命名为SL。
3.2对结构体进行初始化
//第一种方法 void SeqListInit(SL* ps) { ps->a = (SLDateType*)malloc(sizeof(SLDateType) * 4); if (ps->a == NULL) { perror("malloc"); exit(-1); } ps->size = 0; ps->capacity = 4; } //第二种方法 void SeqListInit(SL s) { s.a = NULL; s.size = 0; s.capacity = 0; }
第二种方法是直接将结构体的内容传给函数,形参只是实参的一份临时拷贝,形参的改变不会影响实参!!!所以第二种方法是错误的!!!
当我们使用第一种方法初始化时,我们就可以适当的开辟一点空间,如何使用malloc开辟空间在之前的博客中有所讲解http://t.csdn.cn/038La。如果开辟失败我们就直接退出程序exit(-1)。
3.2内存销毁
在我们使用动态内存开辟后,一定要及时释放空间并置为NULL,否则会出现内存泄漏等大问题!
void SeqListDestroy(SL* ps) { free(ps->a); ps->a = NULL; ps->size = 0; ps->capacity = 0; }
3.3判断是否需要扩容
在顺序表的增删查改中,我们时不时需要判断存入的数据是否已满,需不需要扩容。所以我们为了方便使用此功能避免程序冗余,我们可以创建此函数用来判断开辟的内存是否需要扩容。
void SLCheckCapacity(SL* ps) { if (ps->size == ps->capacity) { SLDateType* p = (SLDateType*)realloc(ps->a, sizeof(ps->a) * 2); if (*p == NULL) { perror(realloc); exit(-1); } ps->capacity *= 2; ps->a = p; } }
传入结构体指针,判断size与capacity是否相等,如果相等我们就使用realloc函数进行扩容,我们创建新指针指向新创建的空间,开辟比原来大二倍的空间。如果开辟成功我们将更新capacity的值,开辟失败直接退出程序!!!
3.4尾插
void SeqListPushBack(SL* ps, SLDateType x) { SLCheckCapacity(ps); ps->a[ps->size] = x; ps->size++; }
先使用函数SLCheckCapacity进行判断是否需要进行扩容,在进行尾部插入即可。
3.5头插
将需要的数据在顺序表的头部进行插入,对于顺序表而言效率会非常低,但是我们也必须将其实现。
void SeqListPushFront(SL* ps, SLDateType x) { SLCheckCapacity(ps); for (int i = ps->size; i > 0; i--) { ps->a[i] = ps->a[i-1]; } ps->a[0] = x; ps->size++; }
3.6 尾删
尾删非常的简单,我们只需要将数组往前移动一位,size--即可。我们不必担心数据问题,下次再次使用时只需要将内容覆盖即可。
void SeqListPopBack(SL* ps) { assert(ps->size > 0); ps->size--; }
但是在删除时我们得判断顺序表中内容是否为空,防止数组越界。我们使用assert断言函数进行判断即可。
3.7头删
头删与头插的效率都很低下,这也是顺序表的一个大缺点。基本原理与头插差不多,时间复杂度都为O(n),牵一发而动全身。
void SeqListPopFront(SL* ps) { assert(ps->size > 0); for (int i = 0; i < ps->size-1; i++) { ps->a[i] = ps->a[i + 1]; } ps->size--; }
删除时我们必须判断数组是否为空,防止越界。从前向后进行迭代前移进行覆盖,最后将size--即可。
3.8 顺序表查找
为了让我们方便查找顺序表中每个内容的具体位置,然后插入想插入的数据。我们创建一个寻找下标的函数。
int SeqListFind(SL* ps, SLDateType x) { assert(ps->size > 0); for (int i = 0; i < ps->size; i++) { if (ps->a[i] == x) return i; } else return -1; }
我们使用暴力查找法,将数组遍历一遍,如果能找到则返回其下标,如果找不到则返回-1结束。
3.9 顺序表在pos位置插入x
我们使用3.8中的函数,将需要插入位置找到,然后使用此函数将其插入到顺序表中。
void SeqListInsert(SL* ps, int pos, SLDateType x) { assert(pos >= 0 && pos <= ps->size); SLCheckCapacity(ps); int end = ps->size - 1; while (end >= pos) { ps->a[end + 1] = ps->a[end]; --end; } ps->a[pos] = x; ps->size++; }
中间插入,前面的数据不需要移动,在pos后的位置要向后移动。
3.10 顺序表删除pos位置的值
void SeqListErase(SL* ps, int pos) { assert(ps->size > 0); for (int i = pos - 1; i < ps->size-1; i++) { ps->a[i] = pos->a[i + 1]; } ps->size--; }
与插入原理相同,pos后的内容向前一位即可。
顺序表的问题及思考
问题:
1. 中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考:如何解决以上问题呢?下面给出了链表的结构来看看。
我们可以使用链表解决以上问题,下一期我们将走进链表章节。
欲知后事如何,尽情期待下一期!!!