目录
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
而本篇文章讲的便是顺序表
顺序表本质上就是一个数组,但与数组不同的是,顺序表在逻辑上是连续的。
在创建好数组之后,我们为了明确在数组中究竟存储了多少个元素,我们还需要有一个变量存储数据个数。如此顺序表内便有了多个值,我们便可以构建结构体。
- #define N 5
- struct SeqList
- {
- int a[N];
- int size;
- }
可以看到,我们在这里使用的是静态的数组,而我们静态的数组不能随着顺序表的元素个数来合理分配空间,所以我们可以改用动态数组。
同时,在数组中,存储的往往不只是整形,我们需要根据数据类型决定数组的类型,而数组往往会被频繁的使用,因此,为了避免大规模的改动,我们可以使用typedef来重新定义类型名,如此,只需要改动typedef修改的类型即可。同样,我们还可以使用typedef来简化结构体类型
- typedef int SeqListDateType;
- typedef struct SeqList
- {
- SeqListDateType* a;
- int size;
- int capacity;
- }SL;
可以看到,在我们更改为动态数组时,由于不再存在,我们w增加了一个变量capacity表示数组的大小,而size依旧表示动态数组中所存储的数据个数。
在创建好结构体后,我们需要对结构体进行初始化。
由于动态数组中我们使用的是指针,我们需要将其初始化为空指针,而其他的变量初始化为0
- void SeqListInit(SL* ps)
- {
- ps->a = NULL;
- ps->size = 0;
- ps->capacity = 0;
- }
在进行各部分功能的实现时,为了直观看到顺序表的变化,我们可以将顺序表打印出来
- void SepListPrint(SL* ps)
- {
- int i;
- for (i = 0; i < ps->size; i++)
- {
- printf("%d ", ps->a[i]);
- }
- printf("\n");
- }
在顺序表不再使用时,我们需要将动态数组销毁
- void SeqListDestory(SL* ps)
- {
- free(ps->a);
- ps->a = NULL;
- }
在我们实现插入的功能时,顺序表中存储的数据不断增多,而当数据的个数与数组大小相等时(即size等于capacity)动态数组需要进行扩容。
在动态数组的扩容中,我们需要使用realloc函数。
为了避免太多次扩容影响效率,同时为了避免扩容空间过大导致空间浪费,我们选择进行指数型扩容,我们在这里就选择将其扩增到原本的2倍。
而由于我们一开始将数组的空间初始化为0,所以在第一次扩容时显然不能将其扩增到2倍,我们在这里选择将其扩增为4。
- void SepListExpansion(SL* ps)
- {
- int newcapacity = ps->capacity == 0 ? 4:ps->capacity*2;
- SeqListDateType* tmp = (SeqListDateType*)realloc(ps->a, newcapacity*sizeof(SeqListDateType));
- if (tmp == NULL)
- {
- exit(-1);
- }
- else
- {
- ps->a = tmp;
- ps->capacity = newcapacity;
- }
- }
尾插,即在顺序表中数据的最后添加一个数据,实现这项功能,我们只需要注意扩容即可
- void SeqListPushBack(SL* ps, SeqListDateType x)
- {
- if (ps->size == ps->capacity)
- {
- SepListExpansion(ps);
- }
- ps->a[ps->size] = x;
- ps->size++;
- SepListPrint(ps);
- }
尾删,即在顺序表中数据的最后删除一个数据。
而循序表中数据的个数是有限的,当其中不存在数据时,我们不能进行删除操作
- void SeqListPopBack1(SL* ps)
- {
- assert(ps->size > 0);//报错
- ps->size--;
- SepListPrint(ps);
- }
- void SeqListPopBack2(SL* ps)
- {
- if (ps->size > 0)//条件判断
- {
- ps->a[ps->size - 1] = 0;
- ps->size--;
- }
- }
我们在这里写出了两种解决方式,可以根据自己的习惯来选择,在后面的功能实现中,我们统一使用第一种方法。
头插头删与尾插尾删相比,会比较麻烦,因为它们需要对后面的数据进行进行挪动
例如这样一个顺序表,想要头插一个数据x
我们需要自右向左进行挪动,最后在头部放入x
同样的,也需要注意扩容
- void SeqListPushFront(SL* ps, SeqListDateType x)
- {
- if (ps->size == ps->capacity)
- {
- SepListExpansion(ps);
- }
- for (int end = ps->size; end >0 ; end--)
- {
- ps->a[end] = ps->a[end - 1];
- }
- ps->a[0] = x;
- ps->size++;
- SepListPrint(ps);
- }
- 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->a[ps->size - 1] = 0;
- ps->size--;
- SepListPrint(ps);
- }
通常,元素的查找是搭配数据的删除与插入使用的,所以我们选择函数的返回类型为int(与数组的下标一致)
- int SeqListFind(SL* ps, SeqListDateType x, int begin)
- {
- int i = 0;
- for (i = begin; i < ps->size; i++)
- {
- //查找到返回下标i
- if (ps->a[i] == x)
- {
- return i;
- }
- }
- //查找不到返回-1
- return -1;
- }
下标位置的插入,与头插类似,只是需要将数据挪动的起始位置从下标0改为参数中传入的下标。
同时还需要注意传入的下标是否合法。
- void SeqListInsertPos(SL* ps, int pos, SeqListDateType x)
- {
- assert(pos >= 0 && pos <= ps->size);
- if (ps->size == ps->capacity)
- {
- SepListExpansion(ps);
- }
- for (int end = ps->size; end > pos; end--)
- {
- ps->a[end] = ps->a[end - 1];
- }
- ps->a[pos] = x;
- ps->size++;
- SepListPrint(ps);
- }
将下标位置的插入与元素的查找结合,我们便可以实现在某一元素前的插入
- void SeqListInsertData(SL* ps, SeqListDateType x, SeqListDateType data)
- {
- int pos = -2;
- while (1)
- {
- pos = SeqListFind(ps, data, pos+2);
- if (pos!=-1)
- {
- SeqListInsertPos(ps, pos, x);
- }
- else
- {
- break;
- }
- }
- }
例如这样一个顺序表,我们想要在2的前面插入5
首先我们要查找2的位置,函数SeqListFind的参数begin应该为0,此时的返回值应该为1。
之后在返回的pos下标位置插入5
之后,由于我们不确定2的个数,我们需要在前一个2的后面再次查找,由图可知,begin=pos+2
结合这个表达式,我们可以将上面begin=0写作pos=-2; begin=pos+2;
以此类推
当pos=-1时结束。
由于与上面的插入类似,这里就不多做赘述
- void SeqListErasePos(SL* ps, int pos)
- {
- assert(pos >= 0 && pos <= ps->size);
- assert(ps->size > 0);
- int i = 0;
- for (i = pos; i < ps->size-1; i++)
- {
- ps->a[i] = ps->a[i + 1];
- }
- ps->size--;
- SepListPrint(ps);
- }
- void SeqListEraseData(SL* ps, SeqListDateType data)
- {
- int pos = -2;
- while (1)
- {
- pos = SeqListFind(ps, data, pos + 2);
- if (pos !=-1)
- {
- SeqListErasePos(ps,pos);
- }
- else
- {
- break;
- }
- }
- }
我们可以想到,头插、尾插是下标位置插入的特殊化,头删、尾删是下标位置删除的特殊化。
如此,我们便可以将头插、头删、尾插、尾删常态化
- //尾插
- void SeqListPushBack(SL* ps, SLDataType x)
- {
- SeqListInsertPos(ps, ps->size, x);
- }
-
- //尾删
- void SeqListPopBack(SL* ps)
- {
- SeqListErasePos(ps, ps->size-1);
- }
-
- //头插
- void SeqListPushFront(SL* ps, SLDataType x)
- {
- SeqListInsertPos(ps, 0, x);
- }
-
- //头删
- void SeqListPopFront(SL* ps)
- {
- SeqListErasePos(ps, 0);
- }
end