线性表——顺序表

1. 线性表

  线性表:0 个或多个数据元素的有限序列。(这些数据元素具有相同特性) 如果还不太理解,那就这样来说把,线性表,从字面上来理解就是 具有线一般特性的表 ,再把一条线分为好多段,每一段就是一个数据元素,注意这里的线是一个抽象概念。不难发现,线性表中的数据元素都是一对一的关系。我们常说的顺序表和链表也都属于线性表。
  若将线性表中的数据元素记为 L1,L2,L3,L4,······,Ln-2,Ln-1,Ln ; 我们称 L2 是 L3直接前驱元素,而 L4 是 L3直接后继元素,也对数据元素 Li 的下标定义,称 i 是 Li 在线性表中的位序,以此类推······ 在一个表中,第一个数据元素只有直接后继,最后一个数据元素只有直接前驱,当然表中也可以没有数据元素,这种表称之为空表
  前面说到数据元素具有相同的特性,数据元素是一个庞大的概念,是不是一个数据元素只能存在一个数据项呢?这样的话其实很不方便,所以在有些复杂的顺序表中,一个数据元素可以是多个数据项组合而成的。就比如学校的系统中存着学生信息,学生信息并不只有一个姓名,而是由学号、姓名、年龄、联系电话等数据组合,最后再按特定的方法有序存放。
  线性表是一种逻辑结构,它有两种物理结构:顺序存储结构、链式存储结构

2. 顺序存储结构

  线性表的顺序存储结构,指的是用一段地址连续的存储单元 依次存储线性表的数据元素。由于数据元素的属性(类型)都相同,顺序表的实现离不开数组。使用顺序存储结构的线性表简称为 顺序表

  • 线性表地址的计算:

  顺序表要使用一段地址连续的存储单元进行存放数据,要想计算数据元素具体的地址(前提是首个数据元素位置确定),下面就有一个公式计算第 i 个数据元素 Li 的存储位置:

  LOC( Li ) = LOC( L1 ) + ( i - 1 ) * c
  说明:c 表示元素占用存储单元的长度


  • 顺序表的两种分配方式:
  1. 静态顺序表:使用静态分配,在定义数组时,直接使用定长数组(给定数组一个确定的大小)。
  2. 动态顺序表:使用动态分配,数组大小可根据需要改变,一般使用mallocrealloc 等函数进行操作。(这种分配方式用得最多)

若不熟悉 malloc、realloc 可参考:动态内存管理


3. 顺序表的实现

3.1 顺序表的定义

  我们先进行一些基础操作(这里我们对动态顺序表进行实现)

typedef int SLDataType;

typedef struct Sqlist
{
	SLDataType x;	//插入元素,在顺序表的插入中会用到
	int* a;			//动态数组指针,暂未给定大小
	int size;		//有效数据
	int length;		//空间大小
}SL;

图解:
顺序表的定义

3.2 顺序表的初始化

  由于需要对部分数据进行修改,传参时只能传地址,所以用的是传址调用。

void SLInit(SL* psl)
{
	assert(psl);	//判断psl是否为空

	psl->a = NULL;
	psl->size = 0;
	psl->length = 0;
}

assert 断言可参考:assert

3.3 顺序表的销毁

void SLDestory(SL* psl)
{
	assert(psl);		//判断psl是否为空

	if (psl->a != NULL)
	{
		free(psl->a);	//释放动态开辟的内存空间
		psl->a = NULL;	//释放后指针及时置空
		psl->size = 0;
		psl->length = 0;
	}
}

3.4 顺序表的插入

  插入数据的操作就是对数组的修改,存入数据,既然是存放数据,就要考虑内存空间够不够用,就需要先对数组的空间大小进行判断,这里我们可以将判断部分进行封装。

既然顺序表要插入数据,那就要给插入的数据留一个空位进行存放,所以插入的操作就是要对顺序表中的数据进行部分移动。

3.4.1 检查存储空间

void SLChecklength(SL* psl)
{
	assert(psl);		//判断psl是否为空

	if (psl->size >= psl->length)		//是否空间不够
	{
		int newlength = psl->length == 0 ? 4 : psl->length * 2;  //判断length是否为零
		//上一行使用三目操作符,若length为零,则给newlength 4或8的空间大小;不为零newlength则扩大为原来的2倍(2倍不是固定的值,而是最能接受的一个值,可自行更改,)
		SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newlength);
		//对tmp进行动态内存开辟,扩容操作
		if (tmp == NULL)
		{
			perror("realloc fail");		//扩容失败
			return;
		}
		psl->a = tmp;
		psl->length = newlength;
	}
}

3.4.2 头部插入(头插)

  头插需要将整个数组的数据元素向后移动,再插入数据,才能保证数据正确。

void SLPushFront(SL* psl, SLDataType x)	//头插
{
	assert(psl);

	SLChecklength(psl);			//保证有空间可插入

	int end = psl->size - 1;	
	while (end >= 0)			//数据整体向后移动(覆盖)
	{
		psl->a[end + 1] = psl->a[end];	
		--end;
	}

	psl->a[0] = x;		//插入数据x
	psl->size++;		//有效数据个数+1
}

图解:
头插


3.4.3 尾部插入(尾插)

  尾插就不用移动数据,直接在末端插入即可。

void SLPushBack(SL* psl, SLDataType x)		//尾插
{
	assert(psl);

	SLChecklength(psl);		//保证空间足够
	psl->a[psl->size] = x;
	psl->size++;
}

3.4.4 指定位置插入

void SLInsert(SL* psl, int pos, SLDataType x)	//pos表示下标,x表示要插入的数据
{
	assert(psl);
	assert(pos >= 0 && pos < psl->size);		//判断pos的取值在范围内
	
	SLChecklength(psl); 		//保证空间足够

	int end = psl->size - 1;
	while (end >= pos)
	{
		psl->a[end + 1] = psl->a[end];		//向后移动数据操作(覆盖)
		--end;
	}
	
	psl->a[pos] = x;		//在指定位置插入
	psl->size++;			//有效数据长度 + 1
}

3.5 顺序表的删除

3.5.1 头部删除(头删)

void SLPopFront(SL* psl)		//头删
{
	assert(psl);

	//判断psl->size(有效数据)是否为空
	
	//方法1:
	//if (psl->size == 0)
	//{
	//	return;
	//}
	
	//方法2:
	assert(psl->size > 0);	//为假报错(断言)

	int begin = 0;
	while (begin < psl->size)
	{
		psl->a[begin] = psl->a[begin + 1];		//数据整体向前覆盖
		++begin;
	}
	psl->size--;
}

图解:
头删


3.5.2 尾部删除(尾删)

void SLPopBack(SL* psl)		//尾删
{
	assert(psl);

	//判断psl->size(有效数据)是否为空
	
	//方法1:
	//if (psl->size == 0)
	//{
	//	return;
	//}
	
	//方法2:
	assert(psl->size > 0);	//为假报错(断言)

	psl->size--;		//原数组最后一个有效数据变为无效数据,就不用覆盖再删除
}

3.5.3 指定位置删除

void SLErase(SL* psl, int pos)
{
	assert(psl);
	assert(pos >= 0 && pos < psl->size);	//判断pos的取值在范围内

	int begin = pos;

	while (begin < psl->size)
	{
		psl->a[begin] = psl->a[begin + 1];		//数据整体向前覆盖
		++begin;
	}
	
	psl->size--;		//删除后有效数据长度 - 1
}

3.6 顺序表的查找

void SLFind(SL* psl, SLDataType x)
{
	assert(psl);

	for (int i = 0; i < psl->size; i++)
	{
		if (psl->a[i] == x)
		{
			printf("%d的下标是%d\n", x, i);
		}
	}
	return -1;
}

3.7 顺序表的打印

void SLPrint(SL* psl)
{
	assert(psl);
	for (int i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->a[i]);
	}
	printf("\n");
}

以上操作可再通过接口实现

4. 功能综合

  顺序表功能综合实现如下,另外加入了菜单功能,帮助更快对部分功能进行熟悉。(可自行加入代码增加鲁棒性)

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SLDataType;

typedef struct Sqlist
{
	SLDataType x;	//插入元素,在顺序表的插入中会用到
	int* a;			//动态数组指针,暂未给定大小
	int size;		//有效数据
	int length;		//空间大小
}SL;

void SLInit(SL* psl)
{
	assert(psl);	//判断psl是否为空

	psl->a = NULL;
	psl->size = 0;
	psl->length = 0;
}

void SLDestory(SL* psl)
{
	assert(psl);		//判断psl是否为空

	if (psl->a != NULL)
	{
		free(psl->a);	//释放动态开辟的内存空间
		psl->a = NULL;	//释放后指针及时置空
		psl->size = 0;
		psl->length = 0;
	}
}

void SLChecklength(SL* psl)
{
	assert(psl);		//判断psl是否为空

	if (psl->size >= psl->length)		//是否空间不够
	{
		int newlength = psl->length == 0 ? 4 : psl->length * 2;  //判断length是否为零
		//上一行使用三目操作符,若length为零,则给newlength 4或8的空间大小;不为零newlength则扩大为原来的2倍(2倍不是固定的值,而是最能接受的一个值,可自行更改,)
		SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newlength);
		//对tmp进行动态内存开辟,扩容操作
		if (tmp == NULL)
		{
			perror("realloc fail");		//扩容失败
			return;
		}
		psl->a = tmp;
		psl->length = newlength;
	}
}

void SLPushFront(SL* psl, SLDataType x)	//头插
{
	assert(psl);

	SLChecklength(psl);			//保证有空间可插入

	int end = psl->size - 1;
	while (end >= 0)			//数据整体向后移动(覆盖)
	{
		psl->a[end + 1] = psl->a[end];
		--end;
	}

	psl->a[0] = x;		//插入数据x
	psl->size++;		//有效数据个数+1
}

void SLPushBack(SL* psl, SLDataType x)		//尾插
{
	assert(psl);

	SLChecklength(psl);		//保证空间足够
	psl->a[psl->size] = x;
	psl->size++;
}

void SLInsert(SL* psl, int pos, SLDataType x)	//pos表示下标,x表示要插入的数据
{
	assert(psl);
	assert(pos >= 0 && pos < psl->size);		//判断pos的取值在范围内

	SLChecklength(psl); 		//保证空间足够

	int end = psl->size - 1;
	while (end >= pos)
	{
		psl->a[end + 1] = psl->a[end];		//向后移动数据操作(覆盖)
		--end;
	}

	psl->a[pos] = x;		//在指定位置插入
	psl->size++;			//有效数据长度 + 1
}

void SLPopFront(SL* psl)		//头删
{
	assert(psl);

	//判断psl->size(有效数据)是否为空

	//方法1:
	//if (psl->size == 0)
	//{
	//	return;
	//}

	//方法2:
	assert(psl->size > 0);	//为假报错(断言)

	int begin = 0;
	while (begin < psl->size)
	{
		psl->a[begin] = psl->a[begin + 1];		//数据整体向前覆盖
		++begin;
	}
	psl->size--;
}

void SLPopBack(SL* psl)		//尾删
{
	assert(psl);

	//判断psl->size(有效数据)是否为空

	//方法1:
	//if (psl->size == 0)
	//{
	//	return;
	//}

	//方法2:
	assert(psl->size > 0);	//为假报错(断言)

	psl->size--;		//原数组最后一个有效数据变为无效数据,就不用覆盖再删除
}

void SLErase(SL* psl, int pos)
{
	assert(psl);
	assert(pos >= 0 && pos < psl->size);	//判断pos的取值在范围内

	int begin = pos;

	while (begin < psl->size)
	{
		psl->a[begin] = psl->a[begin + 1];		//数据整体向前覆盖
		++begin;
	}

	psl->size--;		//删除后有效数据长度 - 1
}

void SLFind(SL* psl, SLDataType x)
{
	assert(psl);

	for (int i = 0; i < psl->size; i++)
	{
		if (psl->a[i] == x)
		{
			printf("%d的下标是%d\n", x, i);
		}
	}
	return -1;
}

void SLPrint(SL* psl)
{
	assert(psl);
	for (int i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->a[i]);
	}
	printf("\n");
}

void menu()
{
	printf("================================\n");
	printf("1.尾插		2.尾删\n");
	printf("3.头插		4.头删\n");
	printf("5.打印		0.退出\n");
	printf("================================\n");
}

int main()
{
	SL s;
	SLInit(&s);

	int choose = 0;
	do				//这里可以用if 替换do...while、switch...case
	{
		menu();
		printf("请输入选项:");
		scanf("%d", &choose);
		switch (choose)
		{
		case 1:
			printf("请输入要插入的数据个数和数据:");
			int a = 0;
			scanf("%d", &a);
			for (int i = 0; i < a; i++)
			{
				int x = 0;
				scanf("%d", &x);
				SLPushBack(&s, x);
			}
			break;
		case 2:
			SLPopBack(&s);
			printf("操作已完成\n");
			break;
		case 3:
			printf("请输入要插入的数据个数和数据:");
			int b = 0;
			scanf("%d", &b);
			for (int i = 0; i < b; i++)
			{
				int x = 0;
				scanf("%d", &x);
				SLPushBack(&s, x);
			}
			break;
		case 4:
			SLPopFront(&s);
			printf("操作已完成\n");
			break;
		case 5:
			SLPrint(&s);
			break;
		case 0:
			printf("退出.......");
			break;
		default:
			printf("请输入正确的选项......");
			break;
		}
	} while (choose);

	SLDestory(&s);

	return 0;
}

5. 顺序表的优缺点

优点:

  1. 顺序表的物理空间是连续的,不用为数据间的逻辑关系增加额外存储空间。
  2. 可以通过下标进行访问(访问方便)。

缺点:

  1. 尾部插入数据效率高,头部和中间插入删除,需要移动数据,效率较低。
  2. 当空间不够时会进行扩容,而扩容会有一定消耗,也存在一定的空间浪费(扩多了浪费,少了不够,就需要频繁扩容)。

6. 链表

  传送门:链表(点击传送)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值