数据结构之线性表——顺序表

1.文章内容摘抄自严蔚敏、吴伟民《数据结构(C语言版)》,供学习参考使用。
2.数据结构考研完整C语言代码可参照这位博主的文章,超全面:https://blog.youkuaiyun.com/lady_killer9/article/details/82695895
3.顺序表的C语言实现也可参考,和严蔚敏老师书上的算法是很贴近的:https://www.cnblogs.com/summer-4/p/15366530.html

一、线性表的类型定义

线性结构的特点:在数据元素的非空间有限集中,(1)存在唯一的一个被称作为“第一个”的数据元素;(2)存在唯一的一个被称作“最后一个”的数据元素;(3)除第一个之外,集合中的每个数据元素都只有一个前驱;(4)除最后一个之外,集合中的每个数据元素都只有一个后继。

线性表 是最常用且最简单的一种数据结构。简言之,一个线性表是n个数据元素的有限序列。

在复杂的线性表中,一个数据元素可以由若干个 数据项 组成。在这种情况下,常把数据元素称为 记录 ,含有大量记录的线性表又称 文件

线性表中的数据元素可以是各种各样的,但同一线性表中的元素必定具有相同特性,即属于同一数据对象,相邻数据元素之间存在着序偶关系。

线性表中元素个数n定义为线性表的长度,n=0时称为 空表

二、线性表的顺序表示的表示与实现

2.1-顺序表的定义

线性表的顺序表示指的是用一组 地址连续的存储单元 依次存储线性表的数据元素,这种存储结构的线性表也被称为 顺序表,其 特点是以元素在计算机内“物理位置相邻” 来表示线性表中数据元素之间的逻辑关系

若线性表中每个元素需占用L个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储位置,则线性表中第i+1个数据元素的存储位置LOC(ai+1)为:

LOC(a i+1) = LOC(a 1) + (i-1)×L

其中LOC(a1)是线性表的第一个数据元素a1的存储位置,通常被称为线性表的起始位置或基地址。只要确定了存储线性表的起始位置,线性表中任一数据元素都可以实现 随机存取。因此,线性表的顺序存储结构是一种随机存取的存储结构。

2.2-顺序表的动态分配与初始化

1.动态分配:在C语言中可动态分配一维数组,如下:

//———————————————— 线性表的动态分配顺序存储结构 ————————————————————
#define LIST_INIT_SIZE 100    // 线性表存储空间的初始分配量
#define LISTINCREMENT 10     // 线性表存储空间的分配增量
typedef struct{
	ElemType *elem;         //存储空间基址
	int length;            //当前长度
	int listsize;          //当前分配的存储容量(以sizeof(ElemType)为单位)
}SqList;

注:1.数组指针elem指示线性表的基地址;
2.length指示线性表的当前长度;
3.listsize指示顺序表当前分配的存储空间大小,一旦因插入元素而空间不足时,可进行再分配,即为顺序表增加一个大小为存储LISTINCREMENT个数据元素的空间。

2.初始化: 线性表的初始化操作就是为顺序表分配一个预定义大小的数组空间,并将线性表的当前长度设为0。算法如下:

Status InitList_Sq(SqList &L){
	// 构造一个空的线性表
	L.elem = (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType));
	if(! L.elem)exit(OVERFLOW);      //存储分配失败
	L.length= 0//空表长度为0
	L.listsize= LIST_INIT_SIZE;      //初始存储容量
	return OK; 
}// InitLiat_Sq

2.3-顺序表的插入与删除

在顺序存储结构的线性表中某个位置上插入或删除一个数据元素时,其主要时间耗费在移动元素上,移动元素的个数取决于插入或删除的位置。 插入和删除的时间复杂度为 O(n)

1. 插入:在第 i ( 1≤ i ≤n ) 个元素之前插入一个元素时,需要将第n位至第 i 个元素(共n-i+1个元素)向后移动一个位置,并将长度为 n 的顺序表变为长度为 n+1 的顺序表。算法如下:

Status ListInsert_Sq(List &L,int i,ElemType e){
	//在顺序表 L的第 i个位置之前,插入元素 e
	//i的合法范围是[1,ListLength.Sq(L)+1]
	if(i < 1|| i > L.length + 1)return(ERROR);     //i值不合法
	if(L.length >= L.listsize){             //当前存储空间已满,增加分配
		newbase = (ElemType *)realloc(L.listsize + LIST_INIT_SIZE * sizeof(ElemType));
		if(!newbase)exit(OVERFLOW);        //存储分配失败
		L.elem = newbase;                  //新基址
		L.listsize += LISTINCREMENT;       //增加存储容量
	}
	q = &(L.elem(i-1));    //q为插入位置
	for(p = &(L.elem[L.length-1]); p>=q; --p) *(p+1) = *p;  //插入位置及之后的元素右移
	*q = e;             //插入e
	++L.length;        //表长增1
	return OK;
}//ListInsert_Sq

易错点:1.注意判断存储空间是否已满,若已满则需要申请额外的空间。
2.代码第5行,判断存储空间是否已满,用的是 “>=” 而不是 “==”。 L.length 是当前顺序表中实际存储的元素个数,而 L.listsize 是当前分配的总容量(能存储的元素个数),理论上,L.length 的最大值就是 L.listsize,L.length > L.listsize 似乎不可能发生。但实际上,可能会因为其他地方的逻辑错误(比如一个bug导致 length 被错误地增加)导致 length 超过 listsize。如果使用 ==,这个 if 判断就会失效,程序会继续执行,最终可能导致在越界的位置写入数据,引发程序崩溃或数据损坏。所以这里使用“>=”是一种更健壮、更具防御性的编程。
Tips:注意第12行中 for 循环中的步进,使用的是 --p(前缀自减),第14行中用的也是 ++L.length(前缀自增),具体原因可以参考知乎这篇关于 i++ 和 ++i 的区别(https://zhuanlan.zhihu.com/p/391942337),简单来说在这里是实现的效果没区别的,但是 ++i 效率会稍微高一些(感慨一下,严蔚敏老师书里的算法代码真的健壮性太好了)。

2. 删除:删除第 i ( 1≤ i ≤n ) 个元素时,需要将第 i+1 至第 n 个元素(共 n-i 个元素)向后移动一个位置,并将长度为 n 的顺序表变为长度为 n-1 的顺序表。算法如下:

Status ListDelete_Sq(List &L,int i,ElemType e){
	//在顺序表 L的第 i个元素,用 e返回其值
	//i的合法范围是[1,ListLength.Sq(L)]
	if(i < 1|| i > L.length)return(ERROR);     //i值不合法
	p = &(L.elem[i-1]);         //p为被删除元素的位置
	e = *p;                     //被删除的元素值赋给e
	q = L.elem + L.length -1;   //表尾元素的位置
	for(++p; p <= q; ++p) *(p-1) = *p;  //删除位置及之后的元素左移
	--L.length;        //表长减1
	return OK;
}//ListDelete_Sq

同时提供一段笔者的 错误算法 ,以供警示:

Status ListDelete_Sq(List &L,int i,ElemType e){
	//在顺序表 L的第 i个位置处,删除元素 e
	//i的合法范围是[1,ListLength.Sq(L)]
	if(i < 1|| i > L.length)return(ERROR);     //i值不合法
	for(p = &(L.elem[i]); p <= L.length-1; ++p) *p = *(p+1);  //删除位置及之后的元素左移
	--L.length;        //表长减1
	return OK;
}//ListDelete_Sq

错误的点在于:
1.指针 p 的初始位置错误。在C/C语言中,数组索引是从 0 开始的。如果用户想删除第 i 个元素,那它在数组中的实际位置是 i-1,而不是 i 。
2.使用++p(先使用再递增),循环体内部执行的是 *p = (p+1),会导致“先移动,再判断”。
3.循环终止条件错误。
(L.length) 是数组第一个非法元素(L.elem[L.length])的内存,如果删除的是表中最后一个元素,会导致越界访问,表原来的最后一个位置读取并写入不可预测的垃圾数据,可能引发程序崩溃或数据损坏。

易错点:注意越界访问。删除操作的本质是“用后面的元素覆盖前面的元素”。当删除最后一个元素时,它后面已经没有元素了,所以不应该有任何覆盖操作。

2.4-顺序表的按值查找

在顺序表L中访查是否存在和e相同的数据元素的最简便的方法是,令e和L中的元素逐个比较,算法如下:

int LocateElem_Sq(SqList L,ElemType e,Status(* compare)(ElemType,Elemtype)){
	//在顺序表 L中查找第一个元素值为 e的位序
	//若找到,则返回其在 L中的位序,否则返回0
	i=1;
	p=L.elem;
	while(i<=L.length && !(* compare)(* p++,e))++i;
	if(i<=L.length) return i;
	else return 0;
}//LocatteElem_Sq
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值