Romi-知行合一

轻轻的风轻轻的梦,轻轻的晨晨昏昏, 淡淡的云淡淡的泪,淡淡的年年岁岁。
  博客园  :: 首页  :: 新随笔  :: 订阅 订阅  :: 管理

数据结构学习1——顺序表(C语言描述)

Posted on 2012-01-07 15:59  romi  阅读(29359)  评论(1编辑  收藏  举报

数据结构本人主要学习严蔚敏老师的《数据结构(C语言版)》,本人根据自己的需要学习了书中的算法并将其代码实现还加了自己的一些学习心得体会,现将学习历程记录下来以便日后需要时参考。主要是学的东西一多,这些当时掌握了的东西长久不用又会忘,而且自己的思路都是宝贵的财富啊,弃之可惜,所以记录下来需要时随时看看,免得又拿着一本书从头开始还要到处找代码。

线性表是最常用最简单的一种数据结构,一个线性表是n个数据元素的有限序列线性结构的顺序表示指的是用一组地址连续的存储单元一次存储线性表的数据元素,以元素在计算机内"物理位置相邻"来表示线性表中数据元素之间的逻辑关系。

数据结构的学习就从这最简单线性表中最简单的顺序表示开始。

相关概念:直接前驱元素,直接后驱元素,空表,位序等因为很简单在此不予详细介绍。

只要确定了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构由于高级程序设计语言(这里主要用指C)中的数组类型也有随即存取的特性,因此通常用数组来描述数据结构中的顺序存储结构

学习步骤如下:顺序表构造——顺序表初始化——插入元素——删除元素——元素比较——两个顺序表比较。

1.顺序表构造

顺序表构造前进行如下宏定义和变量替换,方便代码的理解:

#define TRUE 1
#define FALSE 0
#define OK 1
#define ok 1
#define ERROR 0
#define error 0
#define INFEASIBLE -1

#define LIST_INIT_SIZE 100;
#define LISTINCREMENT 10;

typedef int ElemType;
typedef int Status;

采用结构体构造一个顺序表,定义顺序表的地址、长度、存储容量的表示,代码如下:

typedef struct{
    ElemType *elem;   //定义了顺序表中元素类型的数组指针,指向顺序表存储空间的基址
    int length;       //顺序表的长度(也即元素个数)
    int listsize;     //当前分配给顺序表的存储容量
}SqList;

 

2.顺序表的初始化

接下来对该顺序表进行初始化,为顺序表分配一个预定义大小的数组空间,并将当前顺序表长度设为0,如果元素个数大于分配的存储容量则再对容量进行扩充(初始化时不扩充,顺序表使用中按需要进行容量扩充)。代码如下:

Status InitList(SqList *L)
{
    (*L).elem=(ElemType*)malloc(100*sizeof(ElemType));
    //不知什么问题不能用LIST_INIT_SIZE,必须用100,下面的realloc函数也是一样?
    if((*L).elem==NULL)
    {
        exit(OVERFLOW);
    }
    (*L).length=0;
    (*L).listsize=LIST_INIT_SIZE;
    return ok;
}

malloc函数的使用:

函数原型:extern void *malloc(unsigned int num_bytes)

函数作用:向系统申请分配指定size个字节的内存空间。返回类型是 void* 类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以强制转换为任何其它类型的指针

头文件:VC中利用malloc函数时要加malloc.h或stdlib.h

返回值:如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。

代码中(*L).elem=(ElemType*)malloc(100*sizeof(ElemType));这句话返回值类型被强制转换为ElemType*,因为返回值指向被配分内存指针(即顺序表*L的elem元素)。分配内存大小为100个ElemType所占字节

为了调试方便,创建一个顺序表并初始化,给其指定一定长度并向表中各元素赋值,代码如下:

SqList* L1=new SqList();
InitList(L1);  //测试InitList函数
(*L1).length=10;
for(int j=0;j<(*L1).length;j++)
{
	(*L1).elem[j]=j;     
}

注意:定义了指针变量后,一定要将其初始化。一个不指向任何地址空间的指针是十分危险的!上面定义了一个SqList结构体指针变量并对其分配了内存空间。

为方便测试、简化代码量,写一个输出函数以供调用,用于输出顺序表L的个元素。代码如下:

void Output_L(SqList *L)
{
	for(int i=0;i<(*L).length;i++)
		cout<<(*L).elem[i]<<" ";
}

可以调用Output_L函数看下顺序表L1的输出情况:Output_L(L1);   显示结果如下:

image

初始化的这个顺序表L1在本文后面进行测试时会大量用到。

 

3.顺序表中元素的插入

功能:在顺序表第i个位置前加入新的元素e。

思路第i个位置加入元素e,那么原来在第i个位置的元素就要移到i+1个位置中,依次向后推。移完后顺序表长度+1。关键是如何移动的问题,如果从先插人e再移动,则插入后第i个位置的值是e,原先第i个位置的值就不知道了,因为存储空间被e占用了,因此移动前还需要用个地址保存原先位置上的值。这样比较麻烦,我们采用高低址向低地址移动,先将顺序表最后一个位置(假设为p)的值传入(P+1)中,这样位置p就空出来了,将p-1的值放入p中,以此类推,最后第i个位置也空出来了,将e放入其中,这样就完成了顺序表的插入工作。

因为要插入元素,所以首先判断插入的位置是否正确,再判断存储空间够不够,不够就增加内存分配。

注意:这里我采用的是插入指针e中的值到顺序表的第i个位置,因为ListInsert这个函数还要用到后面的一些功能中,我在学习过程中调试过很多次,有的函数参数用指针,有的函数参数没用指针,在进行函数调用时很容易出错,所以呢这里统一使用指针,以免函数调用时参数出现各种不匹配改又不好改。代码如下:

Status ListInsert(SqList (*L),int i,ElemType *e)
{
    ElemType *newbase,*p,*q;
    if(i<1||i>(*L).length+1)
    {
        return ERROR;
    }
    if((*L).length>=(*L).listsize)
    {
        newbase=(ElemType *)realloc((*L).elem,((*L).listsize+10)*sizeof(ElemType));
        //不能用LISTINCREMENT,必须用10,下面一行就能用,为甚么?和realloc这个函数有关系吗
        (*L).elem=newbase;
       (*L).listsize=(*L).listsize+LISTINCREMENT;
    }
    q=&((*L).elem[i-1]);
    for(p=&((*L).elem[(*L).length-1]);p>=q;p--)
    {
        *(p+1)=*p;
    }
    *q=*e;
    (*L).length++;
    return ok;
}

需要注意的是for循环中的循环终止条件。

realloc函数的使用:

函数原型:extern void *realloc(void *mem_address, unsigned int newsize)

参数意义:void*表示返回指针的数据类型,可以强制转换为其他类型(这点和malloc是一样的);函数第一个参数表示要改变内存大小的指针名,即要改变的是哪个指针指向的内存;第二个参数表示要分配的新的大小,新的大小一定要大于原来的大小不然会导致数据丢失。

头文件:#include <stdlib.h> 有些编译器需要#include <alloc.h>

功能:先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域,同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。

返回值:如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。

注意:这里原始内存中的数据还是保持不变的。当内存不再使用时,应使用free()函数将内存块释放

下面对ListInsert这个函数进行测试,假设将指针LI2中的值插入顺序表L1中第5个位置,代码如下:

ElemType LI1=8;
ElemType *LI2;
LI2=&LI1;
ListInsert(L1,5,LI2);   
cout<<"测试ListInsert函数:"<<endl;
Output_L(L1);
cout<<endl;

调试结果显示如下:

image

 

4.顺序表中元素的删除

功能:将顺序表的第i个位置的元素参数,并将该元素存到指针e中

思路删除第i个位置的元素后,后面所有的元素所在的位置都要向前移一位。与元素添加后移位的方法相反,元素删除后我们采取从地址低的位置向地址高的位置移动。因为是删除元素,所以就不必判断存储空间够不够了,但删除的位置是否靠谱还是必须要判断的。不要忘了最后顺序表的长度要-1。

代码如下:

Status ListDelete(SqList *L,int i,ElemType *e)  
{
	ElemType *p,*q;
	if(i<0||i>=(*L).length)
	{
		return error;
	}
	q=&((*L).elem[i-1]);   //q为被删除元素的位置
	*e=*q;
	p=&((*L).elem[(*L).length-1]);  //p指向顺序表最后一个元素位置的地址
	for(q;q<p;q++)
	{
		*q=*(q+1);
	}
	(*L).length--;
	return ok;
}

需要注意的是for循环中的循环终止条件,可以和元素插入算法相比较下,有什么差别

对ListDelete函数进行测试,假设删除的是顺序表中L1第4个位置的元素,并将元素存储到指针变量e中,代码如下:

ElemType *e=new ElemType();
cout<<"测试ListDelete函数:"<<endl;
ListDelete(L1,4,e);
Output_L(L1);
cout<<endl;
cout<<"e="<<*e<<endl;

调试结果显示如下:

image

 

5.顺序表中元素比较

功能:在顺序线性表中查找第一个与指针e中的值满足Compare()关系的元素的位置,返回该位置,没有就返回0。

首先,先定义一下Compare()这个函数,就假设这个关系是相等关系吧,利用Compare这个函数实现,如果两个指针中的值相等就返回true,不相等就返回false。代码如下:

bool Compare(ElemType* e1,ElemType* e2)    //参数要就都用指针表示,以免函数间相互调用时参数出现不匹配问题
{
	if(*e1==*e2)
		return true;
	else 
		return false;
}

根据这个关系,要找出顺序表中的元素,明显的思路就是一个个元素进行对比了,看是否满足Compare()关系。这里需要注意的一点是元素的位置和数组元素的表示,第i个位置的元素是(*L).elem[i-1]代码如下:

int LocateElem(SqList *L,ElemType *e)
{
	int i=1;       //i为顺序表中的位置
	ElemType *p;   //p指向顺序表中位序为i的元素
	p=(*L).elem;     //取数组首元素地址
	while(i<=(*L).length&&(!Compare(p,e)))
	{
		i++;
		p++;
	}
	if(i<=(*L).length)
		return i;       //返回满足条件的元素所在位置i
	else 
		return 0;
}

对LocateElem函数进行测试,代码如下:

ElemType LE1=4;
ElemType *LE2;
LE2=&LE1;
cout<<"测试LocateElem函数:"<<endl;
int a=LocateElem(L1,LE2);
cout<<LE1<<"元素的位置:"<<a<<endl;

调试显示结果如下:

image

 

6.两个顺序表之间的运算

功能:将存在线性表Lb中而不存在La中的数据元素插入到线性表La中,是在La中元素后面依次插入,没有按什么顺序插入。

思路这算是顺序表应用的小综合,首先要判断线性表Lb中的各元素在线性表La中是否存在,要用到LocateElem函数,还要对元素进行插入,要用到ListInsert函数。因为这两个函数前面都理解的很透彻了,再来完成这个算法想必会比较简单。

算法涉及到取元素,定义一个函数GetElem()将线性表L中第i个元素存入指针e中,代码如下:

void GetElem(SqList *L,int i,ElemType *e)
{
	if(i>0&&i<=(*L).length)
		*e=(*L).elem[i-1];
}

利用这个函数和前面的两个函数完成该算法就很简单了,直接放代码,如下:

void Collect(SqList *La,SqList *Lb)
{
	ElemType *e=new ElemType();    //局部指针变量分配了内存空间,用完后要注意释放内存空间
	int La_len=(*La).length;
	int Lb_len=(*Lb).length;
	for(int i=1;i<=Lb_len;i++)
	{
		GetElem(Lb,i,e);
		if(!LocateElem(La,e))
		{
			ListInsert(La,La_len+1,e);   //注意这个时候La的长度并没有加1
			La_len++;
		}
	}
	delete(e);
}

对该算法进行测试,测试时要初始化另外一个顺序表L2,代码如下:

cout<<"测试Connect函数:"<<endl;
SqList *L2=new SqList();  
InitList(L2);   //定义了SqList变量并初始化后,在赋值之前千万别忘了使用该函数构造空一个线性表
(*L2).length=10;
for(int j=0;j<(*L2).length;j++)
{
	(*L2).elem[j]=j*2;
}
cout<<"线性表L2:"<<endl;
Output_L(L2);
cout<<endl;
Collect(L1,L2);
cout<<"执行Connect函数后的线性表L1:"<<endl;
Output_L(L1);

调试显示结果如下:

image

 

注意:程序最后不要忘了将用new分配了内存空间的指针变量用delete释放内存空间。顺序表如果不用了,申请的内存空间要free掉。

顺序表的操作基本上是这些,还有其他的操作用到时再学吧,顺序表就到此为止。接下来是线性表的链式表示和实现了,加油!