数据结构的初学者,在此分享一些学习小成果。
我们将一堆数据形如A1,A2,A3,,,,An有序排列称为含有n个元素的线性表。一般情况线性表的实现都是基于数组实现的。在线性表的使用中,我们需要实际估计表的大小以动态申请空间足够的的数组容量,由于稳妥起见,一般都需要将空间估算得大一些,所以就导致了空间浪费,这也是它的一个比较严重的缺点。但是,在一些比较小的地方并且我们能够大致估计容量时就可以使用线性表,一来操作简单 ,二来并不会浪费大量空间。
下面我们来谈谈线性表具体实现。一般分为创建、清空、销毁、插入、获取元素、删除、逆置、获取最大容量和当前长度功能。这些功能也大致满足了我们日常应用所需,那么我向大家一一道来:
在具体实现之前先说明一些函数定义:
typedef void SeqList;
typedef void SeqListNode;//可以接受任何指针类型参数
typedef unsigned int TSeqListNode;
SeqList* SeqList_Creat(int capacity);//创建一个最大容量为capacity的线性表
void SeqList_Destroy(SeqList* list);//销毁一个线性表
void SeqList_Clear(SeqList* list);//清空线性表
int SeqList_Length(SeqList* list);//获取线性表长度
int SeqList_Capacity(SeqList* list);//获取线性表最大容量
int SeqList_Insert(SeqList* list, SeqListNode* node,int pos);//在pos位置插入一个元素
SeqListNode* SeqList_Get(SeqList* list,int pos);//获得线性表元素
SeqListNode* SeqList_Delete(SeqList* list,int pos);//删除一个元素
int SeqList_Reverse(SeqList *list);//元素逆置
上述函数定义方式用到了数据封装的理念,void*接收所有的指针类型,这样就不用考虑线性表内的数据类型。
前面做了一些铺垫然后我们开始具体说明线性表操作的各个实现:
1、创建
一个线性表需要包含一些辅助信息,比如最大容量capacity、当前线性表长度length,以及数据节点数组node[](在结构体中可以使用柔性数组,即在结构体中,最后一个数据成员可以是一个不指定长度数组,由于没有指定长度所以它不占用结构体内存),所以我定义了一个包含重要信息的结构体:
typedef struct _tag_SeqList{
int capacity;
int length;
TSeqListNode* node;
}TSeqList;
线性表创建其实很简单,动态申请一片内存,其大小为头节点大小和数据最大容量大小之和。申请了内存后必须做的一步是检验申请是否成功,然后对空表进行一系列初始化,最后返回内存块首地址供程序使用,具体实现代码如下:
SeqList* SeqList_Creat(int capacity)//创建一个最大容量为capacity的线性表,分配在堆区
{
TSeqList* ret = NULL;
if(capacity >= 0)
{
ret = (TSeqList*)malloc(sizeof(TSeqList) + sizeof(TSeqListNode) * capacity);
}
if(ret != NULL)
{
ret->capacity = capacity;
ret->length = 0;
ret->node = (TSeqListNode*)(ret + 1);
}
return ret;
}
函数本身是比较简单的,但是值得一提是:ret->node = (TSeqListNode*)(ret + 1);这行代码。
ret是指向TSeqList*类型的指针,ret+1表示移动TSeqList类型长度,刚好指向第一个数据节点,最后将其强制
转换成TSeqListNode*类型。
2、销毁和清空
销毁线性表就一步操作,释放从堆区申请回来的内存块。
同样的,清空线性表就是将length = 0;
代码比较简单就不贴出来占空间了。
3、获取长度和最大容量
获取线性表结构体指针的length和capacity成员即可。
接下来就是线性表的重点了,那么,请往下继续阅读。
4、插入和删除
在插入数据前首先我们会想到两个问题。插哪儿?怎么插?好,第一个,插在你想插的任何地方,前提是要
在最大容量范围内。第二个才是重点,假想你现在有一个数组,你想把一个元素插进数组的某个位置,最
理想的是每次都插在表的最后一个位置,那么数组就不需要移动任何一个元素了,那么这是不是太理想了点。
然而实际上很多时候都是在中间部分插入数据,最不堪的是插在第0个数据位置,因为这样原先每个数据都
得向后面挪动一个位置以腾出空间来接纳新来的兄弟。这样大家是不是也都懂了插入的原理了,这个不是很难,
最关键的一点就是插入前需要向后移动数组元素的位置,这是不是显得很糟糕,不管了,既然学习它不管怎样都
还是得会用才行。然而在插入之前我们还有件事情要做,就是进行定位,是吧!如果你不告诉程序将数据插在哪
儿它是不可能知道的。好了,先贴出代码看看:
int SeqList_Insert(SeqList* list, SeqListNode* node,int pos)//在pos位置插入一个元素
{
TSeqList* sList = (TSeqList*)list;
int ret = (sList != NULL);
int i = 0;
ret = ret && ((sList->length + 1) <= (sList->capacity));
ret = ret && (pos >= 0);
if(ret)
{
if(pos >= sList->length)//插入位置长于线性表长度就将它插在最后一个位置
{
pos = sList->length;
}
for(i = (sList->length) + 1;i > pos; i-- )
{
sList->node[i] = sList->node[i-1];
}
sList->node[i] = (TSeqListNode)node;//插入元素
sList->length++;//线性表自增1
}
return ret;
}
过程就是先检测一些数据的合法性,然后通过一个for循环定位到要插入数据的位置的前一个位置并且移动
数据空出位置让新元素进入线性表,然后就是将数据插入,最后一步就是增加线性表的长度。
懂了插入的原理其实删除的原理与之相近,只是这是取出要删除的数据并将该元素之后的所有元素前移一个位置
并将线性表长度自减一次,代码如下:
SeqListNode* SeqList_Delete(SeqList* list,int pos)//删除一个元素
{
TSeqList* sList = (TSeqList*)list;
SeqListNode* ret = SeqList_Get(sList,pos);
int i = 0;
if(ret != NULL)
{
for(i = pos+1; i < sList->length; i++)
{
sList->node[i-1] = sList->node[i];
}
sList->length--;
}
return ret;
}
是不是很简单呢?
5、逆置
终于到最后一步了,就是如何将线性表的数据元素逆置(顺序颠倒),在我的实现中就是将数据排个序而已:
int SeqList_Reverse(SeqList * list)//线性表逆置
{
//原理就是普通数组排序
TSeqList* sList = (TSeqList*)list;
int i = 0,len;
unsigned int tmp[2];
if(sList == NULL) return -1;
len = sList->length-1;
for(i = 0; i < (sList->length)/2; i++)
{
tmp[0] = sList->node[i];
tmp[1] = sList->node[len - i];
sList->node[i] = tmp[1];
sList->node[len - i] = tmp[0];
}
return 0;
}
以上就是线性表的一些基本操作了,当然我落下了一个获取元素的操作,读者自己尝试着实现下吧,不难的。
在main函数中我们可以创建一个线性表并插入一些元素试试打印结果,要有善于实验的习惯,插入可以使用头插法,
尾插法以及中间插入。
最后再讨论一个问题,那就是算法复杂度,相信大家都看到了,每个函数的算法并不复杂,基本都是些O(1)或
O(n),这也说明了实现的代码品质还是可以的。
由于线性表原理还是比较简单的,所以我就全部使用文字描述,相信读者也是能够读得懂的,毕竟连我都懂了,
是吧。偷笑一小会。