一文搞懂!C 语言实现【线性表】:从理论到实践

基于C语言实现——线性表



一、线性表的定义

线性表指的是具有相同数据类型的n(n>=0)个数的有限序列
假设a1是第一个数据元素,称为表头元素;an是最后一个数据元素,称为表尾元素;ai(1<i<n)是第i个数据元素:
则a1有且只有一个后继;an有且只有一个前驱;ai有且只有一个前驱和一个后继。
而线性表是一种逻辑结构,表示元素之间一对一的关系,而它在计算机上的存储结构又分为顺序链式。下面咱们就对这两种存储结构展开论述:

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

2.1、线性表的顺序表示

线性表的顺序表示指的是一组地址连续的存储单元依次存储线性表的数据元素,即元素逻辑顺序和物理顺序相同
这种存储结构一般也称为顺序表
顺序表一般表示形式为:

  • 静态顺序表:使用定长的一维数组存储元素
#define MAX_SIZE 100   //元素个数
typedef int ElemType;  //元素类型
//当然,在实际问题中,肯定是对其他类型进行重命名为ElemType
typedef struct
{
	ElemType data[MAX_SIZE]; //数组
	int length;	    //当前长度
}SqList;
  • 动态顺序表:使用动态分配的一维数组存储
typedef int ElemType;  //元素类型
typedef struct
{
	ElemType *elem; //存储空间基地址
	int length;	    //当前长度
	int listsize;   //当前分配的最大长度(和length单位相同)
}SqList;

一般都为动态顺序表,因为线性表的长度可变,且所需最大存储空间随问题不同而不同。
顺序表既然可以用数组实现,那么,它就具有随机存取的特性。即在数组的大小范围内,想在哪里存取就在哪里存取。

2.2、顺序表的操作实现

咱们就以动态顺序表为例,原因上述已经说明。

1、预定义常量(函数结果状态代码)

#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define OVERFLOW -2
#define MAX_SIZE 100   //元素个数

2、定义顺序表

typedef int ElemType;  //元素类型
typedef struct
{
	ElemType *elem; //存储空间基地址
	int length;	    //当前长度
	int listsize;   //当前分配的最大长度(和length单位相同)
}SqList;

3、初始化顺序表(构造一个空的线性表L)

int InitList_Sq(SqList* L)
{
	L->elem   = (ElemType*)malloc(MAX_SIZE * sizeof(ElemType));//分配可以容纳MAX_SIZE个元素的空间
	if (!L->elem)
		return OVERFLOW;   //内存分配失败
	L->length = 0;         //空表长度为0
	L->listsize = MAX_SIZE;//初始存储量
	return OK;			   //分配成功 
}

4、在第i位置插入

int ListInser_Sq(SqList* L, int i, ElemType e)
{
	if (!(1 < i && i <= L->length + 1))//或者这样写 if (i < 1 || i > L->length + 1) 
		return ERROR;			      //输入不合法
	if (L->length == L->listsize)//需分配内存
	{
		//扩容为原来的2倍
		L->elem = (ElemType*)realloc(L->elem, 2 * L->listsize * sizeof(ElemType));
		if (!L->elem )
			return OVERFLOW;   //内存分配失败
		L->listsize *= 2;
	}
	for (int j = L->length; j >= i; j--)//将插入位置及插入位置后面的元素都依次往后一个位置
	{
		L->elem [j] = L->elem[j-1];
	}
	L->elem[i - 1] = e;
	L->length = L->length + 1;
	return OK;
}

5、删除元素

int ListDelete_Sq(SqList* L, int i, ElemType* e)
{
	if (i<1 || i>L->length)				//输入不合法
		return ERROR;
	*e = L->elem[i - 1];				//将删除元素赋值给e
	for (int j = i; j < L->length; j++) //将删除位置之后的元素一次往前移动一个位置
	{
		L->elem[j - 1] = L->elem[j];
	}
	L->length--;
	return OK;
}

6、在顺序表 L 中查找元素 e

int LocateElem(SqList*L, ElemType e)
{
	for (int i = 0; i < L->length; i++)
	{
		if (L->elem[i] == e)
			return i+1;//返回元素位置
	}
	return 0;
}

7、合并两个顺序表

void union_Sq(SqList* LA, SqList* LB)
{
	//都合并到A中,以A为总表
	int lena = LA->length;
	int lenb = LB->length;
	for (int i = 0; i < lenb; i++)				 //让LB中的元素与LA中的元素一一对比
	{
		ElemType e = LB->elem[i];
		if (!LocateElem(LA, e))					 //判断是否相等
			ListInsert_Sq(LA, LA->length + 1, e);//插入
	}
}

8、线性表LA与LB按值都为非递减,合并为一个新表LC(会用到前面的函数)

void MergeList(SqList* LA, SqList* LB, SqList* LC)
{
	InitList_Sq(LC);	//初始化LC表
	int len_a = 0;
	int len_b = 0;
	while (len_a < LA->length && len_b < LB->length)//只要LA或者LB有一个没有比较完就继续
	{
		if (LA->elem[len_a] < LB->elem[len_b])
			ListInsert_Sq(LC, LC->length + 1, LA->elem[len_a++]);
		else
			ListInsert_Sq(LC, LC->length + 1, LB->elem[len_b++]);
	}
	while (len_a < LA->length)//LA表未完
	{
		ListInsert_Sq(LC, LC->length + 1, LA->elem[len_a++]);
	}
	while (len_b < LB->length)//LB表未完
	{
		ListInsert_Sq(LC, LC->length + 1, LB->elem[len_b++]);
	}
}

9、在顺序表中找第一个与e相同的元素返回其位序

int LocateElem_Sq(SqList*L,ElemType e)
{
	for (int i = 0; i < L->length; i++)
	{
		if (L->elem[i] == e)
			return i + 1;	// 找到元素,返回其位序(位序从1开始)
	}
	return 0;				// 未找到,返回0
}
//当然,也可以让此函数的适用范围更广写
//在顺序表中找第一个与e满足compare()元素返回其位序
typedef int Status;//定义函数返回值类型,可根据实际情况来定义
Status compare(ElemType e1, ElemType e2)//比较函数,以判断相等为例
{
	return e1 - e2;
}
int LocateElem_Sq(SqList* L, ElemType e, Status compare(ElemType e1, ElemType e2))
{
	for (int i = 0; i < L->length; i++)
	{
		if (!(compare(e, L->elem[i])))
			return i + 1;	// 找到元素,返回其位序(位序从1开始)
	}
	return 0;				// 未找到,返回0
}

顺序表小结

顺序表的时间复杂度
从上述代码可以很看出,线性表的顺序存储结果在存数据时的时间复杂度是O(1),而在插入、删除操作的时间复杂度是O(n);
顺序表的特点
逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中的任意元素;
然而,也使得顺序表在存在一个缺点,在进行插入和删除时,需要移动大量的元素。


三、线性表的链式表示与实现

线性表的链式存储结构不需要逻辑上相邻的元素在物理位置上也相邻,所以,它没有上述提到的顺序表的缺点,而在此同时,它也失去了顺序表随机存取的特点。
线性表的链式存储结构又称为链表

3.1、单链表(线性链表)

单链表概念

链表的特点是在空间中用一组任意的存储单元存储线性表的数据元素(这些存储单元可以连续的,也可以是不相邻的)。
正因如此,为了表示两个相邻元素之间的关系,对于一个数据元素来说,除了存储他自身外,还需要存储它直接后继的地址,这两部分组成了一个数据元素的存储映像,称为结点
一个节点存储两部分内容:数据元素本身+直接后继的位置,也可以说是一个节点包括两个域:数据域+指针域,指针域中存储的信息称为指针或者多个结点链结成一个链表在这里插入图片描述

这里说的是每个结点中只包含一个指针域,故又称为单链表
每个链表的存取都要有其头指针,头指针是指向链表第一个数据元素的指针。在这里插入图片描述

但是,有时为了方便操作,会在第一个结点之前附设一个结点,称为头结点,此时,头指针就指向头结点。
头结点之后的结点称为首元结点,头结点的数据域可以什么也不存,也可以存储链表中的元素个数。在这里插入图片描述

在有头结点的单链表中,如果一个链表为空,则头结点中的指针域为空(NULL)。
在这里插入图片描述

在这里。咱们主要说说带有头结点的单链表:

单链表实现

1、单链表定义

typedef int ElemType;
typedef struct LNode
{
	ElemType data;		//数据元素
	struct LNode* next; //直接后继结点的地址
}LNode,*LinkList;       //首先,将struct LNode重命名为LNode,
						//再将typedef struct LNode*重命名为LinkList
						

LNode*LinkList的区别是什么,实际上,两者在类型上并没有什么区别,只是我们在习惯上

LNode*:更侧重于表示单链表中的单个节点,用于对节点进行具体操作。
LinkList:更侧重于表示整个单链表,通常作为单链表的头指针,用于对整个链表进行管理和操作。

2、取出单链表第i个元素

int GetElem_L(LinkList L, int i, ElemType* e)
{
	int j = 1;
	LNode* p = L->next;
	while (j < i && p)//循环停止条件有两个:找到了第i个位置的元素或者p为NULL
	{
		p = p->next;
		j++;
	}
	if (j > i || !p) //没有第i个元素
		return ERROR;
	*e = p->data;//取出第i个元素
	return OK;
}

3、插入

int ListInsert(LinkList L, int i, ElemType e)
{
	//在第i个位置插入元素e
	LNode* p,* s;
	p = L;
	int j = 0;
	while (j < i - 1 && p)//查找第i-1个元素
	{
		j++;
		p = p->next;
	}
	if (j > i - 1 || !p)//输入不合法
		return ERROR;
	s = (LNode*)malloc(sizeof(LNode));//开辟空间
	if (!s)//开辟储存空间失败
		return OVERFLOW;
	//插入L表中
	s->data = e;
	s->next = p->next;
	p->next = s;
}

4、删除

int ListDelete_L(LinkList L, int i, ElemType* e)
{
	//删除第i个元素,并用e返回
	int j = 0;
	LNode* p,* q;
	p = L;
	while (j < i - 1  && p->next )//查找第i个元素,并返回其前驱
	{
		j++;
		p = p->next;
	}
	if (j > i - 1 || !p->next)	  //输入不合法
		return ERROR;
	q = p->next;
	*e = q->data;
	p->next = q->next;
	free(q);
	return OK;
}

5、尾插法

void CreateList_L(LinkList L, int n)
{
	//从表上表尾到表头插入元素
	L = (LinkList)malloc(sizeof(LNode));//建立带有头结点的单链表
	L->next = NULL;
	LNode* p;
	int i = n;
	while (i > 0)
	{
		p = (LNode*)malloc(sizeof(LNode));
		scanf("%d", &p->data);
		p->next = L->next;
		L->next = p;
	}
}

6、两个有序(非递减)链表合成一个有序链表

void MergeList_L(LinkList LA, LinkList LB, LinkList LC)
{
	LNode* pa, * pb, * pc;
	pa = LA->next;
	pb = LB->next;
	LC = LA;			//用LA的头结点作为LC的头结点
	pc = LC;
	while (pa && pb)
	{
		if (pa->data < pb->data)
		{
			pc->next = pa;
			pc = pa;
			pa = pa->next;
		}
		else
		{
			pc->next = pb;
			pc = pb; 
			pb = pb->next;
		}
	}
	pc->next = pa ? pa : pa;//插入剩余段
	free(LB);				//释放LB头结点
}

3.2、循环链表

循环链表就是将表中最后一个结点的指针域指向头结点,整个链表形成一个环。
这样,就会使得从表中任意一个结点出发都可以找到其他结点。会使得某些操作简化。在这里插入图片描述

3.3、双向链表

单链表很明显的缺点:单向性,于是就可以使用双向链表

1、表示

typedef struct DuLNode
{
	ElemType  data;        
	struct DuLNode* prior;	//前驱结点的地址
	struct DuLNode* next; 	//后继结点的地址
}DuLNode, * DuLinkList;

不难看出,在双向链表中,如果d为指向表中任一结点的指针(即d为DuLinkList型的变量),就有这样一种表示关系:

	d->next->prior=d->prior->next=d;

这种关系就很好的体现了双向链表的特性。

2、插入

int ListInsert_DuL(DuLinkList L, int i, ElemType e) {
    if (i < 1) return ERROR;  // 检查 i 是否合法
    
    DuLNode *p = L;
    int j = 0;
    
    // 查找第 i-1 个结点,确保 p 不回到 L
    while (p->next != L && j < i - 1) {
        p = p->next;
        j++;
    }
    
    // 如果 j != i-1,说明 i 超出范围(i > length + 1)
    if (j != i - 1) return ERROR;
    
    // 创建新结点
    DuLNode *s = (DuLNode*)malloc(sizeof(DuLNode));
    if (!s) return OVERFLOW;
    s->data = e;
    
    // 插入新结点
    s->next = p->next;
    s->prior = p;
    p->next->prior = s;
    p->next = s;
    
    return OK;
}

3、删除

int ListDlete_DuL(DuLinkList L, int i, ElemType* e)
{
	if (i < 1 || L->next == L) return ERROR;  // 检查 i 是否合法和是否为空表
	DuLNode* p = L->next ;
	int j = 1;
	// 查找第 i 个结点
	while (p->next != L && j < i ) 
	{
		p = p->next;
		j++;
	}
	if (j != i)
		return ERROR;
	*e = p->data;
	p->next->prior = p->prior;
	p->prior->next = p->next;
	free(p);
	return OK;
}

四、线性表两大存储结构总结

特性顺序表(数组)链表
存储方式内存连续内存分散,通过指针链接
访问效率O(1)(随机访问快)O(n)(需遍历)
插入/删除效率O(n)(需移动元素)O(1)(修改指针即可)
空间分配静态(需预分配)动态(按需申请)
适用场景频繁访问、元素数量固定频繁增删、元素数量动态变化

静态数组后续补加,不好意思

代码纯属个人见解,更好的想法的可以交流,谢谢指点!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值