顺序表的定义
线性表的顺序存储又称为顺序表
来看一个生活中的例子:周末和朋友一起吃火锅,人非常多,我们需要在等候区等候,这个等候区就与顺序表有非常多的相似之处,借助它去理解顺序表的特点。首先,在等候区有非常多的椅子,这些椅子往往是排成一排连续排放的,中间不会空出很大的空间造成浪费。这就与在顺序表中选取存储单元的方法是一样的,选取一段地址连续的存储单元去存放顺序表。接着工作人员会安排我们在椅子上连续的坐下等候。在存储单元当中去进行数据的存放是一样的,也是依次地存放线性表当中的数据元素,中间也不会空出许多存储单元造成空间的浪费。最后结伴而行的朋友也会坐在相邻的椅子上,这与顺序表的存放是相同的。在逻辑上相邻的两个元素在物理位置上也要保证它相邻,也会把它存放在相邻的存储单元上。在这个例子当中,其实椅子就代表着存储单元,而每一个等候的人就是要存放的数据元素。来总结一下顺序表的特点:
一组地址连续存放的存储单元依次存放线性表的元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。
所以有这样的规律:顺序表中逻辑顺序与物理顺序相同
其中在逻辑上相邻的两个数据元素,在顺序表中也存放在相同的存储单元当中,每一个小格子就代表一个存储单元。
在程序语言设计中,往往使用数组来实现顺序表。但是数组和顺序表又有一些差别,第一个差别是数组下标是从 0 开始的,而顺序表是从 1 开始的。还有一个就是数组的容量是不可以增加的,而顺序表的容量是可以增加的。还有一些其他的差别,比如说数组可以是多维的,而顺序表是一维的。
根据顺序存储可以知道,它是可以实现随机存取的。这是因为我们可以从第一个元素的地址直接推算出其他元素的地址。在顺序表当中,每一个存放的元素都属于同一种数据对象。那么每一个数据元素,它的大小都是一样的。根据这一特点,我们可以计算出每一个数据元素存储的地址。
第一个元素的地址假设它是 LOC(A) ,计算第二个元素的地址就可以用第一个元素的地址加上第一个数据元素 a1 所消耗的存储空间,用 sizeof
可求得该数据元素所消耗的存储空间大小。这里需要注意的一点是,n 与 MaxSize 是有含义上的不同的,其中 an 代表的是顺序表中最后一个数据元素,而 MaxSize 代表的是数组的最后一个存储单元。
顺序表数据结构体的定义
顺序表可以用数组来实现。根据数组的两种分配方式,也就有两种描述顺序表的方法。分别是静态描述分配顺序表的方法和动态描述分配顺序表的方法。首先来看数组静态分配时时如何描述一个顺序表的。
#define MaxSize 50
typedef char ElemType; //给char类型重定义别名ElemType
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
这就是描述顺序表的语句。第一句是定义了一个宏,也就是把 MaxSize 定义为 50,这也就是数组的最大容量。接着定义了一个结构体。结构体就是把多个基本数据类型组合到一起构成一个新的数据类型。它的定义语句是用 typedef struct
,然后用大括号圈起来所要包含的基本数据类型。最后 SqList
代表着该结构体的名字。这个结构体当中有一个存放顺序表的数组,它是 ElemType 类型,其中数组大小是 MaxSize,也就是 50,还有一个整型的 length,它是代表顺序表的长度。这就是一个顺序表的程序设计语言描述。
接下来看数组动态分配是如何描述顺序表的。
#define MaxSize 50
typedef char ElemType; //给char类型重定义别名ElemType
typedef struct{
ElemType *data;
int length;
}SqList;
这是动态分配时描述顺序表的语句,观察发现这里用的是指针,指针是存放一个存储单元地址的。顺序表根据第一个数据元素的地址和数据元素的大小,就可以计算出任意数据元素的位置。那么只要定义了第一个数据元素的指针,就可以描述整个顺序表。但是这一个变量它仅仅是一个地址,而没有确切的空间,所以在使用时,需要动态的申请空间。怎样动态的申请空间呢?有这样两条语句:
C L.data = (Elemtype*)malloc(sizeof(ElemType)*InitSize);
C++ L.data = new ElemType[InitSize];
L 是 SqList 类型的一个变量,也就是 L 代表这一个顺序表,接着用 malloc
这个动态函数来申请空间,函数参数部分是申请空间的大小,是用 sizeof
计算每一个数据类型的大小乘以它的个数,就计算出整个需要申请空间的大小,malloc
前面的括号部分可以理解为强调了申请空间的类型。这是 C 语言中的方法。C++ 中直接 new 一个申请空间的类型和大小。
在使用动态分配时,一定要先申请空间才能使用,因为如果没有申请空间,它仅仅是一块地址,而没用所需要的空间。
静态分配和动态分配有什么不同呢?其实也就是数组的不同。在静态分配时,我们在编写的时候,就已经确定了数组的大小。而动态分配时,没有确定它的大小,是根据动态分配语句在运行时才将它的大小进行分配。这样有一点的好处就是,在静态分配时,当我想要存放顺序表的数据元素过超过 50 的时候则会产生错误溢出,而动态分配时,如果一旦超过了分配的空间大小,可以再重新分配一块内存空间,把旧的空间和所增加的数据元素转移到新申请的空间上,这样就不会产生溢出的问题了。这是动态分配的一个优点。
记住,动态分配依旧是一块连续的存储空间,绝非是链式存储。
顺序表的优缺点:
优点:无需为线性表中的逻辑关系增加额外的空间;可以快速的获取表中合法位置的元素
缺陷:插入和删除操作需要移动大量元素;当线性表长度变化较大时难以确定存储空间的容量
创建可复用顺序表实现:
//顺序表头文件SeqList.h
#ifndef _SEQLIST_H_
#define _SEQLIST_H_typedef void SeqList;
typedef void SeqListNode;SeqList* SeqList_Create(int 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);
SeqListNode* SeqList_Get(SeqList* list, int pos);
SeqListNode* SeqList_Delete(SeqList* list, int pos);
#endif
//顺序表源文件SeqList.c
#include <stdio.h>
#include <malloc.h>
#include "SeqList.h"//实际使用的时候需要插入什么类型,TSeqListNode需要定义成什么类型,这里是unsigned int类型
typedef unsigned int TSeqListNode;typedef struct _tag_SeqList
{
int capacity;
int length;
TSeqListNode* node;
} TSeqList;//创建线性表空间大小
SeqList* SeqList_Create(int capacity) // O(1)
{
TSeqList* ret = NULL;
if( capacity >= 0 )
{
//给带指针的结构体分配内存,结构体内存大小加上指针的内存的大小
ret = (TSeqList*)malloc(sizeof(TSeqList) + sizeof(TSeqListNode) * capacity);
}
if( ret != NULL )
{
ret->capacity = capacity;
ret->length = 0;
//ret+1表示增加一个线性表结构体不变来那个的大小,ret+1后才是容量node的起始地址
ret->node = (TSeqListNode*)(ret + 1);
}
return ret;
}void SeqList_Destroy(SeqList* list) // O(1)
{
free(list);
}void SeqList_Clear(SeqList* list) // O(1)
{
//SeqList*是void*类型,需要转化成具体的指针类型
TSeqList* sList = (TSeqList*)list;
if( sList != NULL )
{
sList->length = 0;
}
}int SeqList_Length(SeqList* list) // O(1)
{
TSeqList* sList = (TSeqList*)list;
int ret = -1;
if( sList != NULL )
{
ret = sList->length;
}
return ret;
}int SeqList_Capacity(SeqList* list) // O(1)
{
TSeqList* sList = (TSeqList*)list;
int ret = -1;
if( sList != NULL )
{
ret = sList->capacity;
}
return ret;
}int SeqList_Insert(SeqList* list, SeqListNode* node, int pos) // O(n)
{
TSeqList* sList = (TSeqList*)list;
//1.判断线性表是否合法
int ret = (sList != NULL);
int i = 0;
//2.判断插入位置是否合法,pos位置范围为0到sList->length
ret = ret && (sList->length + 1 <= sList->capacity);
ret = ret && (0 <= pos);
if( ret )
{
if( pos >= sList->length )
{
pos = sList->length;
}
//3.从最后一个元素sList->length开始到pos+1位置分别将它们都向后移动一个位置
for(i=sList->length; i>pos; i--)
{
sList->node[i] = sList->node[i-1];
}
//4.将新元素插入
//node为void类型,需要转化成具体的指针类型,这里是TSeqListNode类型
sList->node[i] = (TSeqListNode)node;
//5.线性表长度加1
sList->length++;
}
return ret;
}SeqListNode* SeqList_Get(SeqList* list, int pos) // O(1)
{
TSeqList* sList = (TSeqList*)list;
SeqListNode* ret = NULL;
//1.判断线性表和pos位置是否合法
if( (sList != NULL) && (0 <= pos) && (pos < sList->length) )
{
//2.直接通过数组下标的方式获取元素
ret = (SeqListNode*)(sList->node[pos]);
}
return ret;
}SeqListNode* SeqList_Delete(SeqList* list, int pos) // O(n)
{
TSeqList* sList = (TSeqList*)list;
SeqListNode* ret = SeqList_Get(list, pos);
int i = 0;
if( ret != NULL )
{
//将pos+1至最后一个元素分别向前移动一个位置
for(i=pos+1; i<sList->length; i++)
{
sList->node[i-1] = sList->node[i];
}
//线性表长度减1
sList->length--;
}
return ret;
}#if 0
SeqListNode* SeqList_Delete(SeqList* list, int pos) // O(n)
{
TSeqList* sList = (TSeqList*)list;
SeqListNode* ret = NULL;
int i = 0;
//判断线性表和pos位置是否合法
if( (sList != NULL) && (0 <= pos) && (pos < sList->length) )
{
//取出删除元素
ret = sList->node[pos];
//将pos+1至最后一个元素分别向前移动一个位置
for(i=pos+1; i<sList->length; i++)
{
sList->node[i-1] = sList->node[i];
}
//线性表长度减1
sList->length--;
}
return ret;
}
#endif//顺序表实现测试接口
//main.c
#include <stdio.h>
#include <stdlib.h>
#include "SeqList.h"/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int main(int argc, char *argv[])
{
SeqList* list = SeqList_Create(5);
int i = 0;
int j = 1;
int k = 2;
int x = 3;
int y = 4;
int z = 5;
int index = 0;
SeqList_Insert(list, &i, 0);
SeqList_Insert(list, &j, 0);
SeqList_Insert(list, &k, 0);
SeqList_Insert(list, &x, 0);
SeqList_Insert(list, &y, 0);
SeqList_Insert(list, &z, 0);
for(index=0; index<SeqList_Length(list); index++)
{
int* p = (int*)SeqList_Get(list, index);
printf("%d\n", *p);
}
printf("\n");
while( SeqList_Length(list) > 0 )
{
int* p = (int*)SeqList_Delete(list, 0);
printf("%d\n", *p);
}
//线性表使用完后必须销毁,因为从堆空间分配内存,必须释放掉,不然会产生内存泄漏
SeqList_Destroy(list);
return 0;
}
参考文献: